wasm逆向学习之旅

目录

概要

 

技术名词解释

1、WebAssembly 指令集概览

1)基本结构

2)数据类型

3)模块和函数

4)指令概览

        1.i32 整数运算

        2.i32 浮点数运算(用法同整数运算)

         3.逻辑运算和位移(用法同整数运算)

        4.内存访问指令

        6.控制流指令

        7.模块和导出指令

        8.其他常见指令

实战wasm补环境

实战wasm反编译还原算法

小结


 

概要

1.wasm 补环境

2.反编译分析伪代码

3.wasm 的指令学习

wasm-reference-manual/WebAssembly.md at master · sunfishcode/wasm-reference-manual · GitHubWebAssembly Reference Manual. Contribute to sunfishcode/wasm-reference-manual development by creating an account on GitHub.https://github.com/sunfishcode/wasm-reference-manual/blob/master/WebAssembly.md#instructions

4.wabt反编译 

Releases · WebAssembly/wabt · GitHubThe WebAssembly Binary Toolkit. Contribute to WebAssembly/wabt development by creating an account on GitHub.https://github.com/WebAssembly/wabt/releases5.gcc反编译

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.add32位有符号整数的加法运算
i32.sub32位有符号整数的减法运算
i32.mul32位有符号整数的乘法运算
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 0local.get 1:这两个指令分别将函数的第一个和第二个参数加载到本地变量栈上。在WebAssembly中,参数从索引0开始。

  • i32.add:这是一个整数加法指令,它从栈顶弹出两个整数值,将它们相加,然后将结果推送回栈顶。在这个例子中,它将栈顶的两个整数(即函数的两个参数)相加。

执行过程

  • 当函数被调用时,传递给函数的两个参数被存储在本地变量中(索引0和索引1)。

  • local.get 0 将第一个参数加载到栈顶。

  • local.get 1 将第二个参数加载到栈顶。

  • i32.add 从栈顶弹出这两个参数,执行加法操作,并将结果再次推送到栈顶,作为函数的返回值。

        2.i32 浮点数运算(用法同整数运算)

f32 指令描述
f32.add32位浮点数的加法运算
f32.sub32位浮点数的减法运算
f32.mul32位浮点数的乘法运算
f32.div32位浮点数的除法
f64 指令描述
f32.add64位浮点数的加法运算
f32.sub64位浮点数的减法运算
f32.mul64位浮点数的乘法运算
f32.div64位浮点数的除法

         3.逻辑运算和位移(用法同整数运算)

运算和位移指令描述
i32.and32位整数的按位与、按位或和按位异或操作
i32.or32位整数的按位与、按位或和按位异或操作
i32.xor32位整数的按位与、按位或和按位异或操作
i32.shl32位整数的左移、有符号右移和无符号右移操作
i32.shr_s32位整数的左移、有符号右移和无符号右移操作
i32.shr_u32位整数的左移、有符号右移和无符号右移操作

        4.内存访问指令

WebAssembly允许程序直接访问内存,执行加载和存储操作。这对于处理大量数据或者与Web页面交互非常有用。

1.内存加载和存储

i32.loadi64.loadf32.loadf64.load:从内存中加载指定类型的数据。
i32.storei64.storef32.storef64.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 0local.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.基本控制流

blockloopifelseend:定义基本块、循环和条件执行及其结束

示例代码👇

(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。

  • 使用 blockend 封装了整个逻辑块,使得条件分支(ifelse)可以安全地组织和执行。

  • 这种结构允许在WebAssembly中实现基本的条件逻辑和数学运算,用于构建更复杂的算法和函数。

执行过程:

  1. 函数参数和块定义

    • 函数接受一个 i32 类型的参数,并返回一个 i32 类型的结果。

    • block 指令定义了一个块,用于封装条件分支和相关操作。

  2. 条件分支

    • local.get 0 将函数的第一个参数(索引为0的本地变量)加载到栈顶。

    • i32.const 0 将常数0推送到栈顶。

    • i32.eq 指令比较栈顶的两个值是否相等,如果相等则将1推送到栈顶;否则将0推送到栈顶。

  3. 条件分支执行

    • if 指令根据前面比较的结果(0或1),决定执行哪个分支:

      • 如果条件为真(栈顶值为1),则执行 i32.const 1,将常数1推送到栈顶作为函数的返回值。

      • 如果条件为假(栈顶值为0),则执行 local.get 0i32.const 1i32.sub,依次将函数参数加载到栈顶,推送常数1,并计算栈顶两个值的差,作为函数的返回值。

  4. 块结束

    • 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.模块定义

modulefuncglobaltablememory:定义模块、函数、全局变量、表格和内存等。

示例代码👇

(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 0local.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 0local.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_i64i64.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 不是一个有效的对象或函数。这可能是由于 wasmCodeimportObject 中的配置问题引起的
但是我们呢是自己编译的wasm,按理说成功编译是不可能出现 wasmCode 引发的问题的

53b19b1047504a4ea8dafc58f1ecbc75.png

进入 WebAssembly.instantiate 看下源码是怎么写的,定位追踪可以发现源码中,需要一个 Imports 的字典对象,像什么env、version需要我们指定下

6fa13cd4fe4c48a790c6fdd42f2856c1.png

e48e44c501934197884bacebaba84b9c.png

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); // 捕获实例化失败的错误
});

83c02cad289e4968982c7dcdf26b701e.png

实战wasm补环境

通过以上的基础学习与环境安装,我们了解了 wasm 指令、编写与编译wasm、wasm调用

现在开始实战环节网站:aHR0cHM6Ly96aHVhbmxhbi56aGlodS5jb20vcC82NjY0Nzg4NDI=

bb26131f1d724dc386450632e891ef58.png

1.全局搜索特征:WebAssembly.instantiate

1.打开fiddle抓包,直接搜索 WebAssembly.instantiate 加载特征,如图所示,发现第一次进入网站,cookies没有生成的时候会加载 wasm 文件,fiddler拦截命令:bpu URL,替换下在WebAssembly.instantiate前面加上 debugger;

720b3b2ee454499699a0ec24a2bfb277.png

代码复制下来,如图所示,现在让我们开始分析 wasm 吧进入 Go 看看

73b00abed31e4945b1994c82762f2ded.png

分析下入库实例 wasm 的地方,我们可以看到它实例了 Go 的类函数,importObject 是它的交互方法,很好现在我们把环境补上吧!

37df607be4d94305a821cec69c7c164d.png

将代码全部复制到nodejs,嘎嘎报错,注意下这个 j 函数,检测了console,注释就好了,方框的是内存爆破别换行就行!!!

6e0ca9b40c1a4a5badfd9175dd49f322.png

984096feeede40bbbdd5d7285973d67d.png

用 Proxy递归代理一下补环境吧,代码就不给你们了,自己去查百度吧

d6f8e6f43dac43dd8925cf494b7be06c.png

57197190be31482e86847cecc2cbeb35.png

然后测试下效果吧,ok正常输出,然后附上补好的环境代码!白嫖记得点赞+关注哦!

e86f1d66238743c1a1ea2f456638e262.png

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

 如果库出现以下异常👇

d8d6b6fde6f844e398b744058d0f9dae.png

编译为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

 

小结

未完成有空待续。。。

 

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值