WebAssembly的安全性问题--Part 1
processor又咕咕咕了吗 WEB安全 10280浏览 · 2018-12-23 01:07

WebAssembly的安全性问题--Part 1

原文地址:

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中的一个函数。

(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文件。然后使用以下函数来获取文件并返回一个实例。实例是模块的单个运行副本。

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方法进行扩展。

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()时,控制格式指定的字符串时,可能能够直接对内存进行读写。 以下示例和输出演示了此操作。

#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; 
}

运行结果为:

0.0.0.0.41414141.

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

#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;
}

运行结果为:

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

#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;
}

编译指令:

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

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

小结

本篇文章介绍了一些基本概念和一些基本的利用方式,在Part 2 中,将会有一些WebAssembly独特的利用方式。

1 条评论
某人
表情
可输入 255