一步步学习Webassembly逆向分析方法

阅读量561813

|评论6

|

发布时间 : 2019-05-31 14:36:37

 

在强网杯2019线上赛的题目中,有一道名为Webassembly的wasm类型题,作为CTF新人,完全没有接触过wasm汇编语言,对该类型无从下手,查阅相关资料后才算入门,现将Webassembly的静态分析和动态调试的方法及过程整理如下,希望能够对于CTF萌新带来帮助,同时如有大佬光顾发现错误,欢迎拍砖予以斧正。

 

1.WebAssembly基本概念

在开始Webassembly逆向分析之前,需要了解其基本概念和基础知识,由于自己也是初学者,防止对大家的学习产生误导,在此将学习资料链接给出。

总体来说,wasm可以理解为一种可以由JavaScript调用,并与html交互的二进制指令格式文件。

wasm

 

2.处理wasm文件

在逆向wasm的过程中,由于其执行的是以栈式机器定义的虚拟机的二进制指令格式,因此直接进行逆向分析难度较大,需要对wasm文件进行处理,增强可操作性,提高逆向的效率。在此参考了《一种Wasm逆向静态分析方法》一文,主要利用了WABT(The WebAssembly Binary Toolkit)工具箱实现。

2.1反汇编

安装WABT工具后,在/wabt/build文件中会有各种小工具。利用wasm2wat工具可以生成wasm汇编文本格式的.wat文件。

./wasm2wat ../webassembly.wasm -o webassembly.wat

输入上述语句可以得到webassembly.wat文件。

2.1反汇编

2.2反编译

利用wasm2c工具可以生成c语言文本格式的*.c*.h代码文件。

./wasm2c ../webassembly.wasm -o webassembly.c

输入上述语句可以得到webassembly.h和webassembly.c文件。

2.2反编译

2.3重新编译

得到webassembly.h和webassembly.c文件后就可以使用gcc编译得到常见的*.o目标文件了,这里需要将/wabt/wasm2c中的wasm-rt.h,wasm-rt-impl.c,wasm-rt-impl.h文件复制出来。

gcc -c webassembly.c -o webassembly.o

输入上述语句可以得到webassembly.o文件。

2.3重新编译

 

3.静态分析

经过了wasm处理之后,对wasm的分析就可以利用webassembly.o文件在IDA中进行了。

3.1寻找main函数

IDA自动分析之后可以直接找到main函数。

3.1main

3.2寻找关键函数

main函数中只调用了f54f15两个函数,进入函数就会发现f54函数比较复杂,进入f15函数可以看到疑似加密过程的函数。

3.2关键函数1

可以搜索魔数0x61C88647寻找加密算法。

3.2关键函数2

其实从汇编语言中可以看到,魔数0x61C88647应该是0x9E3779B9,即可知这里是进行了四次XTEA加密算法。

继续观察f15函数可以看到如下代码。

3.2关键函数3

注意到'x65x36x38x62x62x7d'的二进制数据为字符串'e68bb}',刚好符合flag的尾部格式,应该是未加密部分的数据,可以看出,该程序是对输入的数据进行XTEA加密,如果等于给出的密文,则输入即为flag。

到此,就可以写出exp得到flag了。

 

4.动态分析

上述静态分析过程已经可以得到flag,这里通过动态跟踪该程序整理一下动态调试分析的方法。这里采用chrome浏览器进行动态调试分析,用到了chrome-wasm-debugger工具观察内存信息。

4.1环境搭建

利用python3自带的http服务,输入以下命令,在8888端口开启一个简单的服务器用于动态调试。

python -m http.server 8888

4.1环境搭建1

打开chrome浏览器,输入地址http://127.0.0.1:8888/webassembly.html即可正常加载运行,此时点击WASM debuger插件工具,即可attach到当前浏览器,会显示“‘WASM debugger’正在调试此浏览器”的字样,然后按下Control+Shift+J 打开开发人员工具并转到控制台。

4.1环境搭建2

4.2寻找关键断点

一般程序动态分析的关键就在于断点的寻找,找到合适的断点,便于分析程序的执行流程和数据处理情况。在动态分析Webassembly程序的时候,可以同时在JS脚本和wasm文件下断点,就能更加有效地达成上述目的。

该程序在运行后弹出了一个窗口,并伴有input:的字样,那么就可以在JS脚本中搜索该文字,找到弹出窗口的程序语句,并在此处点击设置断点。

4.2寻找断点1

设置断点之后刷新页面重新运行程序,就可以看到程序断在了此处,找到了关键断点,下面就可以对程序进行调试分析了。

4.3调试分析

找到关键断点后,观察右边的函数调用栈,可以看到程序运行到此处的函数调用过程如下。

f16 --> f54 --> f32 --> f34 --> f28 --> __syscall145 --> doReadv --> read --> read --> get_char

结合IDA静态分析过程,f16即为main函数,可以看到main函数调用了静态分析过程中忽略的f54函数,那么可以猜测该函数功能应该是获得输入内容。

4.3.1JS代码初始化过程

为了搞清楚Webassembly程序的整个运行过程,以及JS与Wasm的交互过程,我们从头开始分析。在Webassembly.js文件的第一行设置断点,按下F11单步跟进。

1.创建线性内存实例

运行到582行的时候,通过调用WebAssembly.Memory()接口创建WebAssembly线性内存实例,并且能够通过相关的实例方法获取已经存在的内存实例(当前每一个模块实例只能有一个内存实例)。内存实例拥有一个buffer获取器,它返回一个指向整个线性内存的ArrayBuffer。

4.3程序运行1

2.初始化内存

运行到591行的时候,调用了updateGlobalBufferViews()函数,该函数的实现中申请了一些内存,在之后的数据处理过程中会被用到。

4.3程序运行2

3.创建Webassembly实例

运行到783行的时候,通过调用createWasm()函数后间接调用到getBinaryPromise()函数,通过fetch()函数编译和实例化Webassembly代码

4.3程序运行3

4.JS导入wasm的导出函数

运行到4413行的时候,JS代码将wasm中的导出函数导入进来,main函数就是在这个过程中被导入到了_main变量当中的。

4.3程序运行4

这些导出函数可以在Webassembly.wat文件的最后位置找到。

  (export "___errno_location" (func 26))
  (export "_free" (func 18))
  (export "_main" (func 16))
  (export "_malloc" (func 17))
  (export "_memcpy" (func 69))
  (export "_memset" (func 70))
  (export "_sbrk" (func 71))
  (export "dynCall_ii" (func 72))
  (export "dynCall_iiii" (func 73))
  (export "establishStackSpace" (func 14))
  (export "stackAlloc" (func 11))
  (export "stackRestore" (func 13))
  (export "stackSave" (func 12))
5.执行wasm的main函数

运行到4594行的时候,JS代码几乎快要执行结束了,这个时候进入run()函数之后,程序最终会调用wasm的main函数,此时程序执行到wasm的代码空间中。

4.3程序运行5

4.3.2数据处理过程

1.Wasm代码调用用户输入

Wasm代码的断点可以在左边视图中wasm的结点中设置,通过上文的函数调用栈,中间函数不需要一步步跟进了,我们可以看到运行到了f28函数后,紧接着调用了__syscall145函数。

4.3wasm执行1

f28函数中看到了f3函数,并没有__syscall145函数,但是如果去IDA中观察的话,是能够看到该函数的。

4.3wasm执行2

其实这个是wasm导入的JS的导出函数,可以在Webassembly.wat文件的最开始位置找到。

  (import "env" "abort" (func (;0;) (type 2)))
  (import "env" "___setErrNo" (func (;1;) (type 2)))
  (import "env" "___syscall140" (func (;2;) (type 3)))
  (import "env" "___syscall145" (func (;3;) (type 3)))
  (import "env" "___syscall146" (func (;4;) (type 3)))
  (import "env" "___syscall54" (func (;5;) (type 3)))
  (import "env" "___syscall6" (func (;6;) (type 3)))
  (import "env" "_emscripten_get_heap_size" (func (;7;) (type 4)))
  (import "env" "_emscripten_memcpy_big" (func (;8;) (type 0)))
  (import "env" "_emscripten_resize_heap" (func (;9;) (type 1)))
  (import "env" "abortOnCannotGrowMemory" (func (;10;) (type 1)))
  (import "env" "__table_base" (global (;0;) i32))
  (import "env" "DYNAMICTOP_PTR" (global (;1;) i32))
  (import "global" "NaN" (global (;2;) f64))
  (import "global" "Infinity" (global (;3;) f64))
  (import "env" "memory" (memory (;0;) 256 256))
  (import "env" "table" (table (;0;) 10 10 funcref))
2.JS处理用户输入

__syscall145函数调用之后,程序又进入了JS代码空间。在此可以跟进到第二个doReadv函数,可以看到这里是在处理的用户输入去了哪里。

4.3wasm执行

如果跟进后面的read可以得知,取出用户输入1024长度的内容,这里终于可以用到WASM debuger工具了,这里在4183行下好断点,运行到断点处,我们在工具窗口中查看ptr中的内容,此时的命令与gdb相同,需要注意3672是10进制数字。

wdb> x/16 0xe58
0x00000e58:  0x00000000 0x00000000 0x00000000 0x00000000
0x00000e68:  0x00000000 0x00000000 0x00000000 0x00000000
0x00000e78:  0x00000000 0x00000000 0x00000000 0x00000000
0x00000e88:  0x00000000 0x00000000 0x00000000 0x00000000
wdb>

之后在函数结束处4187行下好断点,然后输入1024个A的数据,程序中断,在工具窗口中继续查看ptr中的内容。

wdb> x/16 0xe58
0x00000e58:  0x41414141 0x41414141 0x41414141 0x41414141
0x00000e68:  0x41414141 0x41414141 0x41414141 0x41414141
0x00000e78:  0x41414141 0x41414141 0x41414141 0x41414141
0x00000e88:  0x41414141 0x41414141 0x41414141 0x41414141
wdb>

此时,该内存的内容就是用户输入的数据了,同时我们查看iov内存中的内容。

wdb> x/4 0x1b30
0x00001b30:  0x00001b20 0x00000000 0x00000e58 0x00000400
wdb>

可以看出,该内存块中存访了0xe58的内存地址和0x400的内存大小。

那么执行到f25函数之后就有了存放输入内容的内存地址和内存大小的信息了。

4.3wasm执行4

3.输入数据的判断

到这里以后,就可以随心所欲地调试自己的程序了,发现输入的数据进入到wasm代码空间之后并没有进行处理,直接又返回调用到用户输入了。

继续跟进会发现在f54函数当一个判断条件不能触发,那么程序永远都会跳转到第1000行的f32函数,从而重新跳转到了用户输入变成了死循环。因此,我们在判断条件处设置断点。

4.3wasm执行5

发现这里比较的是内存地址和4696的值进行比较,而内存长度只有1024,当内存的每一个字符都比较完了就必定会落入f32函数当中,好像无法跳出循环。继续往下执行发现还有一个条件判断语句。

4.3wasm执行6

发现这里是取内存地址6656,在地址偏移为输入字符的ASCII码值加1处的内容,然后与0进行比较,因此可以查看该内存地址0x1A00处的内容。

wdb> x/48 0x1a00
0x00001a00:  0xfffffeff 0xfffffffe 0x0000ffff 0xfeffffff
0x00001a10:  0xfffffffe 0xfffffffe 0xfffffffe 0xfffffffe
0x00001a20:  0xffff00fe 0xfffffffe 0xfffffffe 0xfffffffe
0x00001a30:  0xfffffffe 0xfffffffe 0xfffffffe 0xfffffffe
0x00001a40:  0xfffffffe 0xfffffffe 0xfffffffe 0xfffffffe
0x00001a50:  0xfffffffe 0xfffffffe 0xfffffffe 0xfffffffe
0x00001a60:  0xfffffffe 0xfffffffe 0xfffffffe 0xfffffffe
0x00001a70:  0xfffffffe 0xfffffffe 0xfffffffe 0xfffffffe
0x00001a80:  0xfffffffe 0xfffffffe 0xfffffffe 0xfffffffe
0x00001a90:  0xfffffffe 0xfffffffe 0xfffffffe 0xfffffffe
0x00001aa0:  0xfffffffe 0xfffffffe 0xfffffffe 0xfffffffe
0x00001ab0:  0xfffffffe 0xfffffffe 0xfffffffe 0xfffffffe

或者查看右边Global中的memory,找到6056的位置。

4.3wasm执行7

可以看到其中有内容为0的地址有6个,当输入为可见字符空格即0x20的时候,会取到6689处的0,之后就会跳出循环进入到后面的验证程序。所以输入的1024个数据中,会截取空格之前的数据传送到后边的程序进行处理。

4.数据加密

我们继续跟进,查看输入数据是否到达了上文静态分析的f15函数中,直接在f15函数第33行设置断点,输入1024个A,并替换其中一个A为空格,运行后程序中断。

4.3wasm执行8

可以看到两个变量的值变为1094795585,这个值转换为十六进制就是0x41414141,即我们刚才输入数据的前四个AAAA,到此完全搞清楚了程序的执行流程和数据处理情况。

4.3.3编写exp得到flag

经过上述分析可以编写exp如下。

#!python3.6
import struct

def decrypt(v0, v1, key):
    delta = 0x9e3779b9
    n = 32
    sum = (delta * n)
    mask = 0xffffffff
    for round in range(n):
            v1 = (v1 - (((v0<<4 ^ v0>>5) + v0) ^ (sum + k[sum>>11 & 3]))) & mask
            sum = (sum - delta) & mask
            v0 = (v0 - (((v1<<4 ^ v1>>5) + v1) ^ (sum + k[sum & 3]))) & mask

    return struct.pack("i",v0) + struct.pack("i",v1)

block = [0xE7689695, 0xC91755b7, 0xCF1e03ad, 0x4B61c56f, 0x2Dfd9002, 0x930aed22, 0xECc97e30, 0xE0B1968c]
k = [0,0,0,0]

flag = ''
for i in range(4):
     flag = flag + ((decrypt(block[i*2], block[i*2+1], k)).decode())

flag = flag + 'x65x36x38x62x62x7d'

print(flag)

得到flag为flag{1c15908d00762edf4a0dd7ebbabe68bb}

若直接输入该字符串并不会显示处结果,因此输入flag和空格,再跟上一些内容组成1024长度的数据,就可以得到成功结果的字符串。

4.3wasm执行9

另外,如果只输入flag,直接点击取消,会有换行符号0x0A加在输入后面,也能够进入判断流程,是可以得到正确结果的,有兴趣的萌新可以跟进调试以下。

 

5.相关信息

在进行Webassembly的动态调试的时候,chrome浏览器存在一些bug,可能导致某些断点虽然设置了,但是并没断下来,这个时候需要关闭浏览器后重新加载一下就可正常运行了。WASM debuger工具并不是必须的,浏览器中也能够观察到相关的内存信息,不过不如该工具方便。由于刚接触Webassembly的逆向分析,可能上述过程并不是最佳方法,如大佬们有更好的调试分析方法,欢迎分享知识指点迷津。

5.1参考资料

5.2工具链接

本文由超能水饺原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/179556

安全客 - 有思想的安全新媒体

分享到:微信
+124赞
收藏
超能水饺
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66