玩轉 WebAssembly && 逆向

前言

近年來CTF出現越來越多的web assembly題,這也暗示著wasm會在將來成為一大主流,更不用提以native code speed運行的效能優勢。想要審計wasm就要會它的逆向,在這篇文章中我打算參考SENSEPOST實作一下wasm的部署以及逆向!

玩轉 web assembly

我直接引用sample code來實作,首先我需要一個C program:

int foo(int x) {
  return x+1;
}

頁面中的javascript可以引用這個foo函數,在response中返回 input+1!
值得注意的是,我們未必需要main function…
要將C/C++轉換成wasm可以透過WasmExplorer,以上代碼經過這個工具編譯轉換後會產生:

(module
 (table 0 anyfunc)
 (memory $0 1)
 (export "memory" (memory $0))
 (export "_Z3fooi" (func $_Z3fooi))
 (func $_Z3fooi (; 0 ;) (param $0 i32) (result i32)
  (i32.add
   (get_local $0)
   (i32.const 1)
  )
 )
)

當然也可以自己透過emscripten來編譯轉換C program,通常會產生三個檔案:.wasm的bin,.js.html。這個html就是web assembly被載入的網頁,此為emcc部署web assembly的方法
不過通常emcc方法所產生的.wasm都相對比較大,這裡我再踹踹看另一個fiddler的方法:WasmFiddle,輸入src然後build,右邊是載入wasm的方法,下面選擇CodeBuffer會產生:

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,134,128,128,128,0,1,96,1,127,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,144,128,128,128,0,2,6,109,101,109,111,114,121,2,0,3,102,111,111,0,0,10,141,128,128,128,0,1,135,128,128,128,0,0,32,0,65,1,106,11]);

因此在網頁中,我就可以使用下面的方法來載入wasm:

<script>
var wasmCode = ...上面的codebuffer...
var m = new WebAssembly.Instance(new WebAssembly.Module(wasmCode));
document.getElementById('out').innerHTML = m.exports.foo(100);
</script>

現在100就會作為參數被送到wasm中的foo function了!
除了codebuffer,還可以使用WebAssembly.instantiateStreaming()來直接fetch wasm:

WebAssembly.instantiateStreaming(fetch('io-simple.wasm')).then(obj => 
    obj.instance.exports.foo(num)
).then(res => 
    document.getElementById('out').innerHTML = res
);

若想做審計,就可以從這裡得知wasm的路徑並且把它載下來逆向!

逆向

我們當然不可能直接分析wasm,這裡有幾種法子:

  • 直接閱讀wat
    wat是比較適合人閱讀的wasm,可以參考理解WebAssembly文本格式,下面則是我通過工具wabtio-simple.wasm輸出成了wat格式:
    (module
     (type (;0;) (func (param i32) (result i32)))
     (func (;0;) (type 0) (param i32) (result i32)
       local.get 0
       i32.const 1
       i32.add)
     (table (;0;) 0 funcref)
     (memory (;0;) 1)
     (export "memory" (memory 0))
     (export "foo" (func 0)))
    

    由於源碼簡單,所以從wat就能看出個大概了。我也可以用上面的wabt將io-simple轉成C program,但是這樣產出的C會非常的巨大繁複,我們需要先優化!

  • 優化而後分析
    優化的原理很簡單,gcc編譯後再逆向就好!wasm2c .wasm -o .c會產生一個c file和header file,將這兩個檔案和wabt根目錄中的wasm-rt.h,`wasm-rt-impl.c`,和wasm-rt-impl.h放到一個資料夾。編譯前有兩點需要注意:1. 這邊的編譯不包括鏈接,所以我們只產生.o而不產生elf!2. 注意一下路徑的問題,wasm2c會用絕對路徑引用header file,所以如果放置的資料夾跟原本.c.h在不同的地方則要記得從中修改!最後一步就是gcc編譯:
    gcc -c io-simple.c -o wasm.o
    

    放進ida反編譯後得到:

    雖然結果還是很醜,至少可以開始逆向了。由於wasm不一定需要main函數,所以跟平常在逆向不一樣,main不非就是入口點!

可以從上面的例子看到,就算經過了優化再做逆向分析,原始碼還是被一定程度上得混淆了。如果源碼更加龐大又複雜,他產生的wasm的複雜程度更可想而知,WebAssembly不只提供了效能更提供了源碼的保護!下個篇章中,我會嘗試去靜態分析和使用瀏覽器去動態分析更複雜的wasm!

reference

  1. Introduction to WebAssembly
  2. 一種Wasm逆向靜態分析方法