导语:在本文中,我们将通过一个非常简单的应用程序,来为读者详细介绍Chrome内置的Wasm调试功能。
最近,我们发表过一篇关于WebAssembly(Wasm)基本概念及其安全问题的文章。作为后续内容,本文将为读者介绍Wasm应用程序的逆向工程方法。当我们遇到一个未知源码的Wasm应用程序,并且想要弄清楚其内部机制的时候,该如何进行分析呢?关于这个主题,目前几乎找不到任何有用的文档,所以,我们决定抛砖引玉。
对于Wasm应用程序,我们可以使用不同的方式进行分析。在本文中,我们将通过一个非常简单的应用程序,来为读者详细介绍Chrome内置的Wasm调试功能。在此过程中,我们将因地制宜地引入一些相关概念。
对于急着了解相关技术内容的读者,如果按耐不住的话,可以先从附录部分获取HTML文件test.html,然后直接跳转到“调试示例应用程序”部分。
为什么要逆向分析Wasm呢?
我们为什么对分析Wasm应用程序如此饶有兴趣呢?在深入学习逆向分析的细节知识之前,让我们先来回答这个问题。
对于安全分析人员来说,我们最感兴趣的就是了解恶意软件作者是如何利用新兴技术的。每当出现新威胁时,例如新的勒索软件家族、物联网蠕虫或更不寻常的东西,安全研究人员都希望深入分析该恶意代码的所有功能。当我们知道了恶意软件是如何工作的,而且了解了它们的特性后,我们就可以编写签名来提供相应的安全保护了。
在分析传统恶意软件的时候,有许多分析工具可选,无论对于混淆过的JavaScript、恶意Flash对象、可移植可执行文件(PE)还是其他软件,都是如此。并且,在分析这些恶意软件的时候,总能找到一种行之有效的方法。
正如我们在本系列的第一篇文章中提到的,在安全分析工具与分析方法上面,Wasm的情况有所不同。关于如何分析Wasm应用程序方面,几乎没有任何文档可用,而且大多数常见的逆向工程工具,当前也不适用于Wasm。因此,我们才决心撰写本文,为读者深入揭示如何逆向分析Wasm二进制文件。
创建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”按钮:
图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”即可。
图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应用程序的文本表示形式:
图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文件,这时会看到:
图5:Chrome中的通知。
我们可以看到,Wasm代码成功的调用了我们的外部函数。
注意:如果您没有看到弹出窗口,可能是您的浏览器不支持Wasm所致。在这种情况下,请尝试更新浏览器,因为目前所有主流浏览器的最新版本都支持Wasm。
调试示例应用程序
现在,我们终于可以通过Chrome开发者工具来调试示例应用程序了。
利用Chrome打开test.html文件后,启动Chrome开发人员工具(按F12键),并选择顶部的Sources选项卡。然后,按Ctrl+R组合键重新加载页面。现在,应该出现一个带有文字“wasm”的小云图标。接下来,请展开它及其下面的项目,选择wasm子树下的叶子项目,具体如下图所示:
图6:Chrome开发人员工具
让我们单步执行这个函数,以便更好的理解其功能。为此,请点击左边以“i32”开头的那行代码,为其设置断点。这时会显示一个蓝条,表明已设置好断点。接下来,按Ctrl+R组合键重新加载页面。现在,将在断点处停下来。这时候,Wasm堆栈是空的。然后,单击调试器中的Step Over按钮(或点击F10键或带有弯曲箭头的图标)以执行指令“i32.const 16”,该指令会将16的值压入堆栈:
图7:将值16压入堆栈。
Wasm中的所有函数都具有对应的编号,编号为0的函数对应于Wasm从JavaScript导入的puts函数(函数编号1对应于hello函数)。因此,下一条调用0的指令实际上就是调用printf/puts函数,并且堆栈中的值“16”是其参数。
那么,值“16”是如何与字符串“Hello World”对应起来的呢? 实际上,该值是指向Wasm应用程序内存空间中的地址的指针。利用Chrome的调试器展开全局树,我们就可以查看Wasm应用程序的内存了:
图8:考察Wasm应用程序的内存。
下面让我们来看看内存中位置编号为16处的内容:
图9:Wasm应用程序内存中的“Hello World”。
运行状态下的Wasm应用程序的内存空间实际上是作为JavaScript数组实现的。该数组的定义位于负责加载Wasm应用程序的HTML文件中。在上面的例子中,变量“h”的定义如下所示;该变量用于保存应用程序的内存空间:
let h = new Uint8Array(m.exports.memory.buffer);
现在,请重新点击Step Over按钮来执行该调用。这样,就能看到相应的JavaScript警报了。
结束语
现在,我们已经对一个简单的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>