SynGDB--同步GDB调试的IDA小插件

需求:

之前在复现 IOT 漏洞,为了捋清调用链进行 GDB 调试,但由于固件的代码量较大,而且函数之间的调用和跳转很多。导致了 GDB 这边每次函数跳转后都需要在 IDA 手动同步一下。久而久之,我发现这是一个重复且无意义且浪费时间的工作,我在想能否开发一个 IDA 插件用来自动同步 GDB 调试时的 PC 寄存器

成果

写了两个 GDB 命令,分别为 localsl ,以及 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 命令的实现逻辑

  1. 执行 show architecture 命令,根据返回值来判断当前程序的架构(因为不同架构的 PC 寄存器的名称不一样)
  2. 执行 x/gx $pc 命令,获取当前 PC 寄存器的值
  3. PC 寄存器的值进行处理(因为有时候接收到的数据可能为 0x411082 <setvbuf@plt> 或者开启 PIE 后无法直接得到地址偏移,所以需要对数据进行提取处理)
  4. 将处理后的地址发送给服务端

更新 v1.0 :修复了开 PIE 保护后无法同步正确偏移给 IDA

import socket
import gdb
import re

host = '192.168.110.172'#IP with IDA host installed
port = 12626

def get_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]
return int(base_address,16)
return None

def handle_pc_address(raw_data):
pattern = r"^0x[0-9a-fA-f]+"
match = re.search(pattern, raw_data)
if match:
tmp=int(match.group(),16)
if tmp > 0x7ffff0000000:
print("[*] The address appears to be in libc and cannot be synchronized to IDA")
return None
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")


def socket_write_data(address):
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((host, port))
client_socket.send(hex(address).encode())
client_socket.close()

def get_pc_name():
arch_info = gdb.execute("show architecture", to_string=True)
if "x86-64" in arch_info:
register_name = "rip"
elif "i386" in arch_info:
register_name = "eip"
else:
register_name = "pc"
return register_name


def local_cmd():
register=get_pc_name()
raw_address=gdb.execute(f"x/gx ${register}", to_string=True)
address=handle_pc_address(raw_address)
if address == None:
return
socket_write_data(address)

def sl_cmd():
gdb.execute("si")
local_cmd()

gdb.execute("define sl\n\tpython sl_cmd()\nend")
gdb.execute("define local\n\tpython local_cmd()\nend")

然后在 .gdbinit 文件中添加如下代码

source /home/zikh/Desktop/gdb_command/bind_ida.py

define sl
python sl_cmd()
end

define local
python local_cmd()
end

IDA插件代码

IDA python 插件编写,要有一个注册插件的函数

def PLUGIN_ENTRY():
return SynGDB()#该函数名要和插件名一样

然后定义一个插件类,名称也为 SynGDB ,下面插件类中的方法和属性都是必须的,少一个内容,IDA 插件都无法正常检测到

class SynGDB(ida_idaapi.plugin_t):
flags = ida_idaapi.PLUGIN_UNL
comment = "Current instructions for synchronous GDB debugging"
wanted_name = "SynGDB"
wanted_hotkey = "Alt-F5"#这里设置的是插件快捷键
help= "Coming soon..."

def init(self):
#初始化操作,比如打印提示信息,导入功能模块;
return ida_idaapi.PLUGIN_OK

def run(self, arg):
#插件的入口函数,调用功能模块的函数
pass

def term(self):
#结束时调用的方法
pass

run 函数中先开启 socket ,等待客户端的连接。这里要进行多线程操作,最初我这里用的是程序中直接开启 socket 等待客户端连接,然后在等待的连接中就直接造成了 IDA 未响应。所以这里必须要再开一个线程来处理接收数据

接收到地址后,用 ida_kernwin.jumpto(address) 函数来执行跳转。

正常来说,我需要在 term 函数中来关闭之前开的 socket ,因为这是插件关闭或者 IDA 退出时触发的函数(至少我在网上搜到是这样描述的),但我测试了一下发现,不知道为什么我这个插件刚运行的时候,term 函数就被触发了(观察下图能发现 SynGDB plugin terminated 这行字符串确实被输出了,而此时我刚刚启动插件,这个 term 函数就被触发)

image-20230920105745918

这意味着我无法在 term 函数中关闭之前开启的 socket ,因为这里的 term 函数并不是我要退出 IDA 时触发的。不关闭 socket 资源,就意味着之前占用的 12626 端口就始终存在。如果 IDA 关闭再打开,启动 SynGDB 插件时会报错,说端口已经被占用。我实在没想到有什么好方法在 IDA 退出时自动关闭掉之前打开的资源,退而求其次是给插件写一个专门的函数,并手动触发进行关闭 socket,但这并不是我期望的这样(这违背了我想减少做重复且无意义的初衷)

最终另辟蹊径,我在每次插件开启时,先去检测固定的端口 12626 是否被某个进程占用,如果被占用的话,则去关闭掉之前的进程,并进行后续操作

# 导入IDA Python模块
import ida_kernwin
import ida_idaapi
import ida_funcs
import socket
import threading
import os
import time

host = '0.0.0.0'
port = 12626

def PLUGIN_ENTRY():# 注册插件
return SynGDB()

def execute_and_jump_to_address(address):
func = ida_funcs.get_func(address)
if func is None:
print(f"invalid address:0x{address:X}")
return
# 执行跳转
ida_kernwin.jumpto(address)

def Ensure_port_availability(port):
with os.popen(f'netstat -aon|findstr "{port}"') as res:
res = res.read().split("\n")
if len(res)>1:
for line in res:
temp=line.split()
if "LISTENING" in temp:
pid=int(temp[-1])
print(f"The PID {pid} occupying port {port}\n")
try:
os.popen(f"taskkill /F /PID {pid}")
time.sleep(1)
except:
print(f"Try to kill {pid},but failed")
return False
print(f"Successfully killed PID:{pid}")
return True
print(f"Port {port} available")
return True

def init_socket():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if Ensure_port_availability(port):
server_socket.bind((host, port))
return server_socket
else:
print(f"Port {port} is not available")
exit(1)

def socket_get_address(server_socket):
server_socket.listen(1)
print(f"Waiting for gdb's connection...")
client_socket, client_address = server_socket.accept()
print(f"Connected to gdb: {client_address}")
data = client_socket.recv(64)
client_socket.close()
received_number = int(data.decode(), 16)
return received_number

# 创建一个IDA插件类
class SynGDB(ida_idaapi.plugin_t):
flags = ida_idaapi.PLUGIN_UNL
comment = "Current instructions for synchronous GDB debugging"
wanted_name = "SynGDB"
wanted_hotkey = "Alt-F5"
help= "Coming soon..."

def init(self):
print("SynGDB plugin initialized")
return ida_idaapi.PLUGIN_OK

def run(self, arg):
try:
server_socket = init_socket()
thread = threading.Thread(target=self.run_thread, args=(server_socket,))
thread.start()
except ValueError:
print("Transfer data failed or invalid address")

def run_thread(self, server_socket):
while True:
address = socket_get_address(server_socket)
if address != 0:
print(f"jump to address 0x{address:X}")
ida_kernwin.execute_sync(lambda: execute_and_jump_to_address(address), ida_kernwin.MFF_WRITE)
else:
break

def term(self):
print("SynGDB plugin terminated")

IDA 中加载插件的方法:

  1. 先将上面的代码保存成一个名为 SynGDB.py 的文件并放到 IDAplugins 目录下
  2. 打开 IDA 选择 Edit->Plugins->SynGDB 即可启动插件(如下图),启动插件的快捷键为 Alt+F5

image-20230920181441570