如何使用调试器实现软函数挂钩?逆向工程新手求教
用WinDBG实现软函数挂钩的具体步骤
软函数挂钩的核心逻辑和Frida底层思路一致——修改目标函数入口的前几条指令,让程序跳转到自定义的钩子逻辑执行,完成后再回退到原函数继续运行。下面是针对x86程序的具体操作步骤(x64差异会单独标注):
1. 准备调试环境
- 启动WinDBG,要么直接用它启动目标CrackMe程序,要么通过
File -> Attach to a Process附加到已运行的目标进程 - 附加/启动后程序会自动暂停,确保处于暂停状态再进行后续操作
2. 定位目标函数的内存地址
- 如果是系统函数(比如
kernel32!MessageBoxA),用x命令查找地址:
执行后会返回类似x kernel32!MessageBoxA0x7ff6e1234567 kernel32!MessageBoxA的结果,记下这个地址 - 如果是CrackMe里的自定义函数,没有符号的话,需要通过反汇编、字符串交叉引用定位:
比如用u命令反汇编可疑内存区域:
(从0x401000开始反汇编32条指令),找到函数的入口地址u 0x401000 L20
3. 备份目标函数的前5字节
x86下的jmp rel32跳转指令需要占用5字节,所以必须先备份原函数的前5字节,后续卸载钩子或回退流程时要用:
- 用
db命令查看并记录这5个字节:
比如输出db 0x目标函数地址 L555 8B EC 81 EC E0,把这串字节完整记下来
4. 写入跳转指令到目标函数入口
- 先找一块可读写可执行的内存区域放钩子逻辑:可以用
!vadump命令查看进程的虚拟内存布局,找Free或Private类型的空闲页,或者用调试器调用VirtualAllocEx分配内存(比如在WinDBG里执行!process 0 0拿到进程ID,再用.process /i 进程地址切换上下文,然后调用VirtualAllocEx) - 计算跳转偏移:偏移值 =
钩子逻辑地址 - 目标函数地址 - 5(因为jmp rel32的偏移是相对于跳转指令的下一条地址,也就是原函数地址+5) - 用
a命令在目标函数地址处汇编跳转指令:
回车后WinDBG会自动计算偏移并生成5字节的机器码,覆盖原函数的前5字节a 0x目标函数地址 jmp 0x钩子逻辑地址 - 也可以手动写入机器码:用
eb命令直接写E9(jmp指令的opcode)加4字节小端序的偏移:eb 0x目标函数地址 E9 XX XX XX XX
5. 编写钩子逻辑
在之前确定的钩子地址处写入自定义逻辑,注意要保存和恢复寄存器,避免破坏原程序的执行环境:
- 用
a命令汇编钩子代码,比如简单的参数打印+回退逻辑:a 0x钩子逻辑地址 pushad ; 保存所有通用寄存器 ; 这里写你的自定义逻辑,比如打印函数参数、修改返回值 popad ; 恢复所有寄存器 jmp 0x目标函数地址+5 ; 跳回原函数的第6字节,继续执行原流程 - x64程序注意:需要保存
rbx、rbp、rsi、rdi、r12-r15这些非易失性寄存器,用push rbx; push rbp; ...和对应的pop指令
6. 验证钩子效果
- 用
g命令让程序继续运行,当目标函数被调用时,会先执行你的钩子逻辑,再回到原函数流程 - 如果要卸载钩子,把之前备份的5字节原指令写回去即可:
eb 0x目标函数地址 原字节1 原字节2 原字节3 原字节4 原字节5
关键注意事项
- 内存权限:目标函数所在的内存页默认是
只读+执行,修改前WinDBG会自动提示申请写权限,确认即可;也可以提前用!vprot 0x目标函数地址查看权限,手动调用VirtualProtectEx修改为可读可写可执行 - 断点冲突:如果之前在目标函数入口设了断点,修改指令后断点会失效,需要先删除断点再进行钩子操作
- 32/64位差异:x64下
jmp rel32同样是5字节,但寄存器调用约定不同,钩子代码要严格遵循调用约定保存寄存器
内容的提问来源于stack exchange,提问作者user13021032
相关产品推荐
相关产品推荐

