这次的题目还是用的Q3第十题的壳
使用x32dbg
调试,依然看到熟悉的操作
在保存完输入的用户名和序列号后便开始解密代码,在解密代码之后依然通过tib
重新设置了程序的栈地址(mov dword ptr fs:[4], eax
和mov dword ptr fs:[8], ebx
),最后通过call eax
跳转到解密后的代码
值得注意的是这一次壳自解密的过程中执行的代码都一样,因此怀疑验证算法是在壳多次自解密完成后才执行的
另外一个和上次题目不同的地方在每次跳转到解密后的代码那里,即call eax
这个指令在后面有变化,因此在写脚本定位特征时需要多判断一个特征:E801000000
,具体见下图:
参考了上次比赛@注册LookLook的脚本,因为我上次写的脚本太冗长了
附件:x32dbg.txt
通过尝试发现壳一共自解密了0x578
次,即1400
次(比上次少了很多)。于是断到最后一次,再进行操作。因为最后一次修改fs:[8]
后还有一次自解密,所以还需要gocode:
这个分支中的操作才能到真正的验证算法
// 程序加载后直接先按空格运行脚本,输入用户名序列号,会自动跑到解密后的代码,并dump dbh bph 004014E0 erun bphc 004014E0 bph fs:[18]+8,w,4 mov $last, 0 mov $cnt, 0 mov $stop_num, 0x577 looop: erun inc $cnt log "Hit {$cnt}" find cip, FFD0, 0x100 mov $next, $result cmp $next, 0 jnz found find cip, E801000000, 0x100 mov $next, $result cmp $next, 0 jz finish found: erun $next cmp $cnt, $stop_num jz gocode jmp looop gocode: sti rtr sto find cip,0F85????????,0x200 mov $next, $result cmp $next,0 jz finish erun $next+6 find cip, FFD0, 0x100 mov $next, $result cmp $next, 0 jz finish erun $next sti sto erun cip+26 savedata "D:\dump01.bin",cip,3454 finish: bphc fs:[18]+8 msg "Done"
脚本执行完后定位到的函数头尾:
然后手动将dump的bin文件末尾一个字节改成c3
,即ret
,就可以用ida创建函数F5反编译了。附件:dump01.bin dump01.idb
抠出来算法,还需要知道这段算法执行的前后干了什么,调试分析一下
在进入算法前:
esi
指向的数据:
esi
指向的数据和上一次的几乎一样,依然是在esi+0x1B0
的位置保存序列号的16进制数据。但是后续对算法的分析发现,算法只用到了序列号的16进制数据,其他数据都没用到,又是障眼法。算法完成后:
算法完成后esi+0x1B0
的位置保存了解密后的数据,然后将解密后的数据同用户名比较,若相同则打印成功
需要注意这里比较用的是repe cmpsb
,判断长度为ecx = 0x11
,即判断17
字节的数据。然后可以发现若用户名不足16
位,则默认有填充数据,也就是用户名KCTF
最后验证时验证的用户名数据是:4B 43 54 46 00 1A 19 18 17 16 15 14 13 12 11 10
。具体如下所示:
附件:FixCrackMe.cpp,FixCrackMe.exe
根据以上逻辑,再将ida反编译的代码优化优化,可以做出一份可独立运行的cpp
代码:
#include <stdio.h> #include <windows.h> #include <stdlib.h> #include <string.h> unsigned char esiData[17] = {0}; #define __ROL2__(x, n) (((x) << (n)) | ((x) >> (16-(n)))) int check(unsigned char* esiData,WORD *calc_r) { WORD k_E_D; // ST5C_2 WORD k_A_9; // ST58_2 WORD k_C_B; // di WORD k_0_F; // ST54_2 WORD k_6_5; // ST60_2 WORD k_2_1; // edx WORD k_8_7; // ebx WORD k_4_3; // ST48_4 WORD v14; // eax WORD v17; // edx WORD v18; // eax WORD v19; // eax WORD v20; // ST00_2 WORD v22; // ecx WORD result; // eax k_2_1 = esiData[0x1] + (esiData[0x2] << 8); k_4_3 = esiData[0x3] + (esiData[0x4] << 8); k_6_5 = esiData[0x5] + (esiData[0x6] << 8); k_8_7 = esiData[0x7] + (esiData[0x8] << 8); k_A_9 = esiData[0x9] + (esiData[0xA] << 8); k_C_B = esiData[0xB] + (esiData[0xC] << 8); k_E_D = esiData[0xD] + (esiData[0xE] << 8); k_0_F = esiData[0xF] + (esiData[0x0] << 8); WORD t0 = k_2_1 ^ k_4_3; WORD t1 = k_6_5 ^ k_8_7; WORD t2 = k_C_B - k_A_9; WORD t3 = k_0_F + k_E_D; WORD x1 = ((((t1 & 0x5555) + ((t1 >> 1) & 0x5555)) & 0x3333) + ((((t1 & 0x5555) + ((t1 >> 1) & 0x5555)) >> 2) & 0x3333)); x1 = ((((x1 & 0xF0F) + ((x1 >> 4) & 0xF0F)) >> 8) + ((x1 & 0xF) + ((x1 >> 4) & 0xF))); v14 = (t2 & ~t0 | t3 & t0); v17 = (t0 * v14 >> x1) + 24; v19 = t2 ^ v17; v20 = v14 | v17; v22 = v14 & v17 | (v19 & v20); calc_r[0] = k_4_3 ^ v22; // 0 4B 43 434B calc_r[1] = k_2_1 ^ v22; // 2 54 64 6454 calc_r[2] = k_0_F + v19; // 4 1A 00 001A calc_r[3] = k_E_D - v19; // 6 19 18 1819 calc_r[4] = k_C_B + v17; // 8 17 16 1617 calc_r[5] = k_A_9 + v17; // A 15 14 1415 calc_r[6] = __ROL2__(v14 ^ k_8_7, x1 & 0xFF); // C 13 12 1213 calc_r[7] = __ROL2__(v14 ^ k_6_5, x1 & 0xFF); // E 11 10 1011 return 1; // K C T F // 4B 43 54 46 00 1A 19 18 17 16 15 14 13 12 11 10 } //十六进制字符串转换为字节流 void HexStrToByte(const char* source, unsigned char* dest, int sourceLen) { short i; unsigned char highByte, lowByte; for (i = 0; i < sourceLen; i += 2) { highByte = toupper(source[i]); lowByte = toupper(source[i + 1]); if (highByte > 0x39) highByte -= 0x37; else highByte -= 0x30; if (lowByte > 0x39) lowByte -= 0x37; else lowByte -= 0x30; dest[i / 2] = (highByte << 4) | lowByte; } return; } // This is an example of an exported function. int main(void) { /* UserName: BE1C6CB1F1616083 Key: 5E2BA658A0E9C5F1B52C4C3C4C5C161C */ char KCTF[32] = { 0x4B, 0x43, 0x54, 0x46, 0x00, 0x1A, 0x19, 0x18, 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10 }; char username[32] = { 0x1F, 0x1E, 0x1D, 0x1C, 0x1B, 0x1A, 0x19, 0x18, 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10 }; printf("Input user name: "); scanf("%s", username); char passwd[64] = { 0 }; WORD calc_result[9] = { 0 }; printf("Input password: "); scanf("%s", passwd); //memcpy(username,"BE1C6CB1F1616083",16); //memcpy(passwd, "5E2BA658A0E9C5F1B52C4C3C4C5C161C", 32); //memcpy(username, KCTF,16); //memcpy(passwd, "6CCDE9D2EC1D469DC67C647E66B4C565", 32); int len = strlen(passwd); HexStrToByte(passwd, esiData , len); int result1 = check(esiData, calc_result); int result = memcmp(username, (char*)calc_result, 0x10); if (!result) printf("Success!\r\n"); else printf("Error!\r\n"); system("pause"); return 0; }
优化后的算法看起来就很清晰了,分别将序列号相邻的两个字节拼在一起,然后进行一波看不懂的运算。知道应该输出什么结果后,就可以逆推算法了。
可以发现:
calc_r[0]^calc_r[1]
抵消v22
calc_r[2]+calc_r[3]
抵消v19
calc_r[4]-calc_r[5]
抵消v17
所以
WORD t0 = k_2_1 ^ k_4_3; WORD t1 = k_6_5 ^ k_8_7; WORD t2 = k_C_B - k_A_9; WORD t3 = k_0_F + k_E_D; // 只有 t1 是变量,其他都常数
只需要穷举 k_6_5 与 k_8_7,速度就快很多
附件:check_cr.cpp
#include <stdio.h> #include <windows.h> #include <stdlib.h> #include <string.h> unsigned char esiData[17] = {0}; #define __ROL2__(x, n) (((x) << (n)) | ((x) >> (16-(n)))) int check_cr(unsigned char* esiData,WORD *calc_r) { WORD k_E_D; // ST5C_2 WORD k_A_9; // ST58_2 WORD k_C_B; // di WORD k_0_F; // ST54_2 WORD k_6_5; // ST60_2 WORD k_2_1; // edx WORD k_8_7; // ebx WORD k_4_3; // ST48_4 WORD v14; // eax WORD v17; // edx WORD v18; // eax WORD v19; // eax WORD v20; // ST00_2 WORD v22; // ecx WORD result; // eax for (k_6_5=0;k_6_5<0xFFFF;k_6_5++) { for (k_8_7=0;k_8_7<0xFFFF;k_8_7++) { WORD t0 = 0x4654 ^ 0x434B; WORD t1 = k_6_5 ^ k_8_7; WORD t2 = 0x1617 - 0x1415; WORD t3 = 0x1A00 + 0x1819; WORD x1 = ((((t1 & 0x5555) + ((t1 >> 1) & 0x5555)) & 0x3333) + ((((t1 & 0x5555) + ((t1 >> 1) & 0x5555)) >> 2) & 0x3333)); x1 = ((((x1 & 0xF0F) + ((x1 >> 4) & 0xF0F)) >> 8) + ((x1 & 0xF) + ((x1 >> 4) & 0xF))); v14 = (t2 & ~t0 | t3 & t0); v17 = (t0 * v14 >> x1) + 24; v19 = t2 ^ v17; v20 = v14 | v17; v22 = v14 & v17 | (v19 & v20); WORD r6 = __ROL2__(v14 ^ k_8_7, x1 & 0xFF); WORD r7 = __ROL2__(v14 ^ k_6_5, x1 & 0xFF); if ((r6 == 0x1213) && (r7 == 0x1011)) { printf("t0=%04X\n",t0); printf("t1=%04X\n",t1); printf("t2=%04X\n",t2); printf("t3=%04X\n",t3); printf("x1=%04X\n",x1); printf("v14=%04X\n",v14); printf("v17=%04X\n",v17); printf("v19=%04X\n",v19); printf("v20=%04X\n",v20); printf("v22=%04X\n",v22); WORD k_2_1 = v22 ^ 0x4654; WORD k_4_3 = v22 ^ 0x434B; WORD k_0_F = 0x1A00 - v19; WORD k_E_D = 0x1819 + v19; WORD k_C_B = 0x1617 - v17; WORD k_A_9 = 0x1415 - v17; printf("k_2_1=%04X\n",k_2_1); printf("k_4_3=%04X\n",k_4_3); printf("k_6_5=%04X\n",k_6_5); printf("k_8_7=%04X\n",k_8_7); printf("k_A_9=%04X\n",k_A_9); printf("k_C_B=%04X\n",k_C_B); printf("k_E_D=%04X\n",k_E_D); printf("k_0_F=%04X\n",k_0_F); } } } return 1; } // This is an example of an exported function. int main(void) { WORD calc_result[9] = { 0 }; check_cr(esiData, calc_result); system("pause"); return 0; }
用z3的道路比较曲折,在后面需要注意的地方有说明
附件:key.py
from z3 import * def __ROL2__(x, n): return (((x) << (n&0xF)) | (LShR(x,(16-(n&0xF))))) k_2_1 = BitVec('k_2_1',32) k_4_3 = BitVec('k_4_3',32) k_6_5 = BitVec('k_6_5',32) k_8_7 = BitVec('k_8_7',32) k_A_9 = BitVec('k_A_9',32) k_C_B = BitVec('k_C_B',32) k_E_D = BitVec('k_E_D',32) k_0_F = BitVec('k_0_F',32) s = Solver() t0 = k_2_1 ^ k_4_3 t1 = k_6_5 ^ k_8_7 t2 = k_C_B - k_A_9 t3 = k_0_F + k_E_D x0 = (((t1 & 0x5555) + (LShR(t1,1) & 0x5555)) & 0x3333) + (LShR(((t1 & 0x5555) + (LShR(t1,1) & 0x5555)),2) & 0x3333) x1 = LShR(((x0 & 0xF0F) + (LShR(x0,4) & 0xF0F)), 8) + ((x0 & 0xF) + (LShR(x0,4) & 0xF)) v14 = t2 & ~t0 | t3 & t0 v17 = LShR(t0 * v14, x1) + 24 v19 = t2 ^ v17 v20 = v14 | v17 v22 = (v14 & v17) | (v19 & v20) s.add(ULT(k_2_1,0x10000)) s.add(ULT(k_4_3,0x10000)) s.add(ULT(k_6_5,0x10000)) s.add(ULT(k_8_7,0x10000)) s.add(ULT(k_A_9,0x10000)) s.add(ULT(k_C_B,0x10000)) s.add(ULT(k_E_D,0x10000)) s.add(ULT(k_0_F,0x10000)) ''' #BE1C6CB1F1616083 s.add(0x4542 == (k_4_3 ^ v22) % 0x10000) s.add(0x4331 == (k_2_1 ^ v22) % 0x10000) s.add(0x4336 == (k_0_F + v19) % 0x10000) s.add(0x3142 == (k_E_D - v19) % 0x10000) s.add(0x3146 == (k_C_B + v17) % 0x10000) s.add(0x3136 == (k_A_9 + v17) % 0x10000) s.add(0x3036 == __ROL2__(v14 ^ k_8_7, x1 & 0xF) % 0x10000) s.add(0x3338 == __ROL2__(v14 ^ k_6_5, x1 & 0xF) % 0x10000) ''' #KCTF s.add(0x434B == (k_4_3 ^ v22) % 0x10000) s.add(0x4654 == (k_2_1 ^ v22) % 0x10000) s.add(0x1A00 == (k_0_F + v19) % 0x10000) s.add(0x1819 == (k_E_D - v19) % 0x10000) s.add(0x1617 == (k_C_B + v17) % 0x10000) s.add(0x1415 == (k_A_9 + v17) % 0x10000) s.add(0x1213 == __ROL2__(v14 ^ k_8_7, x1 & 0xF) % 0x10000) s.add(0x1011 == __ROL2__(v14 ^ k_6_5, x1 & 0xF) % 0x10000) if s.check() == sat: result = s.model() print('%04X' % int(str(result[k_2_1]))) print('%04X' % int(str(result[k_4_3]))) print('%04X' % int(str(result[k_6_5]))) print('%04X' % int(str(result[k_8_7]))) print('%04X' % int(str(result[k_A_9]))) print('%04X' % int(str(result[k_C_B]))) print('%04X' % int(str(result[k_E_D]))) print('%04X' % int(str(result[k_0_F]))) else: print('No')
用z3需要注意的地方:
__ROL2__
函数,虽然python的>>
是无符号的,但是z3是有符号的,所以需要换成LShR
16
位的,但是实测只能用32
位的向量,然后对向量加约束,控制在16位(使用ULT
指令约束)32
位向量是因为:算法中的语句v17 = (t0 * v14 >> x1) + 24;
中乘法得到16位的值去移位,损失信息了首先是脱壳,参考上次的,没有什么特别的,改改脚本就完了
脱壳完成后我们先优化代码,然后尝试用z3求解,但是由于对z3的使用不够熟练总是无解
一开始也没注意到用户名KCTF\0
后面有默认填充的数据,认为全是0
,@ccfer就手动逆推算出来了一个序列号,结果不对,一看才知道有填充数据。因为有了填充数据,没办法手动逆推了
然后@大帅锅和@ccfer优化代码,最终发现只有 k_6_5 与 k_8_7 是变量,@ccfer就写出了爆破代码
到了第二天我们开始研究用z3求解,最终@ccfer找到了错误的原因,完成了用z3解这道题
是的,我的队友们就是这么强大!这就是传说中的带躺吧
最后于 5小时前 被KevinsBobo编辑 ,原因: