如何逆向分析WebAssembly二进制代码

Posted 嘶吼专业版

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何逆向分析WebAssembly二进制代码相关的知识,希望对你有一定的参考价值。

最近,我们发表过一篇关于WebAssembly(Wasm)基本概念及其安全问题的文章。作为后续内容,本文将为读者介绍Wasm应用程序的逆向工程方法。当我们遇到一个未知源码的Wasm应用程序,并且想要弄清楚其内部机制的时候,该如何进行分析呢?关于这个主题,目前几乎找不到任何有用的文档,所以,我们决定抛砖引玉。

对于Wasm应用程序,我们可以使用不同的方式进行分析。在本文中,我们将通过一个非常简单的应用程序,来为读者详细介绍Chrome内置的Wasm调试功能。在此过程中,我们将因地制宜地引入一些相关概念。

对于急着了解相关技术内容的读者,如果按耐不住的话,可以先从附录部分获取html文件test.html,然后直接跳转到“调试示例应用程序”部分。

如何逆向分析WebAssembly二进制代码为什么要逆向分析Wasm呢?

我们为什么对分析Wasm应用程序如此饶有兴趣呢?在深入学习逆向分析的细节知识之前,让我们先来回答这个问题。

对于安全分析人员来说,我们最感兴趣的就是了解恶意软件作者是如何利用新兴技术的。每当出现新威胁时,例如新的勒索软件家族、物联网蠕虫或更不寻常的东西,安全研究人员都希望深入分析该恶意代码的所有功能。当我们知道了恶意软件是如何工作的,而且了解了它们的特性后,我们就可以编写签名来提供相应的安全保护了。

在分析传统恶意软件的时候,有许多分析工具可选,无论对于混淆过的javascript、恶意Flash对象、可移植可执行文件(PE)还是其他软件,都是如此。并且,在分析这些恶意软件的时候,总能找到一种行之有效的方法。

正如我们在本系列的第一篇文章中提到的,在安全分析工具与分析方法上面,Wasm的情况有所不同。关于如何分析Wasm应用程序方面,几乎没有任何文档可用,而且大多数常见的逆向工程工具,当前也不适用于Wasm。因此,我们才决心撰写本文,为读者深入揭示如何逆向分析Wasm二进制文件。

如何逆向分析WebAssembly二进制代码创建Wasm示例应用程序:“Hello World”

首先,让我们来创建一个简单的Wasm应用程序,以便稍后对其进行逆向分析。我们将在浏览器中运行该应用程序,并使用Chrome的开发人员工具对其进行逆向分析。

要在浏览器中运行Wasm应用程序,我们需要使用一个HTML文件来加载和执行Wasm二进制文件。下面,我们开始介绍如何创建这个HTML文件。(如前所述,完整的文件可以在附录中找到。)

首先,建立一个框架(我们将进一步对其进行修改),并将其保存到test.html文件:

<html>

<script>

  function test() {

  }

</script>

<body onLoad="test()">

</body>

</html>

 

为了便于配置并避免安装任何工具,我们这里使用名为WasmFiddler的在线Web应用程序来生成Wasm。在WasmFiddler中,输入以下内容:

 

void hello() {

  printf("Hello World\n");

}

然后点击“Build”按钮:

如何逆向分析WebAssembly二进制代码

图1:使用WasmFiddler编译Wasm应用程序

在上图的右侧,我们可以看到一个名为utf8ToString()的函数。我们需要将该函数复制并粘贴到HTML页面的JavaScript部分,并将其放在test()函数的上方。

在截图的右侧,我们可以在函数utf8ToString()后面看到如下所示的几行JavaScript代码:

let m = new WebAssembly.Instance(new WebAssembly.Module(buffer));

let h = new Uint8Array(m.exports.memory.buffer);

let p = m.exports.hello();

复制这些代码,并将其粘贴到test()函数中。这些代码的作用,就是根据定义在buffer数组中的代码来实例化我们的Wasm,然后执行hello()函数。

那么我们如何定义这个缓冲区的内容(Wasm代码)呢? 在WasmFiddler中,单击源代码下面的下拉菜单(图1中的“Text Format”),然后选择“Code Buffer”即可。

如何逆向分析WebAssembly二进制代码

图2:在WasmFiddler中查看代码缓冲区。

这样,WasmFiddler就会生成二进制Wasm代码,并将其放入JavaScript缓冲区中。这时,我们会看到下列内容(有所删减):

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,...,108,100,0]);

注意:如果得到的只是一个空数组(“var wasmCode = new Uint8Array([null]);”),说明忘了先编译源代码。在这种情况下,可以点击Build按钮,然后重试。

复制这个缓冲区的内容,并将其粘贴到test()函数的开头部分。然后,将数组从wasmCode重命名为buffer,以便与WasmFiddler生成的其他代码的命名相匹配。

如果您读过本系列的第一篇文章的话,就知道Wasm应用程序本身无法将文本打印到屏幕上。所以,需要定义一个JavaScript函数,在我们的Wasm代码中调用printf()函数。在WasmFiddler中,选择下拉菜单中的Text Format选项,以查看编译后的Wasm应用程序的文本表示形式:

如何逆向分析WebAssembly二进制代码

图4:puts()函数的Imports模板。

复制上面wasmImports的定义,并将其粘贴到test()函数的开头部分。然后,我们还需要将Imports的定义提供给Wasm的实例,具体如下所示:

var m = new WebAssembly.Instance(new WebAssembly.Module(buffer),wasmImports);

最后,让我们来定义puts()函数在被调用时应该做些什么。具体来说,就是将其改为下面的样子:

puts: function puts (index) {

  alert(utf8ToString(h, index));

}

现在,我们已经完成了构建演示程序所需的全部步骤。接下来,请在Chrome中加载test.html文件,这时会看到:

如何逆向分析WebAssembly二进制代码

图5:Chrome中的通知。

我们可以看到,Wasm代码成功的调用了我们的外部函数。

注意:如果您没有看到弹出窗口,可能是您的浏览器不支持Wasm所致。在这种情况下,请尝试更新浏览器,因为目前所有主流浏览器的最新版本都支持Wasm。

如何逆向分析WebAssembly二进制代码调试示例应用程序

现在,我们终于可以通过Chrome开发者工具来调试示例应用程序了。

利用Chrome打开test.html文件后,启动Chrome开发人员工具(按F12键),并选择顶部的Sources选项卡。然后,按Ctrl+R组合键重新加载页面。现在,应该出现一个带有文字“wasm”的小云图标。接下来,请展开它及其下面的项目,选择wasm子树下的叶子项目,具体如下图所示:

如何逆向分析WebAssembly二进制代码

图6:Chrome开发人员工具

让我们单步执行这个函数,以便更好的理解其功能。为此,请点击左边以“i32”开头的那行代码,为其设置断点。这时会显示一个蓝条,表明已设置好断点。接下来,按Ctrl+R组合键重新加载页面。现在,将在断点处停下来。这时候,Wasm堆栈是空的。然后,单击调试器中的Step Over按钮(或点击F10键或带有弯曲箭头的图标)以执行指令“i32.const 16”,该指令会将16的值压入堆栈:

如何逆向分析WebAssembly二进制代码

图7:将值16压入堆栈。

Wasm中的所有函数都具有对应的编号,编号为0的函数对应于Wasm从JavaScript导入的puts函数(函数编号1对应于hello函数)。因此,下一条调用0的指令实际上就是调用printf/puts函数,并且堆栈中的值“16”是其参数。

如何逆向分析WebAssembly二进制代码

图8:考察Wasm应用程序的内存。

下面让我们来看看内存中位置编号为16处的内容:

如何逆向分析WebAssembly二进制代码

图9:Wasm应用程序内存中的“Hello World”。

运行状态下的Wasm应用程序的内存空间实际上是作为JavaScript数组实现的。该数组的定义位于负责加载Wasm应用程序的HTML文件中。在上面的例子中,变量“h”的定义如下所示;该变量用于保存应用程序的内存空间:

let h = new Uint8Array(m.exports.memory.buffer);

现在,请重新点击Step Over按钮来执行该调用。这样,就能看到相应的JavaScript警报了。

如何逆向分析WebAssembly二进制代码结束语

现在,我们已经对一个简单的Wasm程序成功地进行了逆向分析。虽然这个例子非常简单,但请不要忘记,千里之行始于足下。

在逆向过程中,我们是通过调用JavaScript声明的导入函数来了解Wasm是如何与外部环境进行交互的。此外,我们还介绍了如何在JavaScript和Wasm之间共享内存。

参考文献

WasmFiddle,在线编译Wasm: https://wasdk.github.io/WasmFiddle/?wvzhb

关于如何在浏览器调试器中调试Wasm的视频: https://www.youtube.com/watch?v=R1WtBkMeGds

在JavaScript和Wasm之间传递值: https://hacks.mozilla.org/2017/07/memory-in-webassembly-and-why-its-safer-than-you-think/

附录: test.html

为了便于参考,以下是我们创建并分析的整个test.html文件:

<html>

<script>

function utf8ToString(h, p) {

  let s = "";

  for (i = p; h[i]; i++) {

    s += String.fromCharCode(h[i]);

  }

  return s;

}

 

function test() {

  var wasmImports = {

    env: {

      puts: function puts (index) {

        alert(utf8ToString(h, index));

      }

    }

  };

  var buffer = new Uint8Array([0,97,115,109,1,0,0,0,1,137,128,128,128,0,2,

    96,1,127,1,127,96,0,0,2,140,128,128,128,0,1,3,101,110,118,4,112,117,

    116,115,0,0,3,130,128,128,128,0,1,1,4,132,128,128,128,0,1,112,0,0,5,

    131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,146,128,128,128,0,2,6,

    109,101,109,111,114,121,2,0,5,104,101,108,108,111,0,1,10,141,128,128,

    128,0,1,135,128,128,128,0,0,65,16,16,0,26,11,11,146,128,128,128,0,1,0,

    65,16,11,12,72,101,108,108,111,32,87,111,114,108,100,0]);

  let m = new WebAssembly.Instance(new WebAssembly.Module(buffer),wasmImports);

  let h = new Uint8Array(m.exports.memory.buffer);

  m.exports.hello();

}

</script>

<body onLoad="test()">

</body>

</html>


以上是关于如何逆向分析WebAssembly二进制代码的主要内容,如果未能解决你的问题,请参考以下文章

《有趣的二进制:软件安全与逆向分析》读书笔记:通过逆向工程学习如何读懂二进制代码

《有趣的二进制:软件安全与逆向分析》读书笔记:通过逆向工程学习如何读懂二进制代码

WebAssembly逆向

Ghidra逆向工具之旅与二进制代码分析

Ghidra逆向工具之旅与二进制代码分析

Ghidra逆向工具之旅与二进制代码分析