适合新手,大大们飘过:D
crackme链接:http://crackmes.de/users/svan70/bb_crackme1/
通过查看可知该crackme为vc6.0所写,无壳。界面为一个序列号输入框和三个按钮,随意输入序列号后单击Check Code无反应,可以猜测只有输入正确时才有提示框,crackme界面如下:
分析的突破点将采用字符串查找,用IDA载入程序,接着转到字符窗口,很容易就找到了Congratulations字符,双击字符串将会来到该变量定义的地方:aCongratulation db 'Congratulations!',0,在变量名aCongratulation上按下‘x’,将显示引用该变量的所有位置,只有一条引用,双击这条引用。这样就来到了.text:00401315处,下面将以这个地方作为突破口开始分析。
首先,来到这个子函数的开头,可以看到程序分别根据arg_4参数和arg_8参数进行相应的跳转,连着这么多跳转很自然会让人想到switch case结构。顺便提一下,因为该子函数对于arg和loc变量的索引不是根据ebp来的,而是直接通过esp的偏移算出来的,所以通过IDA查看各个变量会更舒服些。通过分析可知00401044、00401047、0040104E这三处跳转分别对应Check Code、Exit和About这三个按钮的操作,这其中要关心的当然是Check Code处的跳转。
.text:00401020 8B 44 24 08 mov eax, [esp+arg_4]
.text:00401024 81 EC 1C 01 00 00 sub esp, 11Ch
.text:0040102A 2D 10 01 00 00 sub eax, 110h
.text:0040102F 55 push ebp
.text:00401030 56 push esi
.text:00401031 57 push edi
.text:00401032 74 44 jz short loc_401078
.text:00401034 48 dec eax
.text:00401035 75 19 jnz short loc_401050
.text:00401037 8B 84 24 34 01 00 00 mov eax, [esp+128h+arg_8]
.text:0040103E 25 FF FF 00 00 and eax, 0FFFFh
.text:00401043 48 dec eax
.text:00401044 74 64 jz short loc_4010AA
.text:00401046 48 dec eax
.text:00401047 74 40 jz short loc_401089
.text:00401049 2D E7 03 00 00 sub eax, 3E7h
.text:0040104E 74 0E jz short loc_40105E
双击loc_4010AA后,便来到Check Code按钮所处理的代码处。lea指令是取局部变量地址用的,offset也是取址用的,不过它取的是全局变量的地址。[esp+128h+buffer]表示一个局部变量,esp+128h+buffer表示这个局部变量的地址,因此edx=esp+128h+buffer,这里的buffer是自定义的名称,可以在想更改的名称上按‘n’进行重命名。程序接着调用SendDlgItemMessageA函数,局部变量buffer处将保存输入框中的序列号,注意一下局部变量var_10C、var_108、var_104处写入的三个值,后面会用到。
.text:004010AA 8B BC 24 2C 01 00 00 mov edi, [esp+128h+hWnd]
.text:004010B1 8D 54 24 28 lea edx, [esp+128h+buffer]
.text:004010B5 52 push edx ; lParam
.text:004010B6 68 00 01 00 00 push 100h ; wParam
.text:004010BB 6A 0D push 0Dh ; Msg
.text:004010BD 68 E8 03 00 00 push 3E8h ; nIDDlgItem
.text:004010C2 57 push edi ; hDlg
.text:004010C3 C7 44 24 30 E4 38 28 07 mov [esp+13Ch+var_10C], 72838E4h
.text:004010CB C7 44 24 34 D4 B9 C2 6D mov [esp+13Ch+var_108], 6DC2B9D4h
.text:004010D3 C7 44 24 38 1E ED E1 3A mov [esp+13Ch+var_104], 3AE1ED1Eh
.text:004010DB FF 15 A4 50 40 00 call ds:SendDlgItemMessageA
.text:004010E1 85 C0 test eax, eax
.text:004010E3 74 93 jz short loc_401078
再往后看,发现程序接下来的代码量还蛮多的,至少看起来有很多行,一个办法就是用OD跟一下这段代码,书读百遍其义自现,程序跟多了自然也会有感觉。在004010DB处的SendDlgItemMessageA调用下个断点,F9跑起来,随便输入一串序列号,单击Check Code程序就断下来了。因为Congratulations字符在这个子函数的末尾,也就是说接下来的代码一直要执行到末尾才有机会去运行Congratulations处的代码,这之间如果有retn返回,那么就肯定出错了。目标很明确了,F8单步往下跟,由于序列号是随意输入的,要是正确那才奇怪,所以当碰到jnz这些条件转移指令时,若发现按当前的跳转执行下去就出错返回了,那么可以直接在寄存器区域修改ZF标志位,改变程序的执行流程,当然这样修改后程序很可能会跟奔掉。多跟几遍就会发现接下来的程序可以分成三段,每一段几乎是一样的,然后就到Congratulations处的代码了。下面将分析这三段中最开始的那段,也就是004010E5到0040119A之间的代码,其它两段是相似的。
由IDA可知[0x406330]变量(IDA中为dword_406330)是恒为1的,后面将多次用到该变量,因此004010F1往下的几条指令可以不用看了,直接到00401109处。
004010E5 A1 30634000 MOV EAX,DWORD PTR DS:[0x406330]
004010EA BD 01000000 MOV EBP,0x1
004010EF 3BC5 CMP EAX,EBP
004010F1 7E 16 JLE SHORT bb_cme1.00401109
004010F3 8B4424 28 MOV EAX,DWORD PTR SS:[ESP+0x28]
004010F7 6A 04 PUSH 0x4
004010F9 25 FF000000 AND EAX,0xFF
004010FE 50 PUSH EAX
004010FF E8 9E050000 CALL bb_cme1.004016A2
00401104 83C4 08 ADD ESP,0x8
00401107 EB 16 JMP SHORT bb_cme1.0040111F
结合着IDA,ESP+0x28实际上就是输入序列号的栈起始地址,也可以从OD的堆栈中看出来,接着edx将会被赋值为0040612E,通过随后的分析可知以0040612E为起始地址的数据段可以看成一张数表,指令00401119其实就是一个查表操作,每次查表的偏移对应输入序列号中单个字符的ASCII码值乘2,因此只要关心以0040612E为起始地址的256个字节数据。
00401109 8B4C24 28 MOV ECX,DWORD PTR SS:[ESP+0x28]
0040110D 8B15 24614000 MOV EDX,DWORD PTR DS:[0x406124] ; bb_cme1.0040612E
00401113 81E1 FF000000 AND ECX,0xFF
00401119 8A044A MOV AL,BYTE PTR DS:[EDX+ECX*2]
0040111C 83E0 04 AND EAX,0x4
0040111F 85C0 TEST EAX,EAX
00401121 75 0E JNZ SHORT bb_cme1.00401131
00401123 5F POP EDI
00401124 8BC5 MOV EAX,EBP
00401126 5E POP ESI
00401127 5D POP EBP
00401128 81C4 1C010000 ADD ESP,0x11C
0040112E C2 1000 RETN 0x10
这个表后面将多次用到,通过IDA可以查看这256个字节。接着看0040111C处开始的指令,要想在jnz跳转时不走到retn,al和0x4相与的结果必须为1。结合下面的表,因此指令00401119处查表后得到的结果必须为0x84,因此查表的偏移必须为0x30到0x39。对于ASCII码值为0x30~0x39是不是有点熟悉,也就是字符1~9,所以输入的序列号必须以数字开始,接着将跳转到00401131处。
0040612E 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00
0040613E 20 00 28 00 28 00 28 00 28 00 28 00 20 00 20 00
0040614E 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00
0040615E 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00
0040616E 48 00 10 00 10 00 10 00 10 00 10 00 10 00 10 00
0040617E 10 00 10 00 10 00 10 00 10 00 10 00 10 00 10 00
0040618E 84 00 84 00 84 00 84 00 84 00 84 00 84 00 84 00
0040619E 84 00 84 00 10 00 10 00 10 00 10 00 10 00 10 00
004061AE 10 00 81 00 81 00 81 00 81 00 81 00 81 00 01 00
004061BE 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00
004061CE 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00
004061DE 01 00 01 00 01 00 10 00 10 00 10 00 10 00 10 00
004061EE 10 00 82 00 82 00 82 00 82 00 82 00 82 00 02 00
004061FE 02 00 02 00 02 00 02 00 02 00 02 00 02 00 02 00
0040620E 02 00 02 00 02 00 02 00 02 00 02 00 02 00 02 00
0040621E 02 00 02 00 02 00 10 00 10 00 10 00 10 00 20 00
继续分析,接下来的代码和刚刚分析过的还蛮像的,毫无疑问00401139处的跳转将来到0040114E处,还是和之前一样的查表操作,不过因为有了esi这个偏移,序列号中的字符将逐个往下分析,如果是数字则跳到00401133处循环执行。最终肯定是要跳出循环的,跳出循环后将来到00401167处,此时导致循环跳出的字符将和0x2D比较,如果不等就出错返回了,因此导致循环跳出的字符ASCII码值为0x2D,也就是字符‘-’,这和查表结果也是相符的。
00401131 8BF5 MOV ESI,EBP
00401133 392D 30634000 CMP DWORD PTR DS:[0x406330],EBP
00401139 7E 13 JLE SHORT bb_cme1.0040114E
0040113B 33C0 XOR EAX,EAX
0040113D 6A 04 PUSH 0x4
0040113F 8A4434 2C MOV AL,BYTE PTR SS:[ESP+ESI+0x2C]
00401143 50 PUSH EAX
00401144 E8 59050000 CALL bb_cme1.004016A2
00401149 83C4 08 ADD ESP,0x8
0040114C EB 12 JMP SHORT bb_cme1.00401160
0040114E 8B15 24614000 MOV EDX,DWORD PTR DS:[0x406124] ; bb_cme1.0040612E
00401154 33C9 XOR ECX,ECX
00401156 8A4C34 28 MOV CL,BYTE PTR SS:[ESP+ESI+0x28]
0040115A 8A044A MOV AL,BYTE PTR DS:[EDX+ECX*2]
0040115D 83E0 04 AND EAX,0x4
00401160 85C0 TEST EAX,EAX
00401162 74 03 JE SHORT bb_cme1.00401167
00401164 46 INC ESI
00401165 EB CC JMP SHORT bb_cme1.00401133
00401167 807C34 28 2D CMP BYTE PTR SS:[ESP+ESI+0x28],0x2D
0040116C 0F85 B4010000 JNZ bb_cme1.00401326
接下来将是三个call调用了,分别是CALL bb_cme1.00401606、CALL bb_cme1.004015EF和CALL bb_cme1.00401340,暂时先不去关心这三处call调用。不过注意一下0040119A处的指令,call调用后的返回参数eax将存到一个局部变量中。
00401172 8D4424 28 LEA EAX,DWORD PTR SS:[ESP+0x28]
00401176 68 88604000 PUSH bb_cme1.00406088
0040117B 50 PUSH EAX
0040117C 46 INC ESI
0040117D E8 84040000 CALL bb_cme1.00401606
00401182 8D4C24 14 LEA ECX,DWORD PTR SS:[ESP+0x14]
00401186 6A 0A PUSH 0xA
00401188 51 PUSH ECX
00401189 50 PUSH EAX
0040118A E8 60040000 CALL bb_cme1.004015EF
0040118F 68 71A3CEF2 PUSH 0xF2CEA371 ; Arg2 = F2CEA371
00401194 50 PUSH EAX ; Arg1 = 3BB2A8F6
00401195 E8 A6010000 CALL bb_cme1.00401340 ; bb_cme1.00401340
0040119A 894424 2C MOV DWORD PTR SS:[ESP+0x2C],EAX
至此,第一段004010E5到0040119A之间的代码就过一遍了,因为接下来的两段代码和第一段几乎是一样的,因此可以判断序列号是分为三段的,每一段将是一串数字字符并以‘-’字符结束。当然,对第三段代码的分析可知第三段数字字符串并不是以‘-’结束的,而直接是字符串结束标志,因此得到序列号的格式应该是形如xxxx-xxxx-xxxx的字符串,x表示数字,至于x的个数仍然是未知的。最后,终于来到了子函数末尾的Congratulations代码处。
这段代码就是依次比较以局部变量var_118和var_10C为起始的连续0Ch个字节,如果这0Ch个字节都相等,那么将出现Congratulations提示窗,否则出错返回。还记得最开始SendDlgItemMessageA处有三个局部变量的赋值操作吗?没错,就是在这里用到了,而且也应该记得处理完每一段序列号的最后都有一个局部变量赋值操作。现在应该清楚了,xxxx-xxxx-xxxx这三段序列号每一段处理完后程序的期望返回值分别对应SendDlgItemMessageA下面的72838E4h、6DC2B9D4h和3AE1ED1Eh。当然,可以简单的将0040130B处的指令用nop填充,这样随意输入形如xxxx-xxxx-xxxx的序列号都会跳出Congratulations提示,但该crackme要求不能通过打补丁的方法破解,必须找到正确的序列号。因此,要接着分析前面暂时没关心的那三处call调用。
.text:004012FF 33 C0 xor eax, eax
.text:00401301
.text:00401301 loc_401301:
.text:00401301 8A 54 04 10 mov dl, byte ptr [esp+eax+128h+var_118]
.text:00401305 8A 4C 04 1C mov cl, byte ptr [esp+eax+128h+var_10C]
.text:00401309 32 D1 xor dl, cl
.text:0040130B 75 19 jnz short loc_401326
.text:0040130D 40 inc eax
.text:0040130E 83 F8 0C cmp eax, 0Ch
.text:00401311 7C EE jl short loc_401301
.text:00401313 6A 00 push 0 ; uType
.text:00401315 68 74 60 40 00 push offset aCongratulation ; "Congratulations!"
.text:0040131A 68 30 60 40 00 push offset aYourCodeIsCorr ; "Your code is correct!\nPlease send your"...
.text:0040131F 57 push edi ; hWnd
.text:00401320 FF 15 9C 50 40 00 call ds:MessageBoxA
首先来到第一处CALL bb_cme1.00401606,F7跟进去。程序看起来有很多行,不过执行的操作却很简单。程序先对‘-’字符生成一个标记操作,当序列号xxxx-xxxx-xxxx执行同样的一套流程后,会在‘-’处跳出来,这时程序将‘-’地址处置零,这样序列号就以‘-’分割开了,于是eax返回的是字符串xxxx的首地址,而剩下的字符串xxxx-xxxx的首地址将保留在[0x4085B4]里。
这里有一个地方要说明,如下,执行完前四条指令后,eax为0、ecx为8、edi为loc8处的起始地址,当执行第五条指令后以edi为起始地址的8个4字节将全部被置零。
00401612 6A 08 PUSH 0x8
00401614 33C0 XOR EAX,EAX
00401616 59 POP ECX
00401617 8D7D E0 LEA EDI,[LOCAL.8]
0040161A F3:AB REP STOS DWORD PTR ES:[EDI]
接着再看第二处CALL bb_cme1.004015EF,F7跟进去,又是一处call调用,F7再跟进去,发现这个子函数还是蛮复杂的,老办法,用OD跟着跑几遍。可以注意到CALL bb_cme1.00401340将会用到这个子函数的返回参数eax,而eax最终又是由子函数中的loc2对其赋值,跟了几遍后逆着往前推,就注意到如下这个地方:
00401542 0FAF75 10 IMUL ESI,[ARG.3]
00401546 03F1 ADD ESI,ECX
00401548 8975 F8 MOV [LOCAL.2],ESI
0040154B 8B45 FC MOV EAX,[LOCAL.1]
0040154E FF45 FC INC [LOCAL.1]
00401551 8A18 MOV BL,BYTE PTR DS:[EAX]
00401553 E9 64FFFFFF JMP bb_cme1.004014BC
其中arg.3是定值0xa,也就是10,ecx为序列号中的字符ASCII码值减0x30后的值,粗略分析后不难发现返回值eax就是入参字符串xxxx对应的整数值。
是时候来看一下CALL bb_cme1.00401340处的内容了,这个调用是很关键的,该子函数有两个参数,其中arg.1为CALL bb_cme1.004015EF的返回参数,另一参数arg.2则为定值0xF2CEA371。由于该调用期望的正确返回结果是已知的,可判断arg.1是小于arg.2的,因此程序给eax和ebx初始化后,将重复执行mul eax、div ebx、mov eax,edx这三条指令。对于mul eax,程序将执行eax*eax,并将结果存于edx:eax中,接着div ebx,程序将执行edx:eax/ebx,并将商存于eax中,余数存于edx中,最后执行mov指令,其实,这三条指令就相当于eax=eax*eax mod ebx。
00401340 55 PUSH EBP
00401341 8BEC MOV EBP,ESP
00401343 8B45 08 MOV EAX,[ARG.1]
00401346 8B55 0C MOV EDX,[ARG.2]
00401349 3BC2 CMP EAX,EDX
0040134B 73 74 JNB SHORT bb_cme1.004013C1
0040134D 53 PUSH EBX
0040134E 8B45 08 MOV EAX,[ARG.1]
00401351 8B5D 0C MOV EBX,[ARG.2]
00401354 F7E0 MUL EAX
00401356 F7F3 DIV EBX
00401358 8BC2 MOV EAX,EDX
……
此处重复MUL EAX、DIV EBX、MOV EAX,EDX
……
004013B4 8B5D 08 MOV EBX,[ARG.1]
004013B7 F7E3 MUL EBX
004013B9 8B5D 0C MOV EBX,[ARG.2]
004013BC F7F3 DIV EBX
004013BE 8BC2 MOV EAX,EDX
004013C0 5B POP EBX
004013C1 5D POP EBP
004013C2 C3 RETN
因此,上述子函数的伪代码就等价于:
a=arg.1,b=arg.2;
c=a;
for(i=0; i<16; ++i){
c=(c*c)%b;
}
c=(c*a)%b;
其中,程序最终的期望值c是已知的,目标就是使得到的c值正确。如果真如之前对CALL bb_cme1.004015EF所粗略分析的那样,bb_cme1.004015EF调用将形如xxxx-xxxx-xxxx序列号中的每一段xxxx转成整数,即类似atoi函数,那么要是能逆推出a的话,这三段序列号自然也就得到了,将整数a转成字符串看一眼就应该出来了:D。现在将精力放在这短短的几行伪代码上,已知条件就a未知,好像用穷举法可以搞定,但不切实际。记得这个crackme有一个hint:Little crypto knowledge is advantage,也就是如果了解加解密的话,这将很有优势。常见的加密算法有DES、AES、RSA等,而这段子函数的主要操作是平方取模,很自然的会联想到RSA里的幂模运算。关于幂模运算举一个简单的例子,假设计算3^9 mod 5,可分解为如下计算:
3^1=(3^0)^2*3 mod 5=3 mod 5=3
3^2=((3^0)^2*3))^2 mod 5=3^2 mod 5=4
3^4=(((3^0)^2*3))^2)^2 mod 5=4^2 mod 5=1
3^9=((((3^0)^2*3))^2)^2)^2*3 mod 5=1^2*3 mod 5=3
这样分解后每一步的运算就相对简单了,不会遇到特别大的数值,原理也还是较好理解的。为了后面的分析,还是有必要简单提一下RSA。RSA算法涉及三个参数,n、e1、e2。其中,n是两个大质数p、q的积,e1和e2是一对相关的值,e1可以任意取,但要求e1与(p-1)*(q-1)互质,再选择e2,要求(e2*e1)mod((p-1)*(q-1))=1。(n,e1),(n,e2)就是密钥对。设A为明文,B为密文,则:A=B^e2 mod n;B=A^e1 mod n,e1和e2可以互换使用。因为e1、e2可能会比较大,直接进行幂模运算会遇到特别大的值,因此会用到上面提到的分解方法。
再来看该子函数,由前面的分析可知形如xxxx-xxxx-xxxx的序列号将以‘-’分为三段,每一段字符串转化为整数,成为该子函数的arg.1参数,经过加密后(现在可以这么猜测了)这三段的密文分别为0x72838E4, 0x6DC2B9D4, 0x3AE1ED1E。对应到公式B=A^e1 mod n,B、e1和n都已知了,比如序列号的第一段,密文B=0x72838E4,n毫无疑问就是arg.2了,也就是n=0xF2CEA371=4073628529,而该子函数中用了16次平方取模后又乘了一次明文,因此e1=2^16+1=65537。如果之前的猜测都正确的话,那么n=4073628529应该为两个质数的乘积,通过简单的求约数方法,n对2到sqrt(n)之间的数取模得到约数47051,而4073628529=47051*86579,经过验证47051和86579确实都是质数,看来很有希望了。这样p=47051,q=86579,通过辗转相除法,可以验证e1确实与(p-1)*(q-1)互质,再通过公式(e2*e1)mod((p-1)*(q-1))=1算出e2=3057436473,这里一定要写个小程序去算,看是看不出来的,记得数据类型要为unsigned long long,如果是unsigned int数值会溢出的。最后,通过公式A=B^e2 mod n就可以算出A了,也就是arg.1的值,而通过arg.1的值显然就可以得到猜测的序列号了,代码如下:
#include <stdio.h>
#include <stdlib.h>
int main()
{
unsigned int encrypt[]={0x72838E4, 0x6DC2B9D4, 0x3AE1ED1E}, decrypt[3];
unsigned int n=4073628529, m=3057436473, flag;
unsigned long long x;
int i, j;
for(i=0; i<3; ++i)
{
x=1;
flag=0x80000000;
for(j=0; j<32; ++j)
{
x=(x*x)%n;
if((m&flag)>>(31-j) == 1){
x=(x*encrypt[i])%n;
}
flag=flag>>1;
}
decrypt[i]=x;
}
printf("key: %u-%u-%u\n", decrypt[0], decrypt[1], decrypt[2]);
system("pause");
return 0;
}
程序的输出结果为key: 580276954-895936478-64598366
如果分析都正确的话,那么输入这个key就会有Congratulations了:D bb_cme1.zip
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
上传的附件: