由“正方”jiam、jiemi之逆向思及Base64之逆編碼表

wyzsk發表於2020-08-19
作者: RedFree · 2014/01/06 11:02

申明:本文旨在技術交流,並不針對“正方”(新版正方教務系統密碼處理方式也換了,只是用這個做個引子而已……)!本文並沒有什麼深度,僅探討已知明文、密文和演算法的情況下逆向得Key的可能!

0x00 背景


經常遇到基友求助類似Base64編碼的解碼(先不說是不是Base64,下文會做說明)。如:p6FDpXlnQ1tHlLZK+NvA1hwfeND8NdXt1q6whqP6WODTEBP4UzzjnDQ== 這個很像Base64編碼吧,但你用標準Base64去解碼得到的結果明顯不是想要的(這個有N多可能)。

0x01 編解碼


且從編碼、解碼說起吧,最簡單的編碼(為什麼不叫加密、解密呢?看你用它幹什麼了……)是將字元對映,例如:

a=e
b=f
c=g
……
v=z
w=a
x=b
y=c
z=d

這只是個例子,還有數字、特殊字元等沒寫進去呢。當然你也可以用沒有規律的對映方式去編碼。這樣的編碼如果用於加密密碼的話會有什麼缺陷呢?
假定某人已經擁有了幾組明文和對應的密文,那麼他可以用手上的這幾組明文和密文尋找規律來還原對映關係,然後再由這個對映關係輕鬆破解其它密文。

由此,我們將可還原明文的加密演算法分為如下幾類(為什麼要密文可還原?還是讓廠商來回答吧!):

① 如:bcdef=fghij   cdefg=ghijk…… 這樣的加密不需要工具,目測到規律然後還原。  
② 略複雜一點的加密演算法,密文找不到規律,可以防止使用者的破解。但是如果加密解密演算法側漏了,那樣所有使用者的密碼都會慘遭破解!  
③ 複雜的加密演算法,使用自定義Key加密明文,也就是說密碼是明文和Key的某種運算之後生成的。這種演算法有個優點,就算演算法側漏,沒有Key也是無法解密的(下面開始討論)。類似:pass=encode(str,key); str=decode(pass,key) 。  
④ 更復雜的演算法……(待補充) 

0x02 再議“正方”之jiam、jiemi


為什麼是再議呢?因為正方教務系統的“加密”、“解密”運作方式早已被討論N多次了,網上甚至可以找到此演算法的N多版本(可自行Baidu、Google)。 兩個典型的場景是:

① 有多組密文,已知幾組密文對應的明文和加密演算法,但是由於無法獲得加密所使用的Key,無法解密其它密文。
② 由系統某缺陷可以獲得加密後的密文(當然明文是自己的密碼這個是可控的),獲得了別人的密文後由於無Key,無法解密。

為了研究此演算法嘗試逆得Key,我決定再寫個VB版的。由於事先已從網上查知演算法位於/bin/zjdx.dll中,所以找起來就簡單了,網上找得一份“原始碼”使用Reflector載入zjdx.dll,由登入頁Default2跟進到jiam方法如圖:

2014010421455783468.jpg

原始碼如下(已寫上註釋):

public string jiam(string PlainStr, string key)
{
      string text3;
      int num3 = 1;
      int num4 = Strings.Len(PlainStr);//取密碼明文長度
      for (int num1 = 1; num1 <= num4; num1++)
      {
            string text6 = Strings.Mid(PlainStr, num1, 1);//從第一位開始每一次迴圈取密碼中的下一個字元
            string text2 = Strings.Mid(key, num3, 1);
            if (((((Strings.Asc(text6) ^ Strings.Asc(text2)) < 0x20) | ((Strings.Asc(text6) ^ Strings.Asc(text2)) > 0x7e)) | (Strings.Asc(text6) < 0)) | (Strings.Asc(text6) > 0xff))//將明文中截到的某位字元的ASSCII碼和Key中截到的某位字元的ASSCII碼進行異或運算,若結果是不可列印字元的ASSCII碼則臨時字串直接加上明文中截到的這個字元
            {
                  text3 = text3 + text6;
            }
            else//若異或後的結果是可列印字元的ASSCII碼(33-126),則臨時字串加上ASSCII碼是異或結果的字元
            {
                  text3 = text3 + StringType.FromChar(Strings.Chr(Strings.Asc(text6) ^ Strings.Asc(text2)));
            }
            if (num3 == Strings.Len(key))//假如Key中截到的字元用到最後一位了,則從頭開始使用Key
            {
                  num3 = 0;
            }
            num3++;
      }
      if ((Strings.Len(text3) % 2) == 0)//假如最後的臨時結果字元長度是偶數個,那麼最終結果=反轉字元的前半部分+反轉字元的後半部分
      {
            string text4 = Strings.StrReverse(Strings.Left(text3, (int) Math.Round(((double) Strings.Len(text3)) / 2)));
            string text5 = Strings.StrReverse(Strings.Right(text3, (int) Math.Round(((double) Strings.Len(text3)) / 2)));
            text3 = text4 + text5;
      }
      return text3;//返回最終結果
} 

可見jiami函式中傳入了兩個引數,其中PlainStr為要加密的明文,key為加密使用的key。此函式邏輯如註釋。

關於異或運算請參見:http://technet.microsoft.com/zh-cn/subscriptions/csw1x2a6(v=vs.80).aspx

為了方便於理解下文,作如此解釋:三個數a,b,c 假如(a xor b=c)那麼(a xor b xor a=b),(a xor b xor b=a)。

ASSCII碼對照表請參見:http://www.96yx.com/tool/ASC2.htm 33-126為可列印字元。

至此,已完全清晰了jiam的邏輯,將此函式移植於VB程式,程式碼如下:

Public Function jiam(ByVal PlainStr As String, ByVal key As String) As String
        Dim strChar, KeyChar, NewStr As String
        Dim Pos As Integer
        Dim i, intLen As Integer
        Dim Side1, Side2 As String
        Pos = 1
        For i = 1 To Len(PlainStr)
            strChar = Mid(PlainStr, i, 1)
            KeyChar = Mid(key, Pos, 1)
            If ((Asc(strChar) Xor Asc(KeyChar)) < 32) Or ((Asc(strChar) Xor Asc(KeyChar)) > 126) Or (Asc(strChar) < 0) Or (Asc(strChar) > 255) Then
                NewStr = NewStr & strChar
            Else
                NewStr = NewStr & Chr(Asc(strChar) Xor Asc(KeyChar))
            End If
            If Pos = Len(key) Then Pos = 0
            Pos = Pos + 1
        Next
        If Len(NewStr) Mod 2 = 0 Then
            Side1 = StrReverse(Left(NewStr, CInt((Len(NewStr) / 2))))
            Side2 = StrReverse(Right(NewStr, CInt((Len(NewStr) / 2))))
            NewStr = Side1 & Side2
        End If
        jiam = NewStr
    End Function 

那麼如果已知Key的情況下,要解密該如何書寫解密的程式碼呢?

在zjdx.dll中反編譯得到的解密函式如下:

public string jiemi(string PlainStr, string key)
{
      string text3;
      int num2 = 1;
      if ((Strings.Len(PlainStr) % 2) == 0)
      {
            string text4 = Strings.StrReverse(Strings.Left(PlainStr, (int) Math.Round(((double) Strings.Len(PlainStr)) / 2)));
            string text5 = Strings.StrReverse(Strings.Right(PlainStr, (int) Math.Round(((double) Strings.Len(PlainStr)) / 2)));
            PlainStr = text4 + text5;
      }
      int num3 = Strings.Len(PlainStr);
      for (int num1 = 1; num1 <= num3; num1++)
      {
            string text6 = Strings.Mid(PlainStr, num1, 1);
            string text2 = Strings.Mid(key, num2, 1);
            if (((((Strings.Asc(text6) ^ Strings.Asc(text2)) < 0x20) | ((Strings.Asc(text6) ^ Strings.Asc(text2)) > 0x7e)) | (Strings.Asc(text6) < 0)) | (Strings.Asc(text6) > 0xff))
            {
                  text3 = text3 + text6;
            }
            else
            {
                  text3 = text3 + StringType.FromChar(Strings.Chr(Strings.Asc(text6) ^ Strings.Asc(text2)));
            }
            if (num2 == Strings.Len(key))
            {
                  num2 = 0;
            }
            num2++;
      }
      return text3;
}

其實不需要這個函式,稍懂程式設計的人便可根據加密函式寫出對應的解密函式。

解密邏輯:

+--判斷密文長度是否是偶數|下一步
|是(重組密文) 否|下一步
|一位位截得密碼和key的某位異或|下一步
|根據異或結果組合字元|下一步
+最終結果|

根據此邏輯書寫的VB程式碼如下:

Public Function jiemi(ByVal PlainStr As String, ByVal key As String) As String
        Dim strChar, KeyChar, NewStr As String
        Dim Pos As Integer
        Dim i As Integer
        Dim Side1 As String
        Dim Side2 As String
        Pos = 1
        If Len(PlainStr) Mod 2 = 0 Then
            Side1 = StrReverse(Left(PlainStr, CInt((Len(PlainStr) / 2)))) '反轉明文密碼最左邊一半的字元
            Side2 = StrReverse(Right(PlainStr, CInt((Len(PlainStr) / 2)))) '反轉明文密碼最右邊一半的字元
            PlainStr = Side1 & Side2
        End If
        For i = 1 To Len(PlainStr)
            strChar = Mid(PlainStr, i, 1) '一個一個處理plainstr中的字元
            KeyChar = Mid(key, Pos, 1) '迴圈使用key中的字元
            If ((Asc(strChar) Xor Asc(KeyChar)) < 32) Or ((Asc(strChar) Xor Asc(KeyChar)) > 126) Or (Asc(strChar) < 0) Or (Asc(strChar) > 255) Then
                NewStr = NewStr & strChar
            Else
                NewStr = NewStr & Chr(Asc(strChar) Xor Asc(KeyChar))
            End If
            If Pos = Len(key) Then Pos = 0
            Pos = Pos + 1
        Next
        jiemi = NewStr
    End Function

現在加密和解密的函式都已完全寫出來了,由此得到了兩個公式:jiam(明文,key)=密文    jiemi(密文,key)=明文

但是在上面的場景是已知明文和密文,有沒有可能運算得到Key呢:GetKey(明文,密文)=key ?

答案是肯定的,因為由jiam()函式得知:明文和密文的長度一定相等。由於已知異或運算的法則如此:明文(異或)迴圈key=密文、{明文(異或)key}(異或)明文=迴圈Key,所以密文(異或)明文=迴圈Key。

根據此邏輯書寫VB程式碼如下:

Public Function GetKey(ByVal PlainStr As String, ByVal Pass As String) As String
        Dim strChar, KeyChar, NewStr As String
        Dim Pos As Integer
        Dim i As Integer
        Dim Side1 As String
        Dim Side2 As String
        Pos = 1
        If Len(PlainStr) Mod 2 = 0 Then
            Side1 = StrReverse(Left(PlainStr, CInt((Len(PlainStr) / 2)))) '反轉明文密碼最左邊一半的字元
            Side2 = StrReverse(Right(PlainStr, CInt((Len(PlainStr) / 2)))) '反轉明文密碼最右邊一半的字元
            PlainStr = Side1 & Side2
        End If
        For i = 1 To Len(PlainStr)
            strChar = Mid(PlainStr, i, 1) '一個一個處理plainstr中的字元
            KeyChar = Mid(Pass, Pos, 1) '迴圈使用pass中的字元
            If strChar = KeyChar Then
                NewStr = NewStr & "*"
            Else
                NewStr = NewStr & Chr(Asc(strChar) Xor Asc(KeyChar))
            End If
            Pos = Pos + 1
        Next
        GetKey = NewStr
End Function

到這裡,我們遇到了一個問題,那就是密文中的有些字元並沒有經過異或運算而直接加入到了結果之中,也就是Key在某此字元處並沒有起作用,所以在這些地方無法逆出相應的Key字元。那該怎麼辦呢?前面我們已經提到了,我們有幾組明文和密碼的對照,所以可以透過N組逆運算來確定最終的Key(當然如果密碼足夠長也可以達到此效果,因為key的長度是一定的[一直在迴圈使用]),未參與運算部分的key用號來代替(如果Key中有號呢,就用其它符號代替唄!)

測試了幾組對照的明文密文,得到如下key,

A***l*36*j*
Ac**lf****w
A*xy*f*65*w
Acxy*f3**j*
……

最終可以確定key值為:Acxylf365jw

0x03 反觀Base64編碼


看一看類似的可以編碼且可還原原碼的Base64編碼吧!

Base64的介紹請看:http://zh.wikipedia.org/zh-cn/Base64

Base64的邏輯是:字串>>每個字元的8位二進位制連線>>每6位轉換為十進位制對應編碼表連線轉換後字元;如果要編碼的位元組數不能被3整除,最後會多出1個或2個位元組,那麼可以使用下面的方法進行處理:先使用0位元組值在末尾補足,使其能夠被3整除,然後再進行base64的編碼。在編碼後的base64文字後加上一個或兩個'='號,代表補足的位元組數。

由於6位二進位制最大為111111即63最小為000000所以編碼表中共有64個字元。

Base標準編碼表:

2014010421512778349.jpg

示例:

2014010421514440450.jpg

2014010421515961709.jpg
A   QQ==
BC  QkM=

正常情況下Base64的編碼表是:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=,但如果將編碼表中的字元換其它順序排一下結果又是什麼樣子呢?

我使用了這個編碼表(等同於key):abcdefghijklmnopqrstuvwxyz0123456789+/=ABCDEFGHIJKLMNOPQRSTUVWXYZ,編碼字元:I Love You!

得到字串:ssbm1Qz/if/I3se=,但使用標準編碼表解碼得到:ƦՌȞǀ 幾個“亂碼”…… 那麼在已知編碼前字元和編碼後字元但編碼表不知的情況下能不能根據陣列對照來確定編碼表呢?

答案也是肯定的:首先根據明文和密文的長度來確定使用的有沒有可能是Base64編碼(詳情Baidu、Google),然後再將明文轉換為8位二進位制每6位再轉為十進位制。

假如我們有明文:11CA467C5B1C3C0AB1D6C8A81104CC868CDC0A91 和密文:mtfdqtq2n0m1qJfdm0mWquiXrdzdoee4mteWnendody4q0rdmee5mq==那確定編碼表的過程如下:

Step1:明文轉為8位二進位制:

00110001001100010100001101000001001101000011011000110111010000110011010101000010001100010100001100110011010000110011000001000001010000100011000101000100001101100100001100111000010000010011100000110001001100010011000000110100010000110100001100111000001101100011100001000011010001000100001100110000010000010011100100110001

Step2:每6位二進位制數轉為十進位制(我以|分割便於檢視):

12|19|5|3|16|19|16|54|13|52|12|53|16|35|5|3|12|52|12|48|16|20|8|49|17|3|25|3|14|4|4|56|12|19|4|48|13|4|13|3|14|3|24|56|16|52|17|3|12|4|4|57|12|16|

Step3對應加密後字串:

mtfdqtq2n0m1qJfdm0mWquiXrdzdoee4mteWnendody4q0rdmee5mq==

得到m在編碼表中的位置是12,t在編碼表中的位置是19,f在編碼表中的位置是5……(因後面的m都是12,t都是19……,所以可以確定這是換了編碼表的Base64)。在使用了多組明文和密文對照之後得到了變異的編碼表:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/= 至此成功完成了編碼表的逆算。

對於一次Base64編碼的的運算可以透過幾組對照的明、密文輕鬆逆得編碼表,假如是兩次使用同編碼表的Base64編碼,還有沒有可能得到編碼表呢?

可以做如下分析:(簡單的舉例,因第一個字元編碼後不受後面字元的影響,所以下面分析的是每一次編碼後的第一個字元)

第一次編碼後字元空間位於編碼表的8位至31位(可列印字元的ASSCII碼為32-126)

……
Bin(A)= 01000001  Base64(A)=編碼表中第16位
Bin(E)= 01000101  Base64(E)=編碼表中第17位
Bin(I)= 01001001  Base64(I)=編碼表中第18位
Bin(M)= 01001101  Base64(M)=編碼表中第19位
Bin(Q)= 01010001  Base64(Q)=編碼表中第20位
Bin(U)= 01010101  Base64(U)=編碼表中第21位
Bin(Y)= 01011001  Base64(Y)=編碼表中第22位
……

由於第一次編碼後字元範圍一定在下面這些字元範圍中:

abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=

其中ASSCII碼最小的為”43” 最大的為”122”

所以二次編碼後字元空間位於編碼表的第10位至30位。

由於二次編碼的結果是知道的,所以可以用多組密文來確定編碼表位於10-30之間的所有字元(但順序是不知道的)。

……
Base64(Base64(A))= Base64(編碼表中第16位的字元)
Base64(Base64(E))= Base64(編碼表中第17位的字元)
Base64(Base64(I))= Base64(編碼表中第18位的字元)
……

這樣編碼表10-30位所有字元的Base編碼結果都知道了(結果字元還位於10至30位之間)

由此得到了一個21元的方程(如果解的出來那麼編碼表中的一部分字元的位置就確定了,不過還沒有嘗試能不能解出來……)

可見如果經過了二次編碼,要還原編碼表還是很困難的(大牛支招吧!)

0x04 反思


本文只嘗試了從首字元去還原二次編碼的Base64編碼表,是不是有其它方法能很輕易的還原編碼表呢?

對於類似edcode(str,key)一次加密得到的密文且已知演算法(密碼可還原)的情況下,是不是都可以透過明文和密文逆到key呢?

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章