好友
阅读权限 10
听众
最后登录 1970-1-1
1.以壳解壳的原理
I.什么是stolen code。
=====================
关于stolen bytes
=====================
稍微说明一下:
每一种编译工具例如 : VC++ , Delphi , Borland , etc..
在OEP有一个唯一的/相同的PE头
其中的一些是这样的:
Push EBP
MOV Ebp,Esp
Add ESP , -010
Mov EAX, SOME_VALUE
(共11bytes)
或者:
Push EBP
MOV Ebp,Esp ;*
Add ESP , -010 ;**
Push EBX ;***
Push ESi
Push EDi
Mov EAX, SOME_VALUE ;****
(共14 bytes)
1.对于*的部分
原程序的OEP,通常是一开始以 Push EBP 和MOV Ebp,Esp这两句开始的,不用我多说大家也知道这两句的意思是以EBP代替ESP,作为访问堆栈的指针。
为什么要这样呢?为什么几乎每个程序都是的开头能?
因为如果我们写过C等函数的时候就应该清楚,程序的开始是以一个主函数main()为开始的,而函数在访问的过程中最重要的事情就是要确保堆栈的平衡,而在win32的环境下保持平衡的办法是这样的:
1.让EBP保存ESP的值;
2.在结束的时候调用
mov esp,ebp
pop ebp
retn
或者是
leave
retn
两个形式是一个意思。
这样做的好处是不用考虑ESP等于多少,PUSH了多少次,要POP多少次了,因为我们知道EBP里面放的是开始时候的ESP值。
2.对于**的部分
Add ESP , -010这种代码的意思是在堆栈区域开辟一块区域保存和使用局部变量,用EBP-XX来调用。
3.对于***的部分
在下来就是保存寄存器的初始值。
4.对于****的部分
初始化寄存器。
小节:我们知道了这就是程序在编译后程序OEP的一般形式,而某些壳在处理OEP的代码时,把这些固定的代码nop掉,然后把他们放到壳代码的空间里面(而且还常伴随着花指令)!使原程序的开始从壳空间开始,然后再JMP回程序空间。如果我们脱掉壳了以后,这一部分就会遗失,也就达到了反脱壳 的目的。这就是stolen code技术,或者更确切的说是stolen OEP code技术。
II.Replace Code
这是一种将原程序代码抽出,放到壳代码中执行的技术。应该就是一般说的SDK(Software Development Kit)技术。(其实我也不是很懂^^)
怎么实现的不清楚,但是他是如何体现出来的呢?
当你脱完壳了以后,运行发现提示“XXXXXX处不易读取”十有八九就是因为采用了SDK技术。因为当我们脱壳后这部分代码都没有了,我们怎么能读取呢?
总之我把replace code看成是又一种形式的stolen code,他偷掉的是程序中的的code。
III.以壳解壳的提出
上面的两种反脱壳的技术是横在我们面前的拦路虎,我们如何解决他呢?
办法有2个:
1.跟踪并且分析代码然后自己补充完整。
2.用以壳解壳的办法。
对于一些stolen OEP code我们好象还可以接受(还不太多嘛),但是如果SDK把成段成段的抽调代码,那我估计你能把那些代码补全了还不如自己去去把这个程序写出来^-^。而我们知道这些被偷掉的代码全在壳里面,只不过他不属于程序的空间,所以我们不能把他dump下来。那么我们想:如果我们把他们全都dump下来以后,那不就不用自己去补了。在这种想法的促使下,以壳解壳的方法诞生了。
接下来的问题便是:我们怎么dump下来呢?
但是在这个问题的前面应该是dump什么部分?按照前面所说的,如果我们在dump下程序了以后运行发现程序在读某一块内存区域,但是那一块区域却什么都没有,这时你便应该知道要dump什么区域了吧!
那么如何dump呢?
使用LordPE的 dump region...(区域dump)功能就可以了,选种自己想要的区域就可以直接dump下来了!
在接下来是该如何装入原来的文件呢?
先给大概的出步骤:
1.使用PE Editor的Load section from disk...(从硬盘载入区段头)功能增加一个区域,放入壳代码;
2.修改那个新区段的RVA;
3.重建PE文件(在重建选项一般保留“验正PE”就可以了)
对于1没什么好说的,我们来解释一下为什么要修改这个RVA。
代码在文件中有他们自己的位置,我们称为文件地址。而当他映射到内存的时候并不是像文件中的地址那样排列。他是按照RVA(相对虚拟地址)的地址+基址 来确定自己在内存中的位置的。所以我们要将原来壳代码所在的位置减掉基址添入就可以了。编辑使用edit section head....的功能。最后因为实际的PE文件和PE头里面的信息有写不一样(你都添加了一个区段当然不一样了),那么需要修复PE头,所以只选用验正PE,不建议在这个时候优化程序,因为我们一般的步骤是先添加区段壳代码,然后修复IAT,这个也是是要添加一个区段的哦!
----------------------------------------------------------------
3.总结
现在我们来回答开始提出的三个问题:
1.什么是以壳解壳?
以壳解壳是针对主程序的部分代码被偷,进而提出来的一种解决办法。采用添加原壳代码的的手段,达到使程序运行时仍能像未脱壳以前那样正确的访问到壳代码空间。可以说是一种“懒”办法,但是对于某些程序被偷得太多的时候却是一种无奈之举!
2.什么时候要以壳解壳?
当你发现被偷掉的代码很多,自己不能手动补充完整的时候。
3.如何以壳解壳?
上面给出了一个以壳解壳的实例。下面也是一个(明天再帖^^)
4.以壳解壳有什么不足?
因为以壳解壳采用了壳中的代码,那么如果在壳中代码中有部分是校验,或者是一些检验脱壳的代码,那么我们无异于是引狼入室。而且保留了壳代码,不仅增加了文件大小而且留下了原程序本来就没有的东西,总让人心理不是很爽。所以我的向来主张是以壳解壳的办法是最后的救命稻草,不要太过于依赖。
5.什么时候不能使用以壳解壳的办法?
当壳代码的RVA小于基址的时候!上面的方法将不在适用。
例如:壳代码的RVA=300000 而程序的基址=400000。
那时我们怎么修改这个RVA呢?我还没有找到合适的办法,期待那位能告诉我 ^-^。
问题在这个帖子里面解决了:
I.准备知识
1.内存状态
在一个进程里面可拥有的内存空间是4G,但是可以使用的仅2G。但是这2G是拿来就可以用吗?我们想写就写,想读就读?
不是的!
一个进程的内存空间里面分为3中状态:占用状态(commit),自由状态(free),保留状态(reserve)
占用状态:已经是可以读写的内存空间(其属性通常为:可读,可写,可执行......)
自由状态:不能读写的空间(其属性通常为:不可访问--NO ACCESS)
保留状态:已经申请了空间,但是却不能访问的状态(其属性通常为:不可访问--NO ACCESS)。这个状态很难理解,但是基本上我们可以这么认为:程序希望在自己喜欢的内存空间里面,先占好一个位置,虽然是“占着茅坑不拉屎”。但是不允许别人抢去!!
注意:这一篇我们重点讨论一下自由状态和占用状态,对于保留状态我们将不涉及。如果对内存管理这一章感兴趣的,请到罗云彬的《Windows 环境下32位汇编程序设计》的P372去看看。
2.VirtualAlloc函数
来看下函数VirtualAlloc的说明:
The VirtualAlloc function reserves or commits a region of pages in the virtual address space of the calling process. Memory allocated by this function is automatically initialized to zero.
LPVOID VirtualAlloc
(
LPVOID lpAddress, // address of region to reserve or commit
DWORD dwSize, // size of region
DWORD flAllocationType, // type of allocation
DWORD flProtect // type of access protection
);
我把大概的意思说一下,这个函数的用途是一个虚拟内存管理函数进程空间里面申请一段内存空间的。也就是将不可读写的内存转变成可读写的内存的用途。
再来看看他的参数:
lpAddress的意思是:你想申请的一个内存空间的首地址,比如是300000或者是600000;当他是NULL(就是0)的时候表示随机申请一个最合适的地址。
dwSize的意思是:申请的地址的大小。
flAllocationType的意思是:将申请的是占用状态(commit)还是保留状态(reserve),如果是占用状态此参数就是1000,如果是保留状态此参数就是2000。如果是占用状态+保留状态那就是1000+2000=3000。
flProtect的意思是:内存的属性。就是这个内存是可读的还是可执行的,或者是他们的组合。
最后要说的是函数的返回值:
大家都知道,函数的返回值都是存放在EAX里面的,如果这个函数成功的申请了一块内存,那么他的返回值就是这块内存的首地址。也就是说如果你填写了参数lpAddress,那么成功申请以后EAX=lpAddress。如果申请不成功呢?那么EAX=0。
II.解决低地址的思想
知道上面的东西,解决起低地址问题那就很简单了,这里只是介绍其中之一的一种思想。
首先,在多数壳里面都是使用VirtualAlloc函数申请内存空间的。
下面随着问题我们慢慢跟进:
Q1:壳里面有很多次的申请,用到了很多次的VirtualAlloc函数,我怎么知道那一个是申请对应的内存空间的?
A1:我们只要断下他,然后看他的返回值EAX,我们就可以知道他申请的是哪一个空间的地址了。
例如:我们下断bp VirtualAlloc后,用ALT+F9返回。看看EAX的值我们就可以知道他是在申请哪一段空间了。
Q2:可能你这时候会问一个问题:你直接看他的参数lpAddress是多少不就行了吗?
A2:如果你能提出这个问题,说明前面的知识你都掌握了。这个问题的答案是:很遗憾很多壳在申请空间的时候lpAddress是置0的,也就是 说他是随机申请的空间。
Q3:既然是随机的,那又为什么这些空间往往都老是同一个地址呢?
A3:还记得我前面对lpAddress解释的最后一句话吗。对!“最合适的地址”,也就是说壳在申请空间的时候老是认为那个低地址的空间就是最合适的,所以他就老是申请同一个地址。
Q4:我们知道了是哪一个函数又能如何呢?
A4:知道了是哪一个函数就好办了,还记得返回值吗?只要我们修改他成为一个已经申请好的高地址的内存空间,那么将来他写入的代码不都会进入高地址空间吗?
这里要说明一下:正因为是随机申请的内存空间,那么一切的写入这个空间的地址就决定于EAX,如果你还是觉得随机申请导致了你找到正确的申请函数而苦恼的话,那么我要说正因为是随机申请的我们才能用修改EAX的办法哦,因为如果壳早就知道要写入的是低地址的空间那么,他所有的代码都去访问一个低地址空间,那么即使你将这段代码放到了高地址空间又有什么用呢?但是大家也不必为此担心,壳作者一般考虑到壳的兼容性,一个壳不可能认准了就是那个低地址,因为如果那个地址正好被占用了,那就运行不了了。所以作者在用VirtualAlloc函数的时候都尽可能的使用随机申请,而写入申请到内存空间的代码的一切依据就来源于返回值EAX。
III.实战演习
目标:上一篇的老版hying的notpad,经过了上次,我们已经知道了replace code的地方是在370000开始的段。
http://www.popbase.net/bbs/dispb ... ;ID=1796&page=1
方法1:使用fly的“增肥法”
准备工作:1.先用winhex新建一个大小为16000(十进制)的文件(全为0)换算成十六进制就是大概在4000左右吧。
2.用lordpe将他填加为一个新的区段
3.rebuilr一下PE,注意只保留“验证PE一项”
这样一个全零的高位区段就被预先导入到PE文件里面,当他运行的时候就能一起在内存空间里面了,注意了这个区段开始的地址是412000。如图1
下面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
对命令行下bp VirtualAlloc F9后到这里:
7C809A81 k> 8BFF mov edi,edi ; ntdll.7C930738 //断在这里
7C809A83 55 push ebp
7C809A84 8BEC mov ebp,esp
7C809A86 FF75 14 push dword ptr ss:[ebp+14]
看看堆栈
0012FF9C 0040D174 /CALL 到 VirtualAlloc 来自 unpackme.0040D16E
0012FFA0 00000000 |Address = NULL //随机的
0012FFA4 000038D4 |Size = 38D4 (14548.) //大小大概是4000吧
0012FFA8 00001000 |AllocationType = MEM_COMMIT //申请占用模式
0012FFAC 00000004 \\Protect = PAGE_READWRITE //可读可写的属性
好了ALT+F9,回到壳代码
0040D174 50 push eax //回到这里
0040D175 8985 BB000000 mov dword ptr ss:[ebp+BB],eax
0040D17B 8B9D 8F000000 mov ebx,dword ptr ss:[ebp+8F]
EAX=370000
跟踪到过一次OEP以后你就会知道,370000这里就是replace code的地方。所以我们初步确定是这里。
将EAX改为412000,就可以让程序对内存为412000这个已经做好的高地址空间进行写代码了。
取消断点。
可以利用上次我说的内存断点方法到OEP去,这里采用一个快的办法。
在先不忽略了唯一的一个int3中断,接着不忽略“除零”异常以后来到这里:
00414660 F7F3 div ebx //到这里
00414662 90 nop
下面查找指令jmp eax
00414987 5D pop ebp
00414988 5B pop ebx
00414989 59 pop ecx
0041498A 5A pop edx
0041498B 5E pop esi
0041498C - FFE0 jmp eax ; unpackme.004010CC //跳到OEP去了
我们到OEP去看看吧~
004010CC FFD7 call edi ; unpackme.004010CE
004010CE 58 pop eax
004010CF 83EC 44 sub esp,44
004010D2 56 push esi
004010D3 90 nop
004010D4 E8 B5380100 call unpackme.0041498E //注意这里
004010D9 8BF0 mov esi,eax
看到004010D4这一行了没有,原来call向低地址的call,现在call向了我们指定的位置。现在就可以直接dump下来了。
总结一下:使用“增肥法”的本质是以壳解壳,利用PE文件在装入内存中就会申请到高位内存的办法,预先将一个无用的高位地址设置好,然后人为的改造壳写入代码的方向(修改EAX),最后dump到了一个包含了replace code的程序。
方法2.申请高位内存
对于方法1,我们总是觉得不是很舒服,因为内存中明明已经申请了一个内存,你又不用他,真是浪费啊。有背于现在提倡“绿色”“环保”的概念。所以下面我引入了方法2,但是其本质和方法1是一样的,只是顺序不同罢了,抛块砖给各位启发一下思维,理清一些思路。
我想:我们要想不浪费资源,我们就直接申请一个高位内存地址不就可以了。说干就干。
不需要任何的准备工作,直接载入OD:
对命令行下bp VirtualAlloc F9后到这里:
7C809A81 k> 8BFF mov edi,edi ; ntdll.7C930738 //断在这里
7C809A83 55 push ebp
7C809A84 8BEC mov ebp,esp
7C809A86 FF75 14 push dword ptr ss:[ebp+14]
看看堆栈
0012FF9C 0040D174 /CALL 到 VirtualAlloc 来自 unpackme.0040D16E
0012FFA0 00000000 |Address = NULL //随机的
0012FFA4 000038D4 |Size = 38D4 (14548.) //大小大概是4000吧
0012FFA8 00001000 |AllocationType = MEM_COMMIT //申请占用模式
0012FFAC 00000004 \\Protect = PAGE_READWRITE //可读可写的属性
到这里的时候,我们人为的修改参数lpAddress的值,让他指向高地址,让他在高地址空间申请内存。
修改为420000(这里要说明一下:不能修改成412000,因为涉及到内存页面对齐的问题,如果申请412000的内存,系统会认为你是要申请410000的内存,但是这一段已经是占用状态了,所以会申请失败)
如果只修改这个参数,那么我们是不能申请成功的。
我的理解是好象内存直接从自由状态到占用状态的过程我们认为他中间要经历过一个保留状态,所以在对flAllocationType参数赋值的时候,要加上一个MEM_RESERVE表示两种状态都进行申请。
但是你可能要问:为什么随机申请的时候就不用经历这个过程呢?
我的理解是随机申请,就像是系统在帮你做事不用交代这么清楚也可以的了^^
上面的解释都是我个人对这些参数的理解,只是便于记忆,可能有错误的地方!
下面是修改还的堆栈:
0012FF9C 0040D174 /CALL 到 VirtualAlloc 来自 unpackme.0040D16E
0012FFA0 00420000 |Address = 00420000 //一个高地址
0012FFA4 000038D4 |Size = 38D4 (14548.) //size就不用改了
0012FFA8 00003000 |AllocationType = MEM_COMMIT|MEM_RESERVE //MEM_COMMIT=1000 MEM_RESERVE=2000
0012FFAC 00000004 \\Protect = PAGE_READWRITE //原封不动
好了,不放心就用ALT+F9返回看看是否申请成功了。
0040D174 50 push eax //停在这里
0040D175 8985 BB000000 mov dword ptr ss:[ebp+BB],eax
看到已经申请到了420000的内存了没有,EAX=420000
再看看我们OD的内存空间,如图2。
好了,到OEP去看看吧
004010CC FFD7 call edi ; unpackme.004010CE
004010CE 58 pop eax
004010CF 83EC 44 sub esp,44
004010D2 56 push esi
004010D3 90 nop
004010D4 E8 B5180200 call 0042298E //呵呵~~和上面的不一样哦。
004010D9 8BF0 mov esi,eax
现在一个低地址的replace code就被放到了高地址,下面就用常规的以壳解壳的办法吧~我就不唠叨了。
总结一下:方法2和方法1本质上没有什么不同,也不是说更加简单了,只不过方法1把组装的工作做在前面,直接dump下了个带replace code的东西,但是方法2虽然更直观,让我们原来熟悉常规办法解壳的人容易上手,但是他需要了解的知识也更多一些^^
方法还有很多,但是在这里就不多说了。留给大家思考吧!!
--------------------------------------------------
3.总结
这一篇文章主要是讲述了低地址(低于基址的地址)转化为高地址原理和方法,扩大了以壳解壳的办法适用性。如果说对于修复的时候replace code的处理,难到了很多人。那么理解并使用以壳解壳的方法,无异于修复中的ESP定律那么简单,直观,方便。彻底掌握以壳解壳的办法将让我们对付90%的replace code不再畏惧。
相关文章:1.http://www.popbase.net/bbs/dispb ... ;ID=1662&page=2
2.Fly大侠的《梦幻OllyDbg 之ACPr修复篇——Divx Avi Asf Wmv Wma Rm Rmvb V3.23》
--------------------------------------------------
4.思考题
在定位VirtualAlloc函数的时候会发现多次申请到同样的内存,这是为什么呢?如果正好申请此地址就是replace code的区段应该怎么确定哪一个才是正确的呢?
--------------------------------------------------
5.附录
WINDOWS.INC中关于相关参数的定义
PAGE_NOACCESS equ 1
PAGE_READONLY equ 2
PAGE_READWRITE equ 4
PAGE_WRITECOPY equ 8
PAGE_EXECUTE equ 10h
PAGE_EXECUTE_READ equ 20h
PAGE_EXECUTE_READWRITE equ 40h
PAGE_EXECUTE_WRITECOPY equ 80h
PAGE_GUARD equ 100h
PAGE_NOCACHE equ 200h
MEM_COMMIT equ 1000h ***
MEM_RESERVE equ 2000h ***
MEM_DECOMMIT equ 4000h
MEM_RELEASE equ 8000h
MEM_FREE equ 10000h
MEM_PRIVATE equ 20000h
MEM_MAPPED equ 40000h
MEM_RESET equ 80000h
MEM_TOP_DOWN equ 100000h
--------------------------------------------------
发帖前要善用【论坛搜索 】 功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。