BASE64編碼原理分析指令碼實現及逆向案例
BASE64編碼原理分析指令碼實現及逆向案例
0x01 簡單介紹
資料傳送時並不支援所有的字元,很多時候只支援可見字元的傳送。但是資料傳送不可能只傳送可見字元為解決這個問題就誕生了base64編碼。base64編碼將所有待編碼字元轉換成64個可見字元表中的字元。
0x02 編碼原理
被Base64編碼之後所得到的所有字元都是在以下這個表當中的。
上表中總共有64個字元,2^6=64所以只需要6個bit位就足夠描述所有的表中字元了。計算機中1個位元組8個bit,一個ASCII碼佔1個位元組。因此多出來的兩位用0填充。比如我用00000110來表示表中數值為6的字元即G
計算機中1個位元組8個bit,一個ASCII碼佔1個位元組。因此多出來的兩位用0填充。比如我用00000110來表示表中數值為6的字元即G
那麼如何用上表中的字元來表達所有的字元呢?
Base64在編碼時,首先將所有的待轉換字元轉成二進位制的形式。
例如將”abc”轉成110000111000101100011 之後在每個6位位元之前加上00也就是將3個8位位元組轉換成4個6位位元組
由於base64編碼是將3個8位位元組變成4個6位位元組因此最後所得到的位元組數目一定是4的倍數。如果不是4的倍數要用=填充
我們從一個例子當中來具體體會一下轉換過程
假設待轉化的字元是 “example”
轉化成二進位制之後得到
01100101 01111000 01100001 01101101 01110000 01101100 01100101
example的長度是7因此為了使得最後得到的字元是4的倍數我們要再添上兩個字元
01100101 01111000 01100001 01101101 01110000 01101100 01100101 00000000 00000000
然後我們將其按照6位1字元排好
011001 010111 100001 100001 011011 010111 000001 101100 011001 010000 000000 000000
填充00之後得到
00011001 00010111 00100001 00100001 00011011 00010111 00000001 00101100 00011001 00010000 00000000 00000000
再將這些二進位制轉換成十進位制
25 23 33 33 27 23 1 44 25 16 0 0
對照表用字元替換之後得到
ZXhhbXBsZQAA
再將最後的AA換成==即可
ZXhhbXBsZQ==
放到python中解碼驗證一下
0x03 Python指令碼實現
先附上程式碼
-
def myBase64Encode(preCoding) :
- charTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' #字元表
- if len(preCoding) < 0 :
- return '' #字串為空則返回空
- lackCharNums = 3-len(preCoding)%3
- if lackCharNums == 3 : lackCharNums = 0 #整除說明不缺字元
- #待轉換字元不是3的倍數的情況補全它
- for i in range(lackCharNums) :
- preCoding = preCoding + b '\x00'
- result = '' #用於儲存最終結果的str資料
- rp = '' #處理補全字元時的暫存變數
- #每三個字元處理一輪
- for i in range(int(len(preCoding)/3)) :
- threeChar = preCoding[i* 3:i*3+3] #取三個字元出來
- tCode = '' #用於存放三個字元拼接後的二進位制數值 文字形式
- pCode = '' #暫存變數
- for j in range(3) :
- pCode = bin(threeChar[j])[ 2:] #把省略的0補上
- lackZeroNums = 8-len(pCode) #省略的0的個數
- for x in range(lackZeroNums) :
- pCode = '0'+pCode
- tCode = tCode + pCode
- pCode = ''
- for j in range(4) : #每6位一個字元
- pCode = tCode[j* 6:j*6+6]
- rp = rp + charTable[int(pCode, 2)]
- #處理補全的00字元
- result = rp[ :len(rp)-lackCharNums]
- for j in range(lackCharNums) :
- result = result + '='
- return bytes(result,encoding="utf-8")
- def myBase64Decode(encodedBin) :
- charTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' #字元表
- #如果字元不是4的倍數 返回空
- if not len(encodedBin)%4 == 0 :
- return ''
- tCode = '' #用於存放最終的二進位制文字字串
- pCpde = '' #暫存變數
- #遍歷encodedBin每一個字元
- for i in encodedBin :
- for j in range(len(charTable)) : #找到表中對應座標
- if chr(i) == charTable[j] :
- pCode = bin(j)[ 2:] #轉二進位制去除開頭的0b
- lackZeroNums = 6-len(pCode) #省略的0的個數
- for x in range(lackZeroNums) :
- pCode = '0'+pCode
- tCode = tCode + pCode
- pCode = ''
- result = '' #儲存最終結果
- for i in range(int(len(tCode)/8)) :
- pCode = tCode[i* 8:i*8+8]
- result = result + chr(int(pCode, 2))
- return bytes(result,encoding="utf-8")
寫的不是很好,僅供參考。這裡的解碼函式只支援ASCII,如果需要支援所有的字元,可以去了解一下UTF-8 UTF-16 unicode的關係
0x04 逆向案例分析
這是一道在Bugku上看到的逆向題。先來執行看看
提示輸入flag。一般這種題目就要看演算法了
在確認無殼之後用IDA開啟分析
先開啟strings視窗
找到right flag跳轉
檢視交叉引用
找到主要執行的部分
直接F5檢視虛擬碼
注意到需要讓Dest和Str2相同才會輸出right flag
而Str2是已知的,那麼就可以大概推測出需要將Str2進行某種逆運算才能知道flag
再往上看可以看到
將Dest每一位都進行了移位處理,再往前看關注到一個函式
跟進檢視一下
為了方便理解我已經把一些變數名重新命名。
其中input為我們輸入的字串,length為輸入字串的長度
可以看到v9首先將總長度除以3。然後在將v9*4 仔細分析之後會發現v10儲存的值就是最後Dst變數的長度。這跟Base64編碼前和編碼後的長度關係是非常相似的。
v11變數儲存了length的值作為迴圈的條件變數,然後將threeChar陣列初始化。
在經過一個迴圈之後threeChar陣列中存放了i個字元。
三個case分別為三種情況。case 3即i=3時,threeChar正好取了三個字元。
case 2即i=2時,threeChar只取了兩個字元。case 1即i=1時,threeChar只取了一個字元。
我們先來分析case 3
首先將threeChar[0]字元右移2位,相當於只取這個字元的前6位並在6位前補上2個0。補全了之後轉成了signed int也就變成了字元表中的位置。charTable就是Base64編碼的字元表
同樣的道理我們再往下看
由於剛剛threeChar[0]字元的前6位已經被取過,我們應該取threeChar[0]的後兩位和threeChar[1]的前4位拼接成一個6位再補全0成8位。
這裡首先將threeChar[1]與0xF0進行與運算。0xF0轉成二進位制是11110000,即只保留threeChar[1]的前4位後4位則置0。接著的右移操作將這4位資料移動到二進位制的最低位。後邊的threeChar[0]&3運算則是保留threeChar[0]的最後2位前邊6位置0
看到這裡就會發現和Base64編碼的原理都對應上了。另外兩種情況也都是類似的分析,只不過最後的時候加了一個補全=的程式碼
到此就可以寫出解題指令碼了
- import base64
- a = '[email protected]@dH'
- b = []
- for i in range(len(a)) :
- b. append(chr(ord(a[i])-i))
- print(base64.b64decode(''.join(b)))
理解都比較初淺,如果有哪裡講得不對,希望各位大佬多多指點~