160CrackMe练手之082
本帖最后由 Pnmker 于 2015-7-4 14:21 编辑160CrackMe练手之082ByPnmker刷一下存在感,这个CM说起来并不算难,只不过是太繁琐了,为了写出注册机竟然花费了我2个晚上的时间。话又说回来这个CM也可能会代表某种类型的验证机制,这里记录下大概的破解过程。首先,这个CM在我的XP虚拟机内是不能够运行的,经验证WIN95和WIN98是可以运行,我选择的是WIN98系统。OD使用的是坛子里的吾爱破解论坛版本的,但是没有使用任何的plugin插件,使用的插件的话OD启动时就会报致命错误。工具就这么多。首先OD载入断在入口点处,大致浏览了下汇编代码,想要了解下为什么该CM不能运行在XP系统下,浏览过程中发现在入口点附近的代码很正常,但从004012FF开始的代码刚一看到的时候吓了我一跳,几乎全是jmp指令。还看到了pushad和pushfd指令,是不是加壳了呀?这时才想起这一次竟然忘记PEiD查壳了。好吧,赶紧返回去PEiD了一下,奇怪也是没有加壳的呀,不过编程工具是TASM/MASM。说实话,82个CM了,我还真是比较喜欢ASM汇编写得CM,代码非常简洁,OD载入之后几乎不用查找字符串只需要从入口点处上下浏览一下代码就可以找到验证按钮的代码地址。 可是,怎么这一个就这么奇怪呢,为什么会有这么多的jmp指令呢?是不是OD分析代码错误了呢,试着用其他的W32DASM和IDA分析下静态代码,跟OD也是一样的!好吧,我承认这是作者故意搞出来的,只好硬着头皮按着一个一个F8单步跟踪下去。我的妈呀,真的不好跟踪啊,跟了10几分钟,就知道程序在00401202,00401224,0040128A等等好多函数之间跳来跳去,可是jmp指令太多了,非常不好分析。 但不能因此就放弃,我不知道这些jmp指令算不算是花指令,win98下插件也用不了(当然我没具体试去花指令的插件,感兴趣的自己去试验),那只好把这些代码复制出来手动去掉了,然后静态分析结合动态调试看能有什么收获吧。还是得感叹一下汇编语言写出来的程序实在是太简洁了,这个程序总共才400多行,去掉多余的jmp指令还会更少吧! 在复制代码之前,有一个地方需要强调一下,从00401256开始到0040127C这一段代码OD分析是有误的,需要现在OD里面手动纠正过来。 很明显的就是00401256开始的两个字节EB 02,根据上面代码可以推测这是一个jmp指令,在一开始盲目跟踪的时候也有注意到这里是被看做代码执行的。只要手动把从00401256开始到0040127C间的代码纠正一下,就可以从OD里复制代码出来了。至于纠正方法,是OD的基本功能,这里就不再详细解说了。
嗯,果真手动删除掉jmp指令和无用的db数据之后,代码量减少了许多,只有200多行了。首先把注册按钮的处理过程贴出来:004010FF $55 push ebp
00401100 .8BEC mov ebp,esp
00401112 >BB 0B304000 mov ebx,ULTRASCH.0040300B
0040111B >BE 89314000 mov esi,ULTRASCH.00403189
00401124 >33C0 xor eax,eax
0040112A >50 push eax
0040112B .53 push ebx
00401131 >8BF8 mov edi,eax
00401137 >50 push eax
0040113C >3D FF000000 cmp eax,0xFF
00401145 >0F8F 88000000 jg ULTRASCH.004011D3
00401154 >D7 xlat byte ptr ds:
00401155 >50 push eax
0040115F >8B541F 05 mov edx,dword ptr ds:
00401167 >59 pop ecx
0040116C >58 pop eax
00401172 >51 push ecx
00401177 >66:8B4C18 09mov cx,word ptr ds:
00401180 >8B5C1F 01 mov ebx,dword ptr ds:
00401184 .58 pop eax
00401189 >FF1430 call dword ptr ds:
00401190 >5B pop ebx
00401195 >58 pop eax
0040119A >8BC1 mov eax,ecx
004011A0 >803D 0D114000>cmp byte ptr ds:,0xCC
004011AB >^ 0F85 79FFFFFF jnz ULTRASCH.0040112A
004011B5 >C605 0D114000>mov byte ptr ds:,0x90
004011C1 >9D popfd
004011C6 >61 popad
004011CB >C9 leave
004011CC .C2 0400 retn 0x4
代码也不是很多,30几行而已。这个处理过程就是动静分析的核心地方,虽然很短,但从0040112A到004011AB间的循环可以循环多达40次,不管是动态跟踪和静态分析都是一个不小的力气活。首先当然是进行静态分析一下这个处理过程了,OD动态跟踪还是要面对那些个让人心烦的jmp指令。大致分析这段代码可以得知,它主要是在访问0040300B地址一些信息,然后整段代码中只有一个call,是在00401189处的call dword ptr ds:,esi的值代码一开始就被设置为了00403189。那就先把0040300B处的256个字节数据和00403189处的数据复制出来查看一下吧。由于00403189的数据都是被call调用的,必定都是一些函数地址,因此复制的时候就使用DWORD方式。至于0040300B处的数据为什么要256个字节,这是因为在0040113C处有一句cmp eax,0xFF的判断,后面通过动静结合的分析也可以确定256个字节就足够用了。0040300B10 E0 00 00 00 F0 03 00 00 0B 00 10 F5 00 00 00.????.
0040301BF1 03 00 00 16 00 08 DC 00 00 00 DC 00 00 00 21?.?.?.?
0040302B00 04 DC 00 00 00 5A 01 00 00 2C 00 20 E0 00 00?ü.?.,.
0040303B00 4E 01 00 00 37 00 04 4E 01 00 00 56 01 00 00一???.?.
0040304B42 00 04 4A 01 00 00 DC 00 00 00 4D 00 08 52 01B??.???
0040305B00 00 56 01 00 00 58 00 18 52 01 00 00 00 00 2C.?.X刘.?
0040306B00 63 00 20 66 01 00 00 52 01 00 00 6E 00 0C 4A挀??.?.n?
0040307B01 00 00 DC 00 00 00 79 00 04 52 01 00 00 62 01?.礀??.?
0040308B00 00 84 00 08 4E 01 00 00 62 01 00 00 8F 00 18.?丈戀輀?
0040309B4E 01 00 00 00 00 63 00 9A 00 24 7A 01 00 00 F1?..c?稤?
004030AB03 00 00 A5 00 08 4A 01 00 00 7A 01 00 00 B0 00???.?.°
004030BB18 4A 01 00 00 00 00 C6 00 BB 00 14 2C 01 00 00?.????.
004030CB00 00 00 00 D1 00 14 0A 01 00 00 00 00 00 00 D1..??..?
004030DB00 1C 00 00 00 00 00 00 00 00 00 00 70 6E 6D 6B?.......
004030EB70 6E 6D 6B 65 72 00 00 00 00 00 00 00 00 00 00........
004030FB00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00........
00403189004011DF ULTRASCH.004011DF
0040318D00401202 ULTRASCH.00401202 ;add dword ptr ds:, dword ptr ds:
0040319100401224 ULTRASCH.00401224 ;sub dword ptr ds:, dword ptr ds:
0040319500401256 ULTRASCH.00401256 ;xor dword ptr ds:, dword ptr ds:
004031990040128A ULTRASCH.0040128A ;get name or serial
0040319D004012E0 ULTRASCH.004012E0 ;pop message box
004031A100401343 ULTRASCH.00401343 => test ?0 if <>0 then goto
004031A50040136F ULTRASCH.0040136F
004031A900401380 ULTRASCH.00401380 ;get name substring of four chars which start decided by edx, and save 30E7
004031AD004013C0 ULTRASCH.004013C0 ;get serial as number through GetDlgItemInt
004031B100000000
004031B500004000
004031B900000007
首先,说一下0040300B起始的数据,这是一个结构化的数据。每11个字节构成一个结构,在0040112A处push eax中的eax给出是每一个结构数据相对于0040300B(ebx)的偏移量,eax初始为0.其中每一组11个字节内的大概含义是这样的:BYTE 1字节给出00401189处call函数地址相对于00403189(esi)的偏移量,下记为byFuncOffset;DWORD 4字节给出00401189处call函数需要的一个参数存入ebx中,下记为dwParamEbx;DWORD 4字节给出00401189处call函数需要的一个参数存入edx中,下记为dwParamEdx;WORD2字节给出下一组结构化数据相对于0040300B(ebx)的偏移量,下记为wNextOffset;
然后,00403189(esi)处给出的10多个函数地址大概用途上面已经做了简单注释,具体代码功能可以根据函数地址挨个进行分析,这里就不再具体给出了。感兴趣的朋友自己进行静态分析当然也要结合一下动态跟踪。
下面,我主要就是展开004010FF这个验证过程的大致流程:第1次循环:在0040112A处push eax中的eax初始为0,所以数据全来自0040300B起始的11个字节,10 E0 00 00 00 F0 03 00 00 0B 00 ,所以byFuncOffset=0x10, dwParamEbx=000000E0,byParamEdx=000003F0,wNextOffset=0x000B其最终call的函数为ds:=0040128A,作用是获取用户名,其中参数dwParamEbx是用户名存放地址相对于0040300B的偏移量即存放在004030EB,参数dwParamEdx则是用户名文本框的控件ID即十进制1008第2次循环:由前次循环知使用的结构数据相对于0040300B的偏移量为0x0B, 10 F5 00 00 00 F1 03 0000 16 00,所以byFuncOffset=0x10,dwParamEbx=000000F5,dwParamEdx=0000003F1,wNextOffset=0x0016最终call的函数同样为0040128A,作用是获取输入的序列号存放在00403100处;
00401286 .EB 02 jmp short ULTRASCH.0040128A
0040128A >51 push ecx ;Get Name and serial
00401294 >81C3 0B304000 add ebx,ULTRASCH.0040300B
004012A7 >6A 15 push 0x15
004012AD >53 push ebx
004012B2 >52 push edx
004012B3 .FF35 B8314000 push dword ptr ds:
004012BE >68 D5124000 push ULTRASCH.004012D5
004012C7 >68 3C144000 push <jmp.&USER32.GetDlgItemTextA>
004012D0 >C3 retn
004012D5 >59 pop ecx
004012DA >C3 retn第3次循环,由前次循环结构数据相对于0040300B的偏移量为0x16,08 DC 00 00 00 DC 00 00 00 21 00,则byFuncOffset=0x08,dwParamEbx=0x000000DC,dwParamEdx=0x000000DC,wNextOffset=0x0021,其最终call的函数为ds:=00401224,参数dwParamEbx是相对于数据地址0040300B的偏移量,dwParamEdx也是相对于这个地址的偏移量,函数00401224的作用就是sub dword ptrds: , dword ptr ds:;00401224 .EB 02 jmp short ULTRASCH.00401228
00401228 >C605 7A124000>mov byte ptr ds:,0x29
00401233 >68 46124000 push ULTRASCH.00401246
0040123C >68 56124000 push ULTRASCH.00401256
00401245 >C3 retn这里要对00401224这个函数做一下说明,这个函数只有几行而已,但却包含这很大玄机。首先movbyte ptr ds:,0x29是将0x0040127A处的一个字节改变为0x29,然后push了两个地址。通过OD跟踪可以知道,push进去的两个地址都是函数地址,也就是00401224会再去调用这个两个函数,首先是00401256,然后再调用00401246,而mov byte ptr ds:,0x29修改的这一字节数据恰好是位于00401256函数代码内部,也就是说他修改了代码!!00401256 .EB 02 jmp short ULTRASCH.0040125A
0040125A >51 push ecx
0040125F >81C3 0B304000 add ebx,ULTRASCH.0040300B
00401269 >81C2 0B304000 add edx,ULTRASCH.0040300B
00401274 >8B12 mov edx,dword ptr ds:
0040127A >3113 xor dword ptr ds:,edx
00401280 >59 pop ecx ;
00401285 >C3 retn查看00401256的代码可知道这个被修改的代码是将xor指令改为了sub指令!!而00401246作用则是再次将0040127A恢复为xor指令00401246 .EB 02 jmp short ULTRASCH.0040124A
0040124A >C605 7A124000>mov byte ptr ds:,0x31
00401255 >C3 retn因此这里我们可以00401224理解为在做减法运算,而00401256是在做异或运算,同样在分析到00401202时则是在运用相同的原理在做加法运算。第4次循环,第5次循环,……等等其他的循环都可以一次进行分析,这里就不再进行一一分析了,感兴趣的朋友可以自己动手验证。经过这种静态分析结合分析方式,我们可以对程序的验证流程作如下概况:1. 首先获取用户名存放在004030EB处,共21字节(包含了C风格字符串的结束字节00),这里计算出21字节是有目的的,下面的分析会给出。这里以输入”pnmker”为例2. 然后获取输入的序列号存放在00403100处,这里以输入“123456”。在获取了用户名和序列号之后的内存数据如下,从004030EB为起始地址:004030EB70 6E 6D 6B 65 72 00 00 00 00 00 00 00 00 0000........004030FB00 00 00 00 00 31 32 33 34 35 36 00 00 00 0000........3. 对用户名进行循环处理,从首地址004030EB开始,每4个字节构成一个无符号整数进行求和,这里共循环了20次。这里第一步给出21字节的目的就在于这里。由于循环了20次,而用户名的起始地址004030EB距离序列号00403100仅有00403100-004030EB=0x15=21个字节,那么这就使得这20次的求和循环的最后两次使用到了序列号内的字节数据,这两个无符号整数分别为0x31000000和0x32310000;这里记最终求得的和为sum4. 将第三步求得和与00403171起始的5个无符号整数依次进行异或运算:0040317194 C0 E1 D4 11 98 1F FF C8 BB FA 91 10 94 1D78??頑??釺鐐砝00403181BC FA 8F 91因此sum =sum^0xD4E1C094^0xFF1F9811^0x91FABBC8^0x78DA9410^0x918FFABC5. 将sum与序列号(转换为整数,调用函数为GetDlgItemInt)然后进行相减,差等于0,则验证通过,否则不通过。
验证过程就是这样子的,注册机也就有了眉目,但这里遇到了一个小小的问题,就是第三步在求和的时候使用的了序列号的两个字节。这个确实有点小麻烦,因为注册算法就是为了计算出序列号,可是序列号还没算出来的时候怎么可能知道前两个字节是什么呢?这里有一个小技巧。我首先想到的是将序列号的前两个字节固定,那么就不用担心注册算法求和时这两个字节不断变化的可能性呢,可是这两个字节该取什么固定字符呢,由第五步我们又可以知道不能取字母,因为这样子的话GetDlgItemInt从序列号文本框中获取到的整数始终为0,显然不太可能。那这两个字节取数字,比如0x31,0x32,好像也不太行,从OD动态跟踪中发现第四步计算完的整数并不是12XXXXX这种形式的,即便可能会有的话,那么这个注册算法写起来也是非常困难的。这时灵感突现,固定为两个空格呀!这样既不影响第五步的GetDlgItemInt取整数,也不会对写注册算法带来很大的困难。嗯,就抱着这个态度,试着写了一下注册机如下:
extern "C" __declspec(dllexport) char * keygen(const char* user)
{
char* pRet = new char;
memset(pRet, 0 , 100);
if(user==NULL)return pRet;
int len = strlen(user);
char pTemp={0};
memset(pTemp, 0, 0x14+5);
strcpy(pTemp, user);
pTemp=0x20;
pTemp=0x20;
unsigned long u= 0;
unsigned long t;
for(int i = 0; i<0x14; ++i)
{
t=*((unsigned long*)(pTemp+i));
u += t;
}
unsigned char mask[] = {0x94,0xC0,0xE1,0xD4,
0x11,0x98,0x1F,0xFF,
0xC8,0xBB,0xFA,0x91,
0x10,0x94,0x1D,0x78,
0xBC,0xFA,0x8F,0x91,
0x00,0x00,0x00,0x00};
for(int i=0;i<0x14; i+=4)
{
t = *((unsigned long*)(mask+i));
u ^= t;
}
sprintf(pRet, "%u", u);
return pRet;
}
编译成功后,赶紧输入用户名生成序列号试一下吧:
嗯哼,果真成功了!注意生成的序列号前面是有两个空格哦!!!事后又想了想其实也可以用两个0来替代两个空格的,也可以用一个0和一个空格,这里就不再一一验证这些替换后的注册机了,有兴趣的朋友可以验证一下。总之序列号的前两个输入不参与GetDlgItemInt获取整数的计算。
大牛就是不一样,虽然看不怎么懂,但从整体文字表达说明,还是能看懂一二。
感谢楼主的教程,期待更好的作品。{:301_993:} 太高深了,收藏以后再看 第82,当初可能是因为不能运行而直接忽略了,想不到有这么多好玩的,会火的感觉 1999年的,好老的程序,汇编写的东西IDA F5记下所有的CALL清晰可见
前面那些jmp都是花。。。居然没有用
页:
[1]