☆ 背景介紹
現在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
<html>
<head>
<meta charset="utf-8">
head>
<body>
<script type="module">
const PrivateDecode = (src) => {
let i = 0;
let ret = '';
while ( src[i] !== 0 ) {
ret += String.fromCharCode(src[i++]);
}
return ret;
};
(async() => {
function js_puts ( off ) {
let mem = new Uint8Array( memory.buffer, off );
console.log( PrivateDecode( mem ) );
}
let response = await fetch( 'hello.wasm' );
let buf = await response.arrayBuffer();
let importObject = {
env: {
puts: js_puts,
},
};
let {
instance
} = await WebAssembly.instantiate(
buf,
importObject
);
let memory = instance.exports.memory;
instance.exports.hello();
})();
script>
body>
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 );
6) hello_inline.html
下例將hello.wasm_from_wat的內容直接寫在html中。
(見TXT)
☆ 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_emu.py,與前述插件不是一回事,是個單獨運行的腳本。在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,建議初次接觸者仔細閱讀前述所有參考鏈接。