首页
/ Ghidra P-code Modeling 实战:构建增强仿真器(Augmented Emulation)——Userops、抽象算术、状态模型与 GUI 集成

Ghidra P-code Modeling 实战:构建增强仿真器(Augmented Emulation)——Userops、抽象算术、状态模型与 GUI 集成

2026-09-06 10:05:32作者:何举烈Damon

本篇指南基于 Ghidra 官方 Debugger 课程文档 B4-Modeling,系统讲解如何使用 p-code 建模(Modeling)技术为 Ghidra 构建"增强仿真器":在具体执行(concrete execution)之上叠加污点标签、符号表达式等辅助模型,实现动态污点分析、混合执行(concolic execution)等高级动态分析能力。读完并动手实践后,你将掌握:p-code userop 的三种实现方式(Java 回调、预编译 Sleigh、Structured Sleigh)、PcodeArithmetic 抽象算术的实现模式、PcodeExecutorStatePiece 配对状态模型、AuxEmulatorPartsFactory 增强仿真器的组装方式,以及如何把自定义仿真器注册进 Ghidra GUI 的 Debugger。

1. 什么是 Modeling:增强仿真(Augmented Emulation)

"Modeling" 在 Ghidra 语境下是一个含义很重的术语。B4 模块聚焦它在增强仿真中的用法:核心思想是复用仿真器完成具体执行,同时叠加一个辅助模型(auxiliary model),例如污点标签(taint labels)或符号表达式(symbolic expressions)。Ghidra 的抽象仿真器实现(位于 Ghidra/Framework/Emulation 模块)刻意支持将相互独立的模型**组合(composition)**起来,因此只要实现时注意抽象性,辅助模型可以复用到其他场景,甚至用于静态分析。

该模块覆盖以下方面:

  • 环境建模(Environment):p-code userops 与函数桩(stubbing);
  • 算术运算建模(Arithmetic operations);
  • 存储、寻址与内存操作建模(Storage, addressing, and memory operations);
  • 在动态分析中的使用;
  • 在静态分析中的使用;
  • 与 GUI 的集成。

前置知识:本模块假设你已完成 B2-EmulationB3-Scripting,并且对 Ghidra 的 low p-code 有相当深入的了解。

开发环境:Modeling 是一项开发任务。每个方面都有对应的接口,Ghidra 通常会提供抽象实现供你选择使用或忽略。如果尚未搭建开发环境,可以:使用 Eclipse 的 GhidraDev 插件并关联一个 Ghidra 安装,或者克隆 ghidra 源码仓库并在 Eclipse 中准备开发环境(仓库根目录的 DevGuide.md 有说明)。原型阶段最简单的做法是开发一个 GhidraScript——本文示例均采用这种方式。

2. 环境建模:p-code Userops

2.1 Userop 的定位

x86-64 的 SYSCALL 指令只会触发一个 syscall() userop,它就是实现系统调用等行为的挂钩(hook)。对于 p-code 未建模的一切行为(外部函数、系统调用、内核对象等),都可以通过 userop 来补充。Ghidra 对"系统调用"这种高频场景提供了专门的编程接口。外部函数桩(stubbing)的另一半内容在 B2-Emulation 中已覆盖:通过在 userop 库中提供通用桩,用户可以在 Sleigh 注入(injection)中断点处调用相应的 userop 来桩掉外部函数。

userop 库通过实现 PcodeUseropLibrary 接口创建,实践中通常继承 AnnotatedPcodeUseropLibrary。从源码看,该类通过 AnnotationUtilities.collectAnnotatedMethods(PcodeUserop.class, cls) 反射收集所有带 @PcodeUserop 注解的方法,并用内部 ParamAnnotProc 枚举解析参数注解:@OpExecutorPcodeExecutor)、@OpStatePcodeExecutorState)、@OpLibraryPcodeUseropLibrary)、@OpOutput(输出 Varnode)、@OpOpPcodeOp)。也就是说,方法名即 userop 名,参数由注解声明,框架通过 MethodHandle 完成绑定与调用。

2.2 方式一:Java 回调

以提供 strlen 桩为例:

public static class JavaStdLibPcodeUseropLibrary<T> extends AnnotatedPcodeUseropLibrary<T> {
	private final AddressSpace space;
	private final Register regRSP;
	private final Register regRAX;
	private final Register regRDI;
	private final Register regRSI;

	public JavaStdLibPcodeUseropLibrary(SleighLanguage language) {
		space = language.getDefaultSpace();
		regRSP = language.getRegister("RSP");
		regRAX = language.getRegister("RAX");
		regRDI = language.getRegister("RDI");
		regRSI = language.getRegister("RSI");
	}

	@PcodeUserop
	public void __x86_64_RET(
			@OpExecutor PcodeExecutor<T> executor,
			@OpState PcodeExecutorState<T> state) {
		PcodeArithmetic<T> arithmetic = state.getArithmetic();
		T tRSP = state.getVar(regRSP, Reason.EXECUTE_READ);
		long lRSP = arithmetic.toLong(tRSP, Purpose.OTHER);
		T tReturn = state.getVar(space, lRSP, 8, true, Reason.EXECUTE_READ);
		long lReturn = arithmetic.toLong(tReturn, Purpose.BRANCH);
		state.setVar(regRSP, arithmetic.fromConst(lRSP + 8, 8));
		((PcodeThreadExecutor<T>) executor).getThread()
				.overrideCounter(space.getAddress(lReturn));
	}

	@PcodeUserop
	public void __libc_strlen(@OpState PcodeExecutorState<T> state) {
		PcodeArithmetic<T> arithmetic = state.getArithmetic();
		T tStr = state.getVar(regRDI, Reason.EXECUTE_READ);
		long lStr = arithmetic.toLong(tStr, Purpose.OTHER);
		T tMaxlen = state.getVar(regRSI, Reason.EXECUTE_READ);
		long lMaxlen = arithmetic.toLong(tMaxlen, Purpose.OTHER);

		for (int i = 0; i < lMaxlen; i++) {
			T tChar = state.getVar(space, lStr + i, 1, false, Reason.EXECUTE_READ);
			if (arithmetic.toLong(tChar, Purpose.OTHER) == 0) {
				state.setVar(regRAX, arithmetic.fromConst(Integer.toUnsignedLong(i), 8));
				break;
			}
		}
	}
}

这里用 Java 回调实现桩。这种方式在建模 Ghidra 机器状态定义之外的事物时尤其有用,例如模拟底层操作系统的内核对象。当然它也可以建模简单的状态变化。用户会在调用点(call site)或调用目标处放置断点,让它调用 __libc_strlen(),然后根据断点位置调用 emu_skip_decoded()__x86_64_RET()

注意类型参数 T 的陷阱。上例中我们把 TtoLong/fromConst 转成了具体值——这对纯具体仿真器(T = byte[])没有问题;但如果仿真器被增强(下文详述),粗心的 userop 计算出的值在抽象模型看来就是一个"字面常量",符号信息就此丢失。正确做法是始终保持 T 抽象,所有算术运算都通过 arithmetic 对象完成。

2.3 方式二:预编译 Sleigh 语义

另一种选择是用预编译的 Sleigh 代码实现 userop:

public static class SleighStdLibPcodeUseropLibrary<T> extends AnnotatedPcodeUseropLibrary<T> {
	private static final String SRC_RET = """
			RIP = *:8 RSP;
			RSP = RSP + 8;
			return [RIP];
			""";
	private static final String SRC_STRLEN = """
			__result = 0;
			<loop>
			if (*:1 (str+__result) == 0 || __result >= maxlen) goto <exit>;
			__result = __result + 1;
			goto <loop>;
			<exit>
			""";
	private final Register regRAX;
	private final Register regRDI;
	private final Register regRSI;
	private final Varnode vnRAX;
	private final Varnode vnRDI;
	private final Varnode vnRSI;

	private PcodeProgram progRet;
	private PcodeProgram progStrlen;

	public SleighStdLibPcodeUseropLibrary(SleighLanguage language) {
		regRAX = language.getRegister("RAX");
		regRDI = language.getRegister("RDI");
		regRSI = language.getRegister("RSI");
		vnRAX = new Varnode(regRAX.getAddress(), regRAX.getMinimumByteSize());
		vnRDI = new Varnode(regRDI.getAddress(), regRDI.getMinimumByteSize());
		vnRSI = new Varnode(regRSI.getAddress(), regRSI.getMinimumByteSize());
	}

	@PcodeUserop
	public void __x86_64_RET(@OpExecutor PcodeExecutor<T> executor,
			@OpLibrary PcodeUseropLibrary<T> library) {
		if (progRet == null) {
			progRet = SleighProgramCompiler.compileUserop(executor.getLanguage(),
				"__x86_64_RET", List.of(), SRC_RET, PcodeUseropLibrary.nil(), List.of());
		}
		progRet.execute(executor, library);
	}

	@PcodeUserop
	public void __libc_strlen(@OpExecutor PcodeExecutor<T> executor,
			@OpLibrary PcodeUseropLibrary<T> library) {
		if (progStrlen == null) {
			progStrlen = SleighProgramCompiler.compileUserop(executor.getLanguage(),
				"__libc_strlen", List.of("__result", "str", "maxlen"),
				SRC_STRLEN, PcodeUseropLibrary.nil(), List.of(vnRAX, vnRDI, vnRSI));
		}
		progStrlen.execute(executor, library);
	}
}

要点:

  • 构造时捕获(capture)需要使用的 varnode。也可以直接在 Sleigh 源码里写死,但这里演示**别名(alias)**能力,使 Sleigh 源码可跨目标架构复用;
  • 每个 userop 在首次被调用时惰性编译SleighProgramCompiler.compileUserop 编译为 PcodeProgram);
  • 技术上这仍是 Java 回调,只是实现委托给 executor,由它执行编译好的 p-code 程序;
  • 优势:p-code 会正确地使用底层 arithmetic(自动保持 T 的抽象性)。但某些模型未必想要这种行为——有些符号模型可能更希望只看到一个抽象的 strlen() 调用,而不是展开后的指令序列。

2.4 方式三:Structured Sleigh

预编译 p-code 的缺点是样板代码多、要手工处理 Sleigh 编译;而且桩 C 函数时还得操心类型,复杂场景下你会怀念 C 式的控制结构。同一个库可以用一个孵化中(incubating)的特性 Structured Sleigh 实现:

public static class StructuredStdLibPcodeUseropLibrary<T>
		extends AnnotatedPcodeUseropLibrary<T> {
	public StructuredStdLibPcodeUseropLibrary(CompilerSpec cs) {
		new MyStructuredPart(cs).generate(ops);
	}

	public static class MyStructuredPart extends StructuredSleigh {
		protected MyStructuredPart(CompilerSpec cs) {
			super(cs);
		}

		@StructuredUserop
		public void __x86_64_RET() {
			Var RSP = lang("RSP", type("void **"));
			Var RIP = lang("RIP", type("void *"));
			RIP.set(RSP.deref());
			RSP.addiTo(8);
			_return(RIP);
		}

		@StructuredUserop
		public void __libc_strlen() {
			Var result = lang("RAX", type("long"));
			Var str = lang("RDI", type("char *"));
			Var maxlen = lang("RSI", type("long"));

			_for(result.set(0), result.ltiu(maxlen).andb(str.index(result).deref().eq(0)),
				result.inc(), () -> {
				});
		}
	}
}

这是用 Java 表达 p-code 行为所能达到的最简洁形式。虽然它看起来像是"回调进一个使用特殊状态操作 API 的 Java 方法",但并不完全准确:该 Java 方法只会被调用一次,用于把 Structured Sleigh "转译"(transpile)成标准 Sleigh 语义代码;该代码再被编译为 p-code,之后每次 userop 被调用时执行的是 p-code。可以说 Structured Sleigh 是一个寄宿在 Java 中的 DSL

局限:Java 无法重载运算符,只能使用方法调用表达运算;依赖 CompilerSpec 做类型解析;并非所有场景都适合——比如表达 __x86_64_RET 这类底层操作时,给 RSP/RIP 标注高级类型就显得别扭。好消息是这些实现技术可以混用:同一个库中 RET 用预编译 Sleigh、strlen 用 Structured Sleigh。

2.5 系统调用建模

系统调用建模(Modeling System Calls)在课程中不深入展开,但仓库中有优秀范例:

在 IDE 中搜索所有 EmuSyscallLibrary 的实现可以找到更多。注意:Linux 系统调用库是不完整的,只提供少量简单文件操作,但足以演示"模拟底层操作系统"的思路;它们也可以被扩展和组合(compose)来提供更多系统调用。

2.6 在脚本中使用自定义 Userop 库

在独立仿真脚本中使用自定义库很直接——关键是在匿名 PcodeEmulator 子类中覆写 createUseropLibrary()

public class CustomLibraryScript extends GhidraScript {
	@Override
	protected void run() throws Exception {
		PcodeEmulator emu = new PcodeEmulator(currentProgram.getLanguage()) {
			@Override
			protected PcodeUseropLibrary<byte[]> createUseropLibrary() {
				return super.createUseropLibrary()
						.compose(new ModelingScript.StructuredStdLibPcodeUseropLibrary<>(
							currentProgram.getCompilerSpec()));
			}
		};
		emu.inject(currentAddress, """
				__libc_strlen();
				__X86_64_RET();
				""");

		// TODO: Initialize the emulator's memory from the current program

		PcodeThread<byte[]> thread = emu.newThread();

		// TODO: Initialize the thread's registers

		while (true) {
			monitor.checkCancelled();
			thread.stepInstruction(100);
		}
	}
}

几个要点:

  • 要礼貌地用 super.createUseropLibrary().compose(...) 与父类提供的库组合,否则会移除已有 userop,导致后续意外崩溃;
  • 示例中包含了一段注入(injection)代码,用于调用自定义库中的 userop;
  • 机器和线程的初始化(内存映像、寄存器)留给脚本作者——仿真的状态并不隐式关联到当前 program!你必须把程序映像拷贝进仿真状态,并选择合适的注入位置。可参考 SystemEmulation 模块的示例脚本(Ghidra/Features/SystemEmulation/ghidra_scripts)。

如果想在GUI 中(临时)使用自定义 userop 库,可以设置 GUI 的 emulator factory:

public class InstallCustomLibraryScript extends GhidraScript implements FlatDebuggerAPI {
	public static class CustomPcodeEmulator extends PcodeEmulator {
		private CustomPcodeEmulator(Language language, PcodeEmulationCallbacks<byte[]> cb) {
			super(language, cb);
		}

		@Override
		protected PcodeUseropLibrary<byte[]> createUseropLibrary() {
			return super.createUseropLibrary()
					.compose(new ModelingScript.SleighStdLibPcodeUseropLibrary<>(getLanguage()));
		}
	}

	public static class CustomBytesDebuggerPcodeEmulatorFactory
			extends DefaultEmulatorFactory {
		@Override
		public PcodeMachine<?> create(PcodeDebuggerAccess access, Writer writer) {
			return new CustomPcodeEmulator(access.getLanguage(), writer.callbacks());
		}
	}

	@Override
	protected void run() throws Exception {
		getEmulationService().setEmulatorFactory(new CustomBytesDebuggerPcodeEmulatorFactory());
	}
}

这样你的自定义 userop 就可以在 Sleigh injection 中使用了。注意:目前没有途径把自定义 userop 引入 Watches 或 Go To 对话框。

3. 算术运算建模

3.1 为什么要实现 PcodeArithmetic

其余章节讨论的是超越具体仿真的建模。在多数动态分析场景中,我们会用一个抽象执行模型(动态污点分析、混合执行等)**增强(augment)**具体仿真器。Ghidra 的仿真框架偏重执行模型组合:让你专注于抽象模型本身,之后再与具体模型组合成完整的增强模型。这也便于创建可复用组件——但仍需要一些设计上的前瞻性。

作为演示,课程实现了一个构建符号表达式树的模型:经过若干步仿真后,用户不仅能看到变量的具体值,还能看到"由步进调度开始时的哪些变量生成它"的表达式。不做简化或进一步分析——那需要真正的 SMT 求解器,超出本教程范围。

PcodeArithmetic 接口定义在 Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/PcodeArithmetic.java。从接口 Javadoc 看,Ghidra 明确推荐的实现模式正是课程所采用的:

  • 当字节序(endianness)重要时,实现应为 enum,一个常量对应大端、一个对应小端,并提供静态方法 forEndian(...)forLanguage(Language) 按字节序/处理器语言取实例;
  • 当字节序无关时,采用单例模式(此时 getEndian() 应返回 null,并覆写 fromConst(BigInteger, int, boolean)fromConst(long, int) 以及 toBigInteger/toLong)。

接口的 Purpose 枚举区分了取值的具体化目的:DECODE(解析指令)、CONTEXT(反汇编上下文)、CONDITION(条件分支判定)、BRANCH(间接分支地址)、LOAD/STORE(加载/存储地址)、BY_DEF(p-code 规定为常量)、OTHER(其他/userop 使用)、INSPECT(用户或工具检查)。实现 userop 时选择正确的 Purpose 是保证抽象模型不被错误具体化的细节之一。

3.2 表达式模型

常量为字面量(literal),每执行一个运算就往表达式树上叠加节点。算子数量可能很庞杂,你的用例/目标未必需要全部;但如果希望模型被广泛采用,应尽量做到合理的完备,至少在你预见到需要改动或添加功能的地方预留扩展点。课程示例省略了非必需的算子:

public class ModelingScript extends GhidraScript {
	interface Expr {
		int size();
	}

	interface UnExpr extends Expr {
		Expr u();
	}

	interface BinExpr extends Expr {
		Expr l();

		Expr r();
	}

	record LitExpr(BigInteger val, int size) implements Expr {}

	record VarExpr(Varnode vn) implements Expr {
		public VarExpr(AddressSpace space, long offset, int size) {
			this(space.getAddress(offset), size);
		}

		public VarExpr(Address address, int size) {
			this(new Varnode(address, size));
		}

		@Override
		public int size() {
			return vn.getSize();
		}
	}

	record InvExpr(Expr u, int size) implements UnExpr {}

	record AddExpr(Expr l, Expr r, int size) implements BinExpr {}

	record SubExpr(Expr l, Expr r, int size) implements BinExpr {}
}

很容易看出如何补充更多表达式类型来完善模型。p-code 操作命名有一些微妙的惯例差异,务必仔细阅读文档。如果不确定某个操作的行为,查看 OpBehaviorFactory;也可以研究字节数组上的具体实现 BytesPcodeArithmetic。模型本身无需继承或实现任何 Ghidra 特定接口(当然也可以)。

3.3 将模型映射到 p-code:ExprPcodeArithmetic

把模型映射到 p-code 即实现 PcodeArithmetic 接口。多数情况下实现是一个枚举(大端/小端各一),少数是单例。按惯例还应提供按字节序/语言取实例的静态方法:

public enum ExprPcodeArithmetic implements PcodeArithmetic<Expr> {
	BE(Endian.BIG), LE(Endian.LITTLE);

	public static ExprPcodeArithmetic forEndian(Endian endian) {
		return endian.isBigEndian() ? BE : LE;
	}

	public static ExprPcodeArithmetic forLanguage(Language language) {
		return language.isBigEndian() ? BE : LE;
	}

	private final Endian endian;

	private ExprPcodeArithmetic(Endian endian) {
		this.endian = endian;
	}

	@Override
	public Class<Expr> getDomain() {
		return Expr.class;
	}

	@Override
	public Endian getEndian() {
		return endian;
	}

	@Override
	public Expr unaryOp(int opcode, int sizeout, int sizein1, Expr in1) {
		return switch (opcode) {
			case PcodeOp.INT_NEGATE -> new InvExpr(in1, sizeout);
			default -> throw new UnsupportedOperationException(PcodeOp.getMnemonic(opcode));
		};
	}

	@Override
	public Expr binaryOp(int opcode, int sizeout, int sizein1, Expr in1, int sizein2,
			Expr in2) {
		return switch (opcode) {
			case PcodeOp.INT_ADD -> new AddExpr(in1, in2, sizeout);
			case PcodeOp.INT_SUB -> new SubExpr(in1, in2, sizeout);
			default -> throw new UnsupportedOperationException(PcodeOp.getMnemonic(opcode));
		};
	}

	@Override
	public Expr modBeforeStore(int sizeinOffset, AddressSpace space, Expr inOffset,
			int sizeinValue, Expr inValue) {
		return inValue;
	}

	@Override
	public Expr modAfterLoad(int sizeinOffset, AddressSpace space, Expr inOffset,
			int sizeinValue, Expr inValue) {
		return inValue;
	}

	@Override
	public Expr fromConst(byte[] value) {
		if (endian.isBigEndian()) {
			return new LitExpr(new BigInteger(1, value), value.length);
		}
		byte[] reversed = Arrays.copyOf(value, value.length);
		ArrayUtils.reverse(reversed);
		return new LitExpr(new BigInteger(1, reversed), reversed.length);
	}

	@Override
	public Expr fromConst(BigInteger value, int size, boolean isContextreg) {
		return new LitExpr(value, size);
	}

	@Override
	public Expr fromConst(long value, int size) {
		return fromConst(BigInteger.valueOf(value), size);
	}

	@Override
	public byte[] toConcrete(Expr value, Purpose purpose) {
		throw new UnsupportedOperationException();
	}

	@Override
	public long sizeOf(Expr value) {
		return value.size();
	}
}

实现说明:

  • 字节序只在编码常量时体现fromConst(byte[]) 必须按字节序把 byte[] 转换为大整数(小端先反转)。选择 BigInteger 只是偏好——LitExpr 完全可以直接封装 byte[],把解释推迟到后面。所有 fromConst() 重载都被覆写,以避免 BigIntegerbyte[] 之间的来回转换;
  • unaryOp()/binaryOp() 的实现直白:switch opcode,构造相应表达式。这里也是值得提供扩展点的地方;
  • 提示:如果想捕获位置信息(是哪条指令执行了该操作),应覆写默认 unaryOp(PcodeOp, T) / binaryOp(PcodeOp, T1, T2)——它们接收完整的 PcodeOp 对象,可同时得到 opcode 和序列号(地址、索引)。此时把整数 opcode 版本的签名直接抛 AssertionError 即可;
  • modBeforeStore()/modAfterLoad() 是桩实现,它们提供了捕获解引用(dereference)信息的机会:例如在动态污点分析器中,modAfterLoad() 可记录"某值是通过被污点的地址取回的"。inValue 是从仿真存储中实际取回(或即将存入)的 ExprinOffset 是使用的偏移。这两个方法涉及存储与寻址(下一节详述),但它们建模的是"尚不需要存储机制参与的"内存操作部分;
  • 示例未实现 toConcrete()sizeOf():因为要增强具体仿真器,这些方法由具体那一半提供。如果该模型日后要独立用于静态分析,则值得实现这两个方法,失败时可抛异常。

4. 存储、寻址与内存操作建模

仿真器的存储模型是 PcodeExecutorState。由于我们要构建增强仿真器,需要提供 PcodeExecutorState<Pair<byte[], Expr>>——即告诉 Java:状态能够同时携带具体状态抽象模型状态。该状态中的地址也是成对的。增强仿真通常直接借用具体的寻址模型,因此寻址只用 byte[] 那一半。

"相同寻址模型的状态组合"足够常见,Ghidra 提供了抽象组件。真正需要实现的接口是 PcodeExecutorStatePiece,实现方式是继承 AbstractLongOffsetPcodeExecutorStatePiece

注意:如果你不想要具体寻址模型,可以直接实现 PcodeExecutorState<Expr>——"state" 也是地址模型与值模型相同的 "state piece",因此状态之间仍然可以组合。抽象寻址状态可同时用于静态与动态分析,而具体寻址的 piece 只适合动态分析;但动态分析中当出现别名(aliasing)和间接引用时,你可能难以对齐具体 piece 与抽象 piece。

public static class ExprSpace {
	protected final NavigableMap<Long, Expr> map = new TreeMap<>(Long::compareUnsigned);
	protected final ExprPcodeExecutorStatePiece piece;
	protected final AddressSpace space;

	protected ExprSpace(AddressSpace space, ExprPcodeExecutorStatePiece piece) {
		this.space = space;
		this.piece = piece;
	}

	public void clear() {
		map.clear();
	}

	public void set(long offset, int size, Expr val, PcodeStateCallbacks cb) {
		// TODO: Handle overlaps / offcut gets and sets
		map.put(offset, val);
		cb.dataWritten(piece, space.getAddress(offset), size, val);
	}

	public Expr get(long offset, int size, Reason reason, PcodeStateCallbacks cb) {
		// TODO: Handle overlaps / offcut gets and sets
		Expr expr = map.get(offset);
		if (expr == null) {
			byte[] aOffset =
				piece.getAddressArithmetic().fromConst(offset, space.getPointerSize());
			if (cb.readUninitialized(piece, space, aOffset, size, reason) != 0) {
				return map.get(offset);
			}
		}
		return null;
	}

	public Entry<Long, Expr> getNextEntry(long offset) {
		return map.ceilingEntry(offset);
	}
}

public static class ExprPcodeExecutorStatePiece
		extends AbstractLongOffsetPcodeExecutorStatePiece<byte[], Expr, ExprSpace> {

	protected final Map<AddressSpace, ExprSpace> spaceMap = new HashMap<>();

	public ExprPcodeExecutorStatePiece(Language language, PcodeStateCallbacks cb) {
		super(language, BytesPcodeArithmetic.forLanguage(language),
			ExprPcodeArithmetic.forLanguage(language), cb);
	}

	@Override
	public MemBuffer getConcreteBuffer(Address address, Purpose purpose) {
		throw new UnsupportedOperationException();
	}

	@Override
	public void clear() {
		for (ExprSpace space : spaceMap.values()) {
			space.clear();
		}
	}

	@Override
	protected ExprSpace getForSpace(AddressSpace space, boolean toWrite) {
		if (toWrite) {
			return spaceMap.computeIfAbsent(space, s -> new ExprSpace(s, this));
		}
		return spaceMap.get(space);
	}

	@Override
	public Entry<Long, Expr> getNextEntryInternal(AddressSpace space, long offset) {
		ExprSpace s = getForSpace(space, false);
		if (s == null) {
			return null;
		}
		return s.getNextEntry(offset);
	}

	@Override
	protected void setInSpace(ExprSpace space, long offset, int size, Expr val,
			PcodeStateCallbacks cb) {
		space.set(offset, size, val, cb);
	}

	@Override
	protected Expr getFromSpace(ExprSpace space, long offset, int size, Reason reason,
			PcodeStateCallbacks cb) {
		return space.get(offset, size, reason, cb);
	}

	@Override
	protected Map<Register, Expr> getRegisterValuesFromSpace(ExprSpace s,
			List<Register> registers) {
		throw new UnsupportedOperationException();
	}
}

public static class BytesExprPcodeExecutorState extends PairedPcodeExecutorState<byte[], Expr> {
	public BytesExprPcodeExecutorState(PcodeExecutorStatePiece<byte[], byte[]> concrete,
			PcodeStateCallbacks cb) {
		super(new PairedPcodeExecutorStatePiece<>(concrete,
			new ExprPcodeExecutorStatePiece(concrete.getLanguage(), cb)));
	}
}

结构说明:

  • 抽象类采用每个地址空间一个专属对象的策略,每个对象通常维护一张 long 偏移 → 模型类型(Expr)的映射。ExprSpace 承载了大部分实际存储逻辑;
  • ExprPcodeExecutorStatePiece 提供 ExprSpacespaceMap 的真实映射。构造时拿到语言与两个 arithmetic(具体寻址用 BytesPcodeArithmetic,值计算用 ExprPcodeArithmetic),直接传给 super 构造函数即可;
  • getConcreteBuffer() 不需要实现——只有当你需要从抽象存储中解码指令时才需要具体缓冲。动态分析中具体缓冲从 concrete piece 获取而非抽象 piece;静态分析则要自行决定是使用静态反汇编出的指令还是尝试从抽象模型解码;
  • clear() 通过清空地址空间映射实现。注意抽象实现不提供这张映射表,需要自己维护并提供清理逻辑;
  • getForSpace()/setInSpace()/getFromSpace() 是从映射取空间、以及在空间中读写值的三个方法;getRegisterValuesFromSpace() 主要用于用户检查,暂时可以不实现;
  • 最后 BytesExprPcodeExecutorState 的实现很简单:接收 concrete piece,用 PairedPcodeExecutorStatePiece(见 PairedPcodeExecutorStatePiece.java)把它与新建的模型 piece 配对(见 PairedPcodeExecutorState.java)。

警告:为演示简洁,示例刻意忽略了许多细节——最明显的是**写入重叠(overlaps)和读取越界(offcut)**的情况。这看似小问题,实则非常常见,尤其 x86 寄存器是结构化(structured)的:向 RAX 写一次、再从 EAX 读一次就能立刻暴露该问题。课程将其留作练习。

5. 模型专属 Userops

课程未深入展开,但仓库中有两个范例(位于 TaintAnalysis 模块):

  • TaintPcodeUseropLibrary:提供给变量打污点标记的手段。与 Expr 模型"首次读取变量时自动生成 VarExpr"不同,污点分析器假设初始状态无污点。注意该库不使用泛型 T,而是要求 T = Pair<byte[], TaintVec>,从而保证它只与污点增强仿真器一起使用;
  • TaintFileReadsLinuxAmd64SyscallLibrary:演示如何扩展 Ghidra 的系统调用库——不仅能增加新的系统调用,还能叠加新的模型。

6. 组装增强仿真器:AuxEmulatorPartsFactory

Ghidra 通过 AuxEmulatorPartsFactory<Expr> 接口(位于 Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/emu/auxiliary/AuxEmulatorPartsFactory.java)支持构建增强仿真器,这类工厂通常是单例:

public enum BytesExprEmulatorPartsFactory implements AuxEmulatorPartsFactory<Expr> {
	INSTANCE;

	@Override
	public PcodeArithmetic<Expr> getArithmetic(Language language) {
		return ExprPcodeArithmetic.forLanguage(language);
	}

	@Override
	public PcodeUseropLibrary<Pair<byte[], Expr>> createSharedUseropLibrary(
			AuxPcodeEmulator<Expr> emulator) {
		return PcodeUseropLibrary.nil();
	}

	@Override
	public PcodeUseropLibrary<Pair<byte[], Expr>> createLocalUseropStub(
			AuxPcodeEmulator<Expr> emulator) {
		return PcodeUseropLibrary.nil();
	}

	@Override
	public PcodeUseropLibrary<Pair<byte[], Expr>> createLocalUseropLibrary(
			AuxPcodeEmulator<Expr> emulator, PcodeThread<Pair<byte[], Expr>> thread) {
		return PcodeUseropLibrary.nil();
	}

	@Override
	public PcodeExecutorState<Pair<byte[], Expr>> createSharedState(
			AuxPcodeEmulator<Expr> emulator, BytesPcodeExecutorStatePiece concrete,
			PcodeStateCallbacks cb) {
		return new BytesExprPcodeExecutorState(concrete, cb);
	}

	@Override
	public PcodeExecutorState<Pair<byte[], Expr>> createLocalState(
			AuxPcodeEmulator<Expr> emulator, PcodeThread<Pair<byte[], Expr>> thread,
			BytesPcodeExecutorStatePiece concrete, PcodeStateCallbacks cb) {
		return new BytesExprPcodeExecutorState(concrete, cb);
	}
}

public static class BytesExprPcodeEmulator extends AuxPcodeEmulator<Expr> {
	public BytesExprPcodeEmulator(Language language,
			PcodeEmulationCallbacks<Pair<byte[], Expr>> cb) {
		super(language, cb);
	}

	public BytesExprPcodeEmulator(Language language) {
		this(language, PcodeEmulationCallbacks.none());
	}

	@Override
	protected AuxEmulatorPartsFactory<Expr> getPartsFactory() {
		return BytesExprEmulatorPartsFactory.INSTANCE;
	}
}

样板代码很多,但 parts factory 本质上只是提供了一个扁平接口,用于给出组装增强仿真器所需的全部部件:模型 arithmetic、机器级与线程级的 userop 库、机器级与线程级的 state。本例中:arithmetic 按语言直接提供;userop 库返回空库(如果有自定义库或模型专属库,就在这里组合);state 则直接拿框架提供的 concrete state 构造出增强 state。BytesExprPcodeEmulator 继承 AuxPcodeEmulator 并覆写 getPartsFactory() 完成绑定。

7. 动态分析中使用

到这里构建的组件已足以在脚本中构建和使用增强仿真器。用法与纯具体仿真器同样直接,唯一的例外是访问状态时要意识到"成对"

public class ModelingScript extends GhidraScript {

	// ...

	@Override
	protected void run() throws Exception {
		BytesExprPcodeEmulator emu = new BytesExprPcodeEmulator(currentProgram.getLanguage());
		// TODO: Initialize the machine
		PcodeExecutorState<Pair<byte[], Expr>> state = emu.getSharedState();
		state.setVar(currentAddress, 4, true,
			Pair.of(new byte[] { 1, 2, 3, 4 }, new VarExpr(currentAddress, 4)));
		PcodeThread<Pair<byte[], Expr>> thread = emu.newThread();
		// TODO: Initialize the thread
		while (true) {
			monitor.checkCancelled();
			thread.stepInstruction(100);
		}
	}
}

注意:当状态以 paired 方式访问时,任何 set 都会同时影响两半。如果用 arithmetic 生成值,它会分别在两个子 arithmetic 上调用 fromConst 生成 pair——你可能无意中把右半设成了 LitExpr。若只想修改 pair 的一侧,把 state 转型为 PairedPcodeExecutorState,再用 getLeft() / getRight() 取回单独的 piece 操作。

8. 静态分析中使用

静态分析部分课程未深入,因为相关基础设施尚未成形(许多基础工具还未被抽取)。不过,UnwindAnalysis 及其同目录文件是一个好例子:Debugger 的栈展开器(stack unwinder)在静态分析中使用了 PcodeArithmeticPcodeExecutorState 接口。展开完整调用栈固然属于动态分析,但对单个函数做栈帧信息恢复的分析是纯静态的

9. GUI 集成

GUI 集成比早期版本轻松得多:唯一需要提供的功能是Expr 序列化到 trace 数据库的手段。理想情况下序列化还应是人类可读的,因为那样可以直接在 UI 中展示。只需为新 state piece 提供一个 PieceHandler

public static class ExprPieceHandler
		extends AbstractSimplePropertyBasedPieceHandler<byte[], Expr, String> {
	@Override
	public Class<byte[]> getAddressDomain() {
		return byte[].class;
	}

	@Override
	public Class<Expr> getValueDomain() {
		return Expr.class;
	}

	@Override
	protected String getPropertyName() {
		return "Expr";
	}

	@Override
	protected Class<String> getPropertyType() {
		return String.class;
	}

	@Override
	protected Expr decode(String propertyValue) {
		return Unfinished.TODO("Left as an exercise");
	}

	@Override
	protected String encode(Expr value) {
		return Unfinished.TODO("Left as an exercise");
	}
}

该 piece handler 声明自己适用于"地址域为具体 byte[]、值域为抽象 Expr"的 piece;声明属性名 "Expr",并告知框架属性映射使用 String;最后提供编解码器(课程留作练习)。如果想顺便练习实现"分段/重叠变量访问",可以改用 AbstractPropertyBasedPieceHandler

然后是最终的 Expr 增强仿真器工厂:

public static class BytesExprEmulatorFactory implements EmulatorFactory {
	@Override
	public String getTitle() {
		return "Expr";
	}

	@Override
	public PcodeMachine<?> create(PcodeDebuggerAccess access, Writer writer) {
		writer.putHandler(new ExprPieceHandler());
		return new BytesExprPcodeEmulator(access.getLanguage(), writer.callbacks());
	}
}

它只是接过框架提供的 trace Writer,把 ExprPieceHandler 挂进去。该工厂可以通过脚本安装。脚本会把你的工厂设为整个工具的当前 emulator factory,但脚本安装的工厂不会出现在菜单里;且每次改动仿真器实现后必须重新运行脚本,最好同时使 emulation cache 失效:

public class InstallExprEmulatorScript extends GhidraScript implements FlatDebuggerAPI {
	@Override
	protected void run() throws Exception {
		getEmulationService()
				.setEmulatorFactory(new ModelingScript.BytesExprEmulatorFactory());
	}
}

一旦仿真器"生产可用"(production ready),推荐的做法是用 Eclipse 的 GhidraDev 插件创建正式的 Ghidra Module 工程(参考仓库根目录的 DevGuide.md),把脚本中的嵌套类拆分成独立文件。只要你的工厂类是 public 的、类名以 EmulatorFactory 为后缀、实现该接口并处于 Ghidra classpath 中,Ghidra 就会发现它,并列入 Debugger → Configure Emulator 菜单。

9.1 在 UI 中显示与操作抽象状态

有了 emulator factory,大部分工作就完成了。但此时用户只能通过脚本,或在 Go To Time 对话框中通过 patch step 调用自定义 userop 来与抽象部分交互。若要在 UI 中显示抽象状态,还需开发两个组件:一个用于 Dynamic Listing(内存状态显示),一个用于 Registers 窗口(寄存器状态显示)。(Watches 与 P-code Stepper 面板不支持显示自定义状态。)与 emulator factory 不同,这些组件不能通过脚本安装,必须以类的形式打包在正式的 Ghidra Module 中。

由于基于字符串的序列化可能是常见情况,Ghidra 未来可能会提供抽象实现来简化该场景。目前请参考 Taint 增强仿真器的实现:

超出此范围的交互则需要完全自定义的 provider、plugin 等。

10. 小结

回顾整条技术链路,Ghidra 的 P-code Modeling 把"增强仿真"拆解为五个正交、可独立替换的组件:

  1. Userop 库PcodeUseropLibrary):Java 回调 / 预编译 Sleigh / Structured Sleigh 三种技术可混用,负责环境行为(系统调用、外部函数桩);
  2. 抽象算术PcodeArithmetic<T>):BE/LE 双常量枚举 + forLanguage 静态方法是官方推荐模式,负责值的运算语义;
  3. 状态 piecePcodeExecutorStatePiece):继承 AbstractLongOffsetPcodeExecutorStatePiece 按地址空间组织存储,PairedPcodeExecutorState 把具体与抽象 piece 组合,负责存储、寻址与内存操作;
  4. Parts factoryAuxEmulatorPartsFactory<T>)+ AuxPcodeEmulator:把上述部件组装为增强仿真器;
  5. GUI 接入PieceHandler(序列化到 trace 库)、EmulatorFactory(注册进 Debugger)、Field/Register 显示组件(必须以 Module 提供)。

所有接口与抽象组件均可在 Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/execGhidra/Framework/Emulation/src/main/java/ghidra/pcode/emu/auxiliary 下找到源码;完整的参考实现见 TaintAnalysis 模块(Ghidra/Debug/TaintAnalysis)。课程示例为教学目的做了多处简化(如忽略寄存器结构化带来的重叠读写问题、编解码留作练习),在实际项目中落地时务必补齐这些细节。

登录后查看全文
热门项目推荐
相关项目推荐