一个有着简单解码过程的Crackme分析
发表于:
2006-7-28 09:57
6922
看大家最近破CrackMe破得那么欢,我不出手是否有点跟不上形势了呢 下载地址:http://www.crackmes.de/users/ox87k/cry0k/
使用工具:IDA 这是个汇编写的Crackme,用OllyDbg无法加载,一加载就“非法操作”(是的,仅仅是加载,还没
按F9键运行,马上就报非法操作然后退出OD,找了半天没找到抗OD的地方在哪里,另外笔者对于反加载
的原理也不太了解,望这方面的高人指导是荷)。用IDA加载以后停在入口点处: ================================================================
.0x87k:0041F000 start proc near
.0x87k:0041F000 push ecx
.0x87k:0041F001 push edx
.0x87k:0041F002 push ebx
.0x87k:0041F003 push esp
.0x87k:0041F004 push ebp
.0x87k:0041F005 push esi
.0x87k:0041F006 push edi
.0x87k:0041F007 jmp short loc_41F030
; 一些垃圾字节
.0x87k:0041F030 loc_41F030: ; CODE XREF: start+7j
.0x87k:0041F030 xor eax, eax
.0x87k:0041F032 mov eax, 0BFE000h
.0x87k:0041F037 add eax, 666h
.0x87k:0041F03C mov ecx, 0FFF666h
.0x87k:0041F041 xor eax, ecx ; eax == 401000h
.0x87k:0041F043 mov edi, eax
.0x87k:0041F045
.0x87k:0041F045 loc_41F045: ; CODE XREF: start+65j
.0x87k:0041F045 xor ecx, ecx ; 解码器
.0x87k:0041F047 movzx ecx, byte ptr [edi]
.0x87k:0041F04A add cl, 3
.0x87k:0041F04D xor cl, 8Ah
.0x87k:0041F050 add cl, 2
.0x87k:0041F053 xor cl, 6Fh
.0x87k:0041F056 add cl, 1
.0x87k:0041F059 xor cl, 0CCh
.0x87k:0041F05C mov [edi], cl
.0x87k:0041F05E inc edi
.0x87k:0041F05F cmp edi, offset sub_401434
.0x87k:0041F065 jl short loc_41F045 ; 解码器
.0x87k:0041F067 mov eax, edi
.0x87k:0041F069 sub eax, 433h ; sub eax, 434h
.0x87k:0041F06E dec eax
.0x87k:0041F06F jmp short loc_41F081 ; 检测调试器
; 一些垃圾字节
.0x87k:0041F081 loc_41F081: ; CODE XREF: start+6Fj
.0x87k:0041F081 mov ecx, large fs:18h ; 检测调试器
.0x87k:0041F088 mov ecx, [ecx+30h]
.0x87k:0041F08B movzx ecx, byte ptr [ecx+2]
.0x87k:0041F08F dec ecx
.0x87k:0041F090 jl short loc_41F0FC ; 无调试器则跳
; 一些垃圾字节,当作代码执行就会非法操作,意图明显是抗调试的
.0x87k:0041F0FC loc_41F0FC: ; CODE XREF: start+90j
.0x87k:0041F0FC pop edi
.0x87k:0041F0FD pop esi
.0x87k:0041F0FE pop ebp
.0x87k:0041F0FF pop esp
.0x87k:0041F100 pop ebx
.0x87k:0041F101 pop edx
.0x87k:0041F102 pop ecx
.0x87k:0041F103 jmp eax
.0x87k:0041F103 start endp
================================================================ 入口点的地址和区段名都不寻常,但是这不要紧,当你看到eax == 401000h这里应该眼前一亮吧!这不
就是大多数程序的代码区段默认起始地址吗!然后接下来的这小段一看就知道是在给地址为401000到
401434的代码段解码,呵呵,为什么一看就知道呢?因为它让我想到几个名词:病毒,多态变形……扯
远了吧,因为那几个异或操作太典型了,如果你的病毒用这种操作进行编码加密的话,绝对只有被杀毒
程序秒杀的份。
运行IDA中附带的调试器,让这段代码解码完毕,等EIP到达41F06F时把它重置到41F0FC,再执行几
句,就到达程序的真正入口点了。(据说可以用IDC脚本命令模拟这段解码的过程,可惜我不会用。)
入口点附近一如既往的可以看到GetModuleHandle,DialogBoxParam和ExitProcess这些函数,从中找出
对话框过程的指针并依此找到对话框过程,进入到处理WM_COMMAND消息的分支,来到: ================================================================
.text:004010BC loc_4010BC: ; CODE XREF: .text:004010B3j
.text:004010BC cmp byte_406134, 0
.text:004010C3 jz short loc_4010CC
.text:004010C5 mov byte_406134, 0 ; 停止校验和的线程,因为下面要改写代码段
.text:004010CC
.text:004010CC loc_4010CC: ; CODE XREF: .text:004010C3j
.text:004010CC push 32h
.text:004010CE push offset byte_406089
.text:004010D3 push 68h ; 序列号输入框ID
.text:004010D5 push dword ptr [ebp+8]
.text:004010D8 call loc_4013FE ; GetDlgItemText
.text:004010DD cmp eax, 0Fh ; 正确序列号必须是15字符
.text:004010E0 jnz short loc_401127
.text:004010E2 mov edi, offset loc_401111
.text:004010E7 xor ecx, ecx
.text:004010E9 xor edx, edx
.text:004010EB
.text:004010EB loc_4010EB: ; CODE XREF: .text:004010FEj
.text:004010EB movzx ecx, byte_406089[edx]
.text:004010F2 xor cl, 87h
.text:004010F5 add cl, 1 ; 对输入内容进行解码
.text:004010F8
.text:004010F8 loc_4010F8: ; 写入到下面nop组成的空白区
.text:004010F8 mov [edi], cl
.text:004010FA inc edx
.text:004010FB inc edi
.text:004010FC cmp edx, eax
.text:004010FE jnz short loc_4010EB
.text:00401100 mov edi, offset loc_401111
.text:00401105 mov cl, [edi]
.text:00401107 cmp cl, 68h
.text:0040110A jz short loc_401111
.text:0040110C cmp cl, 90h
.text:0040110F jnz short loc_401120
.text:00401111
.text:00401111 loc_401111: ; CODE XREF: .text:0040110Aj
.text:00401111 ; DATA XREF: .text:004010E2o ...
.text:00401111 nop
.text:00401112 nop
.text:00401113 nop
.text:00401114 nop
.text:00401115 nop
.text:00401116 nop
.text:00401117 nop
.text:00401118 nop
.text:00401119 nop
.text:0040111A nop
.text:0040111B nop
.text:0040111C nop
.text:0040111D nop
.text:0040111E nop
.text:0040111F nop
.text:00401120
.text:00401120 loc_401120: ; CODE XREF: .text:0040110Fj
.text:00401120 pop esi
.text:00401121 pop edi
.text:00401122 pop ebx
.text:00401123 leave
.text:00401124 retn 10h
.text:00401127
.text:00401127 loc_401127: ; CODE XREF: .text:004010E0j
.text:00401127 push offset aInsertARightSer ; "Insert a right serial ;)"
.text:0040112C push 6Ah ; 信息显示框ID
.text:0040112E push dword ptr [ebp+8]
.text:00401131 call loc_40141C ; SetDlgItemText
================================================================ 这里只能找到显示“序列号不正确”信息的代码,却没有显示“序列号正确”的,这是为什么呢?查看
一下数据区段(起始地址在406000处)就会发现“正确信息”还是存在的,而且就在偏移量等于406000
的地方,但是却没有任何指向它的引用。再看看401111到40111F这个地方,为什么是一片空白的nop指
令呢?按理说这个地方本应该是“显示正确信息”的代码才合理呀!再结合4010EB开始的这个解码循环
(又是典型的异或操作,一看就知道是在解码!),程序的意图就明朗化了:把输入的序列号(字符编
码)当作加密过的机器指令序列进行解码,解码后写入到401111开始的空白区,如果输入的是正确的序
列号,解码完毕以后应该变成恰当的指令序列,使得“序列号正确”信息的字符串能够被访问到,并且
将其在信息显示框中显示出来。
让我们考虑这片空白区中都能填入什么,最自然的是填入一个完整的SetDlgItemText调用:
00401111 68 00604000 push 00406000 ; 406000 = offset szInfoSerialIsRight
00401116 6A 6A push 6A
00401118 FF75 08 push dword ptr [ebp+8] ; [ebp+8] = Arg1 = hWnd
0040111B E8 FC020000 call 0040141C
00401120 ....
因此,机器码是:
68 00 60 40 00 6A 6A FF 75 08 E8 FC 02 00 00
算一下正好15个字节,刚好填满空白区。但这不是唯一的答案,因为注意到下面显示“序列号错误”用
的也是同一个函数调用,只有最后一个参数不同,我们完全可以在将“正确信息”的部分参数入栈后跳
到下面那个调用的相应位置,“借用”该调用的剩余部分。所以答案也可以是:
00401111 68 00604000 push 00406000
00401116 EB 14 jmp 0040112C
...
或者是:
00401111 68 00604000 push 00406000
00401116 6A 6A push 6A
00401118 EB 14 jmp 0040112E
...
甚至还可以是:
00401111 68 00604000 push 00406000
00401116 6A 6A push 6A
00401118 40 inc eax
00401119 48 dec eax
0040111A EB 12 jmp 0040112E
...
(又想到了一个词:多态变形……奇怪,为什么我最近总是想到这个词)注意:jmp指令后剩余的空白
区填入什么已经不重要了,哪怕不能当作代码执行也不要紧,因为它们终归会被跳过,这些余下的空白
区实际上是可以任意填写的。总之答案的数目可以相当多,我们只以第一个为例,解码操作是字节先与
87h异或再加1,那么加密操作就应该是先减1再与87h异或,因此:
68 00 60 40 00 6A 6A FF 75 08 E8 FC 02 00 00
67 FF 5F 3F FF 69 69 FE 74 07 E7 FB 01 FF FF 逐字节减1
E0 78 D8 B8 78 EE EE 79 F3 80 60 7C 86 78 78 与87h异或――得到序列号
结果中有许多字符已经不是可显示字符了,象编码等于E0的字符甚至连ASCII字符都不是,我不知道这
种字符在文本输入框中应该怎么打。
顺带说一下:对这个CrackMe还是以静态分析为主,调试器只起到辅助解码代码段的作用。不要尝
试在主要代码区域下断点或者修改字节之类的操作,因为程序在对话框初始化的时候建了四个线程,第
一个线程循环扫描主要代码区域是否有Int3断点,第二个线程循环检测建立第一个线程的call指令是否
被修改过,第三个线程计算初始化时主要代码段的双字校验和,第四个线程循环计算程序运行期间的校
验和是否与初始值相等,这四个线程中除了第三个在计算一遍完毕以后就退出以外,其他三个都是常驻
的(当然在写入上述空白区域之前要先让第四个线程退出,否则体现程序正常功能的写入操作也将被视
为企图打补丁),只要让它们中任何一个发现不对劲,马上就向主窗口发送关闭消息。当然原理上说是
可以把建立这些线程的代码全部刷白,不过我连抗OD的部分都没找到,对于哪些地方该刷,心里也很没
谱。
按照惯例本来应该把这个程序逆向出源代码来的,只是程序中似乎有声音资源,处理这个资源的代
码远比主要代码繁杂,不是一时半会能搞定的,所以只能先干正事了。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!