1. 概述
WebAssembly,俗称wasm
是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。
它设计的目的不是为了手写代码而是为诸如 C、C++ 和 Rust 等低级源语**言提供一个高效的编译目标。
只能说跟asm
很像,wasm
速来就有运行在浏览器的汇编的说法。
它有很多新的特性
-
- 二进制格式: wasm是一种基于栈式虚拟机的二进制指令集(和JVM很像),这使得它更紧凑、更快速地加载和解析。
-
- 跨平台: wasm是一个跨平台的执行格式,可以在不同体系结构和操作系统上运行,而不受特定编程语言或硬件的限制。道理也很简单,毕竟它运行在浏览器上,只能你的系统装了浏览器,它就能跑~
-
- 性能: wasm被设计为在现代硬件上实现高性能,因此它通常比传统的JavaScript执行更快。
-
- 安全:wasm被设计在
sandbox
里运行,增加了安全性。它通过强制执行严格的类型检查和内存访问限制来减少安全漏洞的风险。
- 安全:wasm被设计在
1.1 wasm文件类型
这里不过多分析wasm文件的文件格式是什么,有兴趣的同学可以自行官网搜索~
总的来说,wasm
文件类型设计两种:.wasm和.wat
其中
.wasm就是可以直接在浏览器上运行的程序,而.wat是人类可读的.wasm代码的转换
可以看一下.wat
的文件长什么样:
(module
(type $type0 (func (param i32)))
(type $type1 (func))
(func $import0 (import "sys" "print") (param i32))
(memory $memory0 200 200)
(export "memory" (memory $memory0))
(export "main" (func $func1))
(func $func1
i32.const 0
call $import0
)
(data (i32.const 0) "Hello, world\00")
)
2. wasm静态分析
2.1 wabt
静态分析的方法,个人推荐github开源项目https://github.com/WebAssembly/wabt
该项目中带有很多好用的工具,包括wasm2c
这种能够直接把wasm
转换成c
代码的工具。
有了它,你就能得到wasm的类c语言来阅读,或者得到相应的wat格式
虽然有release包
可以让你直接使用,但是个人推荐直接编译一份,如果你要用release包也可以,记得把github项目拷到本地来
原因很简单,因为你把wasm改成了c,又想用其编译成x86的可执行文件话,需要项目中的头文件和库
编译命令如下:
gcc -c target_folder/*.c -o target_folder/output.o -Iwasm2c -O2 -fno-stack-protector -fno-plt -fno-pic
这样编译出来的文件虽然不能直接执行,但是拖入IDA
分析是可行的
2.2 ghidra
强强工具可以直接用来逆向分析,官网在https://github.com/NationalSecurityAgency/ghidra
2.3 IDA
直接wasm拖入ida分析,有一个好用的github仓库:
https://github.com/vient/wasm2ida,用这里的工具结合wabt后,就能生成好看的.o文件进行分析~
1. 首先使用在linux命令行中使用如下命令
git clone https://github.com/vient/wasm2ida.git
2. 创建目录target_folder,把你要反编译的wasm文件放入目录,使用如下命令:
3. ./wasm2ida.py target_folder/output.wasm target_folder/output.o
# 该程序会自动下载wabt项目,只不过因为是几年前的项目,有些新版wasm解析会出问题~,推荐在下载个wabt最新版备用
感觉IDA比其他都猛,静态分析直接用IDA吧
PS: 感觉用完IDA后前面几个都是狗屎!
3. wasm动态调试
有的时候啊,这wasm
真的是又臭又长,看得烦了,动态调试就是个很好的方法。
chrome浏览器
直接提供了开发者工具进行调试(F12)大法好。
当然,为了调试,你需要在wasm所在的目录下运行 python -m http.server 88888开启网页,这样才能调试,直接在目录里点开页面会因为同源策略导致无法调试。
值得一提的是,为了更方便得调试,你可以在index.html
中添加如下js
代码:
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("output.wasm"), go.importObject).then((result) => {
go.run(result.instance);
wasm = result.instance.exports;
memories = [wasm.memory]
viewDWORD = (addr) =>{
const arr = new Uint32Array(memories[0].buffer.slice(addr, addr + 16));
return arr;
};
viewChar = (addr, size = 16) =>{
const arr = new Uint8Array(memories[0].buffer.slice(addr, addr + size));
return String.fromCharCode.apply(null, arr);
};
viewHEX = (addr, size = 16) =>{
const arr = new Uint8Array(memories[0].buffer.slice(addr, addr + size));
return (Array.from(arr, x =>x.toString(16).padStart(2, '0')).join(' '));
};
viewHexCode = (addr, size = 16) =>{
const arr = new Uint8Array(memories[0].buffer.slice(addr, addr + size));
return (Array.from(arr, x =>'0x' + x.toString(16).padStart(2, '0')).join(', '));
};
dumpMemory = (addr, size = 16) =>{
const arr = new Uint8Array(memories[0].buffer.slice(addr, addr + size));
return arr;
};
viewString = (addr, size = 16) =>{
const arr = new Uint8Array(memories[0].buffer.slice(addr, addr + size));
let max = size;
for (let i = 0; i < size; i++) {
if (arr[i] === 0) {
max = i;
break;
}
}
return String.fromCharCode.apply(null, arr.slice(0, max));
};
search = function(stirng) {
const m = new Uint8Array(memories[0].buffer);
// vid=35402, 9AAizQZJ
// vid=20268, a3fMpSkB
const k = Array.from(stirng, x =>x.charCodeAt());
const match = (j) =>{
return k.every((b, i) =>m[i + j] === b);
};
const max = Math.min(10_000_000, m.byteLength || m.length);
for (let i = 0; i < max; i++) {
if (match(i)) {
console.info(i);
}
}
console.info('done');
}
});
</script>
3.1 调试tips
由go编写的wasm都有一个特点:在用AES加密的时候,若AES加密需要初始向量IV,IV一般都附在密文的开头16字节!!!
另外,调用函数时的入参基本上都被类似i64.store
的命令存起来了,所以可以在调用函数的前几个i64.store
上下断点,就可以做题了。