我第一次接触 IDA Hook,是从“我想在断点命中时自动打印某些信息”开始的,再往后会发现,Hook 真正的价值并不只是“输出些信息”,而是它能把调试器事件统一交给脚本处理对逆向来说,这个能力很实用。很多题并不是静态分析做不动,而是你已经知道突破口在哪里,只是不想重复手点 50 次断点、手抄 40 次寄存器、手改 20 次内存这种机械工作。这个阶段,IDA Hook 往往就是最顺手的提速工具这篇文章主要做四件事:IDA 里有很多种 Hook:逆向题里真正高频使用的,通常就是 Debugger Hook,也就是 ida_dbg.DBG_Hooks它的工作方式可以简单理解成:这些“事件”包括但不限于:也就是说,你可以不再把调试看成“手工点 Continue 和 Step”,而是把它看成一套事件流逆向里有很多高重复动作:这些动作手点当然能做,但非常浪费时间,也很容易抄错Hook 的意义就在于把这些动作脚本化:所以从逆向视角看,Hook 本质上就是三件事:这里的“现场”通常包括:这就是为什么很多 CTF 题里,Hook 往往不是“锦上添花”,而是直接把原本很蠢的重复劳动压缩成一个小脚本DBG_Hooks 是一个“调试器事件监听类”。最常见的写法是继承它,然后只实现你关心的回调然后实例化并注册:如果不再需要:从写脚本的角度看,最常用的成员其实就是下面这些hook() / unhook()这两个方法是整个类最基础的入口。一般来说,脚本最后都会有类似这样的代码:这么写的原因很实际:你在 IDA 里反复执行脚本时,旧的 Hook 对象很可能还活着。如果不先 unhook(),就容易出现:所以在调试阶段,“先卸旧 hook,再挂新 hook”几乎可以当成固定习惯这是最常用的回调,断点命中时会触发它:参数里:通常你会在这个函数里做几件事:很多 Hook 脚本,本质上就是围绕 dbg_bpt() 展开的其他高频回调除了 dbg_bpt(),比较常用的还有这些。dbg_process_start进程启动时触发。常见用途:dbg_process_exit进程退出时触发,常见用途:如果你的脚本是“跑完整个程序,最后统一出结果”的类型,那 dbg_process_exit() 很适合做收尾dbg_library_load模块加载时触发,常见用途:dbg_exception异常发生时触发,常见用途:回调返回值怎么理解大多数情况下你会看到大家在回调末尾写:对常规逆向脚本来说,这么写就够了。更重要的不是返回值本身,而是你在回调里是否显式调用了:如果你调用了这组接口,程序就会在处理完当前事件后继续跑;否则它通常会停在当前事件点,等你手工操作所以平时更应该关心的是:而不是纠结某个回调到底该 return 0 还是别的值。DBG_Hooks 这个类本身最重要的不是它自带什么字段,而是你可以在子类实例上自由挂自己的状态比如:这些自定义字段在实际脚本里特别有用:也就是说,DBG_Hooks 本质上是“事件回调容器”,真正让脚本变好用的,是你自己附加的那层状态管理实际写脚本时,我建议把 DBG_Hooks 当成一个“调度壳”,而不是把所有逻辑都塞进去比较好的风格是:这样脚本会更清晰,也更适合后期扩展ida_dbg.DBG_Hooks 只是 Hook 的入口,真正配合它一起用的通常还有下面几类接口增删断点Hook 负责处理事件,断点负责制造事件。最常见的组合就是:如果目标开了 PIE / ASLR,通常不要直接拿静态地址下断,而是先算运行时地址操作寄存器读取/修改寄存器值非常常用:内存读写这是动态分析脚本最核心的配套接口之一:配合 struct.unpack 很方便:如果改了调试内存,最好刷新一下缓存:自动继续如果你想让程序在处理完事件后自动继续跑,通常会写:这组接口很适合“批量采样型脚本”,也就是你不希望程序每次命中都停下来等你点 Continue,而是希望它一路跑,把你要的数据全记下来比如你只想在某个断点命中时打印 RDI 和 RSI这个例子虽短,但已经完整体现了 Hook 的典型工作流:很多逆向题的动态脚本,本质上都是在这个骨架上继续加逻辑下面给一个比较通用的模板,适合做题时直接抄过去改这个模板实际做题时只用改两处:它很适合用来做:多断点处理是实际写脚本时非常常见的问题,很多人一开始只会写单断点脚本,比如:但如果目标变成:如果还是把所有逻辑都堆在一个 if/elif/elif 里,脚本很快就会变乱这里介绍两种常见的处理方式适合断点少且处理逻辑简单的时候优点是简单直接。缺点也很明显:断点一多就开始失控。这是最推荐的通用写法。思路是把每个断点和它的处理函数绑定起来:这种写法的好处很明显:如果你的脚本断点多且断点后要处理的逻辑比较复杂,建议用这种写法多断点脚本要处理好以下几点:统一管理运行时地址。如果目标开了 PIE,最好不要把一堆绝对地址散落在脚本里,而是统一按 base + rva 组织每个 handler 尽量只做一件事让 dbg_bpt() 只负责分发,真正的采样和 patch 逻辑放到独立函数里,后面维护会轻松很多注意旧 Hook 残留反复执行脚本时,如果旧的 DBG_Hooks 对象还活着,就容易出现重复输出和旧逻辑继续报错其次,建议大家遵循下面的编码规范在上面的回调实现中,对于脚本未主动处理的断点(即不在自动化逻辑内的断点),统一直接 return 0这样处理的好处是:当我们在动态调试过程中手动设置临时断点,想逐步跟进某个函数的内部实现时,命中断点后 IDA 会暂停而不是自动继续。反之,如果回调对所有断点都一律调用 request_continue_process(),那么手动断点也会被自动恢复执行,就失去了手动调试的机会这里用一道VM题做示例,因为VM题静态分析通常是比较费时的,这里讲怎么利用Hook技术来提速题目链接:792K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6K9r3q4J5k6g2)9J5k6i4N6W2K9i4W2#2L8W2)9J5k6h3y4G2L8g2)9J5c8W2W2T1y4U0V1I4b7@1N6#2开头是用户输入key,输入长度不足42则用\0补齐,可以知道flag长度为42 然后是do-while结构的interpreter,程序会把.vmp段的数据作为VM字节码执行这里是根据伪代码稍微分析了一下VM结构体,实际上用hook的思路不怎么需要分析经常写VM题的同学可能知道,很多VM题都是套一层VM解释器但实际上算法并不复杂~~(通常是杂鱼异或 ~~所以我们可以先定位到一些关键的指令实现。比如:异或、比较这两个指令还是比较好找的,分别是opcode 0x4e和0x6b分支xor cmp 接下来写个Hook脚本判断一下是不是简单的单字节异或,这里用到的就是我们之前给出的Hook模板挂上调试器,这里给个建议,有些题目的调试可以开启在程序入口断点这个选项 运行脚本,然后在linux调试端随便输点文本,得到回显很好,程序进行了42次异或运算,和flag长度一致,符合预期;但为什么比较指令比异或多执行了一次呢?Oh!可以猜到程序在密文比对完毕后肯定要根据某个标志位来判
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290998.htm
[原创]逆向手的锋刃:IDA Hook从入门到实战
163 浏览
9 回复
感谢分享
感谢分享
收藏
感谢分享。
厉害,感谢分享。
感谢分享
tql
谢谢
谢谢