实战:使用 Ghidra 进行字符串交叉引用分析
字符串交叉引用,说白了就是找到「谁在引用这个字符串」。在固件逆向里,这招特别管用。我遇到过很多次,固件里藏着一堆日志字符串,顺着它们就能摸到核心函数的入口。
今天咱们就用 Ghidra 来干这件事。我会带着你写脚本、定位字符串、追踪引用,最后把函数调用链恢复出来。
Ghidra 脚本编写基础
Ghidra 支持 Python 和 Java 两种脚本语言。我个人习惯用 Python,因为调试起来快。你打开 Ghidra 的 Script Manager(快捷键 Alt+Shift+R),就能看到脚本列表。
先看一个最简单的脚本骨架:
# 获取当前程序对象
from ghidra.program.model.listing import Program
from ghidra.program.model.address import Address
program = getCurrentProgram()
listing = program.getListing()
memory = program.getMemory()
print("程序名称: " + program.getName())
print("基址: " + str(program.getImageBase()))
这个脚本能拿到固件的基本信息。嗯,这里要注意:getCurrentProgram() 返回的是当前分析的程序对象,所有后续操作都基于它。
字符串定位:从二进制到可读文本
固件里的字符串通常不是明文存储的。我见过三种常见情况:
| 存储方式 | 特征 | 定位方法 |
|---|---|---|
| 明文 ASCII | 以 0x00 结尾 | Ghidra 自动识别 |
| 宽字符 (UTF-16LE) | 每字符 2 字节 | 手动设置字符集 |
| 加密/编码 | 看起来像乱码 | 需要动态分析 |
定位字符串的脚本如下:
def find_strings(min_length=4):
"""查找固件中所有可打印字符串"""
strings = []
data = memory.getAllBytes()
current = []
for i, byte in enumerate(data):
if 0x20 <= byte <= 0x7E:
current.append(chr(byte))
else:
if len(current) >= min_length:
addr = program.getImageBase().add(i - len(current))
strings.append((addr, ''.join(current)))
current = []
return strings
# 使用示例
found = find_strings(6)
for addr, s in found[:20]:
print(f"0x{addr}: {s}")
我在项目中遇到过一种情况:固件把字符串按 4 字节对齐存储,中间插了 0x00 填充。上面的脚本会把这些 0x00 当成结束符,导致字符串被截断。解决办法是跳过连续 0x00 后再继续拼接。
引用追踪:找到谁在用这个字符串
定位到字符串后,下一步是找引用。Ghidra 的 ReferenceManager 能帮我们干这事。
def trace_string_references(string_addr):
"""追踪字符串的所有交叉引用"""
ref_manager = program.getReferenceManager()
references = ref_manager.getReferencesTo(string_addr)
results = []
for ref in references:
from_addr = ref.getFromAddress()
ref_type = ref.getReferenceType()
# 获取引用处的指令
instr = listing.getInstructionAt(from_addr)
if instr:
results.append({
'address': from_addr,
'type': str(ref_type),
'instruction': instr.toString()
})
return results
# 示例:追踪地址 0x1000a000 处的字符串
refs = trace_string_references(toAddr("0x1000a000"))
for r in refs:
print(f"{r['address']} [{r['type']}] {r['instruction']}")
引用类型很重要。常见的有:
- DATA:数据引用,通常用于全局变量初始化
- READ:读取引用,比如
LDR R0, =string - WRITE:写入引用,少见但存在
- UNCONDITIONAL_CALL:无条件调用,函数参数传递
你想想看,如果字符串被用在 printf 或 log 函数里,引用类型通常是 DATA 或 READ。如果是错误信息,往往会有条件跳转指令在前面。
函数调用链恢复:从字符串到完整路径
找到引用后,我们要恢复调用链。说白了,就是从字符串所在函数开始,向上回溯调用关系。
我常用的方法是:
- 找到引用字符串的函数
- 获取该函数的调用者
- 递归向上追踪,直到没有调用者(入口函数)
脚本实现如下:
def get_calling_chain(func, max_depth=10):
"""获取函数的调用链"""
chain = []
current = func
depth = 0
while current and depth < max_depth:
chain.append(current)
# 获取调用者
callers = []
for ref in current.getReferencesTo(current.getEntryPoint()):
if ref.getReferenceType().isCall():
caller_func = listing.getFunctionContaining(ref.getFromAddress())
if caller_func:
callers.append(caller_func)
if not callers:
break
# 选择第一个调用者继续向上
current = callers[0]
depth += 1
return chain
# 使用示例
string_addr = toAddr("0x1000a000")
refs = trace_string_references(string_addr)
for ref in refs:
func = listing.getFunctionContaining(ref['address'])
if func:
chain = get_calling_chain(func)
print(f"\n调用链(从字符串到入口):")
for i, f in enumerate(chain):
indent = " " * i
print(f"{indent}└─ {f.getName()} @ 0x{f.getEntryPoint()}")
我曾经用这个脚本分析过一个 IoT 设备的固件。字符串是 "Firmware update failed",顺着调用链找到了一个隐藏的后门函数。那个函数在正常更新流程之外,可以通过特定网络包触发。
完整实战流程
把上面的脚本组合起来,就是一个完整的分析流程:
def full_string_analysis(target_string):
"""完整的字符串交叉引用分析"""
print(f"[*] 搜索字符串: {target_string}")
# 1. 定位字符串
strings = find_strings(len(target_string))
target_addrs = []
for addr, s in strings:
if target_string in s:
target_addrs.append(addr)
if not target_addrs:
print("[-] 未找到目标字符串")
return
print(f"[+] 找到 {len(target_addrs)} 个匹配位置")
# 2. 追踪引用
for addr in target_addrs:
print(f"\n[*] 分析地址: {addr}")
refs = trace_string_references(addr)
if not refs:
print("[-] 无交叉引用")
continue
# 3. 恢复调用链
for ref in refs:
func = listing.getFunctionContaining(ref['address'])
if func:
chain = get_calling_chain(func)
print(f"\n调用链:")
for i, f in enumerate(chain):
indent = " " * i
print(f"{indent}└─ {f.getName()}")
运行这个脚本,输入目标字符串,就能自动完成定位、引用追踪和调用链恢复。
知识体系总览
下面这张图概括了本章的核心逻辑:
这张图把整个流程串起来了。从左到右看,每一步的输出都是下一步的输入。我建议你把这个流程记在脑子里,以后分析任何固件都能套用。
好了,这一章的内容就到这。脚本已经给你了,找个固件练练手吧。记住,多动手才能记住这些 API 的用法。