Skip to content

标题: WebAssembly入门简介

创建: 2024-05-14 08:39 更新: 2024-05-15 08:36 链接: https://scz.617.cn/web/202405140839.txt


目录:

☆ 背景介绍
☆ Hello World
    1) hello.wat
    2) hello.js
    3) hello.html
    4) hello.c
    5) hello_inline.js
    6) hello_inline.html
☆ F12调试wasm
☆ 反汇编wasm
    1) wasm2wat
    2) wasm-objdump
    3) IDA插件idawasm
☆ 反编译wasm
    1) wasm-decompile
    2) wasm2c+IDA
    3) Ghidra插件
☆ 后记

☆ 背景介绍

现在js与wasm混合编程在WEB前端较常见,前端逆向工程时可能遭遇wasm,本文面向 有二进制逆向能力但从未接触过前端逆向的技术人员做一次wasm科普。

讨论wasm涉及两种层面,一种是wasm本身,另一种是解释、优化、执行wasm的引擎, 后者包括对wasm的JIT等。对于挖掘浏览器0day的安全人员,需要研究的是后者。对 于前端逆向,需要研究的是前者,也即本文范畴。

关于WebAssembly,参看:


https://webassembly.org/

WebAssembly Core Specification https://webassembly.github.io/spec/core/


☆ Hello World

本文演示一个虽然简单但很有代表性的例子。js提供puts函数,接收来自wasm的线性 地址,在Console中输出位于wasm中的字符串常量。js并不直接调用puts函数,而是 调用wasm的导出函数,通过后者间接调用puts函数。

1) hello.wat


(module (import "env" "puts" (func $env_puts (param i32) (result)))

(memory $memory 2 4)
(export "memory" (memory $memory))

(data (i32.const 4) "Hello World\00")

(func $hello (export "hello") (param) (result)
    i32.const 4
    call $env_puts
)

)

wat相当于汇编编程,有x86汇编经验的,上述代码瞎猜都能猜明白。

可从wat生成wasm:

wat2wasm -o hello.wasm_from_wat hello.wat

关于wat,参看:


WABT: The WebAssembly Binary Toolkit https://github.com/WebAssembly/wabt

wat2wasm https://webassembly.github.io/wabt/doc/wat2wasm.1.html

wasm2wat https://webassembly.github.io/wabt/doc/wasm2wat.1.html

wasm-objdump https://webassembly.github.io/wabt/doc/wasm-objdump.1.html

wasm-decompile https://webassembly.github.io/wabt/doc/wasm-decompile.1.html

wasm2c https://webassembly.github.io/wabt/doc/wasm2c.1.html

Raw WebAssembly - Surma [2019-05-17] https://dassur.ma/things/raw-wasm/


2) hello.js


'use strict';

const PrivateDecode = (src) => { let i = 0; let ret = '';

while ( src[i] !== 0 ) {
    ret += String.fromCharCode(src[i++]);
}
return ret;

};

async function RunWasm ( filename ) {

function js_puts ( off ) {
    let mem = new Uint8Array( memory.buffer, off );
    console.log( PrivateDecode( mem ) );
}

let fs              = require('fs').promises;
let path            = require('path');
let filepath        = path.resolve( __dirname, filename );
let buf             = await fs.readFile( filepath );
let importObject    = {
    env: {
        puts: js_puts,
    },
};
let {
    instance
}   = await WebAssembly.instantiate(
    buf,
    importObject
);
let memory          = instance.exports.memory;
instance.exports.hello();

}

RunWasm( 'hello.wasm' ).catch( console.error );

在nodejs中测试:

cp hello.wasm_from_wat hello.wasm node hello.js

3) hello.html



启动测试用HTTP服务端:

python3 -m http.server -b 192.168.x.x 8080

在Chrome中访问:

http://192.168.x.x:8080/hello.html

在F12 Console中查看输出

4) hello.c

用wat编程很不友好,临时Patch尚可,写框架代码费劲儿。就hello.wat而言,可用C 语言实现同样功能。


attribute(( import_module("env"), import_name("puts"), )) extern void env_puts ( int );

attribute((export_name("hello"))) void hello ( void ) { char *s = "Hello World";

env_puts( (int)s );

}

正常使用wasm的程序员,主要用Emscripten编译。逆向工程人员可能不喜欢这种远离 地基的东西,本文直接用clang、llvm编译:

wasi-sdk-22.0/bin/clang \ -Wall -Wextra -Wpedantic \ --target=wasm32 \ -nostdlib \ -nostartfiles \ -Wl,--no-entry \ -Wl,--export-memory \ -Wl,--global-base=4 \ -Wl,--initial-memory=$[2641024],--max-memory=$[4641024] \ -O3 -s \ -o hello.wasm_from_c \ hello.c

编译命令中许多参数非必要,指定它们仅为向hello.wasm_from_wat靠拢

测试:

cp hello.wasm_from_c hello.wasm node hello.js

关于wasi-sdk,参看:


WASI-enabled WebAssembly C/C++ toolchain https://github.com/WebAssembly/wasi-sdk

WebAssembly lld port https://lld.llvm.org/WebAssembly.html

Compiling C to WebAssembly without Emscripten - Surma [2019-05-28] https://dassur.ma/things/c-to-webassembly/


若你的clang版本够高,不用wasi-sdk中的clang亦可。

hello.wasm_from_wat、hello.wasm_from_c并不完全等价,比较如下命令输出:

wasm2wat hello.wasm_from_wat wasm2wat hello.wasm_from_c

后者多了如下内容:

(table (;0;) 1 1 funcref) (global (;0;) (mut i32) (i32.const 65552))

hello.wat将常量字符串置于线性内存偏移4处,hello.c干了同样的事。若常量字符 串在偏移0处,hello.c无论如何也做不到。未能找到办法让hello.c中常量字符串出 现在任意偏移,从wasm生成wat,编辑wat,再从wat生成wasm,这种不算。用memcpy 初始化指定偏移处的内存,这种不算。wasm-ld不支持「链接器脚本」。总之,能试 的都试了,想找__attribute__方案,未果。非真实需求,仅为技术探索。

5) hello_inline.js

hello.js是从文件系统读取hello.wasm,wasm可直接嵌在js中,无需访问文件系统。 下例将hello.wasm_from_wat的内容直接写在js中。


'use strict';

const PrivateDecode = (src) => { let i = 0; let ret = '';

while ( src[i] !== 0 ) {
    ret += String.fromCharCode(src[i++]);
}
return ret;

};

async function RunWasm () {

function js_puts ( off ) {
    let mem = new Uint8Array( memory.buffer, off );
    console.log( PrivateDecode( mem ) );
}

let buf             = new Uint8Array([
0,97,115,109,1,0,0,0,1,8,2,96,1,127,0,96,
0,0,2,12,1,3,101,110,118,4,112,117,116,115,0,0,
3,2,1,1,5,4,1,1,2,4,7,18,2,6,109,101,
109,111,114,121,2,0,5,104,101,108,108,111,0,1,10,8,
1,6,0,65,4,16,0,11,11,18,1,0,65,4,11,12,
72,101,108,108,111,32,87,111,114,108,100,0,
]);
let importObject    = {
    env: {
        puts: js_puts,
    },
};
let {
    instance
}   = await WebAssembly.instantiate(
    buf,
    importObject
);
let memory          = instance.exports.memory;
instance.exports.hello();

}

RunWasm().catch( console.error );

node hello_inline.js

6) hello_inline.html

下例将hello.wasm_from_wat的内容直接写在html中。



python3 -m http.server -b 192.168.x.x 8080 http://192.168.x.x:8080/hello_inline.html

☆ F12调试wasm

假设用Chrome访问

http://192.168.x.x:8080/hello_inline.html

F12 Sources面板有wasm目录,其中会有buf对应的wasm代码,以wat格式展示。可对 具体汇编指令设断、单步调试。假设断在"call $env.puts",Scope中可查看wasm汇 编级栈区;单步会跟入js_puts,调用栈回溯中混杂有js、wasm函数,无缝衔接。

☆ 反汇编wasm

1) wasm2wat

wasm2wat some.wasm | less wasm2wat -o some.wat some.wasm

2) wasm-objdump

wasm-objdump -x -d some.wasm | less

3) IDA插件idawasm

wasm2wat、wasm-objdump可以反汇编wasm,但无法显示CFG。fireeye当年有个 IDAPython插件用于反汇编wasm,后来未再更新,有人对之简单更新过,参看:


https://github.com/mandiant/idawasm https://github.com/huangxiangyao/idawasm


小改后,在IDA 7.6.1/8.4.1中测试,能用,可识别函数块、产生字符串交叉引用等。 如遇未被支持的指令,需自行增强,比如多字节操作码。

该文件与前述插件不是一回事,是个单独运行的脚本,对wasm代码片段模拟执行,显 示内存布局变化。在IDA中选中一个block,Alt-F7执行脚本,在Output窗口查看结果。 比如选中这段代码,输出如下:


get_global global_0 i32.const 0x10 i32.sub tee_local $local0 set_global global_0 get_local $local0 i32.const 0x425 ;; "scz is here" i32.store 0, align:2 i32.const 0x43B ;; "(%s)" get_local $local0


globals: global_0: (global_0 - 0x10) locals: $local0: (global_0 - 0x10) stack: 0: (global_0 - 0x10) 1: 0x43B memory: (global_0 - 0x10): 0x25 ((global_0 - 0x10) + 0x1): 0x4 ((global_0 - 0x10) + 0x2): 0x0 ((global_0 - 0x10) + 0x3): 0x0


☆ 反编译wasm

1) wasm-decompile

wasm-decompile some.wasm | less

wasm-decompile的反编译结果虽然是伪码,但可读性还可以

2) wasm2c+IDA

wasm2c -o some.c some.wasm

wasm2c从some.wasm生成some.c、some.h。查看some.h,可能有

include "wasm-rt.h"

部分编译some.c时,需用-I指定"wasm-rt.h"所在目录:

gcc -pipe -O0 -g3 -c \ -I//wabt-1.0.34/include \ -o some.o \ some.c

用IDA反编译some.o。此法不如Ghidra插件,比如字符串全是地址,也不能双击地址 跳过去查看字符串。

3) Ghidra插件

参看


Ghidra Wasm plugin with disassembly and decompilation support https://github.com/nneonneo/ghidra-wasm-plugin


出现"Active Project"界面,拖放some.wasm到其中,右键"Open in Default Tool" 打开CodeBrowser,在"Symbol Tree"中查看Exports,点选具体的导出函数,自动显 示反汇编、反编译结果。有些字符串已经显示出来,有些只显示了0x43b这样的地址。 双击地址跳过去,右键"Data->string",相当于IDA的A键。

☆ 后记

hello示例未涉及WASI,参看:


WebAssembly System Interface (WASI) https://github.com/WebAssembly/WASI


wasm涉及WASI时,想在Chrome中执行,需要其他奇技淫巧,本文未演示。

假设读者是有二进制逆向能力但未接触过wasm的技术人员,省略了大量wasm基础科普, 直接在实践中科普wasm,建议初次接触者仔细阅读前述所有参考链接。