逆向手的锋刃:IDAHook从入门到实战

admin 2026-05-14 11:32:32 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文系统介绍了IDAHook在逆向工程中的实战应用,重点解析了DBG_Hooks类的核心API使用方法,包括断点处理、寄存器操作和内存读写等关键功能。文章提供了可复用的Hook模板和CTF实战案例,展示了如何通过脚本化调试事件来提升逆向分析效率,特别适合处理高重复性动态分析任务。 综合评分: 85 文章分类: 逆向分析,安全工具,实战经验,CTF,二进制安全


cover_image

逆向手的锋刃:IDA Hook从入门到实战

S1nyer S1nyer

看雪学苑

2026年5月13日 18:10 上海

在小说阅读器读本章

去阅读

第一次接触 IDA Hook,是从“我想在断点命中时自动打印某些信息”开始的,再往后会发现,Hook 真正的价值并不只是“输出些信息”,而是它能把调试器事件统一交给脚本处理。

对逆向来说,这个能力很实用。很多题并不是静态分析做不动,而是你已经知道突破口在哪里,只是不想重复手点 50 次断点、手抄 40 次寄存器、手改 20 次内存这种机械工作。这个阶段,IDA Hook 往往就是最顺手的提速工具。

这篇文章主要做四件事:

  • 把 IDA Hook 的概念和工作方式捋清
  • 讲清ida_dbg.DBG_Hooks这一套核心 API 怎么用
  • 给出一套可复用的 Hook 模板,并说明如何同时 Hook 多个函数且每个断点执行不同逻辑
  • 结合CTF题讲解实战运用技巧

IDA Hook 原理

IDA 里有很多种 Hook:

  • UI Hook:界面事件
  • IDP Hook:处理器分析相关事件
  • Hex-Rays Hook:反编译器事件
  • Debugger Hook:调试器事件

逆向题里真正高频使用的,通常就是 Debugger Hook,也就是ida_dbg.DBG_Hooks

它的工作方式可以简单理解成:

调试器产生事件 -> IDA 派发事件 -> 你的 Hook 回调被调用

这些“事件”包括但不限于:

  • 进程启动
  • 动态库加载
  • 断点命中
  • 单步结束
  • 异常发生
  • 进程退出

也就是说,你可以不再把调试看成“手工点 Continue 和 Step”,而是把它看成一套事件流

为什么这对逆向有用

逆向里有很多高重复动作:

  • 每次命中某个函数入口,就看一遍参数
  • 每次某个比较发生,就记下比较两边的值
  • 每次状态机切换,就打印当前状态
  • 每次过某个检查点,就 patch 某个寄存器或内存

这些动作手点当然能做,但非常浪费时间,也很容易抄错。

Hook 的意义就在于把这些动作脚本化:

命中断点 -> 自动读现场 -> 自动记录/修改 -> 自动继续运行

所以从逆向视角看,Hook 本质上就是三件事:

  • 等事件
  • 读现场
  • 改现场

这里的“现场”通常包括:

  • 寄存器
  • 调试内存
  • 调用栈
  • 指令指针附近的代码与数据

这就是为什么很多 CTF 题里,Hook 往往不是“锦上添花”,而是直接把原本很蠢的重复劳动压缩成一个小脚本。

IDA Hook 核心 API

ida_dbg.DBG_Hooks到底是什么

DBG_Hooks是一个“调试器事件监听类”。最常见的写法是继承它,然后只实现你关心的回调:

import ida_dbg

class MyHook(ida_dbg.DBG_Hooks):
def dbg_bpt(self, tid, ea):
print("hit bp at %#x" % ea)
return 0

然后实例化并注册:

hook = MyHook()
hook.hook()

如果不再需要:

hook.unhook()

从写脚本的角度看,最常用的成员其实就是下面这些

hook()/unhook()

这两个方法是整个类最基础的入口。

  • hook()

    :把当前对象注册到 IDA 的调试事件分发器里

  • unhook()

    :取消注册

一般来说,脚本最后都会有类似这样的代码:

try:
    hook.unhook()
except Exception:
    pass

hook = MyHook()
hook.hook()

这么写的原因很实际:你在 IDA 里反复执行脚本时,旧的 Hook 对象很可能还活着。如果不先unhook(),就容易出现:

  • 同一个断点被多个旧回调同时处理
  • 输出重复
  • 旧逻辑还在后台继续报错

所以在调试阶段,“先卸旧 hook,再挂新 hook”几乎可以当成固定习惯

最重要的回调:dbg_bpt()

这是最常用的回调,断点命中时会触发它:

def dbg_bpt(self, tid, ea):
print("breakpoint hit at %#x" % ea)
return 0

参数里:

  • tid

    :当前线程 ID

  • ea

    :命中的地址

通常你会在这个函数里做几件事:

  • 判断当前命中的断点是不是自己关心的那个
  • 读寄存器
  • 读调试内存
  • 记录参数或状态
  • 修改寄存器 / 内存
  • 自动继续运行

很多 Hook 脚本,本质上就是围绕dbg_bpt()展开的

其他高频回调

除了dbg_bpt(),比较常用的还有这些。

dbg_process_start

进程启动时触发。常见用途:

  • 初始化脚本状态
  • 根据模块基址计算运行时断点
  • 自动下断点
def dbg_process_start(self, pid, tid, ea, name, base, size):
print("process start, base = %#x" % base)
return 0

dbg_process_exit

def dbg_process_exit(self, pid, tid, ea, code):
print("process exited with code: %d" % code)
return 0

进程退出时触发,常见用途:

  • 汇总输出结果
  • 打印恢复出的 flag / key / trace

如果你的脚本是“跑完整个程序,最后统一出结果”的类型,那dbg_process_exit()很适合做收尾

dbg_library_load

模块加载时触发,常见用途:

  • 等目标模块装载后再下断
  • 处理延迟加载、动态加载场景

dbg_exception

异常发生时触发,常见用途:

  • 观察反调试逻辑
  • 识别异常驱动控制流
  • 自动忽略某些故意抛出的异常

回调返回值怎么理解

大多数情况下你会看到大家在回调末尾写:

return 0

对常规逆向脚本来说,这么写就够了。更重要的不是返回值本身,而是你在回调里是否显式调用了:

ida_dbg.request_continue_process()
ida_dbg.run_requests()

如果你调用了这组接口,程序就会在处理完当前事件后继续跑;否则它通常会停在当前事件点,等你手工操作。

所以平时更应该关心的是:

  • 这个回调里我要不要自动继续
  • 我要不要在这里停下来人工观察

而不是纠结某个回调到底该return 0还是别的值。

自定义字段

DBG_Hooks这个类本身最重要的不是它自带什么字段,而是你可以在子类实例上自由挂自己的状态。

比如:

class Hook(ida_dbg.DBG_Hooks):
def __init__(self):
super().__init__()
self.bp = 0x401000
self.out = []
self.hit_count = 0
self.done = False

这些自定义字段在实际脚本里特别有用:

  • self.bp

    :保存关键断点地址

  • self.hit_count

    :记录命中次数

  • self.out

    :累计采样结果

  • self.done

    :防止重复输出

  • self.handlers

    :保存断点到处理函数的映射

也就是说,DBG_Hooks本质上是“事件回调容器”,真正让脚本变好用的,是你自己附加的那层状态管理。

DBG_Hooks的正确使用习惯

实际写脚本时,我建议把DBG_Hooks当成一个“调度壳”,而不是把所有逻辑都塞进去

比较好的风格是:

  • DBG_Hooks

    负责接事件

  • dbg_bpt()

    负责分发

  • 真正的采样 / patch 逻辑放到独立 handler 里

这样脚本会更清晰,也更适合后期扩展

配套的断点/内存/寄存器操作接口

ida_dbg.DBG_Hooks只是 Hook 的入口,真正配合它一起用的通常还有下面几类接口

增删断点

Hook 负责处理事件,断点负责制造事件。最常见的组合就是:

import idc

idc.add_bpt(0x401000)
idc.del_bpt(0x401000)

如果目标开了 PIE / ASLR,通常不要直接拿静态地址下断,而是先算运行时地址。

操作寄存器

读取/修改寄存器值非常常用:

import ida_dbg

rax = ida_dbg.get_reg_val("RAX")
rip = ida_dbg.get_reg_val("RDI")

ida_dbg.set_reg_val("RAX", 0)

内存读写

这是动态分析脚本最核心的配套接口之一:

import ida_idd

data = ida_idd.dbg_read_memory(ea, size)
ida_idd.dbg_write_memory(ea, b"\x90\x90")

配合struct.unpack很方便:

import struct

u32&nbsp;=&nbsp;struct.unpack("<I", ida_idd.dbg_read_memory(ea,&nbsp;4))[0]
u64 =&nbsp;struct.unpack("<Q", ida_idd.dbg_read_memory(ea,&nbsp;8))[0]

如果改了调试内存,最好刷新一下缓存:

ida_dbg.invalidate_dbgmem_contents(ea, size)

自动继续

如果你想让程序在处理完事件后自动继续跑,通常会写:

ida_dbg.request_continue_process()
ida_dbg.run_requests()

这组接口很适合“批量采样型脚本”,也就是你不希望程序每次命中都停下来等你点 Continue,而是希望它一路跑,把你要的数据全记下来。

一个最小可用示例

比如你只想在某个断点命中时打印RDIRSI

import&nbsp;ida_dbg
import&nbsp;idc

BP =&nbsp;0x401234

class&nbsp;Hook(ida_dbg.DBG_Hooks):
&nbsp; &nbsp; def dbg_bpt(self, tid, ea):
if&nbsp;ea != BP:
return&nbsp;0

&nbsp; &nbsp; &nbsp; &nbsp; rdi = ida_dbg.get_reg_val("RDI")
&nbsp; &nbsp; &nbsp; &nbsp; rsi = ida_dbg.get_reg_val("RSI")
&nbsp; &nbsp; &nbsp; &nbsp; print("RDI = %#x, RSI = %#x"&nbsp;% (rdi, rsi))

&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.request_continue_process()
&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.run_requests()
return&nbsp;0

hook = Hook()
idc.add_bpt(BP)
hook.hook()

这个例子虽短,但已经完整体现了 Hook 的典型工作流:

  • 下断点
  • 命中断点
  • 读寄存器
  • 自动继续

很多逆向题的动态脚本,本质上都是在这个骨架上继续加逻辑。

IDA Hook 模板

下面给一个比较通用的模板,适合做题时直接抄过去改:

import struct
import idautils
import ida_dbg
import ida_idd
import ida_segment
import idc

HOOK_RVA&nbsp;=&nbsp;0x1234
moduleBase =&nbsp;0

_read_memory =&nbsp;lambda&nbsp;ea,&nbsp;n:&nbsp;ida_idd.dbg_read_memory(ea, n)
r_u8 &nbsp;=&nbsp;lambda&nbsp;ea:&nbsp;_read_memory(ea,&nbsp;1)[0]
r_u16 =&nbsp;lambda&nbsp;ea:&nbsp;struct.unpack("<H", _read_memory(ea,&nbsp;2))[0]
r_u32 =&nbsp;lambda&nbsp;ea:&nbsp;struct.unpack("<I", _read_memory(ea,&nbsp;4))[0]
r_u64 =&nbsp;lambda&nbsp;ea:&nbsp;struct.unpack("<Q", _read_memory(ea,&nbsp;8))[0]

_w =&nbsp;lambda&nbsp;ea,&nbsp;data:&nbsp;ida_idd.dbg_write_memory(ea, data)
w_u8 &nbsp;=&nbsp;lambda&nbsp;ea,&nbsp;x:&nbsp;_w(ea, struct.pack("<B", x &&nbsp;0xFF))
w_u16 =&nbsp;lambda&nbsp;ea,&nbsp;x:&nbsp;_w(ea, struct.pack("<H", x &&nbsp;0xFFFF))
w_u32 =&nbsp;lambda&nbsp;ea,&nbsp;x:&nbsp;_w(ea, struct.pack("<I", x &&nbsp;0xFFFFFFFF))
w_u64 =&nbsp;lambda&nbsp;ea,&nbsp;x:&nbsp;_w(ea, struct.pack("<Q", x &&nbsp;0xFFFFFFFFFFFFFFFF))

invalidate_cache =&nbsp;lambda&nbsp;ea,&nbsp;size:&nbsp;ida_dbg.invalidate_dbgmem_contents(ea, size)
rebase =&nbsp;lambda&nbsp;ea : moduleBase+ea
get_reg =&nbsp;lambda&nbsp;reg_name:&nbsp;ida_dbg.get_reg_val(reg_name)
set_reg =&nbsp;lambda&nbsp;reg_name,&nbsp;val:&nbsp;ida_dbg.set_reg_val(reg_name, val)

def&nbsp;get_main_module_base():
"""获取主模块基址(通常为模块列表的第一个)"""
module&nbsp;= idautils.Modules().__next__()
&nbsp; &nbsp; print(f"[*] Main module: {module.name} at {module.base:#x}")
return&nbsp;module.base

class&nbsp;Hook(ida_dbg.DBG_Hooks):
def&nbsp;__init__(self):
super().__init__()
self.bp = rebase(HOOK_RVA)
self.hit_count =&nbsp;0

def&nbsp;dbg_bpt(self, tid, ea):
if&nbsp;ea !=&nbsp;self.bp:
return&nbsp;0

&nbsp; &nbsp; &nbsp; &nbsp; rax = get_reg("RAX")
&nbsp; &nbsp; &nbsp; &nbsp; rdi = get_reg("RDI")
self.hit_count +=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; print("rax = %#x, rdi = %#x"&nbsp;% (rax, rdi))

&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.request_continue_process()
&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.run_requests()
return&nbsp;0

def&nbsp;dbg_process_exit(self, pid, tid, ea, code):
&nbsp; &nbsp; &nbsp; &nbsp; print("Total hit count -> %d"&nbsp;%&nbsp;self.hit_count)
return&nbsp;0

try:
&nbsp; &nbsp; idc.del_bpt(hook.bp)
&nbsp; &nbsp; hook.unhook()
except&nbsp;Exception:
&nbsp; &nbsp; pass

moduleBase = get_main_module_base()
hook =&nbsp;Hook()
idc.add_bpt(hook.bp)
hook.hook()
print("bp = %#x"&nbsp;% hook.bp)

这个模板实际做题时只用改两处:

  • HOOK_RVA

  • dbg_bpt()

    里的处理逻辑

它很适合用来做:

  • 参数采样
  • 状态变量观察
  • VM 解释器 Hook
  • 批量 patch 检查点

#

多断点处理

多断点处理是实际写脚本时非常常见的问题,很多人一开始只会写单断点脚本,比如:

if ea == bp1:
&nbsp; &nbsp; ...

但如果目标变成:

  • func_a

    入口记录参数

  • func_b

    中间 patch 某个检查

  • func_c

    返回前打印结果

如果还是把所有逻辑都堆在一个if/elif/elif里,脚本很快就会变乱。

这里介绍两种常见的处理方式:

一:最直接的 if-elif

适合断点少且处理逻辑简单的时候:

class&nbsp;Hook(ida_dbg.DBG_Hooks):
def&nbsp;__init__(self):
super().__init__()
&nbsp; &nbsp; &nbsp; &nbsp; self.bp_a =&nbsp;0x401000
&nbsp; &nbsp; &nbsp; &nbsp; self.bp_b =&nbsp;0x402000

def&nbsp;dbg_bpt(self, tid, ea):
if&nbsp;ea == self.bp_a:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.handle_a()
elif&nbsp;ea == self.bp_b:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.handle_b()
else:
return&nbsp;0

&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.request_continue_process()
&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.run_requests()
return&nbsp;0

优点是简单直接。缺点也很明显:断点一多就开始失控。

二:“地址 -> 处理函数”映射表

这是最推荐的通用写法。

思路是把每个断点和它的处理函数绑定起来:

class&nbsp;Hook(ida_dbg.DBG_Hooks):
def&nbsp;__init__(self):
super().__init__()
&nbsp; &nbsp; &nbsp; &nbsp; self.handlers = {
0x401000: self.handle_func_a,
0x402000: self.handle_func_b,
0x403000: self.handle_func_c,
&nbsp; &nbsp; &nbsp; &nbsp; }

def&nbsp;dbg_bpt(self, tid, ea):
&nbsp; &nbsp; &nbsp; &nbsp; fn = self.handlers.get(ea)
if&nbsp;fn&nbsp;is&nbsp;not&nbsp;None:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fn()
else:
return&nbsp;0

&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.request_continue_process()
&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.run_requests()
return&nbsp;0

def&nbsp;handle_func_a(self):
&nbsp; &nbsp; &nbsp; &nbsp; rdi = ida_dbg.get_reg_val("RDI")
print("func_a arg =",&nbsp;hex(rdi))

def&nbsp;handle_func_b(self):
print("patch func_b check")

def&nbsp;handle_func_c(self):
print("func_c hit")

这种写法的好处很明显:

  • 结构清楚
  • 每个断点逻辑互相独立
  • 后面新增断点时只要往字典里塞一个映射

如果你的脚本断点多且断点后要处理的逻辑比较复杂,建议用这种写法。

注意事项

多断点脚本要处理好以下几点:

  • 统一管理运行时地址。 如果目标开了 PIE,最好不要把一堆绝对地址散落在脚本里,而是统一按base + rva组织
  • 每个 handler 尽量只做一件事 让dbg_bpt()只负责分发,真正的采样和 patch 逻辑放到独立函数里,后面维护会轻松很多
  • 注意旧 Hook 残留 反复执行脚本时,如果旧的DBG_Hooks对象还活着,就容易出现重复输出和旧逻辑继续报错

其次,建议大家遵循下面的编码规范:

def&nbsp;dbg_bpt(self, tid, ea):
if&nbsp;ea == self.bp_a:
&nbsp; &nbsp; &nbsp; &nbsp; self.handle_a()
elif&nbsp;ea == self.bp_b:
&nbsp; &nbsp; &nbsp; &nbsp; self.handle_b()
else:
return&nbsp;0

&nbsp; &nbsp; ida_dbg.request_continue_process()
&nbsp; &nbsp; ida_dbg.run_requests()
return&nbsp;0

def&nbsp;dbg_bpt(self, tid, ea):
&nbsp; &nbsp; fn = self.handlers.get(ea)
if&nbsp;fn&nbsp;is&nbsp;not&nbsp;None:
&nbsp; &nbsp; &nbsp; &nbsp; fn()
else:
return&nbsp;0

&nbsp; &nbsp; ida_dbg.request_continue_process()
&nbsp; &nbsp; ida_dbg.run_requests()
return&nbsp;0

在上面的回调实现中,对于脚本未主动处理的断点(即不在自动化逻辑内的断点),统一直接return 0

这样处理的好处是:当我们在动态调试过程中手动设置临时断点,想逐步跟进某个函数的内部实现时,命中断点后 IDA 会暂停而不是自动继续。反之,如果回调对所有断点都一律调用request_continue_process(),那么手动断点也会被自动恢复执行,就失去了手动调试的机会。

IDA Hook 实战

这里用一道VM题做示例,因为VM题静态分析通常是比较费时的,这里讲怎么利用Hook技术来提速。

题目链接:https://share.weiyun.com/Yb691CGu

开头是用户输入key,输入长度不足42则用\0补齐,可以知道flag长度为42:

然后是do-while结构的interpreter,程序会把.vmp段的数据作为VM字节码执行:

&nbsp; v9 =&nbsp;16LL;
&nbsp; regs = vm_ctx.regs;
&nbsp; vm_ctx.code_len =&nbsp;0x7AELL;
while&nbsp;( v9 )
&nbsp; {
&nbsp; &nbsp; *regs++ =&nbsp;0;
--v9;
&nbsp; }
&nbsp; vm_ctx.code = (uint8_t *)&unk_5D2AA7E12040;
&nbsp; *(_QWORD *)&vm_ctx.pc =&nbsp;0xFFC00000000LL;
&nbsp; std::string::_M_assign(&vm_ctx.input, &v15);
&nbsp; vm_ctx.running&nbsp;=&nbsp;1;
&nbsp; vm_ctx.input_cur =&nbsp;0LL;
do
&nbsp; {
if&nbsp;( vm_ctx.pc >= vm_ctx.code_len )
break;
&nbsp; &nbsp; interpreter(&vm_ctx);
&nbsp; }
while&nbsp;( vm_ctx.running&nbsp;);
&nbsp; v11 = vm_ctx.regs[15] !=&nbsp;1;

这里是根据伪代码稍微分析了一下VM结构体,实际上用hook的思路不怎么需要分析

经常写VM题的同学可能知道,很多VM题都是套一层VM解释器但实际上算法并不复杂~~(通常是杂鱼异或 ~~

所以我们可以先定位到一些关键的指令实现。比如:异或、比较

这两个指令还是比较好找的,分别是opcode0x4e0x6b分支。

xor:

cmp

接下来写个Hook脚本判断一下是不是简单的单字节异或,这里用到的就是我们之前给出的Hook模板。

import struct
import idautils
import ida_dbg
import ida_idd
import ida_segment
import idc

moduleBase =&nbsp;0
_read_memory =&nbsp;lambda&nbsp;ea,&nbsp;n:&nbsp;ida_idd.dbg_read_memory(ea, n)
r_u8 &nbsp;=&nbsp;lambda&nbsp;ea:&nbsp;_read_memory(ea,&nbsp;1)[0]
r_u16 =&nbsp;lambda&nbsp;ea:&nbsp;struct.unpack("<H", _read_memory(ea,&nbsp;2))[0]
r_u32 =&nbsp;lambda&nbsp;ea:&nbsp;struct.unpack("<I", _read_memory(ea,&nbsp;4))[0]
r_u64 =&nbsp;lambda&nbsp;ea:&nbsp;struct.unpack("<Q", _read_memory(ea,&nbsp;8))[0]

_w =&nbsp;lambda&nbsp;ea,&nbsp;data:&nbsp;ida_idd.dbg_write_memory(ea, data)
w_u8 &nbsp;=&nbsp;lambda&nbsp;ea,&nbsp;x:&nbsp;_w(ea, struct.pack("<B", x &&nbsp;0xFF))
w_u16 =&nbsp;lambda&nbsp;ea,&nbsp;x:&nbsp;_w(ea, struct.pack("<H", x &&nbsp;0xFFFF))
w_u32 =&nbsp;lambda&nbsp;ea,&nbsp;x:&nbsp;_w(ea, struct.pack("<I", x &&nbsp;0xFFFFFFFF))
w_u64 =&nbsp;lambda&nbsp;ea,&nbsp;x:&nbsp;_w(ea, struct.pack("<Q", x &&nbsp;0xFFFFFFFFFFFFFFFF))
invalidate_cache =&nbsp;lambda&nbsp;ea,&nbsp;size:&nbsp;ida_dbg.invalidate_dbgmem_contents(ea, size)
rebase =&nbsp;lambda&nbsp;ea : moduleBase+ea
get_reg =&nbsp;lambda&nbsp;reg_name:&nbsp;ida_dbg.get_reg_val(reg_name)
set_reg =&nbsp;lambda&nbsp;reg_name,&nbsp;val:&nbsp;ida_dbg.set_reg_val(reg_name, val)

def&nbsp;get_main_module_base():
module&nbsp;= idautils.Modules().__next__()
&nbsp; &nbsp; print(f"[*] Main module: {module.name} at {module.base:#x}")
return&nbsp;module.base

class&nbsp;Hook(ida_dbg.DBG_Hooks):
def&nbsp;__init__(self):
super().__init__()
self.bp_cmp = rebase(0x1BF2)
self.bp_xor = rebase(0x1B28)
self.hita =&nbsp;0
self.hitb =&nbsp;0

def&nbsp;dbg_bpt(self, tid, ea):
if&nbsp;ea ==&nbsp;self.bp_cmp:
self.hita +=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; elif ea ==&nbsp;self.bp_xor:
self.hitb +=&nbsp;1
else:
return&nbsp;0
&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.request_continue_process()
&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.run_requests()
return&nbsp;0

def&nbsp;dbg_process_exit(self, pid, tid, ea, code):
&nbsp; &nbsp; &nbsp; &nbsp; print("[*] cmp hit count -> %d"&nbsp;%&nbsp;self.hita)
&nbsp; &nbsp; &nbsp; &nbsp; print("[*] xor hit count -> %d"&nbsp;%&nbsp;self.hitb)
return&nbsp;0

try:
&nbsp; &nbsp; hook.unhook()
&nbsp; &nbsp; idc.del_bpt(hook.bp_cmp)
&nbsp; &nbsp; idc.del_bpt(hook.bp_xor)
except&nbsp;Exception:
&nbsp; &nbsp; pass

moduleBase = get_main_module_base()
hook =&nbsp;Hook()
idc.add_bpt(hook.bp_cmp)
idc.add_bpt(hook.bp_xor)
hook.hook()
print("[*] cmp breakpoint = %#x"&nbsp;% hook.bp_cmp)
print("[*] xor breakpoint = %#x"&nbsp;% hook.bp_xor)

挂上调试器,这里给个建议,有些题目的调试可以开启在程序入口断点这个选项:

运行脚本,然后在linux调试端随便输点文本,得到回显:

[*] Main&nbsp;module: /home/s1nyer/IDAdebug/IDA8.3/challenge at&nbsp;0x5613b760c000
[*] cmp breakpoint =&nbsp;0x5613b760dbf2
[*]&nbsp;xor&nbsp;breakpoint =&nbsp;0x5613b760db28
Debugger:&nbsp;process has exited (exit&nbsp;code&nbsp;1)
[*] cmp hit count ->&nbsp;43
[*]&nbsp;xor&nbsp;hit count ->&nbsp;42

很好,程序进行了42次异或运算,和flag长度一致,符合预期;但为什么比较指令比异或多执行了一次呢?Oh!可以猜到程序在密文比对完毕后肯定要根据某个标志位来判断是否匹配,从而进入成功/失败分支,类似于下面:

if&nbsp;(matched)
goto&nbsp;success;
else
goto&nbsp;failed;

当然直接这么判断可能有点经验主义了,我们改一下dbg_bpt调试回调逻辑再重新调试。

def&nbsp;dbg_bpt(self, tid, ea):
if&nbsp;ea == self.bp_cmp:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.hita +=&nbsp;1
print("[*] cmp hitted %d"&nbsp;% self.hita)
elif&nbsp;ea == self.bp_xor:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.hitb +=&nbsp;1
print("[*] xor hitted %d"&nbsp;% self.hitb)
else:
return&nbsp;0
#ida_dbg.request_continue_process()
#ida_dbg.run_requests()
return&nbsp;0

在每次断点命中时,输出是哪个指令命中了,并注释掉了它自动continue的代码,由我们手动控制,然后就能在IDA输出窗口顺序看到。

[*] Main&nbsp;module: /home/s1nyer/IDAdebug/IDA8.3/challenge at&nbsp;0x651d75bfc000
[*] cmp breakpoint =&nbsp;0x651d75bfdbf2
[*]&nbsp;xor&nbsp;breakpoint =&nbsp;0x651d75bfdb28
[*]&nbsp;xor&nbsp;hitted&nbsp;1
[*] cmp hitted&nbsp;1
[*]&nbsp;xor&nbsp;hitted&nbsp;2
[*] cmp hitted&nbsp;2
[*]&nbsp;xor&nbsp;hitted&nbsp;3
......

可以看到是先xorcmp并且两者是交替进行(这里如果读者手动操作体验就更明显了

我们再把注释掉的自动继续代码还原,让它一路跑完,可以得到下面输出。

[*]xorhitted41
[*]cmphitted41
[*]xorhitted42
[*]cmphitted42
[*]cmphitted43&nbsp;// 多出的那次比较
Debugger:&nbsp;processhasexited&nbsp;(exit code&nbsp;1)
[*]cmphitcount->&nbsp;43
[*]xorhitcount->&nbsp;42

这个输出就完全印证我们关于VM算法流程的猜想了,接下来我们完善这个脚本,把xor和cmp的右值提取出来就OK啦!(完整代码就不贴了,上核心代码)

class&nbsp;Hook(ida_dbg.DBG_Hooks):
def&nbsp;__init__(self):
super().__init__()
&nbsp; &nbsp; &nbsp; &nbsp; self.bp_cmp = rebase(0x1BF2)
&nbsp; &nbsp; &nbsp; &nbsp; self.bp_xor = rebase(0x1B28)
&nbsp; &nbsp; &nbsp; &nbsp; self.enc =&nbsp;b""
&nbsp; &nbsp; &nbsp; &nbsp; self.key =&nbsp;b""
&nbsp; &nbsp; &nbsp; &nbsp; self.hita =&nbsp;0
&nbsp; &nbsp; &nbsp; &nbsp; self.hitb =&nbsp;0

def&nbsp;dbg_bpt(self, tid, ea):
if&nbsp;ea == self.bp_cmp:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ctx = get_reg("RDX")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; eax = get_reg("EAX")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.enc += (eax &&nbsp;0xff).to_bytes(1)
print("[*] enc[%d] = %x"&nbsp;% (self.hita, eax &&nbsp;0xff))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.hita +=&nbsp;1
elif&nbsp;ea == self.bp_xor:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rdx = get_reg("RDX")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.key += r_u8(rdx +&nbsp;0x18&nbsp;+&nbsp;11&nbsp;*&nbsp;4).to_bytes(1)
print("[*] key[%d] = %x"&nbsp;% (self.hitb, self.key[self.hitb]))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.hitb +=&nbsp;1
else:
return&nbsp;0

if&nbsp;len(self.key) ==&nbsp;42&nbsp;and&nbsp;len(self.enc) ==&nbsp;42&nbsp;:
print("[+] key -> %s"&nbsp;% self.key.hex())
print("[+] enc -> %s"&nbsp;% self.enc.hex())
return&nbsp;0

&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.request_continue_process()
&nbsp; &nbsp; &nbsp; &nbsp; ida_dbg.run_requests()
return&nbsp;0

def&nbsp;dbg_process_exit(self, pid, tid, ea, code):
print("[*] cmp hit count -> %d"&nbsp;% self.hita)
print("[*] xor hit count -> %d"&nbsp;% self.hitb)
return&nbsp;0

try:
&nbsp; &nbsp; hook.unhook()
&nbsp; &nbsp; idc.del_bpt(hook.bp_cmp)
&nbsp; &nbsp; idc.del_bpt(hook.bp_xor)
except&nbsp;Exception:
pass

moduleBase = get_main_module_base()
hook = Hook()
idc.add_bpt(hook.bp_cmp)
idc.add_bpt(hook.bp_xor)
hook.hook()
print("[*] cmp breakpoint = %#x"&nbsp;% hook.bp_cmp)
print("[*] xor breakpoint = %#x"&nbsp;% hook.bp_xor)

输出如下:

......
[*]&nbsp;key[38] =&nbsp;7
[*] enc[38] =&nbsp;33
[*]&nbsp;key[39] =&nbsp;17
[*] enc[39] =&nbsp;22
[*]&nbsp;key[40] =&nbsp;18
[*] enc[40] =&nbsp;2e
[*]&nbsp;key[41] =&nbsp;3a
[*] enc[41] =&nbsp;47
[+]&nbsp;key&nbsp;->&nbsp;6cb47cfc07962a92603fe9b4bae75430c9d3ecfa0c3276711f65ef5944aaca18609cf602c6360717183a
[+] enc ->&nbsp;08d50e887cae1af050078880d9ca6d56adeac1ce3e00155c7d54de6e699af87954ffce3aa00533222e47

当然这个例子只是抛砖引玉,从这个模板出发,下面这些它都能做到:

  • 观察函数参数和返回值
  • 批量追踪状态机 / 分支切换
  • 修改函数参数并批量测试
  • 记录循环中每轮的关键状态
  • 在关键比较点取现场值
  • 自动 patch 某类检查点
  • 做轻量级 trace,而不是全程手动跟
  • ······

它不一定适合所有题,但一旦进入“我已经知道关键点在哪,只是懒得手搓”的阶段,Hook 基本就是性价比最高的方案。

总结

IDA Hook 不是一个高级但冷门的技巧,它本质上是把调试器事件变成脚本入口。

如果把手工调试看成:观察 + 记录 + 修改,那么 Hook 做的事情就是把这三件事:自动化、批量化、事件驱动化。

对逆向来说,最值得记住的一点是这个思路:Hook 的核心不是“自动下断”,而是“在事件发生的瞬间接管现场”。

一旦你习惯了这种思路,很多原本需要手点很多次的动态分析过程,都会自然地变成一个简洁的批处理脚本。

#

看雪ID:S1nyer

https://bbs.kanxue.com/user-home-977553.htm

*本文为看雪论坛精华文章,由 S1nyer 原创,转载请注明来自看雪社区

第十届安全开发者峰会【议题征集】-欢迎投稿

往期推荐

安卓逆向基础知识之frida Hook

2025 强网杯和强网拟态部分题解

在逆向分析方面-unidbg真的适合 MCP 吗?

AI静态分析,内核模块隐藏 Frida 特征,绕过linker私有结构遍历崩溃链

某安全so库深度解析

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 S1nyer S1nyer《逆向手的锋刃:IDA Hook从入门到实战》

评论:0   参与:  0