标题: 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=$[2*64*1024],--max-memory=$[4*64*1024] \ -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,建议初次接触者仔细阅读前述所有参考链接。