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 三者的关系:一张图说清楚
函数对象、符号表、地址空间,它们是怎么配合的?我画了张图:
你看,地址空间是物理基础,函数对象是逻辑抽象,符号表是名字索引。三者缺一不可。
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大部分的分析逻辑了。我刚开始写插件时,经常搞混 getFunctionAt 和 getFunctionContaining。前者只匹配入口地址,后者匹配函数体内的任何地址。这个区别,在分析内联函数时特别重要。
好了,这一章就到这里。记住这三个概念,后面咱们写重命名插件时,会频繁用到它们。