逆向基础-脱壳

脱壳

脱壳的步骤:
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


逆向基础-脱壳_第1张图片

给程序进行脱壳要做的第一步,查看程序的壳信息。

然后我们就知道这是一个UPX的壳,然后是VC6编译的。

然后我们发现区段那些的确都消失了。

UPX壳就是把我们区段进行了合并,然后压缩。

逆向基础-脱壳_第2张图片

逆向基础-脱壳_第3张图片

通过给esp添加硬件断点。

然后我们观察内存,最后一个jmp的确跳的很远。
直接跳回去了第一个区段,我们知道进行加壳之前我们的第一个区段就是我们的代码段。
那么就是说他跳回去代码段了。

很可能就是我们的OEP。
然后我们发现调用的第一个API是GetVersion,那么就确定的确是我们的入口OEP了。

逆向基础-脱壳_第4张图片

然后我们手工dump我们的文件。【其实就是PE文件的逆操作,把PE文件从内存加载到硬盘,唯一不同的是这个时候我们dump出来的是按照内存对齐的了,不是文件对齐的了】

我们一块一块自己粘出来。
  -1.先dump PE头
  -2.然后dump剩下的区段
就是把pe文件dump出来
【dos头,pe头,区段头,区段...】

逆向基础-脱壳_第5张图片

然后dump出来以后我们要对exe进行修复。
通过010 Editor
  -1.修复区段头信息【区段的大小 偏移等信息】修改跟内存一样就好了,因为现在我们是内存状态dump出来的
  -2.通过ImportREC.exe这个工具修复IAT表
    -它就是依据我们上面说的FF15是调用IAT表的函数,它往上和往下进行查找然后把函数名和函数地址那些找到
    -修复的原理就是重新开一个区段然后根据原本我们dump出来的函数地址进行修复。

然后我们就发现IAT表被成功修复了。

然后发现程序就可以正常被执行了

重点:我们dump出来是以内存状态的,这个时候IAT表已经被修复为地址了,所以我们再启动因为不是名字,所以操作系统不会帮我们进行修复。
所以我们要修复为地址先

逆向基础-脱壳_第6张图片

逆向基础-脱壳_第7张图片

逆向基础-脱壳_第8张图片

脱壳练习

发现这是一个VC6.0编译的壳,所以OEP的特点也是GetVersion
然后壳的版本是ASPack

逆向基础-脱壳_第9张图片

逆向基础-脱壳_第10张图片

然后发现第二个程序也是VC6.0编译的,
然后壳的版本是yoda's cryptor 1.2

逆向基础-脱壳_第11张图片

然后我们发现它不是直接存储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做了手脚。

逆向基础-脱壳_第12张图片

逆向基础-脱壳_第13张图片

脱壳的思路:
  -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 "脚本执行完毕"

你可能感兴趣的:(逆向,c++,windows,microsoft)