3. P-Code操作码详解(上):LOAD、STORE、COPY、INT_EQUAL、INT_NOTEQUAL等基础操作码

好,咱们今天来啃P-Code操作码这块硬骨头。说实话,我刚接触Ghidra那会儿,看到P-Code也是一脸懵——这玩意儿到底在干嘛?后来做多了逆向分析才明白,P-Code就是Ghidra的「通用语言」,它把x86、ARM、MIPS这些五花八门的指令集,全部翻译成一套统一的操作码。你想想看,这多方便?

今天我们先讲最基础的一批操作码。它们就像盖房子的砖头,虽然简单,但所有复杂的分析都建立在它们之上。

3.1 LOAD:从内存里把数据「请」出来

LOAD操作码,说白了就是读内存。它的格式长这样:

LOAD  dest,  src,  size

其中dest是目标寄存器,src是内存地址,size是读取的字节数。举个例子:

LOAD  R0,  [R1+4],  4

这条P-Code的意思是:从R1+4这个地址开始,读4个字节,存到R0里。

嗯,这里要注意一点——Ghidra的P-Code里,内存地址都是用方括号括起来的。这是它的语法习惯,你得适应。

我的小技巧: 在分析固件时,我经常用LOAD操作码来定位全局变量。因为全局变量的访问,在P-Code里一定会表现为LOAD操作。你只要在P-Code列表里搜"LOAD",就能快速找到所有内存读取点。

3.2 STORE:把数据「塞」回内存

STORE和LOAD正好相反,它是写内存。格式:

STORE  dest,  src,  size

比如:

STORE  [R2],  R3,  2

意思就是把R3里的2个字节,写到R2指向的内存地址。

我曾经在分析一个恶意软件样本时,发现它频繁使用STORE操作码往栈上写数据。一开始以为是正常操作,后来仔细一看——它是在构造ROP链!每个STORE对应一个gadget地址的写入。这个发现让我对STORE操作码格外敏感。

避坑指南: 注意STORE操作码的size参数。如果size写错了,比如你写4字节但实际只该写2字节,Ghidra的模拟执行就会出问题。我踩过这个坑,调试了半天才发现是size不匹配。

3.3 COPY:数据搬运工

COPY操作码,就是寄存器到寄存器的数据移动。格式:

COPY  dest,  src

例如:

COPY  R4,  R5

把R5的值复制到R4。

你可能会问:「这不就是MOV指令吗?」对,但P-Code里没有MOV,只有COPY。Ghidra的设计者觉得「复制」比「移动」更准确——因为源寄存器的值并没有消失,只是被复制了一份。

我个人习惯在分析函数调用时,特别关注COPY操作码。因为参数传递经常通过COPY来完成。比如:

COPY  R0,  R1    ; 把R1的值传给R0,作为第一个参数
CALL  func

看到这种模式,基本就能确定参数传递关系了。

3.4 INT_EQUAL 和 INT_NOTEQUAL:比较的真相

这两个操作码是条件判断的基础。INT_EQUAL判断相等,INT_NOTEQUAL判断不等。

格式:

INT_EQUAL  dest,  src1,  src2
INT_NOTEQUAL  dest,  src1,  src2

比如:

INT_EQUAL  R6,  R7,  R8

如果R7等于R8,R6就被设为1(真),否则设为0(假)。

INT_NOTEQUAL同理,不等则为1。

为什么我要单独讲这两个?因为在逆向分析中,条件跳转的识别全靠它们。你看下面这个例子:

INT_EQUAL  R6,  R7,  R8
CBRANCH  R6,  label

这对应高级语言里的 if (R7 == R8) goto label;。CBRANCH是条件跳转操作码,我们后面会细讲。

核心要点: INT_EQUAL和INT_NOTEQUAL的结果是布尔值(0或1),不是直接修改标志位。这和x86的CMP指令不同,x86改的是EFLAGS寄存器,而P-Code直接输出一个值。这个差异在写P-Code脚本时一定要注意。

3.5 知识体系总览

说了这么多,咱们用一张图来梳理一下今天的内容。这张SVG图展示了基础操作码的分类和关系:

P-Code基础操作码知识体系 基础操作码 LOAD 从内存读取数据 格式: LOAD dest, [addr], size STORE 向内存写入数据 格式: STORE [addr], src, size COPY 寄存器间数据复制 格式: COPY dest, src INT_EQUAL/NOTEQUAL 比较两个值 结果: 1(真) 或 0(假) 所有操作码都遵循: 操作码 目标, 源1, 源2 的格式

3.6 实战中的组合使用

光讲单个操作码没意思,咱们来看一个实际场景。假设你在分析一个字符串比较函数,P-Code可能是这样的:

LOAD  R0,  [R1],  1      ; 从R1地址读1个字节到R0
LOAD  R2,  [R3],  1      ; 从R3地址读1个字节到R2
INT_EQUAL  R4,  R0,  R2  ; 比较两个字节是否相等
CBRANCH  R4,  equal_label ; 相等则跳转
...

你看,LOAD负责从内存取字符,INT_EQUAL负责比较,CBRANCH负责跳转。三个操作码配合,就实现了字符串的逐字节比较。

我当初在分析一个加密算法时,就是通过这种模式定位到了密钥比较的逻辑。当时找了半天没找到关键判断点,后来直接搜P-Code里的INT_EQUAL,一下子就锁定了位置。

实用技巧: 在Ghidra的P-Code列表视图里,你可以按操作码排序。把所有INT_EQUAL排到一起,然后逐个检查上下文,很快就能找到条件判断的关键点。这个方法比看汇编代码高效多了。

3.7 小结

今天咱们讲了四个基础操作码:LOAD、STORE、COPY、INT_EQUAL和INT_NOTEQUAL。它们虽然简单,但却是P-Code世界的基石。你想想看,没有LOAD和STORE,程序怎么访问内存?没有COPY,数据怎么在寄存器间流动?没有比较操作码,条件判断怎么实现?

嗯,这些基础操作码就像字母表里的字母。单个看没什么,但组合起来就能写出千变万化的程序逻辑。下一批操作码会更复杂一些,比如算术运算、位操作、还有那个让人头疼的CBRANCH。不过别担心,咱们一步一步来。

记住我今天说的:在P-Code的世界里,没有秘密。所有的高级语义,最终都会落在这几十个操作码上。你只要掌握了它们,任何二进制在你面前都是透明的。


专注资料整理