(更新注册机)投鼠忌器——160个crackme之24:Chafe.2
本帖最后由 buddama 于 2017-4-24 14:54 编辑这是楼主见过的比较有意思的一个crackme。它主要的加密方式是修改自身代码(尤其是关键的代码),并对代码进行校验。如果依据校验过程逆向推算出正确的代码并修改的话,这段代码又变了校验值,还是无法通过校验。即便你根据推算出的正确代码修改了校验码,程序还是会随着你输入serial的不同而出现不同的修改。总之就是让人投鼠忌器,无从下手。
下面开始我们的破解之旅。
http://www.52pojie.cn/static/image/hrline/4.gif
界面很简单,输入name和serial的输入框和一个结果提示的标签。错误提示如下:
没有按钮触发检验过程,也没有对话框弹出。所以不能用这些相关函数下断点了。好在搜索文本的话,可以看到如以上二图Status那一栏的提示语:“Your serial is not valid.”或“YES! You found your serial!!!”。好了,双击Your serial is not valid来到函数调用的地方。
004012D7 . /EB 04 jmp short 160-24-C.004012DD
004012D9 |54 push esp
004012DA |45 inc ebp
004012DB |58 pop eax ;kernel32.766DEF8C
004012DC |00AD 33D84975 add byte ptr ss:,ch
004012E2 ?FA cli
004012E3 .81FB FBCFFCAF cmp ebx,0xAFFCCFFB
004012E9 ^ 74 EE je short 160-24-C.004012D9 ;je short 004012D9->JNE
004012EB 68 59304000 push 160-24-C.00403059 ; /Your serial is not valid.
004012F0 .FF35 5C314000 push dword ptr ds: ; |hWnd = NULL
004012F6 .E8 7D010000 call <jmp.&USER32.SetWindowTextA> ; \SetWindowTextA
004012FB .33C0 xor eax,eax ;kernel32.BaseThreadInitThunk
004012FD .C9 leave
004012FE .C2 1000 retn 0x10
00401301 .68 73 30 40 0>ascii "hs0@",0 ;YES! You found your serial!!
00401306 .FF35 5C314000 push dword ptr ds: ; |hWnd = NULL
0040130C .E8 67010000 call <jmp.&USER32.SetWindowTextA> ; \SetWindowTextA
00401311 .33C0 xor eax,eax ;kernel32.BaseThreadInitThunk
00401313 .C9 leave
00401314 .C2 1000 retn 0x10
看这段代码的话,错误提示排在正确提示前面,那么推测他们前面必然有一个关键跳,serial正确的话会跳到00401301;错的话会走向004012EB。因此我们开始往上翻。不对啊,翻到段头也没有看到一个满足这样的跳转逻辑的关键跳。怎么回事?
没办法,只好翻到段首下断点。
00401255 > \3B05 58314000 cmp eax,dword ptr ds:
0040125B .74 0C je short 160-24-C.00401269
0040125D .3B05 54314000 cmp eax,dword ptr ds:
00401263 .0F85 AE000000 jnz 160-24-C.00401317
00401269 C705 D9124000>mov dword ptr ds:,0x584554 ;vengers
00401273 .6A 00 push 0x0 ; /IsSigned = FALSE
00401275 .8D45 FC lea eax,dword ptr ss: ; |0
00401278 .50 push eax ; |pSuccess = kernel32.BaseThreadInitThunk
00401279 .6A 64 push 0x64 ; |ControlID = 64 (100.)
0040127B .FF35 50314000 push dword ptr ds: ; |hWnd = NULL
00401281 .E8 BC010000 call <jmp.&USER32.GetDlgItemInt> ; \GetDlgItemInt
00401286 .837D FC 00 cmp dword ptr ss:,0x0
0040128A .74 5F je short 160-24-C.004012EB
0040128C .50 push eax ;kernel32.BaseThreadInitThunk
0040128D .6A 14 push 0x14 ; /用户名最长不得超过(20.)
0040128F .68 6C314000 push 160-24-C.0040316C ; |用户名
00401294 .FF35 54314000 push dword ptr ds: ; |hWnd = NULL
0040129A .E8 AF010000 call <jmp.&USER32.GetWindowTextA> ; \GetWindowTextA
0040129F .85C0 test eax,eax ;判断name不为空
004012A1 .74 48 je short 160-24-C.004012EB
004012A3 .A1 0B304000 mov eax,dword ptr ds: ;CTEX
004012A8 .BB 6C314000 mov ebx,160-24-C.0040316C ;输入的name
004012AD >0303 add eax,dword ptr ds:
004012AF .43 inc ebx
004012B0 .81FB 7C314000 cmp ebx,160-24-C.0040317C
004012B6 .^ 75 F5 jnz short 160-24-C.004012AD ;这是一轮累加,CTEX开始,依次取用户名四位加和,会溢出。eax中为后八位。
运行程序,输入name:abcdefgh,serial打算用123456。刚输入一个1,运行就被中断到段首。需要注意的是,程序的窗口上,数字1还没有显示出来呢。可见程序的处理流程是接收到用户输入的每一个serial的字符都要先判断一次,确定正确与否之后才能显示在serial的文本框里。Name就不存在这种情况。简单的说序列号是随输随检的。
好了,我们用F8步进,并同时观察数据窗。刚开始就是GetDlgItemInt函数,这是在读取用户输入,还要求是整数形式。好的,我们输入的就是数字。
继续,在0040128F出现了name 的GetWindowTextA函数,这时我们发现输入的name,最长字符长度等参数的入栈过程。这里0x14就是最长长度,20个字符。
在判断name不为空之后,程序对name进行了一种运算。它从字符串CTEX出发,将name截取前四个字符,与CTEX相加。然后截去name的首位,再截取前四个字符,继续与之前的累加和相加。一直进行,截到后面不足4位的就用0补齐。这样就得到一个累加和。
这里有一点需要留意的是,这个累加和会溢出,而eax只能保存后八位。
继续往下走。
004012B8 .5B pop ebx ;取序列号来检测,随输随检,此时界面都还没显示;EBX=1
004012B9 .03C3 add eax,ebx ;160-24-C.0040317C
004012BB 3105 D9124000 xor dword ptr ds:,eax ;
004012C1 .C1E8 10 shr eax,0x10
004012C4 66:2905 D9124>sub word ptr ds:,ax ;
以上几句看起来是很简单的运算,之前的累加和与序列号接收到的数字(此处为1)相加,其和与004012D9处的dword字符串做异或运算,结果保存在004012D9;其和取后四位,再与004012D9处的word做减法,结果保存在004012D9。毫不起眼的几句指令啊。
楼主随即用perl复现了以上流程。
#!/usr/bin/perl
$name="abcdefgh";
#程序对serial随输随检,这里只用serial的第一位;
$serial="1";
$len=length($name);
@rev =$name=~/\w{1}/g;
$rev= join "",reverse(@rev);
$sum=hex(58455443);#"CTEX";
for($i=1;$i<5;$i++){
$out=substr($rev,0,$i);
$out=hex(str2hex($out));
$sum += $out;
}
for($i=1;$i<$len-3;$i++){
$out=substr($rev,$i,4);
$out=hex(str2hex($out));
$sum += $out;
}
print hex($sum),"\n";#报错,这个数会溢出,因此只取后八位。
#取后八位;
$mod=&de_overflow_dec($sum);
$mod += hex($serial);
sub de_overflow_dec(){
$dec = shift;
# print "$dec will be de_overflowed...\n";
$dec %= 4294967296;#0x100000000;
return $dec;
}
sub str2hex{
my $s = shift;
my $str;
for (0..length($s)-1) {
$str .= sprintf "%0x", ord substr($s, $_, 1);
}
return $str;
}
sub dec2hex(){
$dec=shift;
$hex=sprintf "%0x",$dec;
return $hex;
}
一算,完全没问题,跟程序算出来的一模一样。感觉离结果更近了有没有?
继续。
http://www.52pojie.cn/static/image/hrline/4.gif
004012CB .BE EC114000 mov esi,160-24-C.004011EC ;字串的位置,共62个dword
004012D0 .B9 3E000000 mov ecx,0x3E
004012D5 .33DB xor ebx,ebx
004012D7 .EB 04 jmp short 160-24-C.004012DD
004012D9 14 07 adc al,0x7
004012DB 18F0 sbb al,dh
004012DD AD lods dword ptr ds:
004012DE 33D8 xor ebx,eax
004012E0 49 dec ecx
004012E1 ^ 75 FA jnz short 160-24-C.004012DD
004012E3 .81FB FBCFFCAF cmp ebx,0xAFFCCFFB
004012E9 ^ 74 EE je short 160-24-C.004012D9 ;疑似的关键跳转。
继续F8。我们发现程序指定了一个内存位置004011EC做起始,和一个计数器3E(即62),然后跳向004012DD循环读取Dword。以EBX=0起始,每读取依次均与EBX异或,直至循环完成。简单的说,就是0与62个Dword依次异或。最后判断这个结果是否等于0xAFFCCFFB.如果不等的话回到004012D9。这次对al操作先加7再减D,随后又进入上面那样的异或的循环。
你是不是隐约感到抓住关键判断和关键跳转了?Too young too simple, sometimes naïve。 我们不能让004012E9跳转,因为跳转了就会进入一个无穷无尽的循环;可是不跳转马上就报错了,怎么办啊?
于是想到那我修改004012E3处的校验码0xAFFCCFFB不就可以了?嘿嘿,难不到我啦!
楼主还是用perl算啦。先从004011EC到004012E3复制出来一点HEX,然后用它们做异或,只要我得到这个结果,替换原先的校验码0xAFFCCFFB一定可以的,哈哈。
说干就干,还是perl;
#!/usr/bin/perl
#下面这个字符串就是用来校验的;
$cons="558BEC83C4FC8B450C83F810750D6A00E86B02000033C0C9C2100083F80F750E8B4508E81801000033C0C9C2100083F801750633C0C9C210003D110100000F85E70000008B45143B0560314000751A6A00689630400068A7304000FF7508E81702000033C0C9C210003B0558314000740C3B05543140000F85AE000000C705D9124000544558006A008D45FC506A64FF3550314000E8BC010000837DFC00745F506A14686C314000FF3554314000E8AF01000085C07448A10B304000BB6C31400003034381FB7C31400075F55B03C33105D9124000C1E810662905D9124000BEEC114000B93E00000033DBEB049306F100AD33D84975FA81";
$seed="0x00000000";
for($i=1;$i<63;$i++){
$curstr=substr($cons,($i-1)*8,8);
@splitstr=split(//,$curstr);
$curstr=join('',$splitstr,$splitstr,$splitstr,$splitstr,$splitstr,$splitstr,$splitstr,$splitstr);
$seed=hex($seed) ^ hex($curstr);
$seed=sprintf "%0x",$seed;
}
print $seed,"\n";
好了,根据以上脚本的计算结果,楼主兴冲冲的把得到的结果0adcb7fb替换了0xAFFCCFFB。保存,重新运行,我靠了,居然还是“Your serial is not valid.”
http://www.52pojie.cn/static/image/hrline/4.gif
楼主只好重新一步步对比异或循环的结果,一个数值一个数值的对。一直到快接近字串末尾的049306F1运算时,居然跟eax的值不相同了。下一个数值00AD33D8出现了同样的情况。这样异或运算到最后的时候,已经不是0adcb7fb了。
真TM的见鬼了,一个字符串还能不相同,我明明是拷贝过去的啊!
楼主在此盘旋了好久好久,就是想不明白。于是把004011EC到004012E3内存位置下了一个写入断点,看看到底发生了什么诡异事件。
http://www.52pojie.cn/static/image/hrline/4.gif
结果断在了这里:
00401269 C705 D9124000 5>mov dword ptr ds:,0x584554 ;vengers
什么,那个校验用的字符串被修改了,校验值还能对吗?可是这程序把自己修改了,TM的居然还能正常运行。
继续运行,又断在了以下两处:
004012BB 3105 D9124000 xor dword ptr ds:,eax ;
004012C4 66:2905 D912400>sub word ptr ds:,ax ;
我一下恍然大悟了,这不是之前觉得毫不起眼的那几句指令啊!程序把自己修改了,而且修改的一定是关键的跳转,否则我们无法解释找不到合理的关键跳的理由。那怎么找回原来的代码呢?程序之前给出的校验码就能起到这个作用啊。利用校验码倒推!
楼主看了下,这几处修改涉及了第60和61个字符串,根据00401269、004012BB和004012C4的运算可以推出正确的代码应该是04XXXXXXXX和XXAD33D8的样式。我们现在把这些X算出来。
因为其他60个字串时候不变的,可以算出他们的累计的异或值,再与校验码0xAFFCCFFB异或,即为04XXXXXXXX和XXAD33D8异或的值。楼主这里就不贴perl脚本了。这样就算出他们分别是:04EB2654和58AD33D8。
验证一下。
好了,楼主又兴冲冲的在反汇编窗口把那些被修改的指令修改乘了推算的指令,奇迹出现了:
004012D7 . /EB 04 jmp short 160-24-C.004012DD
004012D9 |EB 26 jmp short 160-24-C.00401301 ;被复原的指令
004012DB |54 push esp
004012DC |58 pop eax
004012DD \AD lods dword ptr ds:
004012DE 33D8 xor ebx,eax
004012E0 49 dec ecx
004012E1 ^ 75 FA jnz short 160-24-C.004012DD
004012E3 .81FB FBCFFCAF cmp ebx,0xAFFCCFFB
004012E9 ^ 74 EE je short 160-24-C.004012D9 ;
……
00401301 .68 73 30 40 00ascii "hs0@",0 ;YES! You found your serial!!
00401306 .FF35 5C314000 push dword ptr ds: ; |hWnd = 0037095E ('Your serial is not valid.',class='Edit',parent=00430852)
0040130C .E8 67010000 call <jmp.&USER32.SetWindowTextA> ; \SetWindowTextA
原来被修改的是一个关键的跳转!而且,这个跳转直接指向序列号正确的提示便签那里!
问题好像解决了。保存,运行,我去,还是提示“Your serial is not valid.”!
http://www.52pojie.cn/static/image/hrline/4.gif
什么?指令都按照校验的要求修改过了,怎么还不对?忽然想起曾经被我忽略的那三条语句,它们每次校验前都会修-改-一-次指令,而且修改是根据输入的serial计算的。如果你计算出这个修改,并替换校验码的话,下一次运行它又变了!
那我就不让你修改内存指令了行不行?楼主二话不说,把上面3个指令全部nop掉。而且这次程序虽然不修改指令了,可是你nop掉不是又改了校验字串了吗?对。楼主又把校验那个004012E9的跳转指令从je改为了jne。
运行,serial随便输入一些数字,终于正确了:
http://www.52pojie.cn/static/image/hrline/4.gif
最后提醒一下,将修改后的字符串用我前面提供的脚本计算循环异或值的话,结果是0x1aeea0dd。所以,之前修改je后面的校验值改为这个数。我想起了这个软件的作者恶作剧的脸……
爆破部分就写到这里了。
http://www.52pojie.cn/static/image/hrline/4.gif
注册机。要使被修改了的代码正确的复原,必须是name和serial满足一定的计算关系。楼主用perl来实现serial的计算。
#!/usr/bin/perl
#name和serial需满足的条件:
#(1)以二者为参数进行运算,对内存代码进行修改,使0x004012D8-0x004012DF段成为正确的代码(04EB265458AD33D8);
#(2)为满足1,要求window-sum(name)应小于推算出来的(window-sum(name)+serial);或者说serial必须为正整数;因此name不是随意定的。
print "-" x 50,"\n";
$name=$ARGV;
$len=length($name);
@rev =$name=~/\w{1}/g;
$rev= join "",reverse(@rev);
$sum=hex(58455443);#"CTEX";
print "Window-sum(name) starts......\n";
for($i=1;$i<5;$i++){
$out=substr($rev,0,$i);
$out=hex(str2hex($out));
$sum += $out;
}
for($i=1;$i<$len-3;$i++){
$out=substr($rev,$i,4);
$out=hex(str2hex($out));
$sum += $out;
}
$sum_str=&de_overflow_hex($sum);
print "Window-sum(name) is: $sum_str\n";
print "Window-sum(name) done.\n";
print "-" x 50,"\n";
print "Serial computing starts......\n";
$m_low_digits_str=join("",(split(//,$sum_str)));
$m_high_digits_str=join("",(split(//,$sum_str)));
$low_digits_str=join("",(split(//,$sum_str)));
$high_digits_str=join("",(split(//,$sum_str)));
print "Low_digits,high_digits are in memory: $m_low_digits_str $m_high_digits_str.\n";
print "Actually low and high values are $low_digits_str,$high_digits_str.\n";
$tail=hex("5854")^hex("0058");
print "The high digits are 5854 xor 0058 =",dec2hex($tail),"\n";
$head=((hex("26eb")+$tail))^hex("4554");
print "The low digits are (26eb+",dec2hex($tail),") xor 4554 = ",dec2hex($head),"\n";
print "Window-sum(name)+serial should be: ",join("",dec2hex($tail),dec2hex($head)),"\n";
$serial=hex(join("",dec2hex($tail),dec2hex($head)))-hex($sum_str);
$serial_hex_str=dec2hex($serial);
print "The serial should be: ",$serial," or in hex: ",$serial_hex_str,"\n";
print "Serial computing done......\n";
print "-" x 50,"\n";
if($serial < 0){
print "Illegal name! Please try a new name with short length (<8 letters)and low ASCII value, i. e. 7654321;abcde;\n";
}
else{
print "Your serial is: $serial.\n";
}
print "-" x 50,"\n";
sub de_overflow_hex(){
$dec = shift;
# print "$dec will be de_overflowed...\n";
$dec %= 4294967296;#0x100000000;
$d2h = sprintf("%0x",$dec);
return $d2h;
}
sub de_overflow_dec(){
$dec = shift;
# print "$dec will be de_overflowed...\n";
$dec %= 4294967296;#0x100000000;
return $dec;
}
sub str2hex{
my $s = shift;
my $str;
for (0..length($s)-1) {
$str .= sprintf "%0x", ord substr($s, $_, 1);
}
return $str;
}
sub dec2hex(){
$dec=shift;
$hex=sprintf "%0x",$dec;
return $hex;
}
需要特别留心的是,name不能是随即定出来的,需要满足一定的条件才可以。所以可能要多试试几个Name。
http://www.52pojie.cn/static/image/hrline/4.gif
PS: 这是楼主的第二篇帖子,这一篇耗费的精力更大。之所以起这样一个题目,是为了提现楼主破解这个程序时左右彷徨的心境。楼主完全依照思维推导的流程走,所以里面有些是走了弯路,但完全是合理的。这样写帖子的好处是可以完整细致的与感兴趣的爱好者交换想法。上一贴@hmily大牛很贴心的告诉我要使用代码插入功能,我研究了下,试试,我的浏览器支持并不好。第一帖《破与追-160个crackme之09:Andrénalin.2》的地址在:http://www.52pojie.cn/thread-599913-1-1.html。欢迎大牛们指点。
请大家给我加分鼓励,谢谢!
lazy_bird 发表于 2017-4-23 20:14
萌新请问楼主这用的是什么软件-排版很漂亮的那个,也就是“双击Your serial is not valid来到函数调用的地 ...
这是插入代码功能。我的浏览器不能正常使用,是hmily大牛帮我排的,真是感激不尽。插件里有字符查找的,你找找看。 leyiang 发表于 2017-4-22 10:35
lz写的不错,分析的很仔细。但是另一个问题是,如果追码是不是就没办法了?
还有一种思路。如果让name运算后对代码的修改的与真代码相同就可以。我得算一算。 应该和浏览器没关系啊,你直接点代码框,把代码贴进去就好了,你那有什么不同吗? Hmily 发表于 2017-4-21 16:48
应该和浏览器没关系啊,你直接点代码框,把代码贴进去就好了,你那有什么不同吗?
我点击代码框没有任何反应。我是360极速浏览器是这样。 buddama 发表于 2017-4-21 17:39
我点击代码框没有任何反应。我是360极速浏览器是这样。
我私聊你看下是什么原因。 我先给你编辑下。 还可以呀。一般情况下多次判断可以使用GetWindowTextA,确实能多次断下。对于自修改,我记得之前某位大牛写过脚本来检测rlpack壳自修改的,但是应该也能运用到这些地方。最后膜拜下会perl的 cqr2287 发表于 2017-4-21 21:11
还可以呀。一般情况下多次判断可以使用GetWindowTextA,确实能多次断下。对于自修改,我记得之前某位大牛写 ...
对 可能跟壳的修改代码的行为差不多。我没研究过壳,推测这样。perl好入门的。 这是干什么用的 醉情 发表于 2017-4-21 23:59
这是干什么用的
这不是破解分析吗亲! 系列list列表在哪