脱壳
脱壳的步骤:
1.程序运行先从壳代码运行,壳代码执行完以后会跳转到真正的OEP,也就是我们第一步要做的事情就是找到真正的OEP。
如何查找原始的OEP?或者说如何判断壳代码已经执行完毕了?
大部分情况壳代码都会在一个单独的区段里面,壳代码最后执行完一定会跳转到原始的.text段。
然后我们就可以dump出来了,但是dump出来以后我们还需要进行iat表的修复。
因为dump出来以后已经是内存的状态,是函数的地址,不是函数的名称了。那么操作系统就不会帮我们进行修复了。
根据OEP的特征来判断是否是原始的OEP。
不同程序,不同的版本编译器编译出来的程序的OEP各不相同,大致有以下的特点:
vc6的OEP处第1个API调用的是GetVersion
VS2013是GetSystemTimeAsFileTime
Delphi是GetModuleHandle
2.dump内存文件
3.修复导入表
FF15
E8
两个不同的call:
-E8直接就是call 立即数
-调用的是我们自己写的call
-FF15里面call 的是一个内存单元,这个内存单元中保存的才是真正的地址
-然后通过观察,我们发现FF15就是调用IAT表的一种call
总结:
-E8 call 调用的是我们自己定call
-FF15 call 调用的导入表IAT表的一种call
给程序进行脱壳要做的第一步,查看程序的壳信息。
然后我们就知道这是一个UPX的壳,然后是VC6编译的。
然后我们发现区段那些的确都消失了。
UPX壳就是把我们区段进行了合并,然后压缩。
通过给esp添加硬件断点。
然后我们观察内存,最后一个jmp的确跳的很远。
直接跳回去了第一个区段,我们知道进行加壳之前我们的第一个区段就是我们的代码段。
那么就是说他跳回去代码段了。
很可能就是我们的OEP。
然后我们发现调用的第一个API是GetVersion,那么就确定的确是我们的入口OEP了。
然后我们手工dump我们的文件。【其实就是PE文件的逆操作,把PE文件从内存加载到硬盘,唯一不同的是这个时候我们dump出来的是按照内存对齐的了,不是文件对齐的了】
我们一块一块自己粘出来。
-1.先dump PE头
-2.然后dump剩下的区段
就是把pe文件dump出来
【dos头,pe头,区段头,区段...】
然后dump出来以后我们要对exe进行修复。
通过010 Editor
-1.修复区段头信息【区段的大小 偏移等信息】修改跟内存一样就好了,因为现在我们是内存状态dump出来的
-2.通过ImportREC.exe这个工具修复IAT表
-它就是依据我们上面说的FF15是调用IAT表的函数,它往上和往下进行查找然后把函数名和函数地址那些找到
-修复的原理就是重新开一个区段然后根据原本我们dump出来的函数地址进行修复。
然后我们就发现IAT表被成功修复了。
然后发现程序就可以正常被执行了
重点:我们dump出来是以内存状态的,这个时候IAT表已经被修复为地址了,所以我们再启动因为不是名字,所以操作系统不会帮我们进行修复。
所以我们要修复为地址先
脱壳练习
发现这是一个VC6.0编译的壳,所以OEP的特点也是GetVersion
然后壳的版本是ASPack
然后发现第二个程序也是VC6.0编译的,
然后壳的版本是yoda's cryptor 1.2
然后我们发现它不是直接存储IAT的函数的
而是存了一个地址
那些地址是一条jmp指令
跳转过去才是真正的IAT函数
所以我们就要检测内存中什么时候被修改的,就是什么时候IAT表被替换为一个jmp的函数地址
然后通过观察我们还发现它的代码是动态解密出来的,有一条mov指令是关键指令会把IAT表替换为jmp的地址。
所以我们在它把代码动态解密出来之后,把那条关键的替换IAT表为jmp的地址指令NOP掉,NOP掉之后它就替换不了了,只能正常的进行解密。
然后我们发现我们nop掉,然后正常解密之后想dump出来,却发现程序不能正常执行。
然后通过观察我们发现在修复iat代码解密之后,还有解密下一段代码。
但是因为我们nop掉了,在某个地方它有判断,发现被操作了,所以它就没有继续解密了。
程序就无法进行了。
偷懒的方法:在解密完之后,我们就直接jmp真正的OEP了,其他代码我们也不让他执行了。
发现壳的版本是VC.6.0的,那么入口就是GetVersion
然后我们使用esp定律,但是我们发现程序没有断下来,直接执行了。
然后我们我们下面有一个pushad
我们让pushad执行完毕以后再下断点,然后发现这次停下来了。
也jmp到真正的OEP了,但是我们发现它又对IAT做了手脚。
脱壳的思路:
-1.找OEP
-2.dump内存
-2.1.先把真实的IAT还原回去
-3.修复IAT表
004E0895 8902 mov dword ptr ds:[edx],eax
我们发现EAX的值已经有了,就是它填进去的中转的函数地址
我们要查看EAX值的来源。
call xxxx
-esp - 4
lea esp,dword ptr ss:[esp+0x4]
-esp + 4
==== jmp
刚开始call,会把下一条指令的地址push 进入 然后esp - 4
然后接着我取esp + 4的地址给esp,就相当于把call抵消了
所以相当于一条jmp指令
Shellcode api调用:
我们对API函数名称进行了hash运算,拿着hash值进行比较,去遍历dll的导出表,然后对每一个api函数名称也进行hash运算,然后进行比较。
005C1C81 AC lods byte ptr ds:[esi]
005C1C82 E8 13000000 call 005C1C9A
005C1C87 8D6424 04 lea esort 005C1CA8
005C1C8E ^ EB F1 jmp p,dword ptr ss:[esp+0x4]
005C1C8B 5A pop edx
005C1C8C EB 1A jmp shshort 005C1C81
005C1C90 EB 39 jmp short 005C1CCB
从esi中取一个字节,丢到al中,加密的地方
005B0474 8BD0 mov edx,eax ; kernel32.GetVersionExA
005B0476 E8 86080000 call 005B0D01
005B047B 8D6424 04 lea esp,dword ptr ss:[esp+0x4]
005B047F 0345 C8 add eax,dword ptr ss:[ebp-0x38]
把eax的值给edx
8D6424 04
005B14DC 895401 FC mov dword ptr ds:[ecx+eax-0x4],edx ; kernel32.GetVersionExA
把API地址保存到了[ecx+eax-0x4]
005B0D73 8D6424 04 lea esp,dword ptr ss:[esp+0x4]
005B0D77 8B57 04 mov edx,dword ptr ds:[edi+0x4]
edx值被毁掉了
我们在edx被毁掉之前,把api的真实地址保存到了ebx中
005B0890 ^\E9 D7FCFFFF jmp 005B056C
005B0895 8902 mov dword ptr ds:[edx],eax ; 填充iat
005B0897 E8 39000000 call 005B08D5
填充IAT
然后在这里我们就可以通过ebx进行填充iat
我们就是要在覆盖之前,把它的API地址保存起来,保存到一个我们可以拿到的地方。
然后再用我们保存起来的真实的api地址来填充IAT表,这样IAT表保存的就是真实的API地址。
OD脚本编写:
-1.查找了调用VirtualAlloc之后的地址,下了断点
-2.查找获取API地址处,并转存我们API地址到ebx
-3.找到了填充IAT的地址,使用ebx正确的api地址进行填充回去
-4.执行到OEP并断下来
// 1.先定义变量
VAR VallocAddr
VAR getApiAddr
VAR setIatAddr
VAR oepPoint
VAR realApiAddr // 可以用变量 何必使用寄存器转存
// 2.初始化变量
MOV VallocAddr,0x47A37F
MOV oepPoint,0x47148B
// 这就是转存iat表的地址
MOV getApiAddr,0x14D8 // 距离申请出来的空间的偏移,我们到时候加上基址就好
// 这就是填充iat表的地址指令的下一条
// 我们等他填充了,然后再覆盖它填充的地址为真实地址
MOV setIatAddr,0x0897 // 距离申请出来的空间的偏移,我们到时候加上基址就好
// 3.清除断点信息
BPHWC // 清除硬件断点
BC // 清除断点
// 4.设置我们字节的断点
BPHWS VallocAddr,"x" // 硬件执行断点
BPHWS oepPoint,"x" // 硬件执行断点
RUN // 让程序运行起来 然后到我们的VallocAddr断点停下来
CMP VallocAddr,eip
JNZ error // 那么就是出错了
// 它停下来以后就意味着它申请的内存空间已经申请好了
ADD getApiAddr,eax // 那么我们就可以添加基址了
ADD setIatAddr,eax // 那么我们就可以添加基址了
BPHWS getApiAddr,"x" // 硬件执行断点
BPHWS setIatAddr,"x" // 硬件执行断点
RUN // 让程序运行到转存IAT的位置
CMP getApiAddr,eip
JNZ error // 那么就是出错了
MOV realApiAddr,edx // 保存edx的函数API地址
RUN // 让程序运行到修复完IAT表的下一行地址,即这个时候已经被填充加密之后的了
CMP setIatAddr,eip
JNZ error // 那么就是出错了
MOV [edx],realApiAddr // 然后把正确的地址覆盖下去
RUN // 现在就到OEP了
CMP oepPoint,eip
JNE error
JMP end
error:
MSG "脚本执行错误"
end:
MSG "脚本执行完毕"