如何打造用于分析V8字节码的Ghidra处理器模块(下)
2021-06-11 10:21:59 Author: www.4hou.com(查看原文) 阅读量:172 收藏

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

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

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

测试模块

在这个阶段,我们已经可以通过实践来检查处理器模块的工作情况。下面,我们通过来自JS示例中的bytenode来演示如何编译JSC文件:

1.png

我们将尝试启动根据本文编写的项目,并将获得的JSC文件导入Ghidra。如果某些地方写错了,Ghidra将显示一个错误,而导致错误的字符串的编号将记录到eclipse日志中。当修改代码后,无需重新构建项目:SLEIGH将在下次打开文件时进行重新编译。

这里提供了正常情况下所得到的文件。此外,您也可以在我们的repository中下载为该项目编写的相关工具的完整代码。

由于我们只描述了指令,所以,无法对该文件进行相应的分析并解析成函数。需要注意的是,相关代码位于偏移量0x1c0处,按D键可以将该偏移量处的字节转换成指令,按F键可以创建一个函数,具体如下图所示:1.png

当我们使用其他工具(除了SLEIGH)时,输出会变得更加清晰,这些工具可以供模块开发者使用。例如,当使用pool常量添加work时(cpool关键字已被保留以在SLEIGH中引用),就有机会在LdaGlobal命令中解析数字标识符。这是该函数在我们项目的最新版本中的样子(供对比之用):

1.png

当然,如果结果与用JavaScript编写的源代码具有更好的匹配度的话,那就更好了,但这仅靠在.slaspec(和.sinc)文件中描述指令是难以实现的。不过,下一篇文章为大家带来了更大的想象空间;它描述了一种注入p代码操作的机制,允许在完全访问所有应用程序资源的情况下,对创建p代码树的指令进行操作。然后,利用创建的p代码树,将反编译结果绘制并显示到相应的界面上。

关于寄存器的范围

在V8字节码中,有许多指令都涉及到寄存器的范围、一对寄存器对和三个寄存器。从编码范围的角度来看,有些代码需要知道某个寄存器范围内的第一个寄存器的地址,以及该范围内包含的寄存器的数量。对于使用一对寄存器和三个寄存器的指令,只需要指定初始寄存器即可,因为解释器事先就知道一个给定的指令到底需要使用多少个寄存器。

2.png

由此不难看出,一个简单的解决方案是:在解析指令时显示第一个寄存器及其数量,比如ForInPrepare r9, r10!3。但是,为了提高可读性,我们可以做一些妥协:输出某范围内的第一个和最后一个寄存器,但是,如果要想实现这一点的话,需要借助于包含多个构造器的表。

包含多个构造器的表

作为项目的一部分,为了便于理解,我们决定显示完整的寄存器清单。在显示部分,还没有可用于输出寄存器范围的现成模板。不过,我们可以借鉴ARM处理器模块的相关原则:通过构造器链来输出变量(这里,只能效仿原则本身;由于架构的不同,具体的实现并不适用)。

 1.png

在这里,大家可以清楚地看到一个具有某种标识符的表包含多个构造器的情况。实质上,这些构造器在表头部分具有相同的标识符,在比特模式部分具有不同的条件。看了这个描述,您可能意识到已经实现了一个类似的表,只不过它时用来描述指令的——它们位于根指令表中,并且可以根据操作码的条件进行选择。

在其他构造器中使用标识符时,将选择最合适的选项。例如,如果一个表的构造器被描述为满足某个条件,而下一个构造器没有这个条件要求,那么如果第一个条件为真,就会选择第一个选项,而不考虑第二个,即使第二个在形式上与该条件不矛盾,因为它根本没有施加任何条件。

正如我们可能假设的那样,查看上面的CallProperty指令的逐字节描述,要显示整个范围,我们需要从第一个条目开始输出寄存器,重点关注已知的第一个范围寄存器和其中的元素数量。就显示部分而言,范围由两个构造器创建:rangeSrc和rangeDst。rangeSrc用于初始化,我们在这里保存输入数据,而rangeDst用于根据收到的信息输出寄存器。对于rangeDst来说,我们需要创建由多个构造器组成的表:至少要分别显示aX和rX寄存器的范围。

为了实现这些条件,则必须考虑到一些限制。在这里,我们建议只用等号(=)来检查比特模式部分的值,不能直接使用寄存器的值,也不能在反汇编操作部分给它赋值。这意味着:我们无法使用临时寄存器。另外,起始寄存器和范围的长度可能是不同的值,而范围,如前所述,既可以实现为aX寄存器,也可以实现为rX寄存器;它也可以有一个零长度。在这个阶段已经很清楚了,如果我们不想为每种情况创建大量的定义,那么最好使用一些计数器,以确定我们应该输出多少个寄存器,以及从哪个位置输出。

上下文变量

上下文变量适合用于解析任务。它们的定义类似于标记字段的定义。然而,在这种情况下,字段使用指定寄存器的位,而不是真正的程序位(contextreg,下同)。

当通过寄存器中定义的字段在相同的位范围内执行写入操作时,相应的值将被覆盖(在某些情况下,编译将无法正常工作),因为这些是同一寄存器的位。我们的上下文变量,counter和offStart,在寄存器中使用不同的位范围,因为就其含义而言,它们是不同的值。

值得注意的是,上下文变量必须在构造器之前声明。

3.png

根据文档的解释,上下文变量通常在比特模式部分用来检查某种上下文的存在,并在反汇编操作部分进行相应的修改。因此,在具有表标识符rangeSrc的构造器中,我们将用它来显示范围,在反汇编操作部分,我们把第一个范围寄存器的代码保存到上下文变量offStart中,而数量则存入上下文变量counter中。在显示部分,我们用一个开放的花括号({)来标记范围的开始。

值得注意的是,V8并没有使用range_size寄存器:它是人为引入的,用于保存范围大小,因此,如果需要,可以将该值作为指令构造器语义操作部分的一部分使用,这样会更加方便。而rangeSrc则用于为指令的语义操作部分提供起始寄存器和范围大小。

4.png

作为具有rangeDst标识符的表的一部分,以下实例描述了五个构造器:

·    起始范围的代码对应于a0,计数器等于0(空范围)。

    rangeDst: } is epsilon; counter = 0; offStart = 0x02 {}

·    起始范围的代码对应于r0,计数器等于0(空范围)。

    rangeDst: } is epsilon; counter = 0; offStart = 0xfb {}

·    offStart中的范围寄存器的代码与a0相同;在反汇编操作部分,counter递增,offStart中的寄存器代码递增;跳转到rangedst1构造器。

    rangeDst: ^ a0 rangeDst1 is a0; rangeDst1; offStart = 0x02 [counter = counter -1;offStart = offStart +1;] {}

·    offStart中的范围寄存器的代码与r0相同;在反汇编操作部分,offStart中的counter和寄存器代码递减;跳转到rangedst1构造器。

    rangeDst: ^ r0 rangeDst1 is r0; rangeDst1; offStart = 0xfb [counter = counter -1;offStart = offStart -1;] {}

·    尚未找到起始范围寄存器的代码,跳转到rangedst1构造器(对于本例,构造器中没有任何条件,因此,只有其余构造器都不适合,才会选择它)。

    rangeDst: rangeDst1 is rangeDst1 {}

在第三和第四种情况下,寄存器将被输出到表中。后面的构造器是rangeDstN,其中N是一个自然数;它们由同样的选项组成,只是针对的是aN/rN寄存器。

注意比特模式部分。当描述rangeSrc构造器作为比特模式部分的一部分时,标识范围的两个字节都被部署了,这意味着,作为rangeDst定义的一部分,不需要占用程序的任何位,因为它们不会与描述的指令有牵扯。对于这种情况,我们可以使用预定义的符号epsilon,它与一个空的比特模式相匹配。

下面的例子只描述了rangeDst、rangeDst1和rangeDst2,之所以这样做,只是为了让文章保持简洁。通过它们,就足以让我们了解这类表的类型;完整版本可在github上的项目源文件中找到。实质上,在处理rangeDst时,构造器链将按照rangeDstX中X索引的升序进行处理,直到找到起始寄存器为止。然后,开始处理长度与范围大小相对应的构造器链。

5.png

实际上,带右花括号的构造器看起来非常相似;您可以尝试组合使用。记住,一定要使用逻辑运算符“&”和“|”。

6.png

在已完成的项目中,CallProperty的构造器显示如下:

7.png

这就是我们在listing窗口中看到的内容:

 1.png

现在也许令人困惑的是,用户定义的操作CallVariadicCallOther被用在语义行动部分。在github上的项目中,它被用p-code指令在Java代码中重新定义。使用p-code注入技术而不是通过调用操作实现,是希望在反编译器中看到传递的参数列表(根据Node.js源代码,第一个范围寄存器是接收器,而其余的是传递的参数)。说句实话,只用SLASPEC是很难实现的。

 1.png

如果您想尝试自己来实现范围的处理,可以将语义描述为:

8.png

然后,照葫芦画瓢,我们可以扩展rangeDstХ构造器(我们最多需要包括r7),然后尝试看看console.log(1,2,3,4,5,6)的编译代码是什么样子。您可以通过bytenode自己编译,或者在这里获取已编译好的代码。该函数将位于偏移量0x167处,而指令本身位于0x18b处。

最后,隐含调用的指令(即使具有恒定数量的参数)也以类似的方式实现,以解决反编译过程中出现的问题,因为由于与AX寄存器的初始化存在细微差别,在计算参数数量时经常出现混淆(在启动自动分析功能时,有时会遇到这个问题,不过可以通过修改反编译器中的函数原型来解决,就像在其他体系结构的模块中一样)。

值得注意的是,在我们的项目中,我们把所有的rangeDst构造器都移到了一个单独的文件中,这样就不会因为指令(以及使用2和4字节的操作数的Wide和ExtraWide指令)的描述而使文件变得杂乱:

9.png

小结

这里开发的处理器模块满足了我们为其设定的要求:该工具使得查看文件指定函数的字节码指令成为可能,这些函数必须在项目中解析。此外,我们还得到了一个反编译器,尽管不是一个完美的反编译器,但仍然可以用来更快地浏览应用逻辑。然而,与许多处理器模块一样,在这种情况下,最好直接查看指令,而不是盲目相信反编译器。还需要指出的是,如果有时间和愿望来改进这个工具,那么可以:实现类型存储、设计导入概念,以及消除函数参数的反向顺序问题,这些努力都是值得的。我们希望本教程能让您更轻松地通过SLEIGH语言编写处理器模块。

相关链接

    ghidra.re/courses/languages/html/sleigh.html: SLEIGH的相关文档。

    github.com/NationalSecurityAgency/ghidra/tree/master/Ghidra/Framework/SoftwareModeling/data/languages: 含有.cspec、.spec、.opinion和*.ldefs描述信息的有用文件

    spinsel.dev/2020/06/17/ghidra-brainfuck-processor-1.html: 一篇介绍通过Ghidra实现brainfuck模块的优秀文章。

    github.com/PositiveTechnologies/ghidra_nodejs: 一个带有加载器和分析器的完整版Ghidra处理器模块的存储库。

附注

我们经常被问及从哪里获取指令的描述,以便描述Node.js的其他版本,等等。以下是我们的答案和一些入门小贴士:

指令列表存储在Node.js的bytecodes.h文件中。它们是按指令代码升序排列的。在这里你也可以找到操作数的描述。然而,使用下面的函数从编译好的二进制文件node.exe中复制这个列表会更快更轻松: 

const char *__cdecl v8::internal::interpreter::Bytecodes::ToString(v8::internal::interpreter::Bytecode bytecode)

在interpreter-generator.cc中,您可以看到指令的语义,以便了解这些指令的作用。

在编写指令语义时,不要忘记那些在指令中没有显式用到的寄存器。例如,注意观察累加器,看看其值在哪里发生了变化。由于Ghidra优化了反编译器的输出,所以您可能会丢失一部分的逻辑。

指令和操作数的代码占用一个字节(对于常规指令而言)。对于宽指令和超宽指令,指令代码则占用2个字节,大多数操作数类型分别占用2或4个字节(带有kFlag8的操作数不增长)。先为常规指令编写代码,然后,为更宽的指令进行修改,这种做法能够让事情更容易一些。

实际上,根据用户定义的操作来编写反汇编程序或反编译程序是很容易的。您可以从列表输出和原始的反编译器开始下手,然后再添加语义和其他细节。

文件bytecode-register.cc和bytecode-register.h对于理解寄存器的索引计算将是非常有用的。

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


文章来源: https://www.4hou.com/posts/m8mr
如有侵权请联系:admin#unsafe.sh