分析 WebAssembly 二进制文件:初步感觉和行为分析

几个月前,我们发表了一篇关于逆向工程 WebAssembly (Wasm) 应用程序的介绍性博客文章。鉴于在此期间 Wasm 的使用和覆盖范围不断增加,现在似乎是重新审视该主题以分析未知二进制文件以及同时可用的一些工具的好时机。

这篇文章将介绍 Wasm 的内存结构,然后着眼于对未知(-ish)样本执行高级行为分析,稍后我们将跟进另一篇文章,我们将手动分析相同的 Wasm 样本,但会更深入。

注意:在本系列博客文章中,我们使用了 GitHub 上的几个公开可用的工具。在您的计算机(或者,更好的是,您的 VM)上安装新软件时请谨慎行事,并记住 Forcepoint 不对这些帐户或本博文中讨论的工具负责。

Wasm栈机、局部变量和全局内存

首先,让我们看看 Wasm 是如何处理内存和数据的。虽然这对于理解这篇文章并不是必不可少的,但它有助于理解变量是如何被发送到我们将要查看的样本的,并且是我们将在下一篇文章中进行的更多手动分析的基础。

Wasm 不使用寄存器,而是使用堆栈来处理数据。本质上,在执行时,它会在逻辑上将数字弹出并压入堆栈或从堆栈中压入,并在两者之间进行一些简单的算术运算。(在底层,浏览器将 Wasm 转换为比堆栈机执行效率更高的东西,但从概念上讲,您可以将 Wasm 视为使用堆栈机。)

对于不熟悉的人来说,堆栈是一种后进先出 (LIFO) 数据结构。这意味着您以与添加项目相反的顺序访问堆栈中的项目:
在这里插入图片描述
图 1: “LIFO”数据堆栈(数据堆栈.svg,在 Wikimedia Commons User:Boivie 上)

本质上,每种类型的 Wasm 指令都会将 32 位或 64 位值压入堆栈或从堆栈中弹出一个 32 位或 64 位值。在函数开始时,堆栈是空的。随着函数的执行,堆栈逐渐被填满和清空。

传递给函数的参数以及局部函数变量存储在与堆栈分开的局部内存区域中。算术指令对堆栈变量进行操作,因此通常需要将变量从本地内存复制到堆栈(get_local),或者将结果从堆栈移动到本地内存中的本地变量(set_local)。

例如,假设您要将传递给函数的第一个参数乘以 2。在 Wasm 中会这样做:

  • 使用get_local指令将函数参数从本地内存压入堆栈。
  • 使用i32.const 2指令将数字 2 压入堆栈。
  • 运行指令i32.mul,它会弹出堆栈中的最后两个值,将它们相乘,然后将结果压入堆栈。
    函数的返回值只是留在堆栈上的最终值。

最后,我们有全局内存,这是一个 Wasm 实例的整个内存空间。它在 JavaScript 中被实现为一个数组——我们稍后会看到。

如果这里的 Wasm 内部功能看起来很陌生,请不要担心:我们将在即将发布的博客中更深入地了解这些功能。

未知样本

让我们分析一个可以在这里找到的 Wasm 程序。在我们分析的第一阶段,我们希望对样本有一个初步的感觉。

当然,我们可以作弊,只看文档就知道程序做了什么,但是为了练习逆向工程,我们假设它是一个未知的样本,我们要分析它。假设我们在一个看起来像这样的网页上发现了它:

 1:  <html>
 2:  <body>
 3:  <script>
 4:  request = new XMLHttpRequest();
 5:  request.open('GET', 'quicksort.wasm');
 6:  request.responseType = 'arraybuffer';
 7:  request.send();
 8:  request.onload = function() {
 9:    var bytes = request.response;
10:    let m = new WebAssembly.Instance(new WebAssembly.Module(bytes));
11:    var h = m.exports.memory.buffer;
12:    let intView = new Int32Array(h);
13:    var unsorted = [5,2,9,3,7,1,6,4,8,0];
14:    intView.set(unsorted,0);
15:    console.log('unsorted = ' + intView.slice(0,unsorted.length));
16:    m.exports.sort(0,unsorted.length*4);
17:    m.exports.sort(0,unsorted.length*4);
18:    console.log('sorted = ' + intView.slice(1,unsorted.length+1));
19:  }
20:  </script>
21:  </body>
22:  </html>

请注意全局 WebAssembly 内存缓冲区的声明以及代码将“未排序”值直接放入第 11 行和第 14 行之间的缓冲区的方式。

根据示例的名称,以及显示对名为“sort”的函数的调用的 HTML,该示例看起来确实像一个 QuickSort 实现。查看我们未在此处显示的 JavaScript 控制台,将显示脚本的输出,进一步表明这是一种排序算法。

WebAssembly 二进制工具包

有时您可能只有 Wasm 二进制文件可用,而没有封闭的 HTML。因此,让我们展示另一种查看函数的方法。

我们要做的第一件事是查看 Wasm 二进制文件的导入和导出对象。这可以通过将 Wasm 二进制文件转换为其文本表示来完成。为此,我们可以使用WebAssembly Binary Toolkit中的 wasm2wat 工具。

$ ./wasm2wat quicksort.wasm|grep –E “import|export”

  (export "memory" (memory 0))
  (export "partition" (func 0))
  (export "sort" (func 1)))

我们看到它没有导入任何函数,但它向 JavaScript 导出了两个函数,称为“partition”(函数编号 0)和“sort”(函数编号 1)。
让我们看看 ‘sort’ 函数需要什么参数,以及它返回什么:

$ ./wasm2wat quicksort.wasm|grep "func.*1.*type"
  (func (;1;) (type 1) (param i32 i32) (result i32)

我们看到’sort’函数接受两个整数并返回另一个整数。

即使没有 HTML 包装器的好处:基于 Wasm 二进制文件的名称和导出函数的属性,我们似乎正在处理排序算法。

使用 Life 进行行为分析

在现实生活中,恶意 Wasm 二进制文件的名称可能不太容易泄露,并且函数名称可能会被混淆。因此,我们需要更深入地挖掘代码以找出它实际上做了什么。不过在此之前,可能值得进行一些行为分析,这将使我们能够验证或反驳我们最初的理论,并在以后进行代码分析时引导我们朝着正确的方向前进,从而节省宝贵的分析师时间。

进行行为分析的一种方法是简单地将前面显示的 HTML 代码复制到本地文件,然后更改变量“未排序”的值并查看您在 JavaScript 控制台中得到的结果,添加一些 console.log()如果需要,我们自己的命令。

这可能并不总是实用的,所以让我们介绍一个工具,它可以让我们从命令行查询 Wasm 样本的排序功能。

我们已经知道 HTML 调用的函数(在本例中为“排序”)需要两个参数。我们不知道这些参数是什么,但 HTML 文件中的 JavaScript 表明这些参数可能与输入的长度有关。另外,我们从 JavaScript 中注意到,名为 ‘unsorted’ 的变量的内容并没有输入到 sort 函数中,而是在调用 sort 之前直接放入 Wasm 实例的内存中。这导致我们怀疑要排序的两个参数是传递给函数的数据在内存中所在位置的开始和结束索引。

现在让我们使用Life Wasm VM,它将允许我们从命令行与 Wasm 示例进行交互。

注意: Life 是基于 Go 的,所以需要先安装 Go。在这里可以找到有关如何使用 Life 的完整说明。

使用 Life,我们将创建一个脚本,用我们的示例初始化一个新的 Wasm VM,将任何命令行参数放入实例的内存中,然后最后调用 sort 函数。

本质上,它是对 Go 中早期 JavaScript 的重写,并进行了必要的更改以使其适合 Life 框架。我们将脚本保存为 call_sort.go,它看起来像这样:

 1:  package main
 2:  import (
 3:          "os"
 4:          "fmt"
 5:          "github.com/perlin-network/life/exec"
 6:          "io/ioutil"
 7:          "encoding/binary"
 8:          "strconv"
 9:  )
10:  func main() {
11:    bytes, _ := ioutil.ReadFile(os.Args[1])
12:    vm, _ := exec.NewVirtualMachine(bytes, exec.VMConfig{}, new(exec.NopResolver))
13:    //Put the arguments into the memory of the instance (in little-endian format):
14:    for i := 0; i < len(os.Args[2:]); i++ {
15:      argInt, _ := strconv.Atoi(os.Args[2+i])
16:      binary.LittleEndian.PutUint32(vm.Memory[i*4:], uint32(argInt))
17:    }
18:    id, _ := vm.GetFunctionExport("sort")
19:    vm.Run(id, 0, int64(len(os.Args[2:]))*4)
20:    vm.Run(id, 0, int64(len(os.Args[2:]))*4)
21:    fmt.Printf("Result: ")
22:    for i := 0; i < len(os.Args[2:]); i++ {
23:      fmt.Printf("%d, ",binary.LittleEndian.Uint32(vm.Memory[4+i*4:]))
24:    }
25:    fmt.Println("")
26:  }

现在让我们使用命令行来查询不同输入的排序函数,然后看看我们得到了什么:

$ go run call_sort.go quicksort.wasm 5 2 9 3 7 1 6 4 8 0 2> /dev/null

结果:0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

 $ go run call_sort.go quicksort.wasm 13392 8 4 90000 234234 333 2> /dev/null

结果:4、8、333、13392、90000、234234、

即使没有有用的函数和文件名,行为分析也证实了我们最初的理论,即这是一种排序算法。

当然,正如我们所提到的,真实世界的恶意样本不太可能有这么大的帮助,因此在我们的下一篇文章中,我们将通过更深入地研究 Wasm 文本格式来进行一些更“手动”的分析,从而让我们的双手更加肮脏。

参考

  • https://www.forcepoint.com/blog/security-labs/analyzing-webassembly-binaries
  • https://github.com/WebAssembly/wabt
  • https://github.com/perlin-network/life
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值