一、目標
2023年了,MD5已經是最基礎的簽名算法了,但如果你還只是對輸入做了簡單的MD5,肯定會被同行們嘲笑。加點鹽(salt)是一種基本的提升,但在這個就業形勢嚴峻的時代,僅僅加鹽肯定不夠了。
今天我們就來講一講魔改的MD5,讓這個算法高大上起來。
1、菜卷
最簡單的魔改方法就是改變MD5的初始參數,
context->state[0] = 0x67452301;
context->state[1] = 0xEFCDAB89;
context->state[2] = 0x98BADCFE;
context->state[3] = 0x10325476;
把這四個參數修改一下就行了。通過修改這些參數,我們可以改變MD5的運算結果。但這種方法實在太簡單了,卷不起來。
接下來,我們要介紹更高級的卷法。
2、肉卷
md5會進行64輪運算,每輪運算都會用到一個常量,組成一個常量表K。
K原始值的計算方式是 2^32 * |sin i |,而后取其整數部分。
那么有理想的同學就可以更改這個K值,比如把 sin改成 cos或者tan之類的,這樣就可以卷起來了。
3、卷中卷
//F,G,H,I四個非線性變換函數
#define F(x,y,z) ((x & y) | (~x & z))
#define G(x,y,z) ((x & z) | (y & ~z))
#define H(x,y,z) (x^y^z)
#define I(x,y,z) (y ^ (x | ~z))
//x循環左移n位的操作
#define ROTATE_LEFT(x,n) ((x << n) | (x >> (32-n)))
要真正卷起來,我們需要改變MD5中的四個非線性變換函數F、G、H、I。我們可以加上 異或 或者 減少 與 操作,整個算法就換了個面貌。這種高級卷法可以忽悠住老板,讓算法高大上起來。
我們今天的目標是嘗試還原一個魔改之后的MD5算法,通過這次實踐來了解算法還原的基本方法。
這個樣本我們的入參是字符串: "1677038066553"
返回值是32個字符: "DD89CA684D91818B970710F75A75743D"
二、步驟
第一步
我們需要用Unidbg跑通算法,比起上古時期用ida調試的前輩,Unidbg的出現直接把算法還原的難度降了一個數量級。
第二步
我們需要把結果Z通過反向推導一步一步回到原始輸入A。這種方法叫做倒果為因,是逆向分析的一種基本套路。
我們假設這個樣本是MD5或者是魔改的MD5,我們可以用以下幾種方法來還原算法:
1、調試斷點
2、條件斷點
3、數據打印
4、Trace內存讀寫
5、Trace代碼
1、調試斷點
逆向分析是經驗科學,雖然有一些基本套路,但是還是以試為主,先用IDA打開 libnative-lib.so,從 Exports 導出表里面找到導出函數 Java_com_littleq_cryptography_md5_MainActivity_sign
這個函數的開始地址在0x1234, 結束地址在0x12B4,但是主要的代碼邏輯在函數sub_A3C里面, 我們先在sub_A3C函數的末端下個斷點試試,
text:00000000000011D4 E0 07 40 F9 LDR X0, [SP,#0x110+var_108]
.text:00000000000011D8 03 00 00 90+ ADRL X3, aSSSS ; "%s%s%s%s"
.text:00000000000011D8 63 EC 0A 91
.text:00000000000011E0 E4 83 01 91 ADD X4, SP, #0x110+var_B0
.text:00000000000011E4 E5 43 01 91 ADD X5, SP, #0x110+var_C0
.text:00000000000011E8 E6 03 01 91 ADD X6, SP, #0x110+var_D0
.text:00000000000011EC E7 C3 00 91 ADD X7, SP, #0x110+var_E0
.text:00000000000011F0 01 00 80 92 MOV X1, #0xFFFFFFFFFFFFFFFF
.text:00000000000011F4 02 08 80 52 MOV W2, #0x40 ; '@'
這個 0x11D8 很像是格式化字符串。
我們在Unidbg里面給 0x11D8 下個斷點
Debugger debugger = emulator.attach();
debugger.addBreakPoint(module.base + 0x11D8);
運行一下,順利的斷下來了
debugger break at: 0x400011d8 @ Function64 address=0x40001234, arguments=[unidbg@0xfffe1640[libandroid.so]0x640, 1853170425, 2008362258]
>>> x0=0xbffff690(-1073744240) x1=0x0 x2=0x4 x3=0xbfffed20 x4=0x40230200 x5=0x402302c0 x6=0x1 x7=0xbffff708 x8=0x0 x9=0x0 x10=0x1 x11=0x0 x12=0x8 x13=0x8 x14=0x8
>>> x15=0x8 x16=0x40228d70 x17=0x40177ddc x18=0x8 x19=0x4cf3a208 x20=0x400012b8 x21=0x0 x22=0x68ca89dd x23=0x3d74755a x24=0x72e737bb x25=0xddf5ac1 x26=0xd0d5adc6 x27=0x8b81914d x28=0xf7100797 fp=0xbffff680
LR=RX@0x400011d4[libnative-lib.so]0x11d4
SP=0xbffff570
PC=RX@0x400011d8[libnative-lib.so]0x11d8
nzcv: N=0, Z=1, C=1, V=0, EL0, use SP_EL0
start + 0xae8
=> *[libnative-lib.so*0x011d8]*[03000090]*0x400011d8:*"adrp x3, #0x40001000"
[libnative-lib.so 0x011dc] [63ec0a91] 0x400011dc: "add x3, x3, #0x2bb"
[libnative-lib.so 0x011e0] [e4830191] 0x400011e0: "add x4, sp, #0x60"
[libnative-lib.so 0x011e4] [e5430191] 0x400011e4: "add x5, sp, #0x50"
[libnative-lib.so 0x011e8] [e6030191] 0x400011e8: "add x6, sp, #0x40"
[libnative-lib.so 0x011ec] [e7c30091] 0x400011ec: "add x7, sp, #0x30"
[libnative-lib.so 0x011f0] [01008092] 0x400011f0: "mov x1, #-1"
[libnative-lib.so 0x011f4] [02088052] 0x400011f4: "mov w2, #0x40"
[libnative-lib.so 0x011f8] [5bfdff97] 0x400011f8: "bl #0x40000764"
在Arm匯編里面,調用一個函數之前,會把入參存入到 x0,x1,x2 ……
從這段代碼可以看出 地址 0x400011f8 會調用 0x40000764 函數,并且傳入了 7個參數, 從x0,一直賦值到x7。
Unidbg的調試雖然有些簡陋,但是已經夠用了,有如此神器在手,你還要啥自行車?
調試命令先掌握以下幾個:
s 單步步入,就是遇到函數調用會進入。
n 單步步過,遇到函數調用不會進入函數。
c 繼續執行
b 下斷點
r 取消當前斷點
m 查看內存
我們先 s s s 幾下,單步執行到 0x400011f8
debugger break at: 0x400011f8 @ Function64 address=0x40001234, arguments=[unidbg@0xfffe1640[libandroid.so]0x640, 1853170425, 2008362258]
>>> x0=0xbffff690(-1073744240) x1=0xffffffffffffffff x2=0x40 x3=0x400012bb x4=0xbffff5d0 x5=0xbffff5c0 x6=0xbffff5b0 x7=0xbffff5a0 x8=0x0 x9=0x0 x10=0x1 x11=0x0 x12=0x8 x13=0x8 x14=0x8
LR=RX@0x400011d4[libnative-lib.so]0x11d4
SP=0xbffff570
PC=RX@0x400011f8[libnative-lib.so]0x11f8
nzcv: N=0, Z=1, C=1, V=0, EL0, use SP_EL0
start + 0xb08
=> *[libnative-lib.so*0x011f8]*[5bfdff97]*0x400011f8:*"bl #0x40000764"
這個時間點,入參都已經準備好了,我們來一個一個看看這些入參。
mx7
>-----------------------------------------------------------------------------<
[10:40:26 646]x7=unidbg@0xbffff5a0, md5=d6c164ca9ef531557fc14e1bf7173663,
size: 112
0000: 35 41 37 35 37 34 33 44 00 B3 22 40 00 00 00 00 5A75743D.."@....
0010: 39 37 30 37 31 30 46 37 00 8D 09 40 00 00 00 00 970710F7...@....
0020: 34 44 39 31 38 31 38 42 00 77 12 40 02 00 00 00 4D91818B.w.@....
0030: 44 44 38 39 43 41 36 38 00 1B 17 40 02 00 00 00 DD89CA68...@....
0040: 31 36 37 37 30 33 38 30 36 36 35 35 33 80 00 00 1677038066553...
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
可以看到這次調用 函數 0x40000764, 基本就是在組裝最后的結果了。
我們要做的就是找到這些結果生成的位置,來分析最終結果是如何計算出來的,也就是 Y → Z 的過程。
4、Trace內存讀寫
現在我們已經知道了結果Z的位置,下一步就是需要知道誰計算出了Z。
這就需要用到Unidbg的一個強大功能:內存讀寫監控
這一次我們先把調試斷點下早一點,在 sub_A3C 函數開頭就斷下來。
debugger break at: 0x40000a3c @ Function64 address=0x40001234, arguments=[unidbg@0xfffe1640[libandroid.so]0x640, 1853170425, 2008362258]
>>> x0=0x40004000 x1=0xbffff690 x2=0x0 x3=0x1 x4=0x0 x5=0x1 x6=0x0 x7=0x0 x8=0xfffe0a70 x9=0x3002 x10=0x0 x11=0x1 x12=0x3 x13=0x40003018 x14=0x40003028
>>> x15=0x1 x16=0x40228910 x17=0x0 x18=0x17 x19=0xfffe1640 x20=0xbffff708 x21=0x0 x22=0x0 x23=0x0 x24=0x0 x25=0x0 x26=0x0 x27=0x0 x28=0x0 fp=0xbffff6f0
LR=RX@0x40001280[libnative-lib.so]0x1280
SP=0xbffff690
PC=RX@0x40000a3c[libnative-lib.so]0xa3c
nzcv: N=0, Z=0, C=1, V=0, EL0, use SP_EL0
start + 0x34c
=> *[libnative-lib.so*0x00a3c]*[ff8304d1]*0x40000a3c:*"sub sp, sp, #0x120"
traceWrite 0xbffff5d0 0xbffff5d8
Set trace 0xbffff5d0->0xbffff5d8 memory write success.
c
[11:41:41 656] Memory WRITE at 0xbffff5d8, data size = 1, data value = 0x0, PC=RX@0x40001168[libnative-lib.so]0x1168, LR=null
[11:41:41 657] Memory WRITE at 0xbffff5d0, data size = 8, data value = 0x0, PC=RX@0x4000116c[libnative-lib.so]0x116c, LR=null
[11:41:41 661] Memory WRITE at 0xbffff5d8, data size = 1, data value = 0x0, PC=RX@0x401b48cc[libc.so]0x648cc, LR=RX@0x401b48c8[libc.so]0x648c8
traceWrite 就是監控寫內存命令。
看上去0xbffff5d0這段內存,寫入 DD89CA68 數據的位置是: 0x116c
text:000000000000114C 14 00 00 90+ ADRL X20, unk_12B8
.text:000000000000114C 94 E2 0A 91
.text:0000000000001154 C4 0A C0 5A REV W4, W22
.text:0000000000001158 E0 83 01 91 ADD X0, SP, #0x110+var_B0
.text:000000000000115C 21 01 80 52 MOV W1, #9
.text:0000000000001160 22 01 80 52 MOV W2, #9
.text:0000000000001164 E3 03 14 AA MOV X3, X20
.text:0000000000001168 FF A3 01 39 STRB WZR, [SP,#0x110+var_A8]
.text:000000000000116C FF 33 00 F9 STR XZR, [SP,#0x110+var_B0]
.text:0000000000001170 7D FD FF 97 BL sub_764
0x116c 的指令 STR XZR 是寫入 沒錯,但是看上去不像是寫入數據,而是把 SP,#0x110+var_B0 這個地址的數據清零。
那我們重來一次,(Unidbg的優點就是可以無限重放,比真機調試App方便了不知道多少倍。)
這次往前一點點,在 0x114C 下斷點。
斷下來之后,每s單步一次之后,就去查看 m0xbffff5d0。
最后發現跑完 0x1170 , 0xbffff5d0內存的值就改變成了, DD89CA68 。 這說明 0xbffff5d0 是 sub_764 函數去寫的。
debugger break at: 0x40001170 @ Function64 address=0x40001234, arguments=[unidbg@0xfffe1640[libandroid.so]0x640, 1853170425, 2008362258]
>>> x0=0xbffff5d0(-1073744432) x1=0x9 x2=0x9 x3=0x400012b8 x4=0xdd89ca68 x5=0xe6cd8e62 x6=0x24523012 x7=0x29b9c389 x8=0x40 x9=0x40318041 x10=0xbffff5e0 x11=0x40 x12=0x3d5ebb2b x13=0x6450c165 x14=0xfc63b7e7
>>> x15=0x49ac16b x16=0xac6af723 x17=0xf3d1564b x18=0x18 x19=0x4cf3a208 x20=0x400012b8 x21=0x0 x22=0x68ca89dd x23=0x3d74755a x24=0x72e737bb x25=0xddf5ac1 x26=0xd0d5adc6 x27=0x8b81914d x28=0xf7100797 fp=0xbffff680
LR=null
SP=0xbffff570
PC=RX@0x40001170[libnative-lib.so]0x1170
nzcv: N=0, Z=1, C=1, V=0, EL0, use SP_EL0
start + 0xa80
=> *[libnative-lib.so*0x01170]*[7dfdff97]*0x40001170:*"bl #0x40000764"
不過回到 0x1170,我們發現了一串熟悉的數字 x4=0xdd89ca68 , 好吧,我們的問題又變成了 x4的值是怎么算出來的?
三、總結
首先要習慣看Arm匯編,一步一步單步調試,然后熟悉寄存器的變化。特別對一些關鍵數字要敏感。
要掌握Unidbg的基礎調試命令。
常見的加密算法要熟悉一下,在開發環境里多調試幾遍,熟悉它的算法流程。
1:ffshow
多方分別,是非之竇易開;一味圓融,人我之見不立。