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没有寄存器,所有值都存在线性内存里。
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类型。
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维护的内存空间,存放比如返回地址等。
3.2 内存布局
.data, .rodata, 和.bss在wasm中不区分,所有初值都是0。堆栈的分配如下图,堆由低到高栈由高到低增长,注意b、c中关于stack和数据端存放顺序的不同。
注意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
三步攻击,见图一
- 获得写能力
- 覆盖数据
- 通过写的恶意数据执行恶意操作
下边有一些是根据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),但是溢出可以直接覆盖父栈帧的数据。
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的值。
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导致控制流跳转到恶意函数。
但是这里有两个限制:
- 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的才在。
- 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的平台:
- 浏览器-XSS
- Node.js-RCE
- 独立的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
libpng的1.6.35版存在一个缓冲区溢出(CVE-2018-14550),当将PNM文件转换为PNG文件时,可以利用此漏洞。但当有canary时这个洞没啥用,而wasm中就可以继续用了。通过pnm2png的溢出,我们可以覆盖堆上的img_tag从而xss。溢出到heap的距离是128bits
5.2 RCE in NodeJS
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来运行,而不依赖浏览器。
栈溢出覆盖位于.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。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
自己实现了一个静态分析工具,主要功能:- 提取指令数,函数数量及类型
- 通过Hook函数访问来分析Unmanaged Data Stack
- 分析函数表中已初始化表索引的函数
- 提取call_indirect目标的type,并找出与这个目标type匹配的函数数量,并分析CFI等价类(?)
6.2 Measuring Unmanaged Stack Usage
Static Analysis
- wasm没rsp啥的,因此要获得stack ptr,得通过提取所有修改全局变量的指令,然后选择最常读写的指令来实现。
- 通过与以下指令匹配提取栈帧大小
1 | global.get $i. // 栈指针压栈 |
Results
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
- 多个线性内存区(multiple linear memories):i32.load $mem2只能从mem2线性内存区读,不能影响其他内存区的数据。问题:由于内存访问被静态地限制在某块内存中,因此处理不同区域指针的代码必须被多次复制。
- 多个间接调用表(multiple tables for indirect calls):比如每个so一个表,或者每种等效类一个表。
- 分段(类似c),定义具有固定的大小和生命周期的内存区域。
7.2 Compilers and Tooling
FORTIFY_SOURCE和canary添加到编译器层面。这样不需要更改语言生态和语言本身。
7.3 Application and Library Developers
- 使用尽可能少的不安全语言,比如c(?)
- 不导入危险api
8. Related Work
WebAssembly
- WebAssembly performance与native performance的比较
- formally defined type system
- WASASBI: a general dynamic analysis framework for WebAssembly
Malicious WebAssembly
- 前人对wasm的研究:
- 我们额外做的:
- 第一个提出栈溢出(不是缓冲区溢出)
- 对之前blog文章提出的wasm自带分配器的安全问题进行验证
- 对26个wasm binary file set进行定量安全评估
- the first to estimate how much data resides on the unmanaged stack
- the first to compare WebAssembly’s type-checking of indirect calls with native CFI schemes.
Defensive WebAssembly
通过将库编译为wasm,然后放到wasm runtime中运行,来隔离库的内存和主程序内存。(已被应用到firefox沙箱)Exploiting Native Binaries
参考 Eternal War in Memory,很多native binary的攻击姿势也可以移植到wasmExploit Mitigations
即使有CFI这种东西,还是有Control-flow
bending, data-only attacks和其他高级攻击。
6.3节比较了CFI在wasm中的结果,未来还会有更多人做wasm攻击与防御。
9. Conclusion
wasm中出现了很多十几年前就没了的攻击方式,还有node、vm、browser等端的攻击方式。同时对真实binary的评估量化了开发风险,显示出很大的攻击面,因此需要进一步加强wasm语言、编译器、生态的安全。
NAVEX: Precise and Scalable Exploit Generation for Dynamic Web Applications
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 | VulnerablePaths = g.V().filter{sql_query_funcs.contains(it.code) && isCallExpression(it.nameToCall().next())}.as('sink') |
至于动态爬虫触发路径,实在没觉得有啥意思。。
他用到的工具:
扩展的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/