论文阅读-Binary Security of WebAssembly

Everything Old is New Again: Binary Security of WebAssembly

1. 概述

主要在浏览器、node、WASM独立VM三种平台进行漏洞验证。

WASM基本指令介绍:
global.get: 把全局变量的值压栈
global.set: 从栈顶弹出值赋给全局变量
local.tee: local.tee指令会把操作数留在栈顶,locat.set会弹出。local区别就是局部变量。
i32.const: 立即数压栈
i32.load: 从内存读数压栈
i32.store: 数据弹栈写入内存

2. Background on WebAssembly

  • Overview:wasm是个堆栈机器,最小模块为module,在module中可以import引入host environment中的东西。wasm没有寄存器,所有值都存在线性内存里。
    -w497

  • Type:wasm只有i32、i64、f32、f64四种类型,其他类型如数组、指针在编译时会转换过来。

  • Control-Flow:wasm内存的指令和数据完全隔离,内存中的数据不能执行,因此wasm无法ROP。同时,wasm跳转指令只能在相邻的块间跳转,如果是br_table这种,则只能在branch table里有的索引中跳转。因此直接jmp到任意地址是不存在的。

  • Indirect Calls:间接调用从栈中弹出索引,然后查table,如果索引在table中,则映射到对应函数。注意这里会有type checking,call_indirect的参数是一个type类型。
    -w478

  • Host environment:没有主机环境,wasm程序将无法执行I/O或访问网络。在浏览器中,可以导入XmlHttpRequest,eval或document.write这种来使wasm执行js api

  • Linear, Unmanaged Memory:wasm的线性内存是一个无类型连续字节数组,通过load和store访问。js可以使用Memory.grow增加内存。

3. Security Analysis of Linear Memory

对wasm的特色内存–线性内存的介绍

3.1 Managed vs. Unmanaged Data

Unmanaged Data是编译器维护的那段栈空间,也就是线性内存。
Managed Data是WASM VM维护的内存空间,存放比如返回地址等。
-w277

3.2 内存布局

.data, .rodata, 和.bss在wasm中不区分,所有初值都是0。堆栈的分配如下图,堆由低到高栈由高到低增长,注意b、c中关于stack和数据端存放顺序的不同。
-w472

注意Emscripten的fastcomp后端编译的栈的增长方向是向高地址增长。fastcomp已经在2019年10月被废弃,目前LLVM内置的wasm后端编译出的栈已经和x86一样了。

Q:fastcomp和LLVM的wasm后段是个啥? 为啥编译器不通编译出来的栈增长方式不同?
这个后端是llvm实现的,具体是啥还不太清楚。但是栈的增长方式、位置、堆的定义啥的,完全是可以通过编译器定义的,这个需要注意。因为栈的位置是rbp、rsp决定的,所以理论上位置、增长方式都是编译器编译成汇编的时候可定义的。

LLVM->WASM:
https://www.fastly.com/blog/webassembly-memory-management-guide-for-c-rust-programmers

3.3 内存保护

WASM内存是连续的,没有canary、aslr等,而且所有线性内存页是可写可读的不可执行。
WebAssembly线性内存是完全按照顺序排列,即堆栈和堆的位置可以从编译器和程序中算出来。

4. Attack Primitives

三步攻击,见图一

  1. 获得写能力
  2. 覆盖数据
  3. 通过写的恶意数据执行恶意操作
    下边有一些是根据c/c++的攻击转换到wasm中的,还有一些4.1.2和4.2.3这种,现在的语言不可能有的漏洞在wasm中也有。

4.1 获得写能力

4.1.1 缓冲区溢出写

  • 介绍了普通的c缓冲区溢出和防护措施(canary、FORTIFY_SOURCE)。FORTIFY_SOURCE可以在已知长度时替换strcpy为strncpy。
  • 在WASM中不存在canary,虽然数据和代码隔离(managed/unmanaged),但是溢出可以直接覆盖父栈帧的数据。
    -w574

4.1.2 栈溢出写

如果用户可控alloca的大小或者无限递归,则可能导致栈溢出。通过3.2节介绍的wasm内存布局可以看出,如果栈一直增长,默认emscripten情况下可能会导致覆盖data节或者heap。
在大多数本机平台上,当堆栈增长到将堆栈与其他内存区域分开的特殊保护页时,堆栈溢出将导致程序崩溃。
在WebAssembly中,对非托管堆栈不存在这种保护,因此攻击者控制的堆栈溢出可用于覆盖堆栈之后的潜在敏感数据(第3.2节)。

4.1.3 破坏堆

wasm的堆:
https://stackoverflow.com/questions/57032577/how-to-implement-malloc-in-wasm

wasm没有堆,是wasm编译器emcc通过dlmalloc/emmalloc,把c source里边的malloc转换成了wasm里的线性内存来存数据,然后emcc自己实现的dlmalloc/emmalloc来管理这部分内存,所以他还是管理的wasm的线性内存。
wasm中堆和栈都是连续的,在线性内存中按顺序排列,因此栈溢出可以直接影响堆的数据。
注意:堆和栈本身就是编译器/库自己实现的东西,和os、runtime无关。

wasm中的unlink造成任意地址写:
堆溢出后,对next chunk布局,free bit设0,freeinfo的prev中写入要覆盖的地址,next中写入要覆盖的值,然后*(prev+4) = next。
注意next的值必须是一个合法地址,Emscripten分配的默认栈大小为5M,因此next只能写入小于5×2^20的值。

Region结构体:
https://github.com/emscripten-core/emscripten/blob/87e279958be55eb13468183fde60647869cfe30e/system/lib/emmalloc.cpp#L153

unlink调用:
https://github.com/emscripten-core/emscripten/blob/87e279958be55eb13468183fde60647869cfe30e/system/lib/emmalloc.cpp#L343

4.2 覆盖数据

4.2.1 覆盖栈

可以覆盖栈中的任意数据,比如函数表索引、数组内容啥的。但是由于返回地址等代码和数据段完全隔离,因此rop这种完全没戏。

4.2.2 覆盖堆

如果栈向上增长,则可以覆盖堆,如果向下,则是栈溢出覆盖data。

4.2.3 覆盖Data

栈溢出时,3.2节中的图bc两种栈增长方式都可以导致data被覆盖。

4.3 恶意数据触发

4.3.1 间接调用重定向

覆盖索引,然后call_indirect导致控制流跳转到恶意函数。
但是这里有两个限制:

  1. not all functions defined in or exported into a WebAssembly binary appear in the table for indirect calls, but only those that may be subject to an indirect call. 不是所有wasm二进制文件中定义或导出的函数都会出现在函数表。只有可能被indirect call的才在。
  2. all calls, both direct and indirect, are type checked. 类型检查可能通过限定调用函数的地址范围或者table中有相应flag位来实现。

4.3.2 host env 代码注入

比如浏览器的document.write、eval,node的exec等。
通过调用emscripten_run_script实现在wasm中调用js的eval。这样的话,如果eval参数内容被覆盖,则会导致xss、rce等。

4.3.3 Application-specific Data Overwrite

懵逼。
还能盗cookie?

5. end-to-end attack

攻击涵盖了多个支持WebAssembly的平台:

  1. 浏览器-XSS
  2. Node.js-RCE
  3. 独立的WebAssembly VM(例如wasmtime [10]),任意文件写
Host environment Write primitive Overwritten data Location of data Malicious behavior
Browsers (client-side) Stack-based buffer overflow (CVE-2018-14550) Image tag in DOM string Heap Cross-site scripting in JavaScript via document.write()
Node.js (server-side) Heap metadata corruption Function index Stack Inject arbitrary shell command into exec()
Wasmtime (stand-alone runtime) Stack-based buffer overflow String literals “Constant” data Write arbitrary content to chosen file using fprintf()

5.1 XSS in Browser

-w459

libpng的1.6.35版存在一个缓冲区溢出(CVE-2018-14550),当将PNM文件转换为PNG文件时,可以利用此漏洞。但当有canary时这个洞没啥用,而wasm中就可以继续用了。通过pnm2png的溢出,我们可以覆盖堆上的img_tag从而xss。溢出到heap的距离是128bits

5.2 RCE in NodeJS

-w478

memcpy存在堆溢出,接下来free的时候触发unlink,造成任意地址写。途中将func的地址覆盖为exec的地址,然后布局参数字符串,并将布局后参数地址当作input3传入。
exec参数是指针,但是i32和指针在wasm中均为i32类型,因此重定向的调用通过了wasm的类型检查。

注:语言是如何判断指针类型的?通过比如dword ptr, byte ptr等汇编指令区分。

5.3 Arbitrary File Write in Stand-alone VM

https://github.com/appcypher/awesome-wasm-runtimes

wasi:wasm system interface
https://www.zhihu.com/question/319599509

wasm有了wasi,就有了访问文件系统、网络io的能力。这样wasm已经可以通过独立的vm来运行,而不依赖浏览器。

-w513

栈溢出覆盖位于.rodata区的常量,造成任意文件/任意内容写。

6. Quantitative Evaluation

对wasm定量评估,主要从三个方面:

  • RQ1. Unmanaged Data里存的数据量(size)?
  • RQ2. call_indirect使用的普遍性
  • RQ3. indirect的type checking和CFI比较。主要比较CFI equivalence classes and class sizes.

6.1 Experimental Setup and Analysis Process

  • Program Corpus
    两组。第一组是1Password、毁灭战士3、Adobe的某些sdk等,跨不同的应用程序(文档编辑,游戏,编解码器),部署方案(Web app,浏览器扩展),语言(C,C ++,Rust),因此还比较具有代表性;第二组是SPEC CPU 2017 benchmark suite,曾经被用来 evaluate the security of CFI techniques for native code,可以直接用在RQ3。
    Table 2

  • Toolchain and Configuration
    Emscripten 1.39.7(也测试了fastcomp后端),-O3,emcc的libc基于musl-libc,但是堆分配器不用musl的。wasi使用的是wasi-libc。

Q:编译器用glibc,然后如果有个vm解释字节码,这个vm用的musl-libc。我现在甚至觉得,为啥vm要有libc,他不是只需要解释字节码就行了。除非vm对编译器里实现的那些同名函数有不同的实现?call的时候执行不同的字节码?

  • Static Analysis
    自己实现了一个静态分析工具,主要功能:
    1. 提取指令数,函数数量及类型
    2. 通过Hook函数访问来分析Unmanaged Data Stack
    3. 分析函数表中已初始化表索引的函数
    4. 提取call_indirect目标的type,并找出与这个目标type匹配的函数数量,并分析CFI等价类(?)

6.2 Measuring Unmanaged Stack Usage

Static Analysis
  1. wasm没rsp啥的,因此要获得stack ptr,得通过提取所有修改全局变量的指令,然后选择最常读写的指令来实现。
  2. 通过与以下指令匹配提取栈帧大小
1
2
3
4
5
global.get $i.      // 栈指针压栈
i32.const <delta> // 当前栈帧大小压栈
i32.add or i32.sub // 计算新的栈基址
local.tee $j (optional)
global.set $i // 重新设值
Results

-w495

1/3将一些数据存储在非托管堆栈中。13,620个功能(占所有功能的14%)分配了16字节的最小帧大小。栈帧的大小从16字节到1MB。
结论:(1)大量栈数据容易被缓冲区溢出和任意写覆盖。(2)隔离Unmanaged Data上的栈帧,例如使用Canary。

6.3 Measuring Indirect Calls and Targets

Indirect Calls

间接调用相对于所有调用的百分比差异很大,从0.6%到31.3%,所有26个程序中,平均有9.8%的调用指令是间接的,即几乎每十分之一的调用都可能被转移到其他功能上。

Indirectly Callable Functions

call_indirect成功利用还需要存在可被调用的函数,需满足(1)type checking(2)函数索引在table中,这两个条件。
Table 2中的”Indirectly Callable”列是与至少一个call_indirect指令目标类型兼容并且在table中的函数数量。间接可调用函数的百分比在5%到77.3%之间,样本的所有函数的平均比例为49.2%。

Function Pointers in Memory

许多函数是可以间接调用的(平均为49.2%),而大多数函数可以通过简单地覆盖存储在线性存储器中的索引(48.1%)来实现。因此,call_indirect调用会对WebAssembly中控制流的完整性构成严重威胁。

6.4 Comparing with Existing CFI Policies

WebAssembly的type checking与native binary的最新CFI defence比较

Q:Backward edges?Forward edges

Equivalence Classes

Equivalence Classes咋确定的?
wasm通过函数的类型签名区分等效类

Comparing with Native CFI Defenses

与没有任何CFI防御相比,wasm类型检查无疑是向前迈出了一步,但应该采用更复杂的CFI防御。例如,使用Clang的CFI方案,在编译wasm时传递 -fsanitize = cfi

7. Discussion of Mitigations

几种缓解措施

7.1 WebAssembly Language

  1. 多个线性内存区(multiple linear memories):i32.load $mem2只能从mem2线性内存区读,不能影响其他内存区的数据。问题:由于内存访问被静态地限制在某块内存中,因此处理不同区域指针的代码必须被多次复制。
  2. 多个间接调用表(multiple tables for indirect calls):比如每个so一个表,或者每种等效类一个表。
  3. 分段(类似c),定义具有固定的大小和生命周期的内存区域。

7.2 Compilers and Tooling

FORTIFY_SOURCE和canary添加到编译器层面。这样不需要更改语言生态和语言本身。

7.3 Application and Library Developers

  1. 使用尽可能少的不安全语言,比如c(?)
  2. 不导入危险api
  • WebAssembly

    1. WebAssembly performance与native performance的比较
    2. formally defined type system
    3. WASASBI: a general dynamic analysis framework for WebAssembly
  • Malicious WebAssembly

  • Defensive WebAssembly
    通过将库编译为wasm,然后放到wasm runtime中运行,来隔离库的内存和主程序内存。(已被应用到firefox沙箱)

  • Exploiting Native Binaries
    参考 Eternal War in Memory,很多native binary的攻击姿势也可以移植到wasm

  • Exploit Mitigations
    即使有CFI这种东西,还是有Control-flow
    bending, data-only attacks和其他高级攻击。

6.3节比较了CFI在wasm中的结果,未来还会有更多人做wasm攻击与防御。

9. Conclusion

wasm中出现了很多十几年前就没了的攻击方式,还有node、vm、browser等端的攻击方式。同时对真实binary的评估量化了开发风险,显示出很大的攻击面,因此需要进一步加强wasm语言、编译器、生态的安全。

https://www.usenix.org/system/files/conference/usenixsecurity18/sec18-alhuzali.pdf

工具总体分两个阶段:

静态分析阶段

使用符号执行创建Web应用程序的各个模块的行为模型。标记出包含潜在漏洞点的模块。说人话就是通过静态分析找到source到sink的路径,然后动态执行找触发source的请求。

动态分析阶段

使用Web爬虫和concolic执行器去发现可能导致攻击者进入易受攻击模块的可能的HTTP请求路径,之后使用约束求解器生成一组对漏洞进行利用的HTTP输入序列。

整体架构

先静态分析,使用phpjoern构建cpg图,然后通过图遍历分析sink到source,完成污点分析,然后找到一条从source到sink的路径,标记这个模块。然后动态爬虫开始尝试触发这个source所在的模块,这个过程中还用了z3约束求解js和html的一些约束,使请求更满足条件。

污点分析的算法:

从sink到source的回溯,具体的实在看不懂了。在图数据库好像一句话就查出来了:

1
2
3
VulnerablePaths = g.V().filter{sql_query_funcs.contains(it.code)  && isCallExpression(it.nameToCall().next())}.as('sink')
.callexpressions().as('sloop').statements().inE('REACHES').outV.loop('sloop')
{ it.loops < 10 }{ it.object.containsLowSource().toList() != []}.path().toList()

至于动态爬虫触发路径,实在没觉得有啥意思。。

他用到的工具:

扩展的CPG图利用phpjoern进行实现;
扩展的CPG图存储在Neo4j图形数据库中;
图遍历算法使用Apache TinkerPop编写;
约束求解使用z3以及其扩展Z3-str;
爬虫程序使用被约束和z3断言扩展的crawler4j;
爬虫对JavaScript的处理使用了Narcissus JavaScript引擎的扩展;
服务端代码执行跟踪使用Xdebug。

参考:

https://zeroyu.xyz/2019/03/11/NAVEX-Precise-and-Scalable-Exploit-Generation-for-Dynamic-Web-Applications/#3-1-1-Attack-Dictionary
https://uuuunotfound.github.io/2020/05/03/%E5%A4%8D%E6%B4%BBNavex-%E4%BD%BF%E7%94%A8%E5%9B%BE%E6%9F%A5%E8%AF%A2%E8%BF%9B%E8%A1%8C%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90/

Proudly powered by Hexo and Theme by Hacker
© 2020 LFY