首页
论坛
课程
招聘
[翻译]WebAssembly的静态手动逆向分析
2018-11-19 18:58 6013

[翻译]WebAssembly的静态手动逆向分析

2018-11-19 18:58
6013

在我们上一篇关于WebAssembly(Wasm)的博客中,我们初步了解了一个未知的Wasm二进制文件,并对其进行了一些行为分析。今天我们将继续深入研究相同的Wasm样本。我们将通过研究Wasm文本格式手动分析该样本。

为了能够手动分析Wasm文本,我们首先需要学习更多理论。我们之前的博文描述了如何处理内存和数据。在此基础上,我们将介绍一些对逆向Wasm有用的概念,然后应用这些知识来分析Wasm样本。

注意:这篇文章是系列文章的一部分。该系列的上一篇文章介绍了Wasm内存处理,所以可能需要阅读这些文章

Wasm指令集

正如我们在今年早些时候讨论的那样,Wasm本身不能与外界联系。与外部环境的所有通信都需要通过JavaScript API调用。考虑到这一点,我们现在主要讨论那些Wasm中对实际计算一些有用的指令 ,而不是调用JavaScript的指令。
与x86或x64的指令集相比,Wasm的指令集非常小,有几组不同的功能:

  • 算术指令
  • 控制流指令
  • 内存访问指令
  • 比较指令
  • 转换指令

以下是常见Wasm指令的几个示例。有关更全面的说明列表,请参阅参考手册

指令 描述
get_local <variable> 获取本地存储中变量的值,将其压入堆栈使其可用于后续指令
set_local <variable> 设置本地存储中变量的值,从堆栈中弹出值并将该值分配给相关的局部变量
get_global <variable> 获取全局变量的值
set_global <variable> 设置全局变量的值
i32.add 从堆栈中弹出两个数字相加,并将结果压入堆栈
call<func nbr> 直接调用指定的函数号
call_indirect <var> 调用函数号在运行时生成的函数
if/else/end 条件分支
br 无条件分支
br_if 条件分支
loop 定义要循环的代码块
block 定义一个代码块
return 函数返回

了解文本格式

上面给出是WebAssembly文本格式的说明。由于Wasm是二进制格式,因此这些文本指令将在编译文件中用字节码表示。

 

让我们分析一个简单的函数:

(func $max (; 0 ;) (param $0 i32) (param $1 i32) (result i32)
  (select
   (get_local $0)
   (get_local $1)
   (i32.gt_s
    (get_local $0)
    (get_local $1)
   )
  )
 )

第一行表示一个名为'max'的函数,以两个整数作为参数,$0和$1,并返回一个 i32 (32-bit integer)类型的整数。

 

'select'指令有三个参数:第一个操作数(get_local $0),第二个操作数(get_local $1)和一个条件参数(在这种情况下是i32.gt_s指令及其相关的操作数)。如果条件操作数不为零,则'Select'返回第一个操作数,否则返回第二个操作数。

 

在select中,指令'i32.gt_s'检查第一个参数(get_local $0)是否大于第二个参数(get_local $1)。此检查的结果将保存在“select”运算符的条件操作数中。

 

因此,如果第一个参数大于第二个参数,则返回第一个参数,否则返回第二个参数。

 

请注意,不同的工具表示的Wasm文本格式可能会略微不同(就像不同的反汇编器一样)。例如,上面的例子也可以这样表示:

(func (;0;) (type 0) (param i32 i32) (result i32)
  get_local 0
  get_local 1
  get_local 0
  get_local 1
  i32.gt_s
  select)

无论我们使用何种文本表示,它都对应于以下高级语言:

int max(int a,int b){ 
  return a> b?a:b; 
}

有关Wasm文本格式的更多信息,请访问此处

使用wasm2wat进行静态代码分析

现在,我们理解了堆栈机,局部变量,全局内存,数据存储(如我们以前的帖子提到的),指令集和Wasm文本格式的概念,接下来用所学知识深入分析之前的博客文章中的Wasm示例

 

从之前的位置继续:最初的浅层分析和行为分析表明我们正在处理的是排序算法。对于这个样本,分析可能会在此时就完成了,具体取决于您可以在一个样品上花费多少时间。

 

如果我们处理的是一个功能不那么明显的样本怎么办?您可能经常需要查看源代码,接下来进行这项操作。

 

为了处理代码,我们将再次使用我们之前的博文中的wasm2wat工具。我们已经发现sort函数的函数号为1。下面是Wasm文本表示的该函数的第一部分:

$ ./wasm2wat quicksort.wasm
[snip]
  (func (;1;) (type 1) (param i32 i32) (result i32)
    (local i32)

它从函数定义开始,表示函数使用两个整数并返回一个整数。然后定义了一个局部变量local i32。用高级伪代码表示:

func sort(int param1, int param2) {
  int var1;

Wasm代码:

    get_local 0
    get_local 1
    i32.ge_s
    if  ;; label = @1
      get_local 1
      return
    end

前两个指令get_local0/1分别获取第一个和第二个函数参数的值,并将它们压入堆栈。然后第三条指令i32.ge_s将对堆栈上的这两个值进行操作,通过隐式弹出它们然后判断第一个值是否大于或等于第二个值。比较的结果将被压入堆栈。如果堆栈顶部的值为非零值,则后续if语句将为true。换句话说,if语句的分支将取决于前面的三条指令。

 

如前所述,相同的代码可以用不同的方式表示。如果上面的if语句难以理解,这里有一个替代表示:

  (if
   (i32.ge_s
    (get_local $var$0)
    (get_local $var$1)
   )

到目前为止我们所逆向的内容可以用高级伪代码表示,如下所示:

func sort(int param1, int param2) {
  int var1;
  if (param1 >= param2)
    return param2;

继续使用wasm2wat查看Wasm文本格式,发现sort函数:

    get_local 0
    get_local 1
    i32.add
    i32.const 4
    i32.div_s
    i32.const 2
    i32.div_s
    i32.const 4
    i32.mul

这段代码进行数学计算。get_local 0/1指令获取传递给函数的两个参数值,随后的'i32.add'指令对这两个值进行操作,把它们相加并将结果压入栈中。

 

之后'i32.const 4'指令将4压入堆栈。后续指令i32.div_s将堆栈顶部第二项的值除以最顶部的值。换句话说,先前param1+param2的值将被除以4。在此之后,我们再次执行相同的模式,但这次除以2。同样,接下来的两条指令是i32.mul指令,操作数是4。

 

最终结果是目前获得的值乘以4。可以更简洁地表示为,代码执行以下计算:(param1 + param2)/4/2 * 4。

 

让我们看看随后的Wasm代码,右侧添加了我们的注释:

指令 描述
set_local 2 局部变量var1=堆栈顶部的值:(param1 + param2)/4/2*4
get_local 0 将param1放入堆栈,准备函数调用
get_local 1 将param2放入堆栈,准备函数调用
get_local 2 将var1放入堆栈,准备函数调用
i32.load 弹出栈顶的值,作为指向全局内存的指针,然后获取它指向的数据并将该数据压入堆栈
call 0 使用刚刚设置的参数,调用函数0(名为“partition”)
set_local 2 局部变量var1=函数调用的返回值
 

再一次,用高级伪代码表达:

var1 = (param1 + param2) / 4 / 2 * 4;
var1 = call partition(*var1,param2,param1);

如果这很难掌握,请尝试在Chrome中运行示例,单步执行并在调试器中查看(即DevTools)堆栈和局部变量值的变化,以及全局内存的构造。一篇关于Wasm分析的早期博文介绍了做法。当我们位于'call 0'指令时,在执行之前,局部变量和堆栈将如下所示:

 

图片

 

继续查看Wasm代码:

指令 描述
get_local 0 将param1压入堆栈
get_local 2 将var1压入堆栈
i32.const 4 将4压入堆栈
i32.sub 从var1中减去4(仅在堆栈上,而不是在局部变量内存中)
call 1 调用排序函数
drop 丢弃排序调用中的返回值
get_local 2 将var1压入堆栈
i32.const 4 将4压入堆栈
i32.add var1加4(仅在堆栈上,而不是在局部变量内存中)
get_local 1 将param2放入堆栈
call 1 调用sort函数
drop 丢弃排序调用中的返回值
get_local 2 返回var1(堆栈中最后剩下的项将是返回值)
 

用高级伪代码表示:

call sort(var1 - 4, param1);
call sort(param2, var1 + 4)
return var1;

总而言之,我们的最终结果是:

func sort(int param1, int param2) {
  int var1;
  if (param1 >= param2)
    return param2;
  var1 = (param1 + param2) / 4 / 2 * 4;
  var1 = call partition(*var1,param2,param1);
  call sort(var1 - 4, param1);
  call sort(param2, var1 + 4)
  return var1;
}

我们发现这确实是一个QuickSort实现。如果您愿意,可以通过将上述伪代码与您在Internet上找到的某些现有实现进行比较验证。逆向 partition函数留给读者练习。

总结

我们现在成功地逆向了一个完整的Wasm函数。首先,我们使用wasm2wat程序将Wasm二进制格式转换为其文本格式,并通过分析文本格式,我们能够创建算法的高级伪代码。

 

存在用于执行自动反编译的工具,一种比我们今天所做的更有效的逆向工程方法。虽然自动反编译可以节省时间,但它通常不完美,对手动分析的理解使我们能够解决这些不完善之处。

参考文献

  • https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format
  • https://github.com/sunfishcode/wasm-reference-manual/blob/master/WebAssembly.md#instructions
  • https://www.pnfsoftware.com/reversing-wasm.pdf
  • https://sophos.files.wordpress.com/2018/08/sophos-understanding-web-assembly.pdf

翻译:看雪翻译小组 欢歌笑语
校对:看雪翻译小组 Nxe
原文链接:https://www.forcepoint.com/blog/security-labs/manual-reverse-engineering-webassembly-static-code-analysis


2022 KCTF春季赛【最佳人气奖】火热评选中!快来投票吧~

收藏
点赞1
打赏
分享
打赏 + 2.00雪花
打赏次数 1 雪花 + 2.00
 
赞赏  junkboy   +2.00 2018/11/19
最新回复 (1)
雪    币: 10943
活跃值: 活跃值 (83)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
junkboy 活跃值 2018-11-19 19:26
2
0
支持, 填补论坛空白
游客
登录 | 注册 方可回帖
返回