puahad 和 popad#
加壳程序:unpackme.aspack.2.2
,可从互联网上搜索。
ida 打开,手动加载程序,取消勾选创建输入段。
加壳程序入口
上图中,第一条指令是pushad
,pushad
会把所有通用寄存器的值传到栈上。
pushad
按如下顺序将所有寄存器的值保存到栈上。
pushad 传值顺序
popad
是和pushad
相反的操作,它将栈中的值传出,按如下顺序保存到寄存器。
在大部分简单的壳中,使用了pushad
指令将寄存器的初始状态保存,然后再跳转到OEP
执行内存中的原始代码之前使用POPAD
恢复寄存器的初始状态。
根据这条规律,可以通过pushad-popad法
来轻松找到OEP
。
使用 idapython 进行调试#
什么是pushad-popad法
呢?
使用调试器来运行程序,运行调试器有两种方式,第一种通过菜单栏-调试器-选择调试器,选择local windows debugger
,第二种方式是使用 python 来运行调试器,这次选用第二种方式运行调试器。
1、通过import idc
导入 idc 模块,输入idc.load
然后按 tab 键补全,找到idc.load_debugger
,参数为idc.load_debugger("win32",0) //1为远程调试
,返回 True,代表执行成功。
载入 load_debugger 并设置参数
此时已加载了local windows debugger
。
在 pusha 之后设置断点
pusha 之后设置断点
pushad-popad法
是在执行 pushad 后一条指令之前,找到栈上保存寄存器值的位置,然后在该位置设置一个断点,在程序解密出原始代码之后跳转到 OEP 执行之前会通过 popad 恢复寄存器的初始值,然后触发该断点暂停执行,从而确定 OEP 的位置。
按 F2 键在 pusha 的下一条指令上设置断点,那么执行 pusha 后调试会暂停下来。(pusha 类似于 pushad)
如果使用 python 来设置断点,可以使用如下语句:
idaapi.add_bpt(0x46b002,0,idc.BPT_SOFT)
idaapi.add_bpt(0x46b002,0,0)
- 第一个参数是断点地址
- 第二个参数是断点的长度
- 第三个参数是断点的类型(软件断点 BPT_SOFT 或 0)
选择了调试器并且设置了第一处断点后,现在需要启动调试器运行程序然后暂停在该断点上,按 F9 键或者使用 python 语句运行程序。
idc.StartDebugger("","","");
在高版本(ida 7.7)中,上述命令报错,需要使用下述命令。
idc.start_process("","","")
执行上述语句后,调试器会运行到之前设置的断点处,也就是0x46b002
处。
运行到断点0x46b002
下图中,栈视图中的值就是 pushad 保存的寄存器的值,之后会通过 popad 指令读取,所以可以在第一行上设置一处断点。
在这次调试中,断点的位置是 0x19ff54,就是 esp 执行栈中的位置。
点击 ESP 右侧的小箭头,会跳转到对应窗口的该地址(此处是汇编窗口)。
按 F2 在此处可设置断点,修改成读取和写入时触发,而不是执行时触发。
如果窗口未出现,可以打开菜单调试器 - 断点 - 断点列表,然后右键选择编辑设置。
如果用 python 可以安装如下方式设置断点:
idaapi.add_bpt(0x19ff54,1,3)
断点列表如下
断点列表
上述代码中,第一个参数表示断点地址,第二个参数表示断点的大小,第三个参数表示类型,3 代表 read-write access(读取和写入),执行这语句和手动设置断点一样。
BPT_EXEC = 0,
BPT_WRITE = 1,
BPT_RDWR = 3,
BPT_SOFT = 4,
设置断点类型的参数
在断点列表中,右键单击第一次设置的断点,选择 DISABLE 禁用,或者使用如下 python 语句。
idaapi.enable_bpt(0x46b002,0)
- 第一个参数为断点地址
- 第二个参数如果为 1 则启用断点,0 为禁用断点
0x46b002 标记为绿色代表禁用,栈上的断口是红色为启用。
按 F9 键继续调试或者输入如下的 python 语句
idaapi.continue_process()
下图中,调试在 popad 指令取回寄存器初始值之后中断,然后程序将跳转到 OEP 也就是 0x4271b0,因为 push ret 就和 jmp 类似。
popa 指令
继续单步执行
执行到 oep。
既然找到了 oep 入口,那么就可以重新分析可执行文件,识别函数。
重新分析程序
使用 idapython 进行 dump#
找到了 oep 后,下一步就是进行转存,需要文件的基址以及可执行文件最后一个区段的最高地址。
上图可以知道,基址是 0x40000,最高地址是 0x46e000。
转存脚本如下:
import idaapi
import idc
import struct
start_ea = 0x400000
end_ea = 0x46e000
step = 4 # 每个地址处的数据占用4个字节
file_path = "dump.bin"
with open(file_path, "wb") as f:
for ea in range(start_ea, end_ea, step):
# 读取指定地址处的4个字节数据并进行小端字节序转换
bin_data = struct.pack("<L", idaapi.get_32bit(ea))
f.write(bin_data)
通过文件 - 脚本文件加载上面的 py 脚本,执行后,在当前目录生成一个 dump.bin 文件,修改其扩展名为 exe。
通过 peeditor 打开,打开区段视图,对所有的区段右键单击,选择 dumpfixer。
此时,图标已修复。
修复图标后,使用 Scylla 0.98 附加进程到被调试的加壳文件,目前执行到 OEP 入口。
加载此进程。
输入 oep 为 004271B0,单击 IAT Autosearch 和 Get Imports。
点击 show invalid,会发现有一处未能识别的 API,尝试自动修复失败,需要手动修复。
0x460818 处的 API
上图中,0x460818 是第一个有效区段的 API 函数,它上方更多的是未能识别的 API 地址。
检查第一个未能识别的 0x46080c 处是什么内容,按 D 键改变数据类型重组字节,如下图:
这里的内容不指向任何有效地址,如果按 CTRL+X,也没有任何引用。
而对于真正的 API 函数,应该有引用信息表明它在何处被调用。
所以,由于这些不是 API 函数,所以可以删除。
单击 clear 键,然后点击 IAT Autosearch,弹出的窗口选择否。
现在的 IAT 起点位与 0x460810,然后点击 Get Import,就可以找到所有的 API。
点击 Fix Dump 并转存文件,选择之前导出的文件。
最后脱壳程序正常运行。
到此处,整个程序的脱壳就算是结束了。