目录
概要
1.wasm 补环境
2.反编译分析伪代码
3.wasm 的指令学习
4.wabt反编译
MSYS2Software Distribution and Building Platform for Windowshttps://www.msys2.org/
技术名词解释
1、WebAssembly 指令集概览
WebAssembly的指令集可以分为以下几类:数值操作、内存访问、控制流、模块定义和导入导出等。以下是每个类别中一些常见的指令及其功能
1)基本结构
WebAssembly的指令集由一系列字节码组成,每个字节码对应一个特定的操作码(opcode)。
2)数据类型
WebAssembly支持的数据类型包括整数类型和浮点数类型。整数类型可以是8位(i8)、16位(i16)、32位(i32)或64位(i64);浮点数类型可以是32位(f32)或64位(f64)。
3)模块和函数
WebAssembly代码以模块(module)的形式组织,每个模块包含函数(function)和相关的全局变量、内存(memory)和表格(table)等。函数由一组指令序列构成。
4)指令概览
1.i32 整数运算
i32 指令 | 描述 |
i32.add | 32位有符号整数的加法运算 |
i32.sub | 32位有符号整数的减法运算 |
i32.mul | 32位有符号整数的乘法运算 |
i32.div_s | 有符号32位整数的除法 |
i32.div_u | 无符号32位整数的除法 |
i32.rem_s | 有符号32位整数的求余运算 |
i32.rem_u | 无符号32位整数的求余运算 |
示例代码👇
(func (param i32 i32) (result i32) local.get 0 ;; 获取函数的第一个参数(整数1) local.get 1 ;; 获取函数的第二个参数(整数2) i32.add) ;; 将栈顶的两个整数相加,并将结果作为函数的返回值
代码注解
(func (param i32 i32) (result i32))
:定义了一个函数,它接受两个参数(两个32位有符号整数),并返回一个32位有符号整数作为结果。
local.get 0
和local.get 1
:这两个指令分别将函数的第一个和第二个参数加载到本地变量栈上。在WebAssembly中,参数从索引0开始。
i32.add
:这是一个整数加法指令,它从栈顶弹出两个整数值,将它们相加,然后将结果推送回栈顶。在这个例子中,它将栈顶的两个整数(即函数的两个参数)相加。执行过程
当函数被调用时,传递给函数的两个参数被存储在本地变量中(索引0和索引1)。
local.get 0
将第一个参数加载到栈顶。
local.get 1
将第二个参数加载到栈顶。
i32.add
从栈顶弹出这两个参数,执行加法操作,并将结果再次推送到栈顶,作为函数的返回值。
2.i32 浮点数运算(用法同整数运算)
f32 指令 | 描述 |
f32.add | 32位浮点数的加法运算 |
f32.sub | 32位浮点数的减法运算 |
f32.mul | 32位浮点数的乘法运算 |
f32.div | 32位浮点数的除法 |
f64 指令 | 描述 |
f32.add | 64位浮点数的加法运算 |
f32.sub | 64位浮点数的减法运算 |
f32.mul | 64位浮点数的乘法运算 |
f32.div | 64位浮点数的除法 |
3.逻辑运算和位移(用法同整数运算)
运算和位移指令 | 描述 |
i32.and | 32位整数的按位与、按位或和按位异或操作 |
i32.or | 32位整数的按位与、按位或和按位异或操作 |
i32.xor | 32位整数的按位与、按位或和按位异或操作 |
i32.shl | 32位整数的左移、有符号右移和无符号右移操作 |
i32.shr_s | 32位整数的左移、有符号右移和无符号右移操作 |
i32.shr_u | 32位整数的左移、有符号右移和无符号右移操作 |
4.内存访问指令
WebAssembly允许程序直接访问内存,执行加载和存储操作。这对于处理大量数据或者与Web页面交互非常有用。
1.内存加载和存储
i32.load
、i64.load
、f32.load
、f64.load
:从内存中加载指定类型的数据。i32.store
、i64.store
、f32.store
、f64.store
:将指定类型的数据存储到内存中。
load
示例代码👇(func (param i32) (result i32) local.get 0 ;; 获取函数的第一个参数(内存地址) i32.load) ;; 加载指定内存地址处的整数并将其作为结果返回
代码注解:
(func (param i32) (result i32))
:定义了一个函数,它接受一个32位整数作为参数,并返回一个32位整数作为结果。
local.get 0
:这个指令将函数的第一个参数(索引为0的本地变量)加载到栈顶。在WebAssembly中,参数从索引0开始。
i32.load
:这是一个内存加载指令,它从内存中加载一个32位整数值到栈顶。在这个例子中,它加载的地址是栈顶的值(即函数的参数)所指向的内存地址处的32位整数值。执行过程
当函数被调用时,传递给函数的32位整数参数被存储在本地变量中(索引0)。
local.get 0
将参数加载到栈顶,即将要加载的内存地址。
i32.load
从内存中加载存储在指定地址处的32位整数值,并将加载的值推送回栈顶,作为函数的返回值。
store
示例代码👇(func (param i32 f32) local.get 0 ;; 获取第一个参数(内存地址) local.get 1 ;; 获取第二个参数(要存储的浮点数) f32.store) ;; 将栈顶的浮点数存储到指定内存地址
代码注解:
(func (param i32 f32))
:定义了一个函数,它接受一个32位整数(作为内存地址)和一个32位浮点数作为参数。
local.get 0
和local.get 1
:这两个指令分别将函数的第一个和第二个参数加载到本地变量栈上。在WebAssembly中,参数从索引0开始。
f32.store
:这是一个内存存储指令,它将栈顶的32位浮点数值存储到指定内存地址处。在这个例子中,它将第一个参数(内存地址)指向的位置存储为第二个参数(32位浮点数)的值。执行过程
当函数被调用时,传递给函数的第一个参数是一个32位整数,表示要存储数据的内存地址。
第二个参数是一个32位浮点数,即要存储到内存中的数据。
local.get 0
将第一个参数加载到栈顶,即内存地址。
local.get 1
将第二个参数加载到栈顶,即要存储的浮点数值。
f32.store
将栈顶的浮点数值存储到栈顶的地址处。
2.内存增长
memory.size
:获取当前内存的大小。示例代码👇
(memory 1) (func $getMemorySize (result i32) memory.size)
代码注解:
memory 1
:这个指令定义了一个大小为1页(64KB)的静态内存。在实际应用中,内存可以根据需求定义为更大的页面数。
$getMemorySize
函数定义了一个没有参数的函数,返回当前内存的页数作为32位整数。执行过程:
memory 1
指令定义了一个1页(64KB)大小的静态内存。这个内存是WebAssembly模块的一部分,可以在模块内部进行操作和访问。
$getMemorySize
函数通过memory.size
指令获取当前内存的页数。
memory.size
指令将当前内存的页数推送到栈顶,作为函数的返回值。
memory.grow
:增加内存的大小。
示例代码👇(memory 1) ;; 定义一个1页(64KB)大小的内存 (func $growMemory (param i32) (result i32) local.get 0 ;; 获取函数参数,表示要增加的页数 memory.grow ;; 增加内存的页数 memory.size) ;; 获取新的内存页数
代码注解:
$growMemory
函数定义了一个接受一个i32
类型参数(表示要增加的页数)和返回一个i32
类型结果(表示新的内存页数)的函数。
memory 1
指令定义了一个大小为1页(64KB)的静态内存,作为示例中的初始内存。
memory.grow
指令用于增加内存的页数。它会尝试增加内存的页数,成功时返回新的内存页数,失败时返回-1。执行过程
memory 1
指令定义了一个1页(64KB)大小的静态内存。$growMemory
函数通过local.get 0
获取函数的第一个参数,即要增加的页数。memory.grow
指令尝试增加内存的页数。如果成功,它将新的内存页数推送到栈顶;如果失败(例如超过了内存限制),则推送-1。memory.size
指令获取当前内存的页数,作为函数的返回值。
6.控制流指令
WebAssembly支持灵活的控制流结构,包括条件执行、循环和函数调用。
1.基本控制流
block
、loop
、if
、else
、end
:定义基本块、循环和条件执行及其结束示例代码👇
(func (param i32) (result i32) block ;; 定义一个块 local.get 0 ;; 将函数的第一个参数加载到栈顶 i32.const 0 ;; 将常数0推送到栈顶 i32.eq ;; 比较栈顶的两个值是否相等,结果入栈(0或1) if ;; 如果条件为真,则执行下面的指令 i32.const 1 ;; 将常数1推送到栈顶(即条件为真时的返回值) else ;; 如果条件为假,则执行下面的指令 local.get 0 ;; 再次将函数的第一个参数加载到栈顶 i32.const 1 ;; 将常数1推送到栈顶 i32.sub ;; 将栈顶的两个值相减,计算递减后的值 end ;; 结束条件分支 end ;; 结束块 )
代码注解:
这段代码实现了一个简单的逻辑:如果参数为0,则返回1;否则返回参数减去1。
使用
block
和end
封装了整个逻辑块,使得条件分支(if
和else
)可以安全地组织和执行。这种结构允许在WebAssembly中实现基本的条件逻辑和数学运算,用于构建更复杂的算法和函数。
执行过程:
函数参数和块定义:
函数接受一个
i32
类型的参数,并返回一个i32
类型的结果。
block
指令定义了一个块,用于封装条件分支和相关操作。条件分支:
local.get 0
将函数的第一个参数(索引为0的本地变量)加载到栈顶。
i32.const 0
将常数0推送到栈顶。
i32.eq
指令比较栈顶的两个值是否相等,如果相等则将1推送到栈顶;否则将0推送到栈顶。条件分支执行:
if
指令根据前面比较的结果(0或1),决定执行哪个分支:
如果条件为真(栈顶值为1),则执行
i32.const 1
,将常数1推送到栈顶作为函数的返回值。如果条件为假(栈顶值为0),则执行
local.get 0
、i32.const 1
和i32.sub
,依次将函数参数加载到栈顶,推送常数1,并计算栈顶两个值的差,作为函数的返回值。块结束:
end
指令结束条件分支和块定义。
示例代码👇
(func (param i32) (result i32) local.get 0 ;; 获取函数的第一个参数(整数) i32.const 10 ;; 将常数10推送到栈顶 i32.lt_s ;; 比较栈顶的两个整数,如果第一个小于第二个则推送1,否则推送0 if ;; 如果比较结果为真(栈顶值为1),则执行以下指令 i32.const 1 ;; 将常数1推送到栈顶 return ;; 返回栈顶的值并结束函数 end ;; 结束条件分支 i32.const 2 ;; 如果比较结果为假(栈顶值为0),将常数2推送到栈顶 return) ;; 返回栈顶的值并结束函数
代码注解:
这段代码定义了一个函数,接受一个
i32
类型的参数作为整数,并返回一个i32
类型的整数结果。
local.get 0
将函数的第一个参数(整数)加载到栈顶。
i32.const 10
将常数10推送到栈顶。
i32.lt_s
指令比较栈顶的两个整数,如果第一个整数小于第二个整数,则推送1到栈顶;否则推送0到栈顶。
if
指令根据比较的结果(栈顶值为1或0)决定执行不同的分支:
如果比较结果为真(即第一个参数小于10),则执行
i32.const 1
将常数1推送到栈顶,并使用return
返回栈顶的值并结束函数。如果比较结果为假(即第一个参数不小于10),则执行
i32.const 2
将常数2推送到栈顶,并使用return
返回栈顶的值并结束函数。执行过程:
当这个函数被调用时,它假定参数是一个有效的
i32
类型整数。
i32.lt_s
比较栈顶的整数和常数10,如果小于10则执行条件分支中的第一个分支(推送常数1并返回),否则执行第二个分支(推送常数2并返回)。
2.函数调用和返回
call
:调用函数。
return
:从函数返回。示例代码👇
(func $add (param i32 i32) (result i32) local.get 0 ;; 获取第一个参数 local.get 1 ;; 获取第二个参数 i32.add) ;; 将两个参数相加并返回结果 (func $run (result i32) i32.const 5 ;; 常数5压入栈顶作为第一个参数 i32.const 7 ;; 常数7压入栈顶作为第二个参数 call $add ;; 调用函数add,将返回值推送到栈顶 return) ;; 返回栈顶的值并结束函数
$add代码注解:
$add
是函数的名称,接受两个i32
类型的整数参数,并返回一个i32
类型的整数结果。
local.get 0
将第一个参数(栈中的第一个值)复制到栈顶。
local.get 1
将第二个参数(栈中的第二个值)复制到栈顶。
i32.add
将栈顶的两个整数相加,并将结果推送回栈顶作为函数的返回值。$run代码注解:
这个函数没有名称,只返回一个
i32
类型的整数。
i32.const 5
将常数5推送到栈顶,作为第一个参数。
i32.const 7
将常数7推送到栈顶,作为第二个参数。
call $add
调用之前定义的$add
函数,执行加法操作,并将结果推送到栈顶。
return
返回栈顶的值作为函数的结果,并结束函数的执行。执行过程:
先将常数5和7分别压入栈顶作为参数。
然后,调用
$add
函数进行加法运算。
$add
函数执行完毕后,将结果推送到栈顶。最后,使用
return
返回栈顶的值(加法结果)并结束函数的执行。
7.模块和导出指令
WebAssembly代码以模块的形式组织,可以定义函数、全局变量、表格和内存等,还可以将这些元素导出给外部JavaScript环境。
WebAssembly代码以模块的形式组织,可以定义函数、全局变量、表格和内存等,还可以将这些元素导出给外部JavaScript环境。
1.模块定义
module
、func
、global
、table
、memory
:定义模块、函数、全局变量、表格和内存等。示例代码👇
(module (func $add (param i32 i32) (result i32) ;; 定义一个名为 $add 的函数,接受两个 i32 类型的参数,并返回一个 i32 类型的结果 local.get 0 ;; 获取第一个参数 local.get 1 ;; 获取第二个参数 i32.add) ;; 将两个参数相加并返回结果 (export "add" (func $add)) ;; 导出函数 $add,使其可在模块外部调用,并命名为 "add" )
代码注解:
模块定义:
module
是 Wasm 模块的起始关键字,用于定义一个模块。函数定义:
(func $add (param i32 i32) (result i32) ...)
:定义了一个名为$add
的函数,它有两个i32
类型的参数(param i32 i32)
,并且返回一个i32
类型的结果(result i32)
。
local.get 0
和local.get 1
分别用于获取函数的第一个和第二个参数,将它们压入栈顶。
i32.add
指令将栈顶的两个整数相加,并将结果推送回栈顶作为函数的返回值。导出函数:
(export "add" (func $add))
:通过export
指令将函数$add
导出为模块的公共接口。导出时使用字符串"add"
作为函数的外部名称,以便外部 JavaScript 或其他环境可以调用它。JavaScript中的导出示例代码👇
// 假设模块实例已经加载和实例化 const wasmInstance = ...; // 加载和实例化的 WebAssembly 模块实例 // 调用 WebAssembly 导出的函数 const result = wasmInstance.exports.add(3, 4); console.log(result); // 输出结果为 7
2.导出和导入
export
:导出函数、全局变量等至外部JavaScript环境。
import
:导入外部函数、全局变量等至模块内部。
示例代码👇(module (func $externalFunction (import "env" "externalFunction") (param i32) (result i32)) ;; 导入一个名为 externalFunction 的函数 (export "internalFunction" (func $internalFunction)) ;; 导出一个名为 internalFunction 的函数 )
代码注解:
导入函数定义:
(func $externalFunction (import "env" "externalFunction") (param i32) (result i32))
:定义了一个名为$externalFunction
的函数,它是从外部导入的函数。在模块中,使用import
关键字指定导入函数的名称和命名空间。
import "env" "externalFunction"
表示将名为"externalFunction"
的函数从命名空间为"env"
的环境中导入。这种导入通常用于从宿主环境(如 JavaScript)中导入函数。导出函数定义:
(export "internalFunction" (func $internalFunction))
:使用export
关键字将模块中的函数$internalFunction
导出,以便模块外部的其他代码可以调用它。
"internalFunction"
是导出函数的外部名称,可以通过这个名称在外部环境中引用该函数。注意事项
导入函数:通过
import
关键字可以在 WebAssembly 模块中导入来自宿主环境的函数或其他模块的函数。这种机制使得 WebAssembly 可以与宿主环境进行交互,执行特定的功能或操作。导出函数:通过
export
关键字可以将模块中定义的函数、变量或表导出,使它们对外部可见和可调用。JavaScript中的示例用法
// 加载和实例化 WebAssembly 模块 const module = new WebAssembly.Module(/* 模块的二进制数据 */); const instance = new WebAssembly.Instance(module, { env: { externalFunction: function(param) { // 实现 externalFunction 的具体逻辑 return param * 2; // 例如,简单地将参数乘以 2 并返回 } } }); // 调用导出的 internalFunction const result = instance.exports.internalFunction(); console.log(result); // 输出 internalFunction 的返回值
8.其他常见指令
除了上述基本的指令外,还有一些用于栈操作、类型转换和异常处理等的指令。更多指令请阅读官方文档
1.栈操作
drop
:丢弃栈顶元素。示例代码👇
(func (param i32 i32) (result i32) ;; 定义一个函数,接受两个 i32 类型的参数,返回一个 i32 类型的结果 local.get 0 ;; 获取第一个参数,将其压入栈顶 local.get 1 ;; 获取第二个参数,将其压入栈顶 i32.add ;; 将栈顶的两个整数相加,并将结果压入栈顶 drop ;; 丢弃栈顶的值(即相加后的结果) local.get 0 ;; 获取第一个参数的值,将其压入栈顶作为函数的返回值 )
代码注解:
数签名:
(func (param i32 i32) (result i32))
:定义了一个函数,该函数接受两个i32
类型的参数,并且返回一个i32
类型的结果。指令解释:
local.get 0
和local.get 1
:这两条指令用于分别获取函数的第一个和第二个参数,并将它们的值推送到栈顶。
i32.add
:将栈顶的两个整数相加,并将相加后的结果推送到栈顶。
drop
:丢弃栈顶的值。在这里,它被用来丢弃相加后的结果,表示我们不需要该值,只是将第一个参数的值作为结果返回。
local.get 0
:获取函数的第一个参数的值,并将其作为函数的返回值推送到栈顶。执行过程:
参数传递:
将两个参数依次压入栈顶,栈顶为第二个参数,次栈顶为第一个参数。
加法操作:
使用
i32.add
将栈顶的两个整数相加,将相加后的结果推送到栈顶。结果处理:
使用
drop
指令丢弃栈顶的结果值,保留第一个参数的值在栈顶。返回值:
最后使用
local.get 0
获取并返回第一个参数的值作为函数的结果。
select
:条件选择指令。(func (param i32 i32 i32) (result i32) ;; 定义一个函数,接受三个 i32 类型的参数,并返回一个 i32 类型的结果 local.get 0 ;; 获取第一个参数,将其压入栈顶 if ;; 进入条件分支判断 local.get 1 ;; 如果第一个参数为真(非零),则获取第二个参数的值并将其推送到栈顶 else ;; 否则(第一个参数为假,即为零),执行以下分支 local.get 2 ;; 获取第三个参数的值并将其推送到栈顶 end ;; 条件分支结束 )
代码注解:
函数签名:
(func (param i32 i32 i32) (result i32))
:定义了一个函数,接受三个i32
类型的参数,并返回一个i32
类型的结果。指令解释:
local.get 0
:获取函数的第一个参数,并将其值推送到栈顶。
if
:条件分支的开始标志,表示后续指令是一个条件判断。
local.get 1
:在条件为真(非零)时执行的指令块,获取第二个参数的值并将其推送到栈顶。
else
:条件为假(第一个参数为零)时执行的指令块的开始标志。
local.get 2
:在条件为假时执行的指令,获取第三个参数的值并将其推送到栈顶。
end
:条件分支的结束标志,表示条件判断结束。执行过程
参数传递:
将三个参数依次压入栈顶,栈顶为第三个参数,次栈顶为第二个参数,最底部为第一个参数。
条件判断:
使用
local.get 0
获取第一个参数的值。使用
if
指令根据第一个参数的值(真或假)选择执行相应的分支。返回值:
根据条件判断的结果,将第二个或第三个参数的值作为函数的返回值推送到栈顶。
2.类型转换
i32.wrap_i64
、i64.extend_i32_s
:类型转换指令,例如从64位整数到32位整数的转换示例代码👇
i64.const 4294967312 ;; 64 位整数常量 i32.wrap_i64 ;; 将 64 位整数转换为 32 位整数 i32.const -12345 ;; 32 位带符号整数常量 i64.extend_i32_s ;; 将 32 位整数扩展为 64 位整数
1)i32.wrap_i64
是一条将 64 位整数(i64)转换为 32 位整数(i32)的指令。它用于将一个较大范围的整数(64 位)转换为一个较小范围的整数(32 位),并且仅保留低 32 位的数值部分。如果 64 位整数超出了 32 位整数的表示范围,结果将会截断为低 32 位的值。2)i64.extend_i32_s
是一条将 32 位带符号整数(i32)转换为 64 位带符号整数(i64)的指令。它用于将一个较小范围的整数(32 位)扩展为一个较大范围的整数(64 位),并保持其符号位不变。
- gcc的安装
官网下载 傻瓜式安装,打开MSYS2终端(可以通过开始菜单找到MSYS2 64bit)
1.执行命令来更新软件包数据库和核心软件包:pacman -Syu
2.在MSYS2终端中执行以下命令来安装GCC:pacman -S mingw-w64-x86_64-gcc
3.将MSYS2的bin目录添加到系统的环境变量中:C:\msys64\mingw64\bin
打开“控制面板” -> “系统和安全” -> “系统” -> “高级系统设置”,点击“环境变量”按钮,将上述路径添加到“Path”变量中
4.验证安装版本:gcc --version - wabt 各个可执行文件介绍
EXE操作 描述 spectest-interp.exe 运行和验证WebAssembly规范测试,确保WebAssembly实现符合规范。 wasm2c.exe 将WebAssembly二进制文件(.wasm)转换为C代码,可以用于进一步编译和嵌入应用中。
wasm2wat.exe 将WebAssembly二进制文件转换为文本格式(.wat),便于阅读和编辑。 wasm-decompile.exe 反编译WebAssembly二进制文件为更易读的伪代码,便于理解代码结构和逻辑。 wasm-interp.exe 解释和运行WebAssembly二进制文件,适用于测试和调试。 wasm-objdump.exe 显示WebAssembly二进制文件的内容和结构,类似于Unix系统上的 objdump
工具。wasm-stats.exe 显示WebAssembly模块的统计信息,如函数数量、内存使用等。 wasm-strip.exe 从WebAssembly二进制文件中去除不必要的调试信息和符号表,以减小文件大小。 wasm-validate.exe 验证WebAssembly二进制文件的结构和内容是否符合WebAssembly规范。 wast2json.exe 将WebAssembly文本格式文件转换为JSON格式,便于程序化处理。 wat2wasm.exe 将WebAssembly文本格式文件(.wat)转换为二进制格式(.wasm),用于运行和部署。 wat-desugar.exe 处理WebAssembly文本格式文件中的语法糖,将其转换为更基本的格式。 *.exe --help 选项查看详细的使用说明和选项
编写和编译 Wasm 模块
1.官网安装 emscripten 看官网教程操作,记得安装路径免得找不到位置哦!
2.设置环境变量
C:\Users\*\Desktop\emsdk
C:\Users\*\Desktop\emsdk\upstream\emscripten
3.编写C程序(不一定要C写go、rust、python都可以,多平台代码都大差不差)如下 test.c 👇
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 声明一个外部的 JavaScript 函数 es,它接受一个 char* 类型的内存指针,并返回一个 const char* 类型的字符串指针
extern const char* es(char* memory);
// C 函数,调用 JavaScript 函数 es,并返回其结果作为 const char* 类型
const char* getJavaScriptString(char* memory) {
return es(memory);
}
// C 函数,接受一个时间戳(long 类型),生成一个加密后的字符串
char* encrypt(long timestamp) {
// 分配 100 字节内存空间给 char* 类型的指针 mc
char* mc = (char*)malloc(100);
// 调用 getJavaScriptString 函数,传递 mc 的内存指针,返回 JavaScript 函数 es 的结果
const char* jsString = getJavaScriptString(mc);
// 输出 JavaScript 返回的字符串,用于调试
printf("jsString: %s\n", jsString);
// 如果 JavaScript 返回的字符串为空指针,则返回字符串 "NULL0"
if (jsString == NULL) {
return "NULL0";
}
// 将时间戳转换为字符串形式,存储在 timestampStr 数组中
char timestampStr[20];
snprintf(timestampStr, sizeof(timestampStr), "%ld", timestamp);
// 计算连接后字符串的总长度(原字符串长度 + 时间戳字符串长度 + 1 个字节用于字符串结束符 '\0')
size_t totalLength = strlen(jsString) + strlen(timestampStr) + 1;
// 分配足够的内存空间给 result 指针,用于存储连接后的字符串
char* result = (char*)malloc(totalLength);
// 如果 result 指针为空,则释放 jsString 指向的内存并返回字符串 "NULL1"
if (result == NULL) {
free((void*)jsString);
return "NULL1";
}
// 将 JavaScript 返回的字符串复制到 result 中
strcpy(result, jsString);
// 将时间戳字符串连接到 result 的末尾
strcat(result, timestampStr);
// 返回连接后的结果字符串
return result;
}
// 主函数入口
int main() {
return 0; // 返回 0 表示程序正常结束
}
因为我们要调用JavaScript的函数方法,所以我们要编写一个辅助函数或与 WebAssembly 模块交互的代码如下 test.js
addToLibrary({
es: function() {
alert('hi');
},
});
4.使用 Emscripten 编译 hello.c
文件: emcc test.c -o test.wasm -s EXPORTED_FUNCTIONS="['_encrypt','_malloc']" --js-library test.js
emcc test.c
: 这部分指定了要编译的源文件test.c
。
-o test.wasm
: 这部分指定了编译输出的文件名为test.wasm
,即生成的 WebAssembly 模块文件。
-s EXPORTED_FUNCTIONS="['_encrypt','_malloc']"
: 这部分通过-s
参数来指定编译器选项。EXPORTED_FUNCTIONS
表示要导出的函数列表,包括了两个函数_encrypt
和_malloc
。这些函数名需要在 C 源文件中使用EMSCRIPTEN_KEEPALIVE
宏标记以确保它们被正确导出到 WebAssembly 模块中。
--js-library test.js
: 这部分指定了一个 JavaScript 库文件test.js
,其中可能包含用于在 JavaScript 环境中处理 WebAssembly 模块的额外函数或逻辑。
5.使用nodejs调用wasm,因为浏览器中是使用 WebAssembly 加载 wasm 文件的,所以我们照葫芦画瓢,写一个js程序执行它
const fs = require('fs');
const path = require('path');
/**
* 将从指定指针开始的ASCII编码字符串从指定内存缓冲区转换为JavaScript字符串。
* @param {WebAssembly.Memory} memory - 包含ASCII编码字符串的内存缓冲区。
* @param {number} ptr - 内存缓冲区中的起始指针。
* @returns {string} - 转换后的JavaScript字符串。
*/
function AsciiToString(memory, ptr) {
const new_memory = new Uint8Array(memory.buffer);
let str = '';
while (new_memory[ptr] !== 0) {
str += String.fromCharCode(new_memory[ptr]);
ptr++;
}
return str;
}
const wasmCode = fs.readFileSync( path.join(__dirname, 'test.wasm'));
// 读取 WebAssembly 二进制代码
WebAssembly.instantiate(wasmCode, {}).then(wasmInstance => {
// 获取 WebAssembly 模块和实例对象
module = wasmInstance.module;
instance = wasmInstance.instance;
// 启动与主机环境的交互
wasi.start(instance);
// 导出的加密函数
const { encrypt } = instance.exports;
// 调用加密函数并获取返回指针
const resultPtr = encrypt("123456789");
// 打印加密结果
console.log(AsciiToString(instance.exports.memory, resultPtr));
}).catch(err => {
console.error('Failed to instantiate WebAssembly module:', err);
});
代码准备好了之后执行下,发生这些奇奇怪怪的东西,这个错误通常表示导入的对象 env
不是一个有效的对象或函数。这可能是由于 wasmCode
或 importObject
中的配置问题引起的
但是我们呢是自己编译的wasm,按理说成功编译是不可能出现 wasmCode
引发的问题的
进入 WebAssembly.instantiate 看下源码是怎么写的,定位追踪可以发现源码中,需要一个 Imports 的字典对象,像什么env、version需要我们指定下
OK我们大致写一下代码,大概就这样吧,es
函数是用于 JavaScript 与 C 之间交互执行的函数
所以我们的代码大致写成这个鸟样就行了
const {WASI} = require('wasi');
const options = {
args: process.argv,
env: process.env,
preopens: {
'/': '/'
},
version: "preview1"
};
const wasi = new WASI(options);
const imports = {
"wasi_snapshot_preview1": wasi.wasiImport,
"env": {
"es": function () {
const str = "Hello, From JS";
const ptr = instance.exports.malloc(str.length);
const HEAP8 = new Int8Array(instance.exports.memory.buffer);
str.split('').forEach((char, index) => {
HEAP8[(ptr + index) >> 0] = char.charCodeAt(0);
});
return ptr;
}
}
};
然后完整代码如下👇OK第一个wasm程序就此告一段落了
const fs = require('fs');
const path = require('path');
const { WASI } = require('wasi');
// 创建一个全局 window 对象
window = Object.create({});
// WASI 的配置选项
const options = {
args: process.argv,
env: process.env,
preopens: {
'/': '/'
},
version: "preview1"
};
// 创建 WASI 实例
const wasi = new WASI(options);
// 导入对象,包括 WASI 的导入和自定义的 JavaScript 函数 es
const imports = {
"wasi_snapshot_preview1": wasi.wasiImport,
"env": {
// 自定义 JavaScript 函数 es,在 C 中会调用这个函数
"es": function () {
const str = "Hello, From JS";
const ptr = instance.exports.malloc(str.length); // 在内存中分配空间
const HEAP8 = new Int8Array(instance.exports.memory.buffer); // 获取内存的 Int8Array 视图
str.split('').forEach((char, index) => {
HEAP8[(ptr + index) >> 0] = char.charCodeAt(0); // 将字符的 ASCII 码存入内存
});
return ptr; // 返回指向字符串的指针
}
}
};
// 读取并加载 WebAssembly 代码
const wasmCode = fs.readFileSync(path.join(__dirname, 'test.wasm'));
// 将内存中的 ASCII 码转换为字符串
function AsciiToString(memory, ptr) {
const new_memory = new Uint8Array(memory.buffer);
let str = '';
while (new_memory[ptr] !== 0) {
str += String.fromCharCode(new_memory[ptr]); // 将 ASCII 码转换为字符
ptr++;
}
return str; // 返回字符串
}
var instance, module;
// 实例化 WebAssembly 模块
WebAssembly.instantiate(wasmCode, imports).then(wsm => {
module = wsm.module;
instance = wsm.instance;
wasi.start(instance); // 启动 WASI 环境
const { encrypt } = instance.exports; // 获取导出函数 encrypt
const resultPtr = encrypt("123456789"); // 调用 encrypt 函数,并传入参数
console.log(AsciiToString(instance.exports.memory, resultPtr)); // 打印加密结果的字符串表示
}).catch(err => {
console.error('Failed to instantiate WebAssembly module:', err); // 捕获实例化失败的错误
});
实战wasm补环境
通过以上的基础学习与环境安装,我们了解了 wasm 指令、编写与编译wasm、wasm调用
现在开始实战环节网站:aHR0cHM6Ly96aHVhbmxhbi56aGlodS5jb20vcC82NjY0Nzg4NDI=
1.全局搜索特征:WebAssembly.instantiate
1.打开fiddle抓包,直接搜索 WebAssembly.instantiate 加载特征,如图所示,发现第一次进入网站,cookies没有生成的时候会加载 wasm 文件,fiddler拦截命令:bpu URL,替换下在WebAssembly.instantiate前面加上 debugger;
代码复制下来,如图所示,现在让我们开始分析 wasm 吧进入 Go 看看
分析下入库实例 wasm 的地方,我们可以看到它实例了 Go 的类函数,importObject 是它的交互方法,很好现在我们把环境补上吧!
将代码全部复制到nodejs,嘎嘎报错,注意下这个 j 函数,检测了console,注释就好了,方框的是内存爆破别换行就行!!!
用 Proxy递归代理一下补环境吧,代码就不给你们了,自己去查百度吧
然后测试下效果吧,ok正常输出,然后附上补好的环境代码!白嫖记得点赞+关注哦!
navigator = Object.create({
toString: () => '[object Navigator]',
webdriver: false,
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
});
location = Object.create({
toString: () => location.href,
"href": 'https://www.*.com/question/*',
});
getElementsByTagName = Object.create({"valueOf":[].valueOf});
Object.defineProperty(getElementsByTagName, 'toString', {
value:()=> 'function getElementsByTagName() { [native code] }',
writable: true,
configurable: true,
enumerable: true
});
getElementsByClassName = Object.create({});
getContext = {
CanvasRenderingContext2D: () => {},
toString: () => '[object CanvasRenderingContext2D]'
}
canvas = {
toString: () =>'[object HTMLCanvasElement]',
toDataURL: ()=> "data:image/png;base64,换成你自己显卡绘制的帆布指纹",
getContext: () => getContext
};
document = Object.create({
getElementsByTagName: () => getElementsByTagName,
getElementsByClassName: () =>getElementsByClassName,
getElementById: () =>'[native code]',
location: location,
documentElement: {tagName: 'HTML'},
createElement: () => canvas,
toString: () =>'[object HTMLDocument]',
});
document.all = [document.documentElement]
screen = Object.create({"width": 1920});
history = Object.create({
toString: () => '[object History]'
})
window = Object.create({
"TextEncoder": TextEncoder,
"TextDecoder": TextDecoder,
"BigInt": BigInt,
"Object": Object,
"Array": Array,
"document": document,
"screen": screen,
});
window.window = window;
Object.defineProperty(window.__proto__, 'constructor', {
value: Object.create({}),
writable: true,
configurable: true,
enumerable: true
});
Object.defineProperty(window.__proto__.constructor, 'toString', {
value:()=> 'function Window() { [native code] }',
writable: true,
configurable: true,
enumerable: true
});
实战wasm反编译还原算法
以下命令将 test.wasm
转换为 test.wat
文件将包含易于阅读和编辑的 WebAssembly 文本格式
然而没有人愿意对着 WebAssembly 的汇编代码分析
wasm2wat wasm.wasm -o wasm.wat
以下命令见wasm转换得到C代码,然后并不是很近人意,代码函数太多了,没法正常进行分析行为
wasm2c test.wasm -o test.c
然后我们用gcc编译后在用ida反编译,将test.o拖入到 ida 进行分析,虽然效果不是很好,但是起码比起之前的代码,已经很不错了,现在让我们开始分析伪代码吧
gcc -I C:\Users\*\Desktop\wabt-1.0.35\include -c test.c -o test.o
如果库出现以下异常👇
编译为C: wasm2wat.exe wasm.wasm -o wasm.c
编译伪代码:gcc -std=c99 -Wall -c wasm.c -o wasm.o
之后我们就可以拖入到idea反编译分析伪代码还原算法了尽管和原始的代码差别较大,但好歹可以开始分析了
如果 wasm2wat 出现 openssl 异常处理方式方法一👇
打开 PowerShell(以管理员身份)执行以下命令安装opensll
列出可用的 OpenSSL 版本:choco list --all openssl
安装特定 OpenSSL 版本:choco install openssl --version=1.1.1
如果已有安装OpenSSL 的其它新版本,强制安装旧版: choco install openssl --version=1.1.1 --allow-downgrade
验证版本:openssl version
方法二👇
如果命令无法安装请到 openssl官网 下载安装,记得添加环境变量哦!
1.找到OpenSSL安装的路径:C:\Program Files\OpenSSL-Win64
2.将libcrypto-3-x64.dll复制到 wabt-1.0.35\bin 目录下改名 libcrypto-1_1-x64.dll
3.验证版本:openssl version
小结
未完成有空待续。。。