第一阶段第一题分析+完整逆向代码(看雪金山2007逆向分析挑战赛)
by aker
8:31 2007-8-24
第一题已经结束了,放出分析和逆向代码先;)
我很少做crackme分析,第一次写逆向分析文章,希望没有什么错误。给了个还原出来的crackme的代码,见附件,样子,行为和原来的一模一样;)
od载入,没有什么说的,总共才1.6k的程序,载入就看到下面接受输入的代码,检查名字和序列号是否为空,空则重新接受输入
00400499 |. 8D45 EC lea eax,dword ptr ss:[ebp-14] ; name
0040049C |. 6A 10 push 10 ; /Count = 10 (16.)
0040049E |. 50 push eax ; |Buffer
0040049F |. 68 E9030000 push 3E9 ; |ControlID = 3E9 (1001.)
004004A4 |. FF75 08 push dword ptr ss:[ebp+8] ; |hWnd
004004A7 |. FFD6 call esi ; \GetDlgItemTextA
004004A9 |. 85C0 test eax,eax
004004AB |. 74 6C je short CrackMe.00400519
004004AD |. 8D85 E8EFFFFF lea eax,dword ptr ss:[ebp-1018] ; serial
004004B3 |. 68 00100000 push 1000 ; /Count = 1000 (4096.)
004004B8 |. 50 push eax ; |Buffer
004004B9 |. 68 EA030000 push 3EA ; |ControlID = 3EA (1002.)
004004BE |. FF75 08 push dword ptr ss:[ebp+8] ; |hWnd
004004C1 |. FFD6 call esi ; \GetDlgItemTextA
004004C3 |. 85C0 test eax,eax
004004C5 |. 74 52 je short CrackMe.00400519
下面代码计算用户名字符数,用的repne scas指令,ecx保存字符数,我们还原代码时要不用strlen,要不保存GetDlgItemTextA的返回值都可以达到该效果。
004004C7 |. 8D7D EC lea edi,dword ptr ss:[ebp-14] ; name
004004CA |. 83C9 FF or ecx,FFFFFFFF ; ecx = ffffffff
004004CD |. 33C0 xor eax,eax ; eax = 0
004004CF |. 33D2 xor edx,edx ; edx = 0
004004D1 |. F2:AE repne scas byte ptr es:[edi]
004004D3 |. F7D1 not ecx
004004D5 |. 49 dec ecx
004004D6 |. 85C9 test ecx,ecx ; 计数用户名字符数
004004D8 |. 7E 23 jle short CrackMe.004004FD
下面004004DA代码计算用户名特征码,这个后面有用,注意ebx在前面0040046F处已经初始化了一个常数,发现一个未知寄存器一定要往上找,看什么地方修改过。
0040046F |. BB 68245713 mov ebx,13572468 ; ebx = 13572468
004004DA |> /0FBE4415 EC /movsx eax,byte ptr ss:[ebp+edx-14] ; 对用户名每个字符操作
004004DF |. |03C3 |add eax,ebx
004004E1 |. |69C0 73127203 |imul eax,eax,3721273
004004E7 |. |05 57136824 |add eax,24681357
004004EC |. |8BF0 |mov esi,eax
004004EE |. |C1E6 19 |shl esi,19
004004F1 |. |C1F8 07 |sar eax,7
004004F4 |. |0BF0 |or esi,eax
004004F6 |. |42 |inc edx ; edx -- i
004004F7 |. |3BD1 |cmp edx,ecx
004004F9 |. |8BDE |mov ebx,esi
004004FB |.^\7C DD \jl short CrackMe.004004DA
代码还原c如下,其中name为接受输入的用户名,对等起来看的话,addr相当于开始的eax,adder2相当于esi,ebx保存namecalc,这个值以后用。
for(i=0; i<namelen; i++)
{ // 计算名字特征
adder = (name[i]+ namecalc)*0x3721273+0x24681357;
adder2 = adder<<0x19;
__asm sar adder,7
namecalc = adder2|adder;
}
好,上面计算出了输入用户名的特征码,底下就到了第一个关键函数调用。该调用将用户名特征作为第一个参数,序列号数组做为第二个参数。
004004FD |> \8D85 E8EFFFFF lea eax,dword ptr ss:[ebp-1018] ; serial
00400503 |. 50 push eax
00400504 |. 53 push ebx ; ebx
00400505 |. E8 C2FDFFFF call CrackMe.004002CC
跟进去一看,这个函数好长,开始是老规矩,压栈,申请空间,数据初始化。
首先找找,什么地方对我刚刚调用的参数操作了,因为我传进来的参数才是和用户名,序列号相关的,也就是ebp+8,和ebp+c,分别发现两个。
00400306 |. 8B7D 0C mov edi,dword ptr ss:[ebp+C] ; edi == 输入的序列号
00400367 |> /8B45 08 /mov eax,dword ptr ss:[ebp+8] ; namecalc
00400383 |> /8B45 0C /mov eax,dword ptr ss:[ebp+C] ; eax = &serial[0]
004003A4 |. 8B45 08 |mov eax,dword ptr ss:[ebp+8] ; eax = namecalc
分析一下第一个地方00400306是计算序列号长度,最后ecx = 序列号长度,还原该处和上面说的一样,我们不需要这样做了。另外该处将ebx置1,这个ebx以后都不会动了,就把他当作1看。
第二个地方是个小关键,根据名字特征值namecalc即[ebp+8]的1-8位构造名字表
//汇编代码如下:
00400365 |. 8BFB mov edi,ebx ; edi = 1
00400367 |> 8B45 08 /mov eax,dword ptr ss:[ebp+8] ; namecalc
0040036A |. 8BCF |mov ecx,edi
0040036C |. D3E8 |shr eax,cl ; namecalc >>= i
0040036E |. 22C3 |and al,bl ; (byte)namecalc &= 1
00400370 |. 88443D DC |mov byte ptr ss:[ebp+edi-24],al
00400374 |. 47 |inc edi
00400375 |. 83FF 09 |cmp edi,9
00400378 |.^ 7C ED \jl short CrackMe.00400367
0040037A |. 33FF xor edi,edi ; edi = 0
0040037C |. 885D E5 mov byte ptr ss:[ebp-1B],bl ; bl == 1
// 还原c代码如下
for (i=1; i<9; i++)
{ //根据名字特征值的1-8位构造名字表
nametable[i] = (unsigned char)((unsigned char)(namecalc>>i)&1);
}
nametable[9] = 1;
第三和第四在一个大段里面,其他地方没有了,不用说,肯定就是这儿判断是否序列号为真。这一段比较长,我们先放一下,看看到底是什么地方判断成功的。下翻看到一个msgbox,看看就这一个地方调用msgbox的,那肯定是他跳出判断的,再看到0040041B处文本是个eax,而eax指向[ebp-128],所以[ebp-128]存放判断成功失败的文本。看代码发现和[ebp-128]相关的地方在这一段有4个,开始置0,刚刚msgbox要取数据,另外两个分别在004003FC,0040042B,可以看到最后都是作为call CrackMe.00400240的第二个参数,而第一个参数不一样,但是第一个参数都是一个11字节的数组,而且第一个字节为FF,只是做检查用的。
0040040D |. 8D85 D8FEFFFF lea eax,dword ptr ss:[ebp-128] ; 此处为判断成功失败的文本
00400413 |. 59 pop ecx
00400414 |. 6A 00 push 0 ; /Style = MB_OK|MB_APPLMODAL
00400416 |. 68 70054000 push CrackMe.00400570 ; |Title = ""
0040041B |. 50 push eax ; |Text
0040041C |. 6A 00 push 0 ; |hOwner = NULL
0040041E |. FF15 34024000 call dword ptr ds:[<&USER32.MessageB>; \MessageBoxA
//[ebp-128] 相关1,判断成功
004003FC |. 8D85 D8FEFFFF lea eax,dword ptr ss:[ebp-128] ; 该处返回OK!!
00400402 |. 50 push eax
00400403 |. 8D45 E8 lea eax,dword ptr ss:[ebp-18] ; 成功
00400406 |> 50 push eax
00400407 |. E8 34FEFFFF call CrackMe.00400240
//[ebp-128] 相关2,判断失败
0040042B |> \8D85 D8FEFFFF lea eax,dword ptr ss:[ebp-128] ; 不是数字,或者其他什么都是失败Fail!
00400431 |. 50 push eax ; 第二个参数
00400432 |. 8D45 F4 lea eax,dword ptr ss:[ebp-C] ; 第一个参数
00400435 \.^ EB CF jmp short CrackMe.00400406
跟到call CrackMe.00400240里面看了下,很简单,就是异或还原数据。此处可以不看,只要知道数据传送进去,会异或出fail!和OK!!字样的字符串就好了。
//c还原代码
void calc_alpha(char *xor, char *result_str)
{
int i;
unsigned char xor0[10] = {0x25,0x9a,0xf3,0x6f,0x82,0xda,0x72,0xfe,0xc9,0xb7};
for(i=0; i<10; i++) result_str[i] = xor0[i]^xor[i];
}
//下面是调用者准备好的数据。
char result_str[0x40];
unsigned char xorfail[10] = {0x63,0xfb,0x9a,0x03,0xa3,0xda,0x72,0xfe,0xc9,0xb7}; //fail!
unsigned char xorok[10] = {0x6a,0xd1,0xd2,0x4e,0x82,0xda,0x72,0xfe,0xc9,0xb7}; //OK!!
//调用calc_alpha(xorfail,result_str);会得到fail!字样,calc_alpha(xorok,result_str);会得到OK!!
////注意此处代码没有还原,该处检查标志FF
00400240 /$ 57 push edi
00400241 |. 8B7C24 08 mov edi,dword ptr ss:[esp+8] ; edi = 第一个参数,一已定义数组
00400245 |. 803F FF cmp byte ptr ds:[edi],0FF ; 测试0012EB30是否为0xff
00400248 |. 75 5F jnz short CrackMe.004002A9
好了,上面说了一大通啰嗦的话,下面到了真正关键的代码了,也就是上面说到的第三和第四在一个大段里面,肯定就是这儿判断是否序列号为真。该段是个循环,判断序列号[ebp+C]是否为真。
循环操作如下:
1.依次加载序列号的每一个字节。
2.判断是否为数字字符if(serial[i]<'0' || serial[i]>'9') ,不是数字字符就跳转。
3.循环计算32位整数就是刚才计算的用户名特征码的1/2模10的余数,每次都多计算其1/2,所以可以看到这个数是以32位为循环的。
4.该数字加上刚刚的数字字符串代表的数字再模10,以这个余数为索引到开始计算的名字特征值的名字表中作变换。
5.下面是变换过程
5.1如果为1,则一号位异或1,见004003BE,cmp edx,ebx
5.2否则异或该位置,但是需要该位置的前一个位置为1,再前面所有数据为0,不满足就报错
关键代码就这些
00400383 |> /8B45 0C /mov eax,dword ptr ss:[ebp+C] ; eax = &serial[0]
00400386 |. |8A0407 |mov al,byte ptr ds:[edi+eax] ;
00400389 |. |3C 30 |cmp al,30 ; if(serial[i]<'0' || serial[i]>'9') goto fail;
0040038B |. |8845 FF |mov byte ptr ss:[ebp-1],al ;
0040038E |. |0F8C 97000000 |jl CrackMe.0040042B
00400394 |. |3C 39 |cmp al,39
00400396 |. |0F8F 8F000000 |jg CrackMe.0040042B ; 如果不是数字跳转到该处,一定要是数字,否则fail
0040039C |. |8BC7 |mov eax,edi ;
0040039E |. |6A 1F |push 1F
004003A0 |. |99 |cdq
004003A1 |. |59 |pop ecx ;
004003A2 |. |F7F9 |idiv ecx ; i/0x1f
004003A4 |. |8B45 08 |mov eax,dword ptr ss:[ebp+8] ; eax = namecalc
004003A7 |. |6A 0A |push 0A
004003A9 |. |8BCA |mov ecx,edx ; ecx = 余数
004003AB |. |33D2 |xor edx,edx ;
004003AD |. |D3E8 |shr eax,cl ;
004003AF |. |59 |pop ecx ;
004003B0 |. |F7F1 |div ecx ; remainder = (namecalc >>= (byte)ecx)%0xa
004003B2 |. |0FBE45 FF |movsx eax,byte ptr ss:[ebp-1] ;
004003B6 |. |8D4402 D0 |lea eax,dword ptr ds:[edx+eax-30] ; eax = serial[i]数字值+余数
004003BA |. |33D2 |xor edx,edx ;
004003BC |. |F7F1 |div ecx ;
004003BE |. |3BD3 |cmp edx,ebx ; ebx 一直为1
004003C0 |. |75 05 |jnz short CrackMe.004003C7 ; 如果remainder != 1则跳转
004003C2 |. |305D DD |xor byte ptr ss:[ebp-23],bl ;
004003C5 |. |EB 22 |jmp short CrackMe.004003E9
004003C7 |> |385C15 DB |cmp byte ptr ss:[ebp+edx-25],bl ; nametable[remainder-1]!=1>fail!
004003CB |. |75 5E |jnz short CrackMe.0040042B ; fail!
004003CD |. |8D42 FE |lea eax,dword ptr ds:[edx-2] ; eax = remainder-2; //calc
004003D0 |. |8BCB |mov ecx,ebx ; ecx = 1;
004003D2 |. |3BC3 |cmp eax,ebx ; calc >= 1
004003D4 |. |7C 0B |jl short CrackMe.004003E1
004003D6 |> |385C0D DC |/cmp byte ptr ss:[ebp+ecx-24],bl
004003DA |. |74 4F ||je short CrackMe.0040042B ; fail!
004003DC |. |41 ||inc ecx
004003DD |. |3BC8 ||cmp ecx,eax
004003DF |.^|7E F5 |\jle short CrackMe.004003D6
004003E1 |> |305C15 DC |xor byte ptr ss:[ebp+edx-24],bl ; nametable[remainder] ^= 1;
004003E5 |. |8D4415 DC |lea eax,dword ptr ss:[ebp+edx-24] ; 多余操作
004003E9 |> |47 |inc edi
004003EA |. |3BFE |cmp edi,esi
004003EC |.^\7C 95 \jl short CrackMe.00400383
逆向出来的c代码如下
// check
for (i=0; i<seriallen; i++)
{
if(serial[i]<'0' || serial[i]>'9') goto failinput;
remainder = (serial[i]-0x30+ ((namecalc>>(unsigned char)(i%(TABLE_SIZE-1)))%10) )%10;//remainder 在 0-9之间
if(remainder != 1)
{
if(nametable[remainder-1]==1)
{
int calc = remainder-2;
while(calc >=1)
if(nametable[calc--]==1) goto fail;
nametable[remainder]^=1;
}
else goto fail;
}
else nametable[1] ^=1;
}
最后一个判断是,判断特征表中所有数字都为0,比较简单。
004003EE |> \8BC3 mov eax,ebx ; ebx = 1
004003F0 |> 385C05 DC /cmp byte ptr ss:[ebp+eax-24],bl ; 此时需要table中全为0
004003F4 |. 74 35 |je short CrackMe.0040042B ; fail!
004003F6 |. 40 |inc eax
004003F7 |. 83F8 0A |cmp eax,0A
004003FA |.^ 7C F4 \jl short CrackMe.004003F0
// c代码
for (i=1; i<10; i++)
if(nametable[i]==1) goto failedcheck;
最后给出所有的逆向出来的检查用户名和序列号的c代码,没有什么需要注释的,名字都很清楚了,窗体部分见附件。至此,整个代码的逆向过程都出来了。
过会儿给出写注册机的分析,这个咚咚写注册机有些麻烦,而且不太说的清楚。各位要不先自己试着考虑下象这样的过程该怎么写注册机。昨天我看了半天才想出来怎么写注册机,而且开始走了很大的弯路.
#define TABLE_SIZE 32
#define SERIAL_SIZE 0x1000
char name[0x10];
char serial[SERIAL_SIZE];
unsigned char nametable[10];
int namelen = 0, seriallen = 0;
void calc_alpha(unsigned char *xor, char *result_str)
{// 其实这个地方就是为了好玩才逆向的;)不需要,可以直接sprintf();
int i;
unsigned char xor0[10] = {0x25,0x9a,0xf3,0x6f,0x82,0xda,0x72,0xfe,0xc9,0xb7};
for(i=0; i<10; i++) result_str[i] = xor0[i]^xor[i];
}
char buf[0x20];
int checkserial( char *name,char *serial)
{
int i;
int remainder;
unsigned namecalc = 0x13572468;
unsigned adder,adder2;
unsigned char xorfail[10] = {0x63,0xfb,0x9a,0x03,0xa3,0xda,0x72,0xfe,0xc9,0xb7}; //fail!
unsigned char xorok[10] = {0x6a,0xd1,0xd2,0x4e,0x82,0xda,0x72,0xfe,0xc9,0xb7}; //OK!!
memset(nametable,0,0xa);
//namelen = strlen(name);
//seriallen= strlen(serial);// 我采用GetDlgItemText的返回值计算的。
for(i=0; i<namelen; i++)
{ // 计算名字特征
adder = (name[i]+ namecalc)*0x3721273+0x24681357;
adder2 = adder<<0x19;
__asm sar adder,7
namecalc = adder2|adder;
}
for (i=1; i<9; i++)
nametable[i] = (unsigned char)((unsigned char)(namecalc>>i)&1);
nametable[9] = 1;
// check
for (i=0; i<seriallen; i++)
{
if(serial[i]<'0' || serial[i]>'9') goto failinput;
remainder = (serial[i]-0x30+ ((namecalc>>(unsigned char)(i%(TABLE_SIZE-1)))%10) )%10;
if(remainder != 1)
{
if(nametable[remainder-1]==1)
{
int calc = remainder-2;
while(calc >=1)
if(nametable[calc--]==1) goto fail;
nametable[remainder]^=1;
}
else goto fail;
}
else nametable[1] ^=1;
}
for (i=1; i<10; i++)
if(nametable[i]==1) goto failedcheck;
goto success;
failinput:
fail:
failedcheck:
calc_alpha(xorfail,buf); return 1;
success:
calc_alpha(xorok,buf); return 0;
}
稍微总结一下验证过程,循环读入序列号,根据序列号对nametable的对应位置取反(其中位置判断是否序列号有效),最后序列号结束后判断是否nametable全为0,否则失败。
另外这个题逆推回去有些不容易.
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界