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

栏目: 后端 · 前端 · 发布时间: 5年前

内容简介:在强网杯2019线上赛的题目中,有一道名为Webassembly的wasm类型题,作为CTF新人,完全没有接触过wasm汇编语言,对该类型无从下手,查阅相关资料后才算入门,现将Webassembly的静态分析和动态调试的方法及过程整理如下,希望能够对于CTF萌新带来帮助,同时如有大佬光顾发现错误,欢迎拍砖予以斧正。在开始Webassembly逆向分析之前,需要了解其基本概念和基础知识,由于自己也是初学者,防止对大家的学习产生误导,在此将学习资料链接给出。

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

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

1.WebAssembly基本概念

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

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

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

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文件。

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

2.2反编译

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

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

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

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

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文件。

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

3.静态分析

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

3.1寻找main函数

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

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

3.2寻找关键函数

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

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

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

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

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

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

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

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

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

4.动态分析

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

4.1环境搭建

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

python -m http.server 8888

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

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

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

4.2寻找关键断点

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

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

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

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

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。

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

2.初始化内存

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

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

3.创建Webassembly实例

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

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

4.JS导入wasm的导出函数

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

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

这些导出函数可以在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的代码空间中。

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

4.3.2数据处理过程

1.Wasm代码调用用户输入

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

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

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

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

其实这个是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 函数,可以看到这里是在处理的用户输入去了哪里。

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

如果跟进后面的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 函数之后就有了存放输入内容的内存地址和内存大小的信息了。

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

3.输入数据的判断

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

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

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

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

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

发现这里是取内存地址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的位置。

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

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

4.数据加密

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

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

可以看到两个变量的值变为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长度的数据,就可以得到成功结果的字符串。

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

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

5.相关信息

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

5.1参考资料

5.2工具链接


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

七周七并发模型

七周七并发模型

Paul Butcher / 黄炎 / 人民邮电出版社 / 2015-3 / 49.00元

借助Java、Go等多种语言的特长,深度剖析所有主流并发编程模型 基于锁和线程的并发模型是目前最常用的一种并发模型,但是并发编程模型不仅仅只有这一种,本书几乎涵盖了目前所有的并发编程模型。了解和熟悉各种并发编程模型,在解决并发问题时会有更多思路。 ——方腾飞,并发编程网站长 当看到这本书的目录时,我就为之一振。它涉及了当今所有的主流并发编程模型(当然也包括Go语言及其实现的CSP......一起来看看 《七周七并发模型》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具