如何打造用于分析V8字节码的Ghidra处理器模块(中)

2021-06-08 11:10:00 Author: www.4hou.com
觉得文章还不错?,点我收藏



在这篇文章中,我们继续为读者详细介绍如何打造用于分析V8字节码的Ghidra处理器模块。

(接《如何打造用于分析V8字节码的Ghidra处理器模块(上)》

描述操作数和语义

让我们把这个概念复杂化:我们将为LdaSmi操作描述一个构造器,其中一个整数被加载到累加器中(acc在寄存器空间的定义中),以及为AddSmi操作描述一个构造器,其本质是将累加器中的值与一个整数相加。

为了当前和未来之需,我们将效仿Node.js的bytecodes.h中处理操作数的方式再定义几个字段,并在一个名为“operand”的新标签中创建它们,因为这些字段的用途与“op”标记非常不同。创建几个具有相同位掩码的字段的好处在于,首先是为了便于理解,然后,还可以在一条指令中使用一个标记的多个字段(详见AddSmi的例子):

1.png

从代码展示的角度来看,我们希望看到类似LdaSmi [-0х2]这样的内容。因此,我们在显示部分定义一个助记符,然后在模板中写上字段名,这些字段名应该取自反汇编操作部分或比特模式部分(这里的方括号不是必须的,之所以加上,只是为了美观)。

对于AddSmi指令,在比特模式部分,除了设置操作码限制的op字段外,其他字段还出现在操作数标记中,并以分号(;)为分隔符。它们将作为操作数被放置在显示部分。实际上,这些二进制位是按照字段在比特模式部分指定的顺序来映射的。我们使用文档化的操作在语义操作部分实现指令逻辑;这就是解释器在执行这些指令时要做的事情。

2.png

此外,还可能含有寄存器、上下文变量(稍后将详细介绍),以及一个或多个字段与上下文变量的组合,用分号 (;)分隔。

下面的截图中,展示的是启用了Ghidra listing窗口的Edit the Listing Fields菜单的Pcode字段的情况下,该窗口中带有描述信息的指令。由于代码经过了优化处理,反编译窗口中的内容还缺乏指导性,因此,在这个阶段,只关注中间p代码操作即可。

3.png

在v8字节码中,所有以lda开头的指令都意味着将向累加器中加载一个值,但是,要想在字节码指令中清楚地看到acc寄存器,则需要在构造器中进行相应的定义,以便让其显示出来;就这里来说,acc位于比特模式部分的哪个位置并不重要,因为它不是一个字段,也不占用指令位,尽管它确实需要被写进去。

4.png

1.png 

从函数中返回值的指令是用return关键字实现的,如前所述,在调用函数时,通过累加器返回值是最常见的一种方式:

5.png

通过比特掩码输出寄存器

我们上面描述的指令并没有使用寄存器作为操作数。不过现在,我们实现了Mul指令的构造器,在这个构造器中,累加器将与存储在寄存器中的数值进行乘法运算。

在预处理程序代码的基本定义和宏部分,已经声明了寄存器,但是为了根据字节码中的二进制位来选择所需的寄存器,我们需要将寄存器列表与相应的位掩码绑定到一起。其中,kReg字段的长度为8位。利用附加变量结构,我们依次定义了从0b到1111111b的比特掩码,上述寄存器将对应于构造器中随后使用的集合列表中的字段(在我们的例子中只有kReg)的一部分。例如,从下面的描述中可以看出,编码为0xfb(1111011b)的操作数利用kReg进行描述时,将被解释为r0寄存器。

6.png

现在,当寄存器被附加到kReg变量时,就可以在构造器中使用这个变量了:

7.png

1.png 

当然,我们也可以让构造更加复杂,使构造器与interpreter-generator.cc Node.js源文件中更高层次的指令描述相对应。比如,我们可以将kReg字段移到一个单独的构造器中,其表的标识符位于表头部分,我们将其命名为src。并且,在其语义操作部分还出现了一个新的关键词——export(导出)。我们不打算详细讨论P代码的构造,不过从本质上讲,export意味着定义了必须放在构造器语义操作部分的值,以代替src。不过,这不会对Ghidra的输出产生影响。

8.png

对于指令的例子,在描述方面也是非常类似的,以后在真实文件上测试模块时将会用到它们:

9.png

利用goto跳转到相应的地址

在字节码中,我们会遇到基于当前地址的相对偏移量的有条件跳转和无条件跳转。为了跳转到SLEIGH中的某个地址或标签,我们可以使用goto关键字。在定义方面值得注意的是,比特模式部分使用了kUImm字段,尽管它不是以纯粹的形式使用的。在反汇编操作部分计算的值通过rel标识符输出到显示部分。而inst_start则是SLEIGH的一个预定义符号,用于表示当前指令的地址。

10.png

在这里,SLEIGH编译成功。也就是说,在这个变体中(见下图),从输出结果来看,不能创建包含与特定空间绑定的varnodes(这些是由p-code指令操作的对象)。

 1.png

我们根据开发者推荐的方法,通过创建一个带有dest标识符的额外构造器来取出了部分定义。在这里,结构*[ram]:4 rel并不意味着我们从rel地址处取4个字节。事实上,地址rel是从ram空间中导出的。在SLEIGH语言中,星号(*)运算符意味着解除引用,但在这个特定的例子中,它与创建varnode的细微差别有关(这一点将在后文中详细加以解释)。

实际上,这里可以不用指定[ram]空间(注释中有一个例子),因为在定义过程中,我们将其定义为默认空间。我们可以在p-code指令中看到,偏移量被标记为属于ram。

11.png

 1.png

由于使用了条件结构,JumpIfFalse指令显得有些复杂。在SLEIGH语言中,该指令可以与goto关键字一起使用。为了更符合JS的概念,False值以前被定义为一个寄存器,我们注意到,在*.spec中,它所连接的寄存器空间范围被标记为全局的。真是因为这一点,它在伪代码中是根据寄存器的名称而不是数值来显示的。

12.png

1.png 

在提供的例子中,是跳转到相对于inst_start计算的地址。让我们考虑一下TestGreaterThan指令,它使用goto跳转到相应的标签处(下面的例子中是< true >)和inst_next处。因此,跳转到标签处的操作应该是很直观的:如果条件为真,那么在标签位置之后的指令就会被执行。需要注意的是,标签只在其语义操作部分有效。

goto inst_next结构实际上是结束当前指令的处理,并将控制权移交给下一条指令。注意,“s >”是用来进行有符号的比较的(详情见文档)。

13.png

1.png

多寄存器操作数

以前的指令使用不超过一个寄存器作为操作数。因此,我们先谈一下如何将多个寄存器用于操作数。

通过具有不同表标识符的相同构造器来描述操作数(见下面例子中的构造器)的做法,在另一个表的单个构造器中非常有用。我们将应用这样的做法,不仅是为了符合V8指令的描述,同时,也是为了避免可能的错误。例如,CallProperty2指令的四个操作数都是寄存器,并且在位掩码方面的规定也是相同的。如果将构造器定义为 CallProperty2 kReg, kReg, kReg, kReg, [kIdx],那么,在试图使用处理器模块打开文件时,SLEIGH编译器将会报错。因此,在我们的模块中,将使用构造器来创建类似于别名的东西。

14.png

当然,值得注意的是,即使不定义新的构造器也可以解决这个问题。例如,通过在一个标记中定义和注册callable、receiver、arg1和arg2字段,并随后使用attach命令将它们绑定到寄存器列表中,也可以解决这个问题。

15.png

这些字段的工作方式与前面例子中的kReg相似。至于到底使用哪种具体方法,则要看个人喜好了。

函数调用

在CallProperty2指令中,还值得关注的部分,就是语义操作部分,它使用了调用[callable]结构——在此之前,我们还没有用过这个结构。在V8中,函数参数被存储在aX寄存器中(正如我们在CSPEC中所指出的)。然而,就字节码而言,我们不会在调用函数之前立即将参数放放到这个寄存器中。另外,解释器是可以独立完成这一任务的,因为它本身拥有完成这一任务所需的所有信息。然而,Ghidra却没有完整的信息,所以在语义操作部分,我们需要通过手动方式将参数写入寄存器中。也就是说,我们需要在调用后恢复相关寄存器的值,因为在调用函数中,这些寄存器也可能用于存储其他的值。为此,我们可以使用局部变量来保存它们。

16.png

在CallUndefinedReceiver1示例中使用宏时,我们也可以将参数保存在内存中(就这里来说,实际上是保存在堆栈中:由于sp没有被指令用到,所以不会影响反编译器的显示):

17.png

当编写涉及加载器和分析器的模块时,一定要确保传递参数的顺序与函数参数的顺序一致,这些参数是在Java代码部分定义的。另外,保存的函数参数不能多于调用函数中的参数,虽然这一点非常重要,但在SLEIGH中却很难实现。这个问题的解决方案是,可以参阅Vyacheslav Moskvin关于如何注入p代码指令的相关文章。

用户定义的操作

为了在反编译过程中不丢失指令,在尚未规划或无法描述语义的操作部分中,我们可以使用用户定义的操作。值得注意的是,就acc来说,我们并不需要显式指定其长度,因为寄存器的大小是显式定义的。然而,向这种用户操作传递数字常量时,你必须显式指定传递的值的大小(就像文中后面关于寄存器范围一节中的CallVariadicCallOther的情形那样)。用户定义的操作被定义为define pcodeop OperationName,并在构造器的语义操作部分使用,其格式类似于许多编程语言中的函数调用。

18.png

这些操作可用来在分析器中注入p代码指令:通过callotherfixup标签将相关调用添加到CSPEC文件中,而相关逻辑则在分析器中给出。

如果没有使用Java代码进行重新定义的话,反编译器中的用户操作就就按照它们在语义操作部分定义的那样发挥作用。

1.png

小结

在这篇文章中,我们将为读者详细介绍如何打造用于分析V8字节码的Ghidra处理器模块,由于篇幅过长,我们将分多篇进行发布,更多精彩内容,敬请期待!

本文翻译自:https://swarm.ptsecurity.com/creating-a-ghidra-processor-module-in-sleigh-using-v8-bytecode-as-an-example/如若转载,请注明原文地址



觉得文章还不错?,点我收藏



如果文章侵犯到您的版权,请联系我:buaq.net[#]pm.me