作者:看雪 文章来源:Internet 点击数: 更新时间:2006-5-14 1:16:52  |
|
第六课 寻找OEP
一般的压缩壳,如Aspack等都有专用的脱壳机 。而加密壳(如ASProtect,Armadillo) 一般很少有脱壳机,必须手工脱壳。手工脱壳一般情况是分三步:一是查找程序的真正入口点(OEP);二是抓取内存映像文件;三是输入表重建。(当然现在的加密壳复杂些,要考虑更多的东西) OEP是Original Entry Point缩写,即程序加壳前的真正的入口点。 外壳初始化的现场环境(各寄存器值)与原程序的现场环境是相同的。加壳程序初始化时保存各寄存器的值,外壳执行完毕,会恢复各寄存器内容。其代码形式一般如下:
PUSHFD ; 将标志寄存器入栈保存 PUSHAD ; push eax, ecx, edx, ebx, esp, ebp, esi, edi …… ; 外壳代码部分 POPAD ; pop edi, esi, ebp, esp, ebx, edx, ecx, eax POPFD ; 恢复标志寄存器 JMP OEP ; OEP: …… ; 解压后的程序原代码
为了讲述方便,本节用UPX加壳的Win98记事本来演示。首先用PEid查看加壳前的记事本:
PEid显示Notepad.exe程序是用Microsoft Visual C++ 6.0编译的,接下来用UPX来加壳,方法是开个DOS窗口,用命令upx notepad.exe。如下图所示:

这时再用PEid查看加壳的文件,PEid会给出如下信息:UPX 0.89.6 - 1.02 / 1.05 - 1.24 -> Markus & Laszlo

UPX的壳可以用UPX.exe自身来脱,命令是:upx -d 文件名 。一些变种的UPX壳用UPX.EXE自身脱不了,这时可以试试UPX ShellEx 这款工具。
本节实例下载:notepad.upx.rar
脱壳前建议用PE工具LordPE打开目标文件查看一下区块,以尽可能地多了解一些信息,对脱壳有帮助,如下图:
1.根据跨段指令寻找OEP
推荐用Ollydbg来调试脱壳,比SoftICE和TRW2000方便多了。运行Ollydbg,点击菜单“选项/调试设置”,将第一次暂停设在WinMain函数上。再用Ollydbg打开实例notepad.upx.exe就可中断在外壳的入口点处了:
上图相关代码如下: 0040E8C0 > 60 pushad //一开始Ollydbg就会中断这行,这个就是外壳的入口点,注意这个pushad指令
绝大多数加壳程序在被加密的程序中加上一个或多个段,所以依据跨段的转移指令(JMP)就可找到真正的入口点,此时就会有POPAD/POPFD指令出现。UPX 用了一次跨段的转移指令(JMP),在跳到OEP处会看到虚拟地址的值有一个突变,此时就能确定OEP了。 UPX壳比较简单,大家不必要跟踪去找这个跨段的转移指令,中断WinMain后,只需要在Ollydbg里往下翻屏,就会发现这个跨段转移指令:
上图相关代码如下: 0040EA0E 61 popad //注意这里的popad指令,和开始的pushad对应 0040EA0F - E9 B826FFFF jmp 004010CC //这里跳到OEP,将光标移到这,按F4执行到这行
这一句0040EA0F jmp 004010CC 就是跳到OEP的指令,执行到这,UPX外壳己将程序解压完毕,并模拟Windows加载器的将原始程序加载到内存,004010CC 就是映射到内存目标程序的入口点,此时就可抓取内存映像文件了。
2.根据堆栈平衡原理找OEP
这个堆栈平衡原理其找OEP原理这篇文档描述的比较详细:寻找真正的入口(OEP)--广义ESP定律 作者:Lenus 操作方法:多数壳在运行到OEP的时候ESP=0012FFC4,这就是说程序的第一句是对0012FFC0进行写入操作,只要在0012FFC0下硬件写入断点(命令行里键入HW 12FFC0),我们就能停在OEP的第二句处。
用OllyDBG重新加载实例程序notepad.upx.exe,在命令行下硬件写断点:
按F9执行程序,就会中断在OEP第二行:
此时如果将光标向上移,会发现第一句代码变乱了: 004010C7 000D 0A000055 add [5500000A], cl 004010CD 8BEC mov ebp, esp
这是因为Ollydbg将数据当汇编代码来分析了,你可以按 Ctrl+ALT+向上光标键 将当前显示的代码向上滚动一个字节就可看到正确的汇编代码了:
004010CC 55 push ebp 004010CD 8BEC mov ebp, esp //中断在这行 004010CF 83EC 44 sub esp, 44 004010D2 56 push esi 004010D3 FF15 E4634000 call [4063E4] ; kernel32.GetCommandLineA
中断后,别忘点击菜单“调试/硬件断点/”打开硬件断点面板,将刚才的硬件断点删除。
小知识: 硬件断点的原理 作者:Lenus
3.根据编译语言特点找OEP
各类语言编译的文件入口点都有一些规律,可以这利用这点来寻找入口点。 1)Delphi程序 执行程序,用LordPE(或Prodump)选dump(full)脱壳,存为dump.exe。接着用Hex Workshop打开dump.exe,搜索文本“runtime”,搜到后,向前查找离“runtime”最近的十六进制数字“55 8B EC”,数字所在的地址就是程序的OEP。 2)Visual C程序 可以利用Visual C启动部分的几个函数GetCommandLineA(W)、GetVersion、GetModuleHandleA(W)、GetStartupInfoA(W) 等来定位程序的OEP。
常见的各类编译语言的入口汇编代码都要熟悉,因为一些加密强壳会偷OEP处的代码到壳里,一般情况各编译语言入口代码都相同,到时只需要直接引用相关程序的入口代码,这给我们恢复代码带来方便。
4.用内存断点找OEP
Author:Lenus From: www.popbase.net & www.pediy.com E-mail:Lenus_M@163.com -------------------------------------------------- 1.前言 发现论坛中很多兄弟在询问:什么是二次内存断点,三次内存断点。还有很多人对内存断点的原理不是很明白。其实只要懂得壳是如何解压代码的,那么就完全可以按自己的喜欢来下断。 本文要解决的问题是: 1.什么是内存断点? 2.如何在寻找OEP时使用内存断点。 3.内存断点的局限性。 2.内存断点寻找OEP的原理 i.首先,在OD中内存断点,硬件断点和普通断点(F2下断)是有本质区别的。硬件断点等效与SoftICE命令bpm,他的中断要用到DR0-DR7的调试寄存器,也就是说OD通过这些DR0-DR7的调试寄存器来判断是否断下。 普通断点(F2下断)等效于bpx,他是在所执行的的代码的当前地址的一个字节修改为CC(int3)。当程序运行到int3的时候就会产生一个异常,而这个异常将交给OD处理,把这个异常的regEIP-1以后就正好停在了需要的中断的地方(这个根据系统不同会不一样),同时OD在把上面的int3修改回原来的代码。 而内存断点基本上使用的是对代码使用的保护属性来实现中断。
内存断点分为:内存访问断点,内存写入断点。 我们知道,在程序运行的时候会有3种基本的状态产生:读取,写入,执行。
004AE242 A1 00104000 mov eax,dword ptr ds:[004AE24C] //004AE24C处的内存读取 004AE247 A3 00104000 mov dword ptr ds:[004AE24C],eax //004AE24C处的内存写入 004AE24C 83C0 01 add eax,1 //004AE24C处的内存执行 那么我们应该如何中断在上面的几行呢? 1.当我们对004AE24C下内存访问断点的时候,可以中断在004AE242也可以中断在004AE247。 2.当我们对004AE24C下内存写入断点的时候,只能中断在004AE247。 3.当我们对004AE24C下内存访问断点的时候,能中断在004AE24C。
到这里你可能不明白了,为什么内存访问断点能中断在004AE247这一句对004AE24C的写入,而且还能中断在004AE24C的执行呢?
其实很简单,我们只要仔细体会一下“内存访问”这四个字的含义遍可以知道,当我们对004AE24C进行读取的时候需要“访问”他吧,当我对004AE24C进行写入的时候也需要“访问”他吧!!当然我们要执行内存地址004AE24C的代码的时候也是还是要“访问”他的!
所以我们不难得出下面的结论: 1.内存写入中断的地方,一定是也可以用内存访问中断。 2.内存执行的地方,也可以用内存访问中断。 如果这时你认为,那么内存写入岂不是没用了。呵呵~那我要告诉你当然不是,如果你想快速的准确的定位到004AE247这一行的时候,那么他就大有作用了!
总结一下:内存断点不修改改原代码,不会像普通断点那样因为修改代码被程序校验而导致中断失败;对于区段的访问只是区域大了一点,其原理和上面分析的三行代码是一样的。
ii.如何使用内存断点来寻找OEP呢? 要回答这个问题首先要回答这一个问题:壳是如何解压代码的?
正如我们知道的,壳如果要把原来加密或压缩的代码运行起来就必须要解压和解密原来的代码。而这一个过程我们难道不能将他看做是对代码段(code段)的写入吗?好了,解压完毕了。我们要从壳代码的区段JMP到原来的代码段的时候,难道不正是对代码段(code段)的执行吗? 理清了上面的关系就好办了,那么如果载入OD后,我们直接对code段下内存访问断点的时候,一定会中断在壳对code段的写入的代码的上面,就像上面的004AE247的这一行。而如果当他把code段的代码全部解压解密完毕了以后,JMP到OEP的时候,我们是不是还可以停在OEP的代码上面呢?而且每按下F9都会中断,因为这时code段在执行中哦!
相信很多人到这里已经明白了,为什么在教程中到达了某一个时候,某一行的时候。牛人们就叫我们对code段下内存访问断点了吧。 而如果你还要继续问我为什么一定要到那个地方才可以下断呢?我难道不可以一开始就下断吗? 正入我上面所说的,如果你在前面下断很可能壳对code段还没解压完毕呢,这时如果你不停的按F9,你将会看到OD的下方不断的在提示你“对401000写入中断” “对401002写入中断”“对401004写入中断”.......如果你不介意按F9到他把正个code段写完的话,我除了同情你的“F9”以外,没什么其他的意见! 那么我们就没有别更快一点的办法了吗? 有的!那就是我们呼之欲出的两次内存断点办法。 怎么理解两次内存断点呢?
让我来做一个假设吧,假设我是一个壳的作者。一个EXE文件的有code段,data段,rsrc段.....依次排列在你的内存空间中,那么我会怎么解码呢?呵呵~我比较笨一点,我会先将code段解码,然后再将data段解压,接着是rsrc段......那么聪明的你不难发现,只要你在data断或者rsrc段下内存访问断点,那么中断的时候code段就已经解压完毕了。这时我们再对code段下内存反问断点,不就可以到达OEP了吗?
这里注意上面虽然下了两次内存访问断点,但是本质是不一样的,目的也是不一样的。
1.对data段下内存访问断点而中断是因为内存写入中断,目的是断在对对data段的解压时,这时壳要对data段写数据,但是code段已经解压完毕。 2.对code段下内存访问断点而中断是因为内存执行中断,目的当然就是寻找OEP了。
总结一下:如果我们知道壳在什么地方对code段解压完毕我们就可以使用内存断点,找到OEP。如果不知道,那么我们就依靠2次内存断点去找,如果还不行就用多次内存断点。总之明白了原理在多次的内存断点其实都一样。从这个过程中我们了解的是壳在对区段解码的顺序!
iii.实战
说了这么多,我想大家都越越欲试了吧。 好吧,来弄一个猛壳怎么样: 注:本节实例有些难度,不适合新手练习,新手可以跳过这个实例的学习,等找到合适的实例会补充上来的
本节实例下载:unpackme.rar
这个壳是一个hying的旧版,我们用他来实验一下我们内存断点法。 OD载入以后来到这里
0040D000 u> 56 push esi //这里 0040D001 52 push edx 0040D002 51 push ecx 0040D003 53 push ebx 0040D004 55 push ebp 0040D005 E8 15010000 call unpackme.0040D11F
根据跟过一次的经验我们将先设置,除int3异常以外忽略其他异常,SHIFT+F9
003725B1 64:8925 0000000> mov fs:[0], esp 003725B8 CC int3 003725B9 90 nop //到这里 003725BA 8BCD mov ecx,ebp
然后再设置除“除零”异常外,忽略其他异常。SHIFT+F9
00372660 F7F3 div ebx //到这里 00372662 90 nop 下面是很多的单步异常,太麻烦我们不管他,现在开始用内存断点的方法(记得将所有异常忽略)。
对code段下内存访问断点,希望他已经解压完毕。方法是按ALT+M键打开内存窗口,在.code段按F2设断:
SHIFT+F9执行:
0040D19D A4 movs byte ptr es:[edi],byte ptr ds:[esi] //还没解完呢 0040D19E B3 02 mov bl,2
对data段下内存“写入”断点,试试看他是不是要写data段。
00372712 F3:A4 rep movs byte ptr es:[edi],byte ptr ds:[esi] //断到这里 00372714 5E pop esi
下面再对code段下内存访问断点。F9
00372855 8907 mov dword ptr ds:[edi],eax ; SHELL32.DragFinish //这里是对IAT加密
的地方了!!! 00372857 5A pop edx 00372858 0FB642 FF movzx eax,byte ptr ds:[edx-1] 0037285C 03D0 add edx,eax 0037285E 42 inc edx 0037285F 83C7 04 add edi,4 00372862 59 pop ecx 00372863 ^ E2 A9 loopd short 0037280E 00372865 ^ E9 63FFFFFF jmp 003727CD 0037286A 8BB5 93060000 mov esi,dword ptr ss:[ebp+693] //到这里下断F2
现在如果再对data下访问断点已经是没用了。这时应该格外的小心。
我们现在就想既然这一段是对code解码的,那么我们就绕过他吧!
到0037286A下断F2,然后清除内存断点!!!!
F9以后停在这里,继续对code下内存访问断点。
看看左下角还在解码,哎~真是麻烦!
003728E1 /EB 1D jmp short 00372900 003728E3 |25 FFFFFF7F and eax,7FFFFFFF 003728E8 |0385 83060000 add eax,dword ptr ss:[ebp+683] 003728EE |2B85 8F060000 sub eax,dword ptr ss:[ebp+68F] 003728F4 |8BDE mov ebx,esi 003728F6 |2BD8 sub ebx,eax 003728F8 |8958 FC mov dword ptr ds:[eax-4],ebx //停在这里 003728FB |83C7 08 add edi,8 003728FE ^|EB DB jmp short 003728DB 00372900 \64:FF35 30000000 push dword ptr fs:[30] //清除内存断点以后到这里下断,F9
又是一段解码的代码,再次使用上面的办法手动跳出去。
现在继续对code段下内存访问断点!!F9以后到达这里。
004010CC FFD7 call edi ; unpackme.004010CE //OEP哦 004010CE 58 pop eax 004010CF 83EC 44 sub esp,44 004010D2 56 push esi 004010D3 90 nop 004010D4 E8 B518F7FF call 0037298E 004010D9 8BF0 mov esi,eax
呵呵~虽然不是我们熟悉的OEP,但是地址是没错了,况且根据我们的步骤,我可以很肯定的说这是code段的第一次“执行”中断!
所以这就是OEP了。
总结一下:当我们在寻找OEP的时候,要多次对code下断“赌”一“赌”他解压完毕,如果不是就对别的段试试~如果程序跑飞了,那就没办法了,重来呗~其实说起来要赌的是:当data段,idata段,rsrc段摆在你的面前,你会好好“珍惜”那个段,不过还好上天还会给我们从来一次的机会(ctrl+F2 ^_^),那么我们会对那个不会跑飞的段说3个字----“先断你”如果非要在上面加一个次数,我希望是“一次内存断点就好了”
vi.下面来讨论一下内存断点的局限性问题。 是不是什么壳都可以用内存中断啊? 不是每个都可以的,一些像UPX和ASPACK就不行。 为什么? 呵呵~follew me! 情况1. 我们来看看UPX的壳 首先,他的壳代码在UPX1段。
这里是他要跳到OEP的地方
0040ED4F /77 11 ja short NOTEPAD_.0040ED62 0040ED51 |01C3 add ebx,eax 0040ED53 |8B03 mov eax,dword ptr ds:[ebx] 0040ED55 |86C4 xchg ah,al 0040ED57 |C1C0 10 rol eax,10 //在解码 0040ED5A |86C4 xchg ah,al 0040ED5C |01F0 add eax,esi 0040ED5E |8903 mov dword ptr ds:[ebx],eax 0040ED60 ^|EB E2 jmp short NOTEPAD_.0040ED44 0040ED62 \24 0F and al,0F 0040ED64 C1E0 10 shl eax,10 0040ED67 66:8B07 mov ax,word ptr ds:[edi] 0040ED6A 83C7 02 add edi,2 0040ED6D ^ EB E2 jmp short NOTEPAD_.0040ED51 //回跳解码 0040ED6F 61 popad 0040ED70 - E9 5723FFFF jmp NOTEPAD_.004010CC //跳到OEP
我们看到他在对code段解压完毕的时候马上就JMP到OEP去了,那么我们根本就来不及使用内存断点的办法。
你可能说,我可以在
0040ED6F 61 popad //这一句下段然后使用啊
呵呵~~当然可以,不过你把花在下内存断点的时间,多按下几次F8不更好?!
也就是说当一个壳如果他在JMP 到OEP前的一行代码仍在都在对code段解压,那么我们就不能再使用这种办法了!
或者说我们没必要使用内存断点更贴切一点!
情况2. 对于一些在OEP处有stolen code的代码 我们来看看一个OEP
0049E2F4 u> 55 push ebp //OEP 0049E2F5 8BEC mov ebp,esp 0049E2F7 83C4 F4 add esp,-0C 0049E2FA B8 BCE04900 mov eax,unpack.0049E0BC 0049E2FF E8 048CF6FF call unpack.00406F08 //这里调用子程序 0049E304 A1 B8FE4900 mov eax,dword ptr ds:[49FEB8] 0049E309 50 push eax 0049E30A 6A 00 push 0 0049E30C 68 1F000F00 push 0F001F 0049E311 E8 E68EF6FF call <jmp.&kernel32.OpenFileMappingA> //API 0049E316 A3 60194A00 mov dword ptr ds:[4A1960],eax 0049E31B 833D 60194A00 00 cmp dword ptr ds:[4A1960],0
这个软件在被PESPIN加壳了以后这些全被偷掉了!
也就是说,壳在模拟OEP代码的时候必然会执行
0049E2FF E8 048CF6FF call unpack.00406F08 //这一步
而这个地方是call向code段的。如果我们使用内存访问断点,那么就停在这个子程序的地方
00406F08 50 push eax //会停在这里 00406F09 6A 00 push 0 00406F0B E8 F8FEFFFF call <jmp.&kernel32.GetModuleHandleA> 00406F10 BA 04F14900 mov edx,unpack.0049F104 00406F15 52 push edx
这里既不是处理stolen code的地方,也不是FOEP的地方。这就会对我们的判断产生误导。
当然你可以alt+F9返回到壳处理stolen的地方,然后用内存断点,或者按几下F8到达FOEP处,但试问如果你拿到一个未知的壳的时候又怎么知道应该这么处理呢?
还有其他一些情况留给大家总结吧!
在下的砖已抛出,各位的玉不久矣。
-------------------------------------------------- 3.总结 好了说了很多,大家应该对内存断点的办法有了全面的了解,如果了解了内存断点的原理就不难明白他的使用方法,不难明白为什么有写壳不能使用内存断点的办法,其实任何的一种办法都需要经验的积累。相信如果大家在回答开篇的3个问题,已经不难了。 大家可以结合原理再好好的体会一下《手动脱壳进阶第八篇Skvp1.32》这篇文章。
|