3. 函数签名与符号:理解Ghidra中的函数对象、符号表、地址空间

好,咱们进入第三章。这一章我打算聊聊Ghidra里最核心的几个概念——函数对象、符号表、地址空间。说实话,这几个东西你搞明白了,后面写插件、做自动化分析,基本就顺了。

我记得刚接触Ghidra那会儿,最让我困惑的就是:它到底是怎么把一堆二进制字节码,变成我能看懂的函数的?嗯,这背后就是函数对象和符号表在起作用。

3.1 函数对象:不仅仅是入口地址

在Ghidra里,一个函数不只是一个地址。它是一个完整的对象,包含了:

  • 入口点:函数第一条指令的地址
  • 函数体:从入口到返回指令的所有代码块
  • 局部变量:栈上的变量、寄存器变量
  • 参数:调用约定定义的输入输出
  • 返回类型:void、int、指针等
  • 调用引用:哪些地方调用了这个函数

说白了,Ghidra把函数当成一个数据结构来管理。我习惯用API来获取函数对象:

// 获取当前地址所在的函数
Function func = currentProgram.getFunctionManager()
    .getFunctionContaining(currentAddress);

// 获取函数名
String name = func.getName();

// 获取函数体(地址集合)
AddressSetView body = func.getBody();

// 获取参数列表
Parameter[] params = func.getParameters();

我在做恶意软件分析时遇到过一个问题:有些函数被混淆了,Ghidra没识别出来。这时候就得手动创建函数对象:

// 在指定地址创建函数
Function newFunc = createFunction(address, name);
注意:手动创建函数时,要确保地址处确实是函数入口。我曾经因为误判,在数据段创建了函数,结果整个分析都乱了。

3.2 符号表:名字背后的故事

符号表,说白了就是地址和名字的映射。Ghidra的符号表分几种:

符号类型 来源 例子
导入符号 动态链接库 printf, malloc
导出符号 本模块暴露的接口 DllMain
用户符号 分析师重命名 decrypt_data
标签 Ghidra自动生成 LAB_00401000

你想想看,一个没有符号表的二进制文件,就像一本没有目录的书。Ghidra默认会给未识别的地址生成类似 FUN_00401000 的名字。我个人习惯是,只要分析到关键函数,立刻重命名。

操作符号表的API也很直接:

// 获取符号表
SymbolTable symTable = currentProgram.getSymbolTable();

// 按名字查找符号
Symbol sym = symTable.getSymbol("main");

// 获取符号的地址
Address addr = sym.getAddress();

// 获取符号的父命名空间(比如类名)
Namespace ns = sym.getParentNamespace();
小技巧:用 getSymbols(String name) 返回的是列表,因为同一个名字可能在不同命名空间存在。我一般用 getGlobalSymbol(name) 取第一个。

3.3 地址空间:Ghidra的虚拟世界

地址空间,这个概念其实很简单。Ghidra把二进制文件加载后,会创建一个虚拟地址空间。这个空间里,代码段、数据段、栈、堆,各有各的位置。

Ghidra的地址空间分两种:

  • RAM:可读写的内存,通常是数据段和堆栈
  • ROM:只读内存,通常是代码段和常量

但实际分析时,我们更关心的是 内存块(Memory Block)

// 获取所有内存块
Memory memory = currentProgram.getMemory();
MemoryBlock[] blocks = memory.getBlocks();

for (MemoryBlock block : blocks) {
    String name = block.getName();       // ".text", ".data"
    Address start = block.getStart();    // 起始地址
    Address end = block.getEnd();        // 结束地址
    boolean isExecute = block.isExecute(); // 是否可执行
    boolean isWrite = block.isWrite();     // 是否可写
}

嗯,这里有个坑。我曾经分析一个固件,发现代码段居然是可写的。后来才知道,那是某种自修改代码的机制。如果你发现内存块权限异常,一定要警惕。

3.4 三者的关系:一张图说清楚

函数对象、符号表、地址空间,它们是怎么配合的?我画了张图:

地址空间 .text (代码段) 0x00401000 - 0x00402000 .data (数据段) 0x00402000 - 0x00403000 .rdata (只读数据) 0x00403000 - 0x00404000 包含 函数对象 入口: 0x00401100 参数: int, char* 返回: void 关联 符号表 main -> 0x00401100 映射到

你看,地址空间是物理基础,函数对象是逻辑抽象,符号表是名字索引。三者缺一不可。

3.5 实战:用插件遍历所有函数

光说不练假把式。咱们写个小插件,遍历所有函数,打印它们的签名和地址:

public class ListFunctionsPlugin extends GhidraPlugin {
    
    @Override
    public void run() {
        FunctionManager fm = currentProgram.getFunctionManager();
        SymbolTable st = currentProgram.getSymbolTable();
        
        // 遍历所有函数
        for (Function func : fm.getFunctions(true)) {
            Address entry = func.getEntryPoint();
            String name = func.getName();
            
            // 从符号表获取更多信息
            Symbol sym = st.getSymbol(name, entry);
            if (sym != null) {
                System.out.println("函数: " + name);
                System.out.println("  地址: " + entry);
                System.out.println("  参数数: " + func.getParameterCount());
                System.out.println("  返回类型: " + func.getReturnType());
                System.out.println("---");
            }
        }
    }
}
核心要点
- 函数对象通过 FunctionManager 获取
- 符号表通过 SymbolTable 访问
- 地址空间通过 Memory 操作
- 三者通过地址(Address)关联

说实话,搞懂这三者的关系,你就能理解Ghidra大部分的分析逻辑了。我刚开始写插件时,经常搞混 getFunctionAtgetFunctionContaining。前者只匹配入口地址,后者匹配函数体内的任何地址。这个区别,在分析内联函数时特别重要。

避坑指南:我曾经在分析一个加壳程序时,发现符号表里全是乱码。后来才意识到,加壳程序会破坏原始符号。这时候就得靠函数签名匹配来恢复。记住:符号表不是100%可靠的。

好了,这一章就到这里。记住这三个概念,后面咱们写重命名插件时,会频繁用到它们。


专注资料整理