学习环境:Windows 10 20H2 + Visual Studio 2019 + IDA
参考书籍:《C++反汇编与逆向分析技术揭秘》Chapter 4.5
程序介绍
控制台程序,通过参数输入密码,会打印验证结果
为了减少分析工作量,程序中没有任何错误检查,只有加密与密码检查
目标是还原加密算法
逆向分析
跟着书上的讲解进行分析,书上分为了4个部分,这里也分为4个部分来记录
使用IDA打开,直接就跳转到了main函数:
.text:00401000 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 _main proc near ; CODE XREF: start+AF↓p
.text:00401000
.text:00401000 var_10 = byte ptr -10h
.text:00401000 var_F = byte ptr -0Fh
.text:00401000 var_E = byte ptr -0Eh
.text:00401000 var_D = byte ptr -0Dh
.text:00401000 var_C = byte ptr -0Ch
.text:00401000 var_B = byte ptr -0Bh
.text:00401000 var_A = byte ptr -0Ah
.text:00401000 var_9 = byte ptr -9
.text:00401000 var_8 = byte ptr -8
.text:00401000 var_7 = byte ptr -7
.text:00401000 var_6 = byte ptr -6
.text:00401000 var_5 = byte ptr -5
.text:00401000 var_4 = byte ptr -4
.text:00401000 var_3 = byte ptr -3
.text:00401000 argc = dword ptr 4
.text:00401000 argv = dword ptr 8
.text:00401000 envp = dword ptr 0Ch
刚开始这里是定义的局部变量和参数,在IDA中,值>0是参数,值<0是局部变量
这里var_10~var_3都是1个字节大小的连续局部变量,可以将其看做数组
双击var_10,进入var_10地址里,按*
快捷键,将其转换为14字节的数组,并改名位charNumber14:
接下来开始看代码:
text:00401000 charNumber14 = byte ptr -10h
.text:00401000 argc = dword ptr 4
.text:00401000 argv = dword ptr 8
.text:00401000 envp = dword ptr 0Ch
.text:00401000
.text:00401000 sub esp, 10h ; main参数:argc-命令个数, argv-命令行信息, envp-环境变量信息
.text:00401003 mov ecx, [esp+10h+argv] ; 取出来argv数据放到ecx
.text:00401007 mov al, 1
.text:00401009 mov [esp+10h+charNumber14+6], al ; charNumber[6] = 1
.text:0040100D mov [esp+10h+charNumber14+7], al ; charNumber[7] = 1
.text:00401011 mov edx, [ecx+4] ; edx = argv[1]
.text:00401014 mov [esp+10h+charNumber14+0Ah], al ; charNumber[10] = 1
.text:00401018 mov al, byte ptr [esp+10h+argc] ; al = 参数个数
.text:0040101C push ebx ; 压入 ebx
.text:0040101D mov bl, al ; bl = al
.text:0040101F push esi ; 压入 esi
.text:00401020 dec bl ; bl = bl -1
.text:00401022 push edi ; 压入 edi
.text:00401023 or [edx], bl ; argv[1][0] |= argc - 1
.text:00401025 mov edx, [ecx+4] ; edx = argv[1]
.text:00401028 mov [esp+1Ch+charNumber14], 77h ; charNumber[0] = 0x77
.text:0040102D mov [esp+1Ch+charNumber14+1], 76h ; charNumber[1] = 0x76
.text:00401032 xor [edx+1], bl ; argv[1][1] ^= argc - 1
.text:00401035 mov dl, 6 ; dl = 6
.text:00401037 imul dl ; eax = eax*6
.text:00401039 mov esi, [ecx+4] ; ecx = argv[1]
.text:0040103C sub al, dl ; al = al - 6
.text:0040103E mov [esp+1Ch+charNumber14+2], 0CAh ; charNumber[2] = 0xCA
.text:00401043 mov [esp+1Ch+charNumber14+3], 0F3h ; charNumber[3] = 0xF3
.text:00401048 imul byte ptr [esi+2] ; al = argv[1][2] * (argc-1) * 6
.text:0040104B mov [esi+2], al ; argv[1][2] = argv[1][2] * (argc-1) * 6
.text:0040104E mov esi, [ecx+4] ; esi = argv[1]
.text:00401051 mov [esp+1Ch+charNumber14+4], 0A8h ; charNumber[4] = 0xA8
.text:00401056 mov [esp+1Ch+charNumber14+5], 0Ch ; charNumber[5] = 0xC
.text:0040105B movsx eax, byte ptr [esi+2] ; eax = argv[1][2]
.text:0040105F cdq ; 扩展edx高位
.text:00401060 and edx, 3 ; edx &= 3
.text:00401063 mov [esp+1Ch+charNumber14+8], 0FEh ; charNumber[8] = 0xFE
.text:00401068 add eax, edx ; eax = eax + edx
.text:0040106A mov [esp+1Ch+charNumber14+9], 0DBh ; charNumber[9] = 0xDB
.text:0040106F sar eax, 2 ; eax = argv[1][2] / 4
.text:00401072 mov [esi+3], al ; argv[1][3] = al
.text:00401075 mov eax, [ecx+4] ; eax = argv[1]
.text:00401078 mov [esp+1Ch+charNumber14+0Bh], 0E0h ; charNumber[B] = 0xE0
.text:0040107D mov [esp+1Ch+charNumber14+0Ch], 0FBh ; charNumber[C] = 0xFB
.text:00401082 mov dl, [eax+4] ; dl = argv[1][4]
.text:00401085 mov [esp+1Ch+charNumber14+0Dh], 0 ; charNumber[D] = 0
这里进行了一些计算操作,然后给数组进行了赋值,为啥这两件事是交叉着执行的呢?因为编译器对目标机器代码进行了流水线优化,不能连着用相同的资源
数组charNumber的数据依次为:0x77,0x76,0xCA,0xF3,0xA8,0xC,0x01,0x01,0xFE,0xDB,0x01,0xE0,0xFB,0x00
通过一些计算给argv[1]参数进行了赋值:
argv[1][0] |= argc - 1;
argv[1][1] ^= argc - 1;
argv[1][2] *= (argc-1) * 6;
argv[1][3] = argv[1][2] / 4;
接着往下看代码:
.text:0040108A shl dl, 3 ; argv[1][4] << 3
.text:0040108D mov [eax+4], dl ; argv[1][4] = argv[1][4] << 3
.text:00401090 mov eax, [ecx+4] ; eax = argv[1]
.text:00401093 mov dl, [eax+5] ; dl = argv[1][5]
.text:00401096 sar dl, 2 ; argv[1][5] >> 2
.text:00401099 mov [eax+5], dl ; argv[1][5] = argv[1][5] >> 2
.text:0040109C mov esi, [ecx+4] ; esi = argv[1]
.text:0040109F mov al, bl ; al = argc - 1
.text:004010A1 mov dl, [esi+6] ; dl = argv[1][6]
.text:004010A4 and al, 7 ; al = (argc - 1) & 7
.text:004010A6 and dl, al ; dl &= ((argc - 1)&7)
.text:004010A8 mov [esi+6], dl ; argv[1][6] &= ((argc - 1)&7)
.text:004010AB mov esi, [ecx+4] ; esi = argv[1]
.text:004010AE movsx edx, byte ptr [esi+7] ; edx = argv[1][7]
.text:004010B2 and edx, 80000001h ; edx &= 0x80000001, 保留最高位和最低位
.text:004010B8 jns short loc_4010BF ; SF=0时跳转, 不为负数时跳转, 以下是负数的处理
.text:004010BA dec edx ; dex -= 1, 结果必然是 0x80000000 或 0x7fffffff
.text:004010BB or edx, 0FFFFFFFEh ; 结果是 0xFFFFFFFE 或 0xFFFFFFFF
.text:004010BE inc edx ; 0xFFFFFFFF 或 0 这套运算等价于edx = edx%2
.text:004010BF
.text:004010BF loc_4010BF: ; CODE XREF: _main+B8↑j
.text:004010BF mov [esi+7], dl ; argv[1][7] = argv[1][7] % 2
.text:004010C2 mov eax, [ecx+4] ; eax = argv[1]
.text:004010C5 not bl ; 取反(argc - 1)
.text:004010C7 mov [eax+8], bl ; argv[1][8] = ~(argc - 1)
.text:004010CA mov eax, [ecx+4] ; eax = argv[1]
.text:004010CD mov dl, [eax] ; dl = argv[1][0]
.text:004010CF mov bl, [eax+2] ; bl = argv[1][2]
.text:004010D2 lea esi, [eax+9] ; esi = &argv[1][9] , 看成指针 char *pArgv9 = argv[1][9]
.text:004010D5 sub dl, bl ; dl = dl - bl = argv[1][0] - argv[1][2]
.text:004010D7 mov bl, [esi] ; bl = *pArgv9
.text:004010D9 add bl, dl ; bl = *pArgv9 + argv[1][0] - argv[1][2]
.text:004010DB mov [esi], bl ; *pArgv9 = *pArgv9 + argv[1][0] - argv[1][2]
.text:004010DD mov eax, [ecx+4] ; eax = argv[1]
.text:004010E0 inc esi ; esi = esi + 1 = argv[1][10]的地址
.text:004010E1 movsx edi, byte ptr [eax+7] ; edi = argv[1][7]
.text:004010E5 movsx eax, byte ptr [eax+6] ; eax = argv[1][6]
.text:004010E9 cdq ; 扩展高位
.text:004010EA idiv edi ; eax = argv[1][6] / argv[1][7]
.text:004010EC inc esi ; esi = esi + 1 = argv[1][11]的地址
.text:004010ED mov [esi-1], al ; *(pArgv9 - 1) = argv[1][10] = argv[1][6] / argv[1][7]
.text:004010F0 mov eax, [ecx+4] ; eax = argv[1]
.text:004010F3 mov dl, [eax+3] ; dl = argv[1][3]
.text:004010F6 mov bl, [eax+1] ; bl = argv[1][1]
.text:004010F9 mov al, [esi] ; al = argv[1][11]
.text:004010FB sub dl, bl ; dl = argv[1][3] - argv[1][1]
.text:004010FD add al, dl ; al = argv[1][11] + argv[1][3] - argv[1][1]
.text:004010FF mov [esi], al ; argv[1][11] = argv[1][11] + argv[1][3] - argv[1][1]
.text:00401101 mov eax, [ecx+4] ; eax = argv[1]
.text:00401104 movsx dx, byte ptr [eax+5] ; dx = argv[1][5]
.text:00401109 movsx ax, byte ptr [eax+4] ; ax = argv[1][4]
.text:0040110E imul edx, eax ; edx = argv[1][5] * argv[1][4]
.text:00401111 mov [esi], dx ; *(short)pArgv9 = argv[1][4] * argv[1][5]
.text:00401114 mov ecx, [ecx+4] ; ecx = argv[1]
通过一些计算给argv[1]参数进行了计算赋值:
argv[1][4] = argv[1][4] << 3;
argv[1][5] = argv[1][5] >> 2;
argv[1][6] = argv[1][6] & ((argc - 1) & 7);
argv[1][7] = argv[1][7] % 2;
argv[1][8] = ~(argc - 1);
char* pArgv9 = &argv[1][9];
*pArgv9 = *pArgv9 + argv[1][0] - argv[1][2];
pArgv9 += 1;
pArgv9 += 1;
*(pArgv9 - 1) = argv[1][6] / argv[1][7];
*pArgv9 += argv[1][3] - argv[1][1];
*(short*)pArgv9 = (short)argv[1][4] * (short)argv[1][5];
最后的判断代码:
.text:00401117 lea esi, [esp+1Ch+charNumber14] ; esi = charNumber14的首地址
.text:0040111B
.text:0040111B loc_40111B: ; CODE XREF: _main+13D↓j
.text:0040111B mov dl, [ecx] ; dl = argv[1][0]
.text:0040111D mov bl, [esi] ; bl = charNumber[0]
.text:0040111F mov al, dl ; al = argv[1][0]
.text:00401121 cmp dl, bl ; dl和bl相比, 相同时ZF=1
.text:00401123 jnz short loc_401143 ; ZF=0时跳转,也就是不相同时跳转
.text:00401125 test al, al ; 判断当前密码是否对比完成, 没完成就ZF=0
.text:00401127 jz short loc_40113F ; ZF = 1时跳转, 密码对比完成后跳转
.text:00401129 mov dl, [ecx+1] ; dl = argv[1][1]
.text:0040112C mov bl, [esi+1] ; bl = charNumber[1]
.text:0040112F mov al, dl ; al = argv[1][1]
.text:00401131 cmp dl, bl ; dl和bl相比, 相同时ZF=1
.text:00401133 jnz short loc_401143 ; ZF=0时跳转,也就是不相同时跳转
.text:00401135 add ecx, 2 ; 对比接下来2位
.text:00401138 add esi, 2
.text:0040113B test al, al ; 判断一下是否比对完密码
.text:0040113D jnz short loc_40111B ; 没比对完就跳转回去继续比对
.text:0040113F
.text:0040113F loc_40113F: ; CODE XREF: _main+127↑j
.text:0040113F xor eax, eax ; eax清零
.text:00401141 jmp short loc_401148
.text:00401143 ; ---------------------------------------------------------------------------
.text:00401143
.text:00401143 loc_401143: ; CODE XREF: _main+123↑j
.text:00401143 ; _main+133↑j
.text:00401143 sbb eax, eax ; eax = eax - eax - CF
.text:00401145 sbb eax, 0FFFFFFFFh ; eax = eax + 1 - CF
.text:00401148
.text:00401148 loc_401148: ; CODE XREF: _main+141↑j
.text:00401148 pop edi
.text:00401149 pop esi
.text:0040114A test eax, eax ; eax为0时, ZF=1
.text:0040114C pop ebx
.text:0040114D jnz short loc_401167 ; ZF = 0 时跳转
.text:0040114F push offset asc_406044 ; "密码正确!"
.text:00401154 push offset aS ; "%s \r\n"
.text:00401159 call _printf
.text:0040115E add esp, 8
.text:00401161 xor eax, eax
.text:00401163 add esp, 10h
.text:00401166 retn
.text:00401167 ; ---------------------------------------------------------------------------
.text:00401167
.text:00401167 loc_401167: ; CODE XREF: _main+14D↑j
.text:00401167 push offset asc_406030 ; "密码错误!"
.text:0040116C push offset aS ; "%s \r\n"
.text:00401171 call _printf
.text:00401176 add esp, 8
.text:00401179 xor eax, eax
.text:0040117B add esp, 10h
.text:0040117E retn
.text:0040117E _main endp
这里无意间分析了一波strcmp函数的内部实现....
判断代码如下
if(!strcmp(argv[1],charNumber14)){
printf("密码正确");
}else{
printf("密码错误");
}
代码还原
#include <stdio.h>
#include <string.h>
char charNumber14[14] = { 0x77,0x76,0xCA,0xF3,0xA8,0xC,0x01,0x01,0xFE,0xDB,0x01,0xE0,0xFB,0x0 };
int main(int argc, char* argv[], char* envp) {
argv[1][0] |= argc - 1;
argv[1][1] ^= argc - 1;
argv[1][2] = argv[1][2] *(argc - 1) * 6;
argv[1][3] = argv[1][2] / 4;
argv[1][4] = argv[1][4] << 3;
argv[1][5] = argv[1][5] >> 2;
argv[1][6] = argv[1][6] & ((argc - 1) & 7);
argv[1][7] = argv[1][7] % 2;
argv[1][8] = ~(argc - 1);
char* pArgv9 = &argv[1][9];
*pArgv9 = *pArgv9 + argv[1][0] - argv[1][2];
pArgv9 += 1;
pArgv9 += 1;
*(pArgv9 - 1) = argv[1][6] / argv[1][7];
*pArgv9 += argv[1][3] - argv[1][1];
*(short*)pArgv9 = (short)argv[1][4] * (short)argv[1][5];
if (!strcmp(argv[1], charNumber14)) {
printf("密码正确");
}
else {
printf("密码错误");
}
return 0;
}
测试
大量运用位运算的算法不可逆,无法回推正确密码,这里的正确密码是题中已给出的:www.51asm.com
,将我们的还原代码编译出来,输入这个正确密码来验证一下:
上面那个是我们还原出来的代码编译出来的程序,运行结果为正确
下面那个是原Crackme程序,密码也是正确
总结
做这个大概花了我一下午的时间,这是我第一次逆向程序,这个Crackme虽然很简单,但因为不熟悉吧,做的时候还是有点费劲的
这个简单的CM里,有数组和指针的使用,以及库函数的调用,数组的赋值和计算都还好,到指针这里就有点不好弄了,首先得先判断出来是指针才行,不然做到后面2字节的赋值就搞不来了
emmm
不得不说,还挺好玩,想说的就这么多,下次再整一个哈哈哈哈