wasm逆向

2024-06-20

1. 概述

WebAssembly,俗称wasm 是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。
它设计的目的不是为了手写代码而是为诸如 C、C++ 和 Rust 等低级源语**言提供一个高效的编译目标。
只能说跟asm很像,wasm速来就有运行在浏览器的汇编的说法。
它有很多新的特性

    1. 二进制格式: wasm是一种基于栈式虚拟机的二进制指令集(和JVM很像),这使得它更紧凑、更快速地加载和解析。
    1. 跨平台: wasm是一个跨平台的执行格式,可以在不同体系结构和操作系统上运行,而不受特定编程语言或硬件的限制。道理也很简单,毕竟它运行在浏览器上,只能你的系统装了浏览器,它就能跑~
    1. 性能: wasm被设计为在现代硬件上实现高性能,因此它通常比传统的JavaScript执行更快。
    1. 安全:wasm被设计在sandbox里运行,增加了安全性。它通过强制执行严格的类型检查和内存访问限制来减少安全漏洞的风险。

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上下断点,就可以做题了。

references

关于wasm逆向的实战方法
Webassembly逆向手法