首页
社区
课程
招聘
[原创]第一阶段第一题分析+完整逆向代码(看雪金山2007逆向分析挑战赛)
2007-8-25 12:00 27287

[原创]第一阶段第一题分析+完整逆向代码(看雪金山2007逆向分析挑战赛)

2007-8-25 12:00
27287
第一阶段第一题分析+完整逆向代码(看雪金山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世界

上传的附件:
收藏
点赞7
打赏
分享
最新回复 (57)
雪    币: 2134
活跃值: (14)
能力值: (RANK:170 )
在线值:
发帖
回帖
粉丝
Aker 4 2007-8-25 12:08
2
0
第一阶段第一题注册机思路+完整注册机代码(看雪金山2007逆向分析挑战赛)

by aker
9:10 2007-8-24

上一篇中分析了看雪金山2007逆向分析挑战赛第一阶段第一题crackme的代码,并实际动手逆向实现了该crackme。

主要函数为int checkserial( char *name,char *serial);该函数接受用户名和序列号,通过一系列变换判断是否变换成功。

本篇中分析如何根据关键代码流程写出注册机,附件是注册机及代码。

关键代码的操作过程如下:

循环操作如下:
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,不满足就报错

循环结束后
6 检查名字特征值的名字表中的值,需要都为0才表示序列号为真。

实际代码如下:
    // 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;

昨天这个地方我首先走了弯路,也介绍下:
从该验证过程可以看到,每次读入一位,然后该位肯定为'0'-'9'之间的一位数,所以想最简单的就是写一个外壳,对序列号从1-0x1000验证,每次'0'-'9'变换就是了,原形如下,当时将getserial的返回值作了分类,fail表示错误,不可接受,failedcheck;表示前面都对,但是最后验证错误,success表示成功,这样,我就可以区分是不是需要对序列号数字加一。但是实际上却出问题了,序列号怎么都不能最终使nametable全为0,但是做了这么久,又有点不想放弃这个思路。傍晚刚好停了一下电,出去吃了晚饭,回来电来了,网络断了,正好可以不看论坛:)换个思路做做看。

char buf[ERRO_SIZE];
int shellgo(unsigned namecalc)
{
    int i;
    enum result = OTHER;;
    while(result)
    {
        for (i=0; i<10 && result!=FAILEDCHECK && result!=SUCCESS;; i++)
        {// 每次检查一位
            serial[seriallen-1] = (0x30+i);
            result = getserial(namecalc,serial);
        }

        if(result != SUCCESS;; ) result = OTHER;
        seriallen++;
        if(seriallen>=0x1000) 
        {
            wsprintf(buf,"exceeds..............\n");
            return -1;
        }
    }
    return 0;
}


仔细看了代码,发现一个咚咚,就是((namecalc>>(unsigned char)(i%31))%10)得变化对于每个用户名都是一样的循环变化。
也就是说可以拿出来,先生成。还有就是名字特征的1-8位的数组都是对每个用户名一样的。
// 定义一个32位的数组
    unsigned char table[32];
    for (i=0; i<32; i++) 
        table[i] = ((namecalc>>(unsigned char)(i%31))%10);
// 另外这个是名字特征的1-8位的数组,加上最后一位固定为1
    for (i=1; i<9; i++)
        nametable[i] = (unsigned char)((unsigned char)(namecalc>>i)&1);
    nametable[9] = 1;


现在就容易懂了,每次从table中循环选取一个数字,和当前取到的序列号相加,模10就可以得到名字特征表的位置。
然后对该位置异或。
但是该位置选取有很多限制:
见步骤5.1,5.2
5.1如果为1,则一号位异或1,见004003BE,cmp edx,ebx
5.2否则异或该位置,但是需要该位置的前一个位置为1,再前面所有数据为0,不满足就报错

说简单点就是,如果位置为1,则1号位取反,否则,要保证当前位置前面一位为1,而且再前面所有数据为0,都满足就可以对该位取反。

实例:对名字aker有
            name:      aker
            nametable: *110000111
            pos:       0123456789
            table:     89942689999426310005789942631528

比如第一个序列号为3,则,可以知道 (3+8)%10 == 1,所以对第一位取反。
nametable 为 *010000111
如果不为3,假定为5,则余数为3,2号位为1,但是1号位也为1,所以报错。
如果不为3,假定为4,则余数为2,1号位为1,前面是0号,所以也可以进去,但是我们可以发现,如果随便选取一个的话,有可能进入死循环,出现很多序列号前后完全重复的情况。

序列号按照上面的步骤如果位置为1,则1号位取反,否则,要保证当前位置前面一位为1,而且再前面所有数据为0,都满足就可以对该位取反。使得最后一个序列号完成的时候,nametable中全为0。这样就表示序列号为真。

一些细节:序列号一次和table中的数字相加,得到的余数就是在nametable中的位置。
好了,我们现在应该清楚如何检验序列号正确性的问题了。

稍微总结一下上面所说的:
上面介绍了序列号如何验证,主要就是通过序列号使对应nametable为0。
变换可以写成下式:
nametable[(serial[i]+table[i%31])%10]^=1;而且此时(serial[i]+table[i%31])%10要么为1,要么在最左边的1的下一位,持续该变换过程可使nametable为0。

下面就是问题的关键了:
问题是,我们如何通过table和nametable找serial呢?
根据上面的变换过程我们可以知道,每次异或位置只有两个,1,或者是最左边的1的右边。
那现在的问题就是判断怎么样选取位置了,估计很多人就卡在这里了:)

其实仔细想一下可以明白,肯定要交互选取,这次1,下次就是最左边的1的右边,为什么呢?
因为如果你不交互选取的话,等于是回退了,比如你这次选了1,下次还选1异或,就变为原来的数字了;或者你这次选了最左边的1的右边,那么下次再选,肯定还是这个位置,你再异或不是又回去了不,估计不少看官都恍然大悟了;)

那么,最开始该选1好位置还是选最左边的1的右边呢?
结论是根据nametable中1的个数而不同,奇数选1,偶数选最左边1的右边。
这个问题我不能明白的证明给大家看,自己能够理解,但是感觉说不出来,这样吧,我举例子:

比如,最简单的,只有一个1的情况,假设1在最右边,你说这个时候该怎么选择位置呢?你说白痴才不知道,根据上面的条件,只能选最左边的1啊,好,你说对了;再假定1在最左边,你说位置该怎么选呢?你说,"当然最左边啦,选1号位就直接得结果了,你不急我都要替你急了":P。再假定1个1在2-8的任何位置呢?你说还是一样啊,肯定选最左边的1,选右边,数字又变大了,什么时候才能变回来啊。要变除非再选哪个位置;)
好了1个1的情况很清楚了,肯定是选最左边的1号位。

再接着说有两个1的nametable,还是从最简单的开始,最左边有两个1就不用说了,肯定是第二个1,这样可以直接消掉,最右边两个1,肯定还是右边;)如果左边一个1,右边一个1呢?;)反证一下,如果你选了1号位,下次你还要选这个,肯定是不行的;)如果任意位两个1呢?其实是这样的,如果1的个数为偶数的话,左边的总要往右边靠,靠到了,就可以把他消掉。所
以,一定是选1右边的位置。

感觉口都说干了,谁给我买点水吧,呵呵呵呵。其实这是一种感觉。3个的也类似,反正就两个位置,要不1,要不最左边1的右边,最左边3个,你说怎么最快消掉哦?最右边呢,肯定是把最左边的先去掉才能动后面的啊。类推吧。。。。。再说了,要是有1被异或掉了,那就变成了两个,这就是叫什么什么证明来着,高中的东西都忘光了。

代码如下,直接从验证的倒过来就是了,有些累了,不想多说了。输入用户名后,直接work(),结束后,序列号存放在char serial[SERIAL_SIZE];中,可以看到,整体框架和计算序列号是否有效是差不多的。
代码应该比较容易看懂,函数名和变量名就是注释了。

#define TABLE_SIZE      32
#define SERIAL_SIZE     0x1000
char            name[0x10];
char            serial[SERIAL_SIZE];
unsigned char   nametable[10];
unsigned char   nametablepos[10];
unsigned char   table[TABLE_SIZE];

int namelen = 0, seriallen = 0;

int findmodifypos()
{
    int i,tableitem = 0;
    for (i=1; i<10; i++)
        if (nametable[i] == 1) nametablepos[tableitem++]=i;
    if(tableitem > 0)
    {
        if (tableitem%2) return 1; 
        else return nametablepos[0]+1;
    }else return 0; 
}

void adjusttable( )
{
    int pos;
    while((pos = findmodifypos()) && seriallen<SERIAL_SIZE )
    {
        serial[seriallen]=(10+pos-table[seriallen%(TABLE_SIZE-1)])%10+'0';
        seriallen++;
        if(pos ==1 )nametable[pos]^=1; 
        else nametable[pos]^=1; 
    }
}

int work()
{
    // 局部变量
    int i;
    unsigned adder,adder2;
    unsigned namecalc = 0x13572468;

    memset(nametable, 0, 0xa);
    memset(serial, 0, SERIAL_SIZE);
    seriallen = 0;
    // 函数动作
    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++)
    {   //根据名字特征值的1-8位构造名字表
        nametable[i] = (unsigned char)((unsigned char)(namecalc>>i)&1);
    }
    nametable[9] = 1;
    
    for (i=0; i<TABLE_SIZE; i++) 
        table[i] = ((namecalc>>(unsigned char)(i%(TABLE_SIZE-1)))%10);

    adjusttable();
    return 0;
}


结束语:偶是菜鸟,这个crackme中我学到了很多东西,如隐藏字符串,另外就是看大段汇编代码没有那么头疼了;)
总的来说,这个crackme难易适中,适合我这种菜菜,毕竟只有1.6k:)感觉自己杂七杂八的说了,都没说清楚。希望能给你帮助。
雪    币: 2256
活跃值: (941)
能力值: (RANK:2210 )
在线值:
发帖
回帖
粉丝
逍遥风 55 2007-8-25 12:25
3
0
分析的好详细
学习了
雪    币: 443
活跃值: (200)
能力值: ( LV9,RANK:1140 )
在线值:
发帖
回帖
粉丝
冷血书生 28 2007-8-25 12:33
4
0
学习再学习!!!!
雪    币: 1969
活跃值: (46)
能力值: (RANK:550 )
在线值:
发帖
回帖
粉丝
hawking 12 2007-8-25 12:37
5
0
Google 九连环
雪    币: 41
活跃值: (1855)
能力值: ( LV12,RANK:610 )
在线值:
发帖
回帖
粉丝
ylp1332 15 2007-8-25 12:40
6
0
__asm sar adder,7

这一句用C语言怎么写?谁知道?
雪    币: 1969
活跃值: (46)
能力值: (RANK:550 )
在线值:
发帖
回帖
粉丝
hawking 12 2007-8-25 12:45
7
0
adder = adder >> 0x7;

sar 等价于 shr
雪    币: 1687
活跃值: (681)
能力值: ( LV9,RANK:490 )
在线值:
发帖
回帖
粉丝
yijun8354 12 2007-8-25 14:42
8
0
文章不错,学习了~~
雪    币: 207
活跃值: (10)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
斐波纳契 1 2007-8-25 15:44
9
0
我也来学习一下!~~~~
雪    币: 8188
活跃值: (4216)
能力值: ( LV15,RANK:2459 )
在线值:
发帖
回帖
粉丝
ccfer 16 2007-8-25 16:00
10
0
关键词:格雷码

DWORD MsGen2(HWND hDlg)
{
	char szName[0x100];
	char szCode[0x400];
	int Len;
	int i;
	int K;
	DWORD N;

	if (Len = GetDlgItemText(hDlg, IDC_EDIT_NAME, szName, 16))
	{
		K = 0x13572468;
		for (i=0;i<Len;i++)
		{
			K = (K + szName[i]) * 0x3721273 + 0x24681357;
			K = (K << 0x19) | (K >> 7);
		}

		Len = 0;
		do 
		{
			Len++;
		} while((Len ^ (Len >> 1)) != ((K >> 1) & 0xFF | 0x100));

		for (i=0;i<Len;i++)
		{
			N = Len - i;
			N ^= N - 1;
			N ^= N >> 1;
			__asm
			{
				bsf eax,N
				mov N,eax
			}
			szCode[i] = ((BYTE)(N + 11 - (((DWORD)K >> (i % 31)) % 10)) % 10) + 0x30;
		}
		szCode[Len] = 0;

		SetDlgItemText(hDlg, IDC_EDIT_CODE, szCode);
	}

	return 1;
}
雪    币: 424
活跃值: (10)
能力值: ( LV9,RANK:850 )
在线值:
发帖
回帖
粉丝
大菜一号 21 2007-8-25 16:08
11
0
第一页支持`
雪    币: 116
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
boainfall 2007-8-25 16:09
12
0
我喜欢,哈哈
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
rr日日 2007-8-25 16:18
13
0
base =====>为namehash

char buffer[9] = {0};
unsigned int namehash = base;
for (int i=0; i<8; i++)
{
buffer[i] = (namehash>>(i+1))&0x1;
}
buffer[8] = 1;

int nseqidlen;
char seqid[31];
byte nowbyte;

for (i=0; i<nseqidlen; i++)
{
nowbyte = seqid[i];
int t1 = (namehash>>(i%31))%10;
int tem1 = (t1 + nowbyte -0x30)%10;
if (tem1==1)
{
  buffer[0] ^= 1;
  continue;
}
if (buffer[tem1-2]!=1)
{
  error;
}
if (tem1-2>=1)
{
  for (i=0; i<tem1-2; i++)
  {
   if (buffer[i] == 1)
   {
    error;
   }
  }
}
buffer[tem1-1] ^= 1;
}

通过对tem1 的一系列取值,可以达到对其中一位进行反位的功能,前提是这一位前面都为0.
这个取值是一个数列
让第1位反位        1
让第2位反位        1 2 1
让第3位反位        1 2 1 3 1 2 1
让第4位反位        1 2 1 3 1 2 1 4 1 2 1 3 1 2 1
通项式是 f(n) = f(n-1)+n+f(n-1);f(1)=1;

int count;
void get_reserve_string(int num, char *buffer)
{
    if (num==1)
    {
        sprintf(buffer+count, "%1d", 1);
        count++;
        return;
    }
    get_reserve_string(num-1, buffer);
    sprintf(buffer+count, "%1d", num);
    count++;
    get_reserve_string(num-1, buffer);
    return;
}
用来生成这个数列.

//这里用的是最正常的办法,没有优化!!!! 对buffer中每一个1进行反位
            for (i=0; i<9; i++)
            {
                if (buffer[i]==1)
                {
                    memset(stringbuffer, 0, 520);
                    count = 0;//全局置0
                    get_reserve_string(i+1, stringbuffer);
                    
                    for (int j =0; j<count; j++)
                    {
                        t1 = (namehash>>((key_cur+j)%31))%10;
                        if (t1 <= (stringbuffer[j]-0x30))
                        {
                            key[key_cur+j] = stringbuffer[j] - t1;
                        }
                        else
                            key[key_cur+j] = 10 + stringbuffer[j] - t1;
                    }
                    key_cur += count;                       
                }
            }

唉,我当时注册的时候,是12点以前,但是老是注册不成功,没有办法,运气不好,祝福你们拉

ps
sar就是带符号的右移,定义成int就可以了,如果是shr的话,定义为unsigned int
雪    币: 195
活跃值: (20)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
vxworks 1 2007-8-25 16:30
14
0
sar 和 shr 不一样的

定义成 signed 就 sar,unsigned 就 shr
雪    币: 325
活跃值: (97)
能力值: ( LV13,RANK:530 )
在线值:
发帖
回帖
粉丝
foxabu 13 2007-8-25 16:42
15
0
在vc8中无论如果定义都是sar,不知为什么.高人指点一下。
雪    币: 7300
活跃值: (3758)
能力值: (RANK:1130 )
在线值:
发帖
回帖
粉丝
海风月影 22 2007-8-25 16:54
16
0
太强了,学习
雪    币: 226
活跃值: (15)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
十三少 2 2007-8-25 16:55
17
0
BYTE Ninekey[9] = {0};
char OutPath[1024] = {0};
int  Pathlen = 0;

void UpRing(int idx);
void DownRing(int idx);

void DownRing(int idx)
{
        if( idx > 2)
                DownRing(idx-2);
        OutPath[Pathlen] = idx;
        Ninekey[idx-1] = !Ninekey[idx-1];
        Pathlen++;
        if( idx > 2 )
                UpRing(idx-2);
        if( idx > 1 )
                DownRing(idx-1);
}

void UpRing(int idx)
{
        if(idx > 1)
                UpRing(idx-1);
        if(idx > 2)
                DownRing(idx-2);
        OutPath[Pathlen] = idx;
        Ninekey[idx-1] = !Ninekey[idx-1];
        Pathlen++;
        if( idx > 2 )
                UpRing(idx-2);
}
雪    币: 483
活跃值: (629)
能力值: ( LV9,RANK:1210 )
在线值:
发帖
回帖
粉丝
softworm 30 2007-8-25 17:10
18
0
No,

dwHash = (dwTmp << 0x19) |
                ((dwTmp & 0x80000000)?
                ((dwTmp >> 7) | 0xFE000000):
                (dwTmp >> 7));

不过这个题我做不出
雪    币: 196
活跃值: (135)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
thinkSJ 4 2007-8-25 17:31
19
0
LZ太强了,顶下
雪    币: 195
活跃值: (20)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
vxworks 1 2007-8-25 17:38
20
0
偶这里vc6上是这样的
把vc8的汇编代码贴出来看看?对于unsigned也是sar比较奇怪了
雪    币: 7300
活跃值: (3758)
能力值: (RANK:1130 )
在线值:
发帖
回帖
粉丝
海风月影 22 2007-8-25 17:57
21
0
关键词:递归

int steplist[1000]={0}; //走的步数
BYTE newtmp1[10] = {0}; //状态
void x(int n);//清空n
void y(int n);//填上n
void setstep(int n);//异或一次

void setstep(int n) //异或一次
{
	steplist[pStep++]=n;
	newtmp1[n] ^= 1;
}

void x(int n) //清空n到最前面的
{
	if (n == 1)
	{
		if (newtmp1[1] == 1) setstep(1);
	}
	else
	{	
		y(n-1);//填上n-1,清空n-1前面的
		if (newtmp1[n] == 1) setstep(n); //如果有值就清空
		x(n-1);//清空前面的
	}

}

void y(int n) //填上n,并清空前面的n-1个
{
	if (n == 1)
	{
		if (newtmp1[1] == 0) setstep(1);
	}
	else
	{	
		y(n-1);//填上n-1的,并清空前面的n-2个
		if (newtmp1[n] == 0) setstep(n); //当前如果没值就填上
		x(n-1);//清空前面n-1
	}

}



调用的时候
x(9);
雪    币: 212
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
fool 2007-8-25 18:23
22
0
学习一下,我用了递归,算法不好
雪    币: 313
活跃值: (440)
能力值: ( LV12,RANK:530 )
在线值:
发帖
回帖
粉丝
4nil 13 2007-8-25 19:13
23
0
长见识了,呵呵,我都没用递归...
雪    币: 2134
活跃值: (14)
能力值: (RANK:170 )
在线值:
发帖
回帖
粉丝
Aker 4 2007-8-25 19:25
24
0
[QUOTE=ccfer;350359]关键词:格雷码

DWORD MsGen2(HWND hDlg)
{
        char szName[0x100];
        char szCode[0x400];
        int Len;
        int i;
        int K;
        DWORD N;

        if (Len = GetDlgItemTe...[/QUOTE]

ccfer的代码强;) 我都不知道这是个算法的,就跟着感觉走了;)还有就是我不知直接定义非unsigned 就是编译为sar,学习;)
雪    币: 1505
能力值: (RANK:210 )
在线值:
发帖
回帖
粉丝
bithaha 5 2007-8-25 20:00
25
0
是gray码 谁要早提醒我一下就好了 网上一搜一大堆
偶也偶的贴出来,虽然写的很滥.
GetEdx proc;获取crackme里面那个edx
	mov ecx,1Fh
	cdq
	mov eax,Codelen
	idiv ecx
	mov eax,UserCode
	mov ecx,edx
	xor edx,edx
	shr eax,cl
	mov ecx,0ah
	div ecx
	mov eax,edx
	inc Codelen
	ret
GetEdx endp
GetCode proc ;开始计算注册码
	mov esi,offset lala
  @@:   or ecx,0FFFFFFFFH
	mov edi,offset haha
	mov eax,1
	repne scasb 
	not ecx
	dec ecx
	push ecx
	.if ecx>=9h 
		pop ecx
		ret
	.elseif ecx==8
		pop ecx
		mov edi,offset haha
		jmp @1
	.endif
	mov edi,offset haha
	xor byte ptr [edi+ecx+1],1h
	call GetEdx
	mov ebx,0ah
	pop ecx
	add ebx,ecx
	add ebx,2
	sub ebx,eax
	.if ebx>=0ah
		sub ebx,0ah
	.endif
	add ebx,30h
	mov byte ptr [esi],bl
	inc esi
@1:	xor byte ptr [edi],1h
	call GetEdx
	mov ebx,0Bh
	sub ebx,eax
	.if ebx>=0ah
		sub ebx,0ah
	.endif
	add ebx,30h
	mov byte ptr [esi],bl
	inc esi
	jmp @B
	ret

GetCode endp
游客
登录 | 注册 方可回帖
返回