文章总结: 这篇文章介绍了Go-WASM的基础知识和实际应用案例。WASM是一种二进制指令格式,能让C/C++/Rust/Go等编译型语言在浏览器中高效运行。文章详细解释了WASM的组成(wasm模块、胶水层、前端调用代码)和实例化流程,特别指出Go编译的WASM与C++/Rust的不同之处在于需要依赖Go运行时,并通过_makeFuncWrapper函数来唤醒运行时。文章提供了一个实际案例,展示了如何分析和使用Go-WASM,最后比较了Go-WASM与C++-WASM的差异。
综合评分: 88
文章分类: 逆向分析,WEB安全,二进制安全,爬虫
go-wasm基础和案例分析
原创
可乐还是百事好
爬虫逆向小林哥
2025年11月19日 19:27
江苏
简介
❝
wasm是一种二进制指令格式(体积小、加载快),核心是让 C/C++/Rust/Go 等编译型语言的代码在浏览器中高效运行。前端调用 WASM 实现加密本质是 “编译型语言写加密逻辑→转 WASM 二进制→浏览器加载执行→JS 与 WASM 交互传数据” 的过程。
基础
组成
- wasm模块
wasm模块由C++/Go等静态编程语言编写生成主要包括:导出函数、内存段、元数据等
- 胶水层
纯wasm无法直接操作Dom或与js对象交互,必须要通过胶层。不理解的可以把它类比于安卓开发中java和native中的JNI,可实现java和汇编之间相互调用这里胶水层也是这个功能
- 前端调用代码
主要就是加载wasm,初始化胶水层,最后调用wasm的导出函数
流程
一:加载wasm文件
通常需要fetch一个wasm结尾的url地址获取其响应
二:实例化WebAssembly模块
使用 WebAssembly.instantiate 或 WebAssembly.instantiateStreaming
实例化模块,同时传入必要的导入对象(如 Go 运行时所需的 go.importObject)
三:初始化胶水层
如果使用的是 Go 或其他高级语言生成的 WASM,通常需要初始化对应的胶水
层(例如 Go 的 wasm_exec.js)
四:启动运行时环境
五:等待函数暴露
等待 WASM 中导出的函数准备好,可以通过轮询或回调的方式实现
六:调用wasm函数
两种方式,要么就是在instance.exports.函数名调用,传入对应参数内存,
要么通过胶水层封装好的接口调用wasm函数。
详细介绍下几个点:
WebAssembly
实例化
WebAssembly.instantiate/instantiateStreaming 是一个用于加载和实例化 WebAssembly 模块的函数。它可以接受一个字节数组(Wasm 二进制数据)和可选的导入对象。
WebAssembly.instantiate(bytes, importObject)
WebAssembly.instantiateStreaming(fetchPromise, importObject)
bytes
编译好的 WebAssembly 模块的二进制表示。Wasm 模块必须先编译成这种格式,才能被实例化
importObject
WabAssembly 模块需要调用的外部函数或全局变量。这个对象的结构通常与模块中定义的导入相对应。例如,如果模块需要一个外部函数,你需要在这个对象中定义它(类似于创建类的初始化参数.
返回
instance
表示实例化后的 WebAssembly 模块,包含模块的导出(即可以被调用的函数和变量)
GO-wasm
GitHub 地址:https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js
Go编译使用的wasm跟C++/Rust有很大不同,其官网规定好了通用的胶水代码模板,所有Go编译好的wasm与js进行交互时候都要使用它
案例
aHR0cDovLzIxOC42MC4xNTQuMTo4MS93ZWJzZXJ2ZXIvbG5wdWJsaXNoL2luZGV4Lmh0bWwjL2NvbXBhbnlpbmZvP2tleXdvcmQ9OTEyMTAxMDNNQTEwR1ROUjJLJmNsaWNrZWQ9JUU1JUI3JUE1JUU3JUE4JThCJUU0JUJDJTgxJUU0JUI4JTlB
刷新页面会请求到一个wasm文件
点击查询很快就跟到下面的地方
定位到函数位置把js拿本地分析下
位于一个大的自执行函数里面,后面也看到了我们前面看到的wasm文件
箭头位置分别执行对应了加载wasm文件、初始化胶水层、初始化WebAssembly的操作
来看看胶水层,就是整个Go class了
注意这里WebAssembly传入的第二个参数go.importObject 里面封装了wasm与js交互的一些工具函数
最后启动实例,成功后暴露可调用的函数给回调函数
go.run(obj.instance);
window.wasmready( (module) => {
loadedFn(module)
}
, getTokenFn)
有个不太好理解的点是导出的函数module 中最后都指向Go class里面的_makeFuncWrapper,其实这是go 编译wasm特有的
Ai给的回复是
❝
Go 有复杂的运行时(goroutine、垃圾回收、调度器),而 C++ 是“裸金属”语言,没有运行时依赖。WASM 本身只支持简单数值调用,Go 必须靠 JS 包装层模拟高级特性,C++ 则可以直接导出纯函数。
总结下来就是Go编译的wasm需要依赖Go的运行时,不像C++,C++编译的是纯wasm指令,go的wasm包含GC、内存管理、goroutine调度等。Go WASM 启动后,为了节省资源,运行时可能会进入 休眠状态(比如等待 JS 调用、等待 I/O)。如果直接暴露原始函数,JS 调用时不会触发运行时 “唤醒”——goroutine 处于休眠,原始函数根本不会执行;而 _makeFuncWrapper 包装函数的核心逻辑之一,就是调用 go._resume() 唤醒 Go 运行时,确保 goroutine 能调度到原始函数并执行
查了下资料这套写法是Go官方提供的通用写法,不用太纠结了!
GitHub 地址:https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js
接下来就是用Node实现这个流程
先把Go class这个胶水层扣下来,其中有个getRandomValues需要本地通过crypto补一下
function getRandomValues(typedArray) {
if (!typedArray || typeof typedArray !== 'object') {
throw new TypeError('Expected a TypedArray');
}
return crypto.randomFillSync(typedArray);
}
接着实现
exec = function (dataStr) {
const go = new Go();
let fetchPromise = fetch(url);
fetchPromise.then((resp) => resp.arrayBuffer()).then((bytes) => WebAssembly.instantiate(bytes, go.importObject).then(function (obj) {
go.run(obj.instance);
window.wasmready((module) => {
result = module.e(dataStr)
console.log(result)
console.log(obj.instance.exports)
}
, function () {
return {
token: ""
}["token"]
})
}))
}
整体下来就Ok了
相比较xhs的wasm是不一样的,xhs的应该是C++编译的wasm,导出函数直接暴露到instance.exports
END
❝
有需要用于爬虫、逆向、抓包的测试机又不想花时间折腾Root、刷机、环境配置可以来看下下面联系方式购买,或🐟联系
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:爬虫逆向小林哥 可乐还是百事好《go-wasm基础和案例分析》