之前在复现 IOT 漏洞,为了捋清调用链进行 GDB 调试,但由于固件的代码量较大,而且函数之间的调用和跳转很多。导致了 GDB 这边每次函数跳转后都需要在 IDA 手动同步一下。久而久之,我发现这是一个重复且无意义且浪费时间的工作,我在想能否开发一个 IDA 插件用来自动同步 GDB 调试时的 PC 寄存器
成果
写了两个 GDB 命令,分别为 local 和 sl ,以及 IDA 的一个插件 SynGDB
在 IDA 开启 SynGDB 后, GDB 界面输入 local 命令可以立刻将 IDA 同步到 GDB 当前的 PC 寄存器的位置,sl 命令可以让 GDB 单步执行一条指令并同步给 IDA
演示视频
最初的设计思路
我平常动态调试程序是放到虚拟机 Ubuntu18.04 中完成,而静态分析放到宿主机中的 IDA 进行。因此考虑将 IDA 插件启动一个服务端,虚拟机中写一个 GDB 的命令来启一个客户端。
GDB 命令:首先建立与服务端的连接,监视 PC 寄存器,每当值发生改变时,就把当前 PC 寄存器的值发送给服务端
IDA 插件:与客户端建立连接后,开启一个新的线程,不断循环,等待接受客户端发送过来的数据,用 IDA pro python API 中的函数,跳转到接收到的地址
之前已经将这部分的代码实现完成,但后来发现有问题又给删了,没有保留这部分代码。
该设计思路存在的问题
上述思路最直观的问题就是下了几个断点,然后 C 过去,那么 PC 寄存器会在极短时间内改变多次,从而短时间内多次发送给服务端数据。导致了服务端那边处理异常。
重新思考下这个插件的目的,这个插件只是为了自动同步 GDB 调试的位置,并没有必要每条指令都同步到 IDA 上,通常来说,只有我查看 IDA 的时候,我才希望去同步。那么我就没必要每条指令都同步给 IDA 这里。
改进
因此我决定写成两个命令,一个为 sl ,一个为 local
local 命令是获取当前 PC 寄存器的值,然后发送给 IDA 使其同步。这个命令通常用于在 GDB 中执行了 c 命令或者多次 n 命令后,准备查看 IDA 时,执行 local ,同步 IDA 的位置
sl 命令是对 local 命令的一个封装并添加了执行 si 命令的操作。如果需要具体调试某处地方观察上下文信息,那么需要 GDB 一边调试,一边查看 IDA 代码,那么可以执行 sl 命令, GDB 单步走一次,并同步到 IDA 上
GDB命令的代码
local 命令的实现逻辑
执行 show architecture 命令,根据返回值来判断当前程序的架构(因为不同架构的 PC 寄存器的名称不一样)
执行 x/gx $pc 命令,获取当前 PC 寄存器的值
对 PC 寄存器的值进行处理(因为有时候接收到的数据可能为 0x411082 <setvbuf@plt> 或者开启 PIE 后无法直接得到地址偏移,所以需要对数据进行提取处理)
将处理后的地址发送给服务端
更新 v1.0 :修复了开 PIE 保护后无法同步正确偏移给 IDA
import socket import gdb import re
host = '192.168.110.172'#IP with IDA host installed port = 12626
defget_program_base_address(): recv_data = gdb.execute('vmmap', to_string=True) lines = recv_data.split('\n') for line in lines: if"0x"in line: base_address = line.split()[1] returnint(base_address,16) returnNone
defhandle_pc_address(raw_data): pattern = r"^0x[0-9a-fA-f]+" match = re.search(pattern, raw_data) ifmatch: tmp=int(match.group(),16) if tmp > 0x7ffff0000000: print("[*] The address appears to be in libc and cannot be synchronized to IDA") returnNone elif tmp > 0x555555550000: return tmp - get_program_base_address() else: return tmp else: print("[*] Abnormal data--Unable to parse the value of the current PC register")
在 run 函数中先开启 socket ,等待客户端的连接。这里要进行多线程操作,最初我这里用的是程序中直接开启 socket 等待客户端连接,然后在等待的连接中就直接造成了 IDA 未响应。所以这里必须要再开一个线程来处理接收数据
接收到地址后,用 ida_kernwin.jumpto(address) 函数来执行跳转。
正常来说,我需要在 term 函数中来关闭之前开的 socket ,因为这是插件关闭或者 IDA 退出时触发的函数(至少我在网上搜到是这样描述的),但我测试了一下发现,不知道为什么我这个插件刚运行的时候,term 函数就被触发了(观察下图能发现 SynGDB plugin terminated 这行字符串确实被输出了,而此时我刚刚启动插件,这个 term 函数就被触发)
这意味着我无法在 term 函数中关闭之前开启的 socket ,因为这里的 term 函数并不是我要退出 IDA 时触发的。不关闭 socket 资源,就意味着之前占用的 12626 端口就始终存在。如果 IDA 关闭再打开,启动 SynGDB 插件时会报错,说端口已经被占用。我实在没想到有什么好方法在 IDA 退出时自动关闭掉之前打开的资源,退而求其次是给插件写一个专门的函数,并手动触发进行关闭 socket,但这并不是我期望的这样(这违背了我想减少做重复且无意义的初衷)