实战:使用 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 后再继续拼接。

小技巧: 如果字符串看起来不完整,试试把 min_length 设小一点(比如 3),然后手动检查上下文。

引用追踪:找到谁在用这个字符串

定位到字符串后,下一步是找引用。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。如果是错误信息,往往会有条件跳转指令在前面。

注意: Ghidra 的自动引用分析不一定 100% 准确。有些间接引用(比如通过寄存器计算地址)可能不会被自动识别。这时候需要手动添加引用。

函数调用链恢复:从字符串到完整路径

找到引用后,我们要恢复调用链。说白了,就是从字符串所在函数开始,向上回溯调用关系。

我常用的方法是:

  1. 找到引用字符串的函数
  2. 获取该函数的调用者
  3. 递归向上追踪,直到没有调用者(入口函数)

脚本实现如下:

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()}")

运行这个脚本,输入目标字符串,就能自动完成定位、引用追踪和调用链恢复。

核心思路: 字符串 → 引用 → 函数 → 调用链。每一步都依赖上一步的结果,环环相扣。

知识体系总览

下面这张图概括了本章的核心逻辑:

字符串交叉引用分析流程 字符串交叉引用分析流程 字符串定位 引用追踪 函数定位 调用链恢复 关键点 1. 字符串可能被编码 2. 引用类型区分 3. 间接引用需手动 4. 调用链深度控制 5. 入口函数判断 输出结果 • 字符串地址列表 • 引用位置及类型 • 所属函数名称 • 完整调用链 • 入口函数地址

这张图把整个流程串起来了。从左到右看,每一步的输出都是下一步的输入。我建议你把这个流程记在脑子里,以后分析任何固件都能套用。

好了,这一章的内容就到这。脚本已经给你了,找个固件练练手吧。记住,多动手才能记住这些 API 的用法。