随便拿Delphi7,新建一个VCL窗体程序,画一个按钮,写两行代码。这一步骤讲究的是什么呢?率性而为,反正没什么卵用。比如,俺写的是这玩意。
<span style="color:#666666"><span style="background-color:#ffffff">procedure TForm1.Button1Click(Sender: TObject);beginMessageBox(0, '鸿雁在天鱼在水,惆怅此情难寄!', '', MB_ICONINFORMATION);end;</span></span>
编译,打开,点按钮,确认一切无误。
给它套上一层UPX的外壳。
OllyDbg载入它,找到OEP不用我多说了吧?ESP定律干掉,或者往EP后面找popad,下面的jmp就是了。
中断在OEP后,随便找一个API调用语句,比如jmp dword ptr[xxxxxxxx],或者call dword ptr[xxxxxxxx]啦,具体是编译器来说话的。
比如我找到了这里。
在CALL上面按回车,跟进这个CALL看看。
转到DUMP窗口看看,用ADDRESS模式,发现这里就是IAT了。
这里有一个比较坑爹的问题了,我们知道啊,这桌面时代,就是Microsoft(巨硬)和Borland(宝兰)两个公司在干仗,开发工具一般就是这两家公司的产品。
这编译器构造PE的时候,导入表的处理就有分歧了。
Microsoft的IAT是整整齐齐,用一个DWORD类型的0,隔开每个对应每个DLL的IAT,而Borland就坑爹了,它的IAT是散乱的,也就是说,每个DLL对应的IAT表可能在内存分布中是不连续的。所以,脱壳的时候,修复IAT,这Borland就是急死人了,最好就是找到外壳填充IAT的地方,来Patch它得到一份完整的IAT表。
往下面翻一翻,就能找到MessageBoxA了。可以拉到IAT表头部之前,自己预判,然后往后面找MessageBoxA在你系统上的地址。
如果你用的Delphi是XE版本,那么要注意,你需要找MessageBoxW!
在0x4521AC下一个硬件写入断点(DWORD)长度,重新运行程序,找到外壳在这里填充IAT。
注意,我中断了两次,第一次发现0x4521AC里面是0,第二次才来到正确地方。
<span style="color:#666666"><span style="background-color:#ffffff">0046531A . 57 PUSH EDI ; 压入API名0046531B . 48 DEC EAX0046531C . F2:AE REPNE SCAS BYTE PTR ES:[EDI]0046531E . 55 PUSH EBP ; 压入DLL基址0046531F . FF96 F05A0600 CALL DWORD PTR DS:[ESI+0x65AF0] ; 取地址00465325 . 09C0 OR EAX,EAX00465327 . 74 07 JE SHORT Project1.0046533000465329 . 8903 MOV DWORD PTR DS:[EBX],EAX ; 填充</span></span>
现在,我们来考虑一下怎么给它设计一个API重定向。我参考了一下tELOCK壳。
00A80043 /EB 01 jmp short 00A80046
00A80045 |90 nop
00A80046 \E8 0B000000 call 00A80056
00A8004B 33C4 xor eax, esp
00A8004D 40 inc eax
00A8004E E9 0B000000 jmp 00A8005E
00A80053 98 cwde
00A80054 13C7 adc eax, edi
00A80056 B8 8EBC9725 mov eax, 0x2597BC8E
00A8005B C3 retn
00A8005C 0BC2 or eax, edx
00A8005E 90 nop
00A8005F B8 8B14A800 mov eax, 0xA8148B
00A80064 40 inc eax
00A80065 FF30 push dword ptr ds:[eax] ; kernel32.GetVersion
00A80067 C3 retn
<span style="color:#666666"><span style="background-color:#ffffff">00A8005F B8 8B14A800 mov eax, 0xA8148B00A80064 40 inc eax00A80065 FF30 push dword ptr ds:[eax] ; kernel32.GetVersion00A80067 C3 retn</span></span>
第一条硬编码,应该是壳解码后生成的,第二条加1,然后得到指针,取出API地址,用Push Ret模拟CALL。
我缩水一下,直接
push 地址
ret
这种情况修复很简单,比如ImportREC在不能识别的IAT里面,用上反汇编功能,就OK了。不过,这里不考虑那么多。方便第一。
首先给加壳后的程序加一个区块,区块的实际大小可以为0,可是加载在内存后的大小应该填写一个较大的值(自己把握)。不理解的话,参考《PE权威指南》一书。
区块的属性要可写可读可执行,注意VirtualSize字段。VirtualAddr是0x67000,这是RVA地址。
现在,用OllyDbg打开程序,来到0x467000,这里有了一块全0内存。而且,这块内存的好处是,不在硬盘里面,不占用文件尺寸,是PE被系统装载的时候系统给你的。
我们来设计一下我们的加密伪代码:
首先,我们要解密IAT的代码的机器码是:
68 xx xx xx xx C3
对应了push和ret,xx表示API地址。
一段占用了6字节,然后从壳填充IAT的地方,转到我们的Patch处。保存环境,生成我们新的加密后的IAT地址,转回壳里面,让壳填充。
注意,生成解密IAT代码的时候,我们需要一个全局变量指示新的IAT地址(写入机器码),写完一段递增一次。
现在,给这个程序再添加一个区块,换个工具,因为LordPE不会自己往文件后面补数据。这次要添加在磁盘里面实际存在的区块。用来存放要执行的加密代码。
<span style="color:#666666"><span style="background-color:#ffffff">pushadpushfdmov esi,dword ptr[477000] ;取出指针地址add esi,6mov edx,esimov byte ptr[esi],0x68inc esimov dword ptr[esi],eax ;从壳里面跳过来的时候,EAX是API地址add esi,4mov byte ptr[esi],0xC3mov dword ptr[0x477000],edx ;更新指针popfdpopadmov eax,dword ptr[0x477000]</span></span>
0x477000记得更新为0x466FFA
Patch完成后,看0x467000
修改的地方:
修复时,都是无效IAT。
打包一下,修改前后:
小结:
思路很简单,找到壳填充IAT的地方,添加两个区块,一个放解密IAT的代码(stub),一个放我们加密IAT的代码。从壳跳到我们的区块去执行加密,然后,嘿嘿。