Ghidra源码分析(三)
(发布时后没注意按错了)
接上篇…
每次的 nextBlock 是通过 DisassemblerQueue.getNextBlockToBeDisassembled 函数根据 disassemblerQueue 中的 currentBranchQueue 队列保存的信息和 fallThruAddr 来获取的。该类与 Disassembler 类同包。同时会设置 nextBlock 的 flowFrom 的地址。
每次对 nextBlock 进行反编译之后,nextBlock 的地址范围就确定了。同时反编译出来的代码放在 InstructionMap 成员变量中。
同时会根据具体的指令,设置它的 blockFlows。这个成员变量是一个 InstructionBlock 类型的队列,它保存了 nextBlock 中会产生的目标 Blocks。
比如:invoke-xxx 的下一条指令的地址会作为一个新的 Block,if_xxx 的跳转地址也会作为一个新的 Block。当然已经处理过的 Block 不会被加入。
之后该 nextBlock 会被加入 InstructionSet 中,而 blockFlows 的信息会被加入 disassemblerQueue 的 currentBranchQueue 中。
继续看调用 Disassembler.disassembleInstructionBlock 函数对 nextBlock 进行实际的反编译操作。
可以看到这个函数做了三件事:
- 通过 SleighLanguage.parse 函数生成 InstructionPrototype。这里会得到指令的 Operator 的种类和格式。
- 通过 Disassembler.getPseudoInstruction 函数生成 PseudoInstruction。这里会获取到具体指令的 Operands 的内容。
- 通过 Disassembler.processInstruction 函数处理 Instruction Flows。
变量 instrMemBuffer 是从地址 addr 上读取的值,默认长度是 8 个字节。
实际上调用 SleighLanguage.parse 函数对 instrMemBuffer 上的内容进行指令格式的解析,类在 Framework SoftwareModeling 下面的 ghidra.app.plugin.processors.sleigh 包下面。
(首先看下 SleighLanguage 类。
这个类就是实际用来进行指令反编译的类,它在创建的时候是根据目标语言的类型所初始化的。
它保存了一些针对目标语言的重要内容。
这里来看下初始化完成后的这些信息:
这里 registerBuilder 里面就是 Pcode 和 Dalvik 中会用的 Register。
这里 root 中保存了 Dalvik 字节码所有 Operators。参考之前的之前的 "Dalvik 字节码与指令格式" 一节,总共有 255 中操作符,可以看到 root 下面 children 中也有 255 个 DecisionNode。
第 1 个 DecisionNode 编号是 line285(id0.0),内容是 nop。可以查到 0x00 正好对应的是 nop 指令。
第 99 个 DecisionNode 编号是 line1176(id0.98),内容是 sget_boolean。可以查到 0x63 正好对应的是 sget-boolean 指令。
当然实际情况要更复杂一些,比如 invoke-virtual A vB 和 invoke-virtual A vB vC 这种肯定是要进一步处理的,这也是 DecisionNode 是个 Tree,以及 id0.0 到 id0.290 的原因!!!
所以根据地址上的内容(指令第一个字节的内容)和 root 成员变量,完全可以确定是这个指令的 Operator 和指令格式!!!
此外还有 symtab,它保存了需要用的的符号。
这些东西都会在初始化的时候通过 XML 文件解析的方式读取进来!这里不详细讲了。)
(接着看下是 Constructor 类,这个类和 SleighInstructionPrototype 同一个包。
根据上面 SleighLanguage 中的解释可以看到,具体的指令格式和内容是用这个 Constructor 类来保存的。
可以看到:
operands 其实就是操作数的格式。比如这里的两个操作数是:寄存器 A 长度 8 bit 和一个 16 位的字段。
parent 其实就是之前提到的所有的 Operators 的 Constructor。
printpiece 是指令打印格。)
在创建 SleighInstructionPrototype 对象的时候调用 SleighInstructionPrototype.resolve 这个函数。
继续调用 DecisionNode.resolve 这个函数,这个类和 SleighLanguage 同包。
这里的 DecisionNode 就是之前提到的 root,这是个树的根。需要去遍历这个由 DecisionNode 组成的树来完成对指令操作符及其格式的解析。而 ParserWalker 就是记录遍历到的地址上的偏移和具体哪个节点。
使用 ParserWalker.getInstructionBits 函数根据 DecisionNode 的 startbit 和 bitsize 来获得目标地址上对应位置上的值,也就是 val。这里不详细展开了。
DecisionNode 的这两个成员变量是在创建 SleighLanguage 的时候和其他的信息一样,都是直接从配置中获取到的。比如 root 这个 DecisionNode 本身的 startbit 是 0 而 bitsize 是 8,所以一开始读取 8 个字节。而对于上面提到的,比如代表 invoke-direct 指令的 DecisionNode,由于它还有一些子节点(需要操作符后面 4 bit 来确定参数个数),所以它的 startbit 是 9 而 bitsize 是 3。如果是终节点的话,bitsize 是 0。
根据 val 选择对应的 children 中的 DecisionNode。而且由于是遍历树的,所以继续进行递归调用 DecisionNode.resolve 函数。
再确定了代表这个 Operator 的 Constructor 之后将其返回。
(回到 SleighInstructionPrototype.resolve 函数)
再确定了操作符 Constructor 之后,需要对后续的参数进行处理,因为还需要确定指令范围之类的。
当前要处理的 Constructor 会被封装成 ConstructState 保存在 ParserWalker 的 point 成员变量上。已经处理过的 Constructor 则放在 resolvedStates 这个队列里面。
操作数会被一次从 Constructor 的 operands 数组中取出,是 OperandSymbol 类。有的 OperandSymbol 中还有 tsym 这个成员变量,是 SubtableSymbol 类型,是因为这个操作数还需要细分。比如,参数一的代表名字的 PARAM_A 和代表存储的 reg_a 也是需要分开处理的。这些 Symbols 也是 SleighLanguage 初始化的时候从配置中直接获得的。
ParserWalker 会为每一个要解析的目标创建一个新的 ConstructState,设置当前的 Offset,设置 breadcrumb 的层级等等。有的还需要递归处理,比如为上面的 SubtableSymbol 创建对应的 ConstructState。然后递归处理其中的 Constructor。
以上处理过的东西都保存在 SleighInstructionPrototype 的 rootState 成员变量里面,这个成员变量是 ConstructState 类型的树!
也就是说,一条指令的格式内容或者说结构内容都是以这个 rootState 所代表的 ConstructState 的树来表示的。
(回到 SleighLanguage.parse 函数)
newProto 就是上面得到的 SleighInstructionPrototype 类型对象。
SleighInstructionPrototype.cacheInfo 用来处理 newProto 的 flowStateList、flowType,instrMask、mnemonicState、operandMasks、opRefTypes 和 opresolve 之类的。
比如 invoke-static 对应的 flowType 就是 COMPUTED_CALL,instrMask 是 FFF000000000 之类的。
同时处理出来的指令格式会被 Cache 起来。之后遇到同一类型的指令就不用再次解析了。
(回到 Disassembler.disassembleInstructionBlock 函数)
获取到代表指令格式的 prototype 之后,根据格式来获取真正的反编译指令。
这里是调用 Disassembler.getPseudoInstruction 函数来完成的。具体是 PseudoInstruction.toString 来完成的。这里不展开了。
接下来则是 Disassembler.processInstruction 函数来处理 flow 和 fallthrough。
InstructonBlock 的 flowAddrs 中保存 flow 的目标地址,比如 if_ez 指令会跳转过去的目标地址。
之后会根据 flowAddrs 中的地址来创建新的 InstructionBlockFlow 并添加到 InstructionBlock 和 DisassemblerQueue 的 currentBranchBlock 中。
(回到 Disassembler.disassemble 函数)
获取到的 InstructionSet 是这函数的所有的 InstructionBlock 的集合。
接着是将反编译出来的 InstructionSet 与代表函数地址的 ListingDB 绑定。
之后调用 Disassemble.markUnimplementedPcode 函数来处理 Pcode。
其实还是走到 SleightInstructionPrototype.getPcode 函数。
实际也是根据之前提到的 Constructor 的 templ 成员变量中的 vec 来确定的。这个 vec 其实就是 Instruction 对应的 Pcode 的格式。
这个其实也是在初始化的时候从配置文件中获取的。
比如:
以上就是 DexHeaderFormatAnalyzer.createMethods 函数进行反编译处理的部分。
而根据 DexHeader 部分对 DEX 文件进行的初步处理也就此完成了。
DexMarkupInstructionsAnalyzer
这里主要关注 References 的创建。
比如 DexMarkupInstructionsAnalyzer.analyze 函数。
首先所有包含指令的地址块会被加到 AddressSet 里面,通过 ListingDB 回去所有属于 AddressSet 的指令。
之后就是遍历所有指令并根据指令类型分别进行处理。
比如操作数是 String 的话,就通过 processString 得到字符串内容,能够显示到界面上。
同理 processClass、processField 这些也是一样的。
而 processMethod 和 processString 还要处理 References。
以 DexMarkupInstructionsAnalyzer.processMethod 为例。
参数中 methodIndex 是目标方法的 Index,所以 methodIndexAddress 是目标函数的地址。
只有 ReferenceDBManager 才能处理 References。
调用 ReferenceDBManager.addMemoryReference 来添加 Reference,实际是 ReferenceDBManager.addRef 函数。
首先是要清理原有的从 fromAddr 到 toAddr 的引用。
分别获取 fromAddr 和 toAddr 的 RefList,如果没有的话,就通过 FromAdapterV0.createRefList 和 ToAdapterV0.createRefList 来创建新的 RefList,这两个类和 RefereceDBManager 类同包。
生成的是 RefListV0 类,和上面几个类同包。
(首先看下 RefListV0 类。
每个 Cross-References 的边的 from 和 to 都有一个 RefListV0 来表示,address 就是它们的地址,isFrom 来判断是不是 from。
可以发现 Reference 是以字节数组的形式 refData 进行保存的。)
然后调用 RefListV0.addRef 去给代表 fromAddr 和 toAddr 的 RefList 添加 Reference。
实际上是调用 RefListV0.appendRef 函数。
可以看到它是把引用 encode 成了一段字节码,并且直接加到 RefListV0 的成员变量 refData 字节数组中。代表 References 数量的 numRefs 加 1。
可以看下 RefListV0.encode 函数。
可以看到 RefListV0.putLong 可以将 Address 放入 data 数组中。
看上去还是很简单。而且 RefListV0 提供 decode 函数来直接进行解码。
(回到 ReferenceDBManager.addRef 函数)
接着是获取代表 fromAddr 到 toAddr 的引用的 ReferenceDB,无论是从 fromRefs 还是 toRefs 里面获取,都一样。
(首先看下 ReferenceDB 类。
这里 opIndex 是操作符索引。)
之后再调用 ReferenceDBManager.referenceAdded 函数处理 ReferenceDB,主要是调整 fallThrough。
简而言之是通过 ReferenceDBManager 通过 Address 找到 RefList 在找到 ReferenceDB 的方式来找 Cross-Reference。