CTF中的逆向題,大多是考察對代碼的讀寫能力以及加密混淆算法的識别能力。能夠在一大堆代碼裡快速識别出經典的加密混淆算法,可以大大提高我們的解題效率
計劃從本期開始解析CTF逆向中的常見算法,包括算法的原理、代碼、代碼特征以及常見應用,先從最常見的base64開始
0x1 base64的原理
對于一串字元,算法要求每三個字元為一組,生成四個字元。如果最後一組字元數不足三個,則用等号補充。具體轉換規則如下表所示,三個字元共24位,每6位作為一組計算索引值,再結合base64表查找相應的索引,完成三字元到四字元的轉換

base64表如下:
如下表所示,字元不足三個時,最後一個字元低位補0,計算索引,再添加等号
0x2 base64 程式設計實作:
char *__fastcall base64_encode(char *a1){ int v1; // eax int v2; // eax int v3; // eax int v4; // eax int v5; // eax int v6; // eax int v8; // [rsp+1Ch] [rbp-54h] int v9; // [rsp+20h] [rbp-50h] int v10; // [rsp+24h] [rbp-4Ch] int v11; // [rsp+24h] [rbp-4Ch] int v12; // [rsp+24h] [rbp-4Ch] int v13; // [rsp+28h] [rbp-48h] int v14; // [rsp+2Ch] [rbp-44h] char src[56]; // [rsp+30h] [rbp-40h] unsigned __int64 v16; // [rsp+68h] [rbp-8h] v16 = __readfsqword(0x28u); v1 = strlen(a1); // a1為輸入的字元串 v14 = v1 % 3; // v14為輸入字元串長度除3以後的餘數 v13 = v1 / 3; // v13為3個一組的字元組合數量 memset(src, 0, 0x30uLL); v10 = 0; v8 = 0; v9 = 0; while ( v8 < v13 ) { v2 = v10; v11 = v10 + 1; src[v2] = base64_table[a1[v9] >> 2]; // 第一個:a1[0]左移2位,取前6位作為索引值,查找對應字元 v3 = v11++; src[v3] = base64_table[(16 * (a1[v9] & 3)) | (a1[v9 + 1] >> 4)];// 第二個:a1[0]取後2位與a1[1]的前4位拼接 src[v11] = base64_table[(4 * (a1[v9 + 1] & 0xF)) | (a1[v9 + 2] >> 6)];// 第三個:a1[1]取後4位與a1[2]的前2位拼接,查找對應字元 v4 = v11 + 1; v10 = v11 + 2; src[v4] = base64_table[a1[v9 + 2] & 0x3F]; // 第四個:a1[2]取後6位作為索引,查找對應字元 v9 += 3; ++v8; } if ( v14 == 1 ) // 餘數為1,則需要添加兩個等号 { src[v10] = base64_table[a1[v9] >> 2]; src[v10 + 1] = base64_table[16 * (a1[v9] & 3)]; strcat(src, "=="); } else if ( v14 == 2 ) // 餘數為2,則需要添加1個等号 { v5 = v10; v12 = v10 + 1; src[v5] = base64_table[a1[v9] >> 2]; v6 = v12++; src[v6] = base64_table[(16 * (a1[v9] & 3)) | (a1[v9 + 1] >> 4)]; src[v12] = base64_table[4 * (a1[v9 + 1] & 0xF)]; src[v12 + 1] = '='; } strcpy(a1, src); return a1;}
0x3 base64的代碼特征:
1.base64算法要用到base64表,可以在程式中找到連續的字元串ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
2.根據base64加密原理,代碼中必然存在根據餘數個數判斷是否添加等号的代碼
3.((a[0]&3)<<4)|(a[1]>>4)和(16*(a[0]&3))|(a[1]/16)是等價操作,都表示取a[0]後2位與a[1]前4位拼接,是base64中的常見操作
0x4 base64的擴充應用
- base64變表
顧名思義,就是更改base64的索引表。如之前索引0對應A,變成索引0對應B。可使用如下代碼完成轉換:
import base64base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'#初始表diy_base = 'TSRQPONMLKJIHGFEDCBAUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'#新表s = 'd2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD=='#待解密字元串ss = ''for i in range(len(s)): ss += base[diy_base.find(s[i])]ss += '=='print ssa = base64.b64decode(ss)print a
對于base64變表的逆向題,找到新的base表是關鍵。如果新的base表是寫在靜态程式中,可以通過IDA字元串查找(shift+f12)來擷取或者使用以下指令來發現
strings 可執行程式名 | grep -x '.\{30,\}' | head
如果base表是在程式加載時才出現,可以分析程式加載時base表的生成代碼,靜态分析出新的base64表或者采用動态調試的方法提取base64表
調試步驟:
- 在linux虛拟機的同一目錄下放置二進制程式leve3和IDA的debug服務端linux_server64,運作linux_server64
- IDA中選擇Remote linux debuger,在程式中設定斷點後,在Debugger--Process options配置調試程式的位置
- 運作後,即可以看到新的base64表
2.base64隐寫
base64算法解碼過程:首先把所有的等号去掉,然後查表将字元轉為二進制的索引值,最後每8位一組計算ascii碼還原字元,不足8位則丢棄
仍然以下表為例,被紅框标記的部分由于不足8位被丢棄了,說明其不會影響解碼結果,也就是說放啥都行。是以此處可以隐寫資訊
網上找的base64隐寫解密腳本:
def get_base64_diff_value(s1, s2): base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' res = 0for i in xrange(len(s2)):if s1[i] != s2[i]:return abs(base64chars.index(s1[i]) - base64chars.index(s2[i]))return resdef solve_stego():with open('stego.txt', 'rb') as f: file_lines = f.readlines() bin_str = ''for line in file_lines: steg_line = line.replace('\n', '') norm_line = line.replace('\n', '').decode('base64').encode('base64').replace('\n', '') diff = get_base64_diff_value(steg_line, norm_line)print diff pads_num = steg_line.count('=')if diff: bin_str += bin(diff)[2:].zfill(pads_num * 2)else: bin_str += '0' * pads_num * 2print goflag(bin_str)def goflag(bin_str): res_str = ''for i in xrange(0, len(bin_str), 8): res_str += chr(int(bin_str[i:i + 8], 2))return res_strif __name__ == '__main__': solve_stego()
參考連結:
https://blog.csdn.net/xnightmare/article/details/103774379