导语:本文将为读者详细介绍如何劫持WebAssembly程序的控制流。
虽然事实已经证明,WebAssembly是滋生浏览器攻击面的沃土,但随着越来越多的Web应用程序的编程语言从Javascript迁移至WebAssembly,所以研究和保护WebAssembly程序的重要性日益凸显。当然,WebAssembly在设计之初,就从C和C++等开发语言那里吸取了许多经验,力争避免它们身上曾经出现过的各种常见的安全缺陷,可是WebAssembly毕竟不是铁板一块,所以,攻击者仍然有可乘之机。
在本教程中,我们将为读者详细介绍WebAssembly提供的控制流保护保机制、已知的弱点,以及如何在WebAssembly程序中应用clang控制流完整性(CFI)保护机制来降低控制流劫持的风险。在这个过程中,我们将以一个含有(人为的)类型混淆漏洞的WebAssembly程序为例,来为读者介绍如何劫持WebAssembly程序的控制流。本文中的代码取自Trail of Bits的“听我聊聊CFI”的系列文章,并做了相应的修改——如果读者还不熟悉控制流完整性的话,建议先参阅Trail of Bits的博客系列。
请注意,本文中没有详细介绍WebAssembly的工作原理。如果您不熟悉WebAssembly的话,可以访问webassembly.org上的开发人员指南页面,或在网上搜索相关的文章、教程和视频,以了解更多信息。
WebAssembly:Web新时代
WebAssembly是一种开放的、全行业共同推动的、旨在为Web带来安全、高效的汇编语言。WebAssembly技术由Mozilla、Google、Microsoft和Apple等主要浏览器供应商以及Fastly等非浏览器Web技术公司共同开发的WebAssembly模块可以供当前的大多数浏览器进行下载和执行。同时,AutoCAD、QT等大型开发计划,也越来越多地利用WebAssembly来部署跨桌面、移动和浏览器平台的快速安全的应用程序,并且这些应用都是利用通用的C/C++代码库构建的。WebAssembly生态系统正在快速发展,定期公布新的工具、应用程序和新创意。
因此,WebAssembly自然成为Google Native Client的接班人,我们知道,Google Native Client是一种高性能的软件故障隔离技术,允许开发人员将本机应用程序部署到Google Chrome。同时,WebAssembly不仅提供了一种更加优雅的设计,并且从Native Client的经验教训中获益匪浅,其亮点包括适合单页的声音类型系统、控制流完整性、有限/局部非确定性、内存安全保障等。有关WebAssembly设计的详细信息,请访问2017 PLDI文献或webassembly.org网站。
在未来几个月和几年内,很多传统上用Javascript实现的逻辑,包括安全控制在内,很可能都会用WebAssembly重写。随着技术进一步成为人们关注的焦点,我们还可以期待Web开发人员使用WebAssembly完成更新颖、优美和疯狂的事情。
为避免C、C ++和其他源语言中出现的安全缺陷,WebAssembly在设计之处便支持内存安全,这方面的内容,我们将在后面详细介绍。
WebAssembly程序中的内存安全性
WebAssembly内存安全方面的文档解释了WebAssembly程序为什么可以避免各种类型的内存安全漏洞和相关的漏洞利用技术,如堆栈粉碎、ROP等。这意味着,如果编译和运行WebAssembly代码的程序安全无误的话,那么这些攻击是无法针对WebAssembly程序实施的。这的确非常酷,并且证明WebAssembly的架构的确是经过深思熟虑的。
但是,文档继续写道:
尽管如此,WebAssembly的语义并没有消除其他类型的漏洞。虽然攻击者无法完成直接代码注入攻击,但是,仍然可能使用针对间接调用的代码重用攻击来劫持模块的控制流。
接下来,我们将通过一个类型混淆漏洞利用场景来考察这种设计的实际影响。
一个类型混淆漏洞示例
我们将使用一个简化的C++虚拟调用类型混淆示例程序,代码取自Bit of Bits的博客文章系列中的一个例子。
在这里,攻击者将设法欺骗程序调用错误类型的实例的方法。这种攻击在现实中经常发生,但最常见的情况是,当程序从不可信的源(例如网络)读取实例(或实例选择逻辑)时,没有检查该实例是否为预期的类型就调用了某些对象方法(或函数)。如果攻击者可以向程序提供非预期类型的实例,那么,他们就可能控制程序(或导致其他非预期的事件发生)。
对Trail of Bits提供的代码所做的修改将在后面加以介绍;同时,读者可以在Github上找到本教程中使用的示例代码。
配置教学工具
本节介绍在家中进行试验时如何搭建测试平台。如果没有这方面的需求的话,请跳过这些内容。
我们将使用Docker Ubuntu 16.04客户机将易受攻击的代码编译为本机的WebAssembly目标代码。此外,我们还需要共享一个目录,以便我们可以使用主机上安装的工具编辑文件并使用主机Web浏览器来执行WebAssembly。
docker run -v "$(pwd):/src" -t -i ubuntu:16.04 bash
我们将使用clang编译本机目标代码,并使用emscripten编译WebAssembly目标代码。下面给出我在Ubuntu guest虚拟机中安装相应工具的命令:
[email protected]:/src# apt-get update && apt-get install -y cmake build-essential python2.7 nodejs git wget tmux [email protected]:/src# apt-get install clang-5.0 && ln -s /usr/bin/clang-5.0 /usr/bin/clang && ln -s /usr/bin/clang++-5.0 /usr/bin/clang++ [email protected]:/src# wget https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz && tar -xf emsdk-portable.tar.gz && cd emsdk-portable [email protected]:/src/emsdk-portable# ./emsdk update && ./emsdk install latest && ./emsdk activate latest
为了创建本文中的相关内容,我运行了一个tmux会话(在下文中用[tmux]表示),以用作emscripten环境。[tmux]会话会将emscripten工具链用于WASM目标代码,而常规的客户机shell将使用Ubuntu clang工具链来处理本机目标代码。下面是emscripten环境:
[tmux] [email protected]:/src/emsdk-portable# source ./emsdk_env.sh && which clang /src/emsdk-portable/clang/e1.37.35_64bit/clang
下面是clang/本机环境:
[email protected]:/src/emsdk-portable# which clang /usr/bin/clang
我们可以使用WebAssembly二进制工具包(WABT)将二进制WebAssembly模块转换为文本格式:
[email protected]:/src# git clone --recursive https://github.com/WebAssembly/wabt && cd wabt [email protected]:/src/wabt# make && make install
使用WebAssembly类型检查阻止漏洞利用
WebAssembly嵌入器(即浏览器)通常会(根据其参数和返回值)检查函数的类型,确保函数类型正确之后,再允许WebAssembly程序执行(另请参阅[WebAssembly.validate](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/validate){:target="_blank" rel="noopener"})。但是,间接调用(类似于在C或C++中调用函数指针)的类型检查是在运行时进行的。当间接调用的类型检查失败时,WebAssembly程序将暂停并触发陷阱。在浏览器中,这最终会触发Javascript异常,当然有些异常时用户代码可以处理,而另一些则是用户代码无法处理的。无论如何,得益于WebAssembly设计提供的安全保护,嵌入器(即浏览器)进程可以安全地继续执行,而不用担心未定义的行为(例如:内存损坏)。
为了观察一个成功的类型检查操作,我们将使用Trail of Bits示例代码中的cfi_vcall.cpp文件,当然,这里已经做了相应的修改,修改后的文件名为cfi_vcall_diff.cpp。在cfi_vcall_diff.cpp中,受害者函数(程序试图调用的函数)有一个整数参数,但恶意函数(攻击者以某种方式提供的函数)需要一个浮点参数。回顾一下,攻击者试图让受害者执行下列操作:
virtual void makeAdmin(float * i) { std::cout << "CFI Prevents this control flow " << i << "\n"; std::cout << "Evil::makeAdmin\n"; }
而非执行下列操作:
virtual void printMe(int i) { std::cout << "Derived::printMe " << i << "\n"; }
在下一节中,我们将展示针对这些程序的利用方法。
实验#1:利用本机可执行文件中的类型混淆漏洞
我们可以先用clang编译易受攻击/被利用的程序,观察没有WebAssembly类型检查时会发生什么情况:
[email protected]:/src/clang-cfi-showcase# clang++ -Weverything -Werror -Wno-weak-vtables -o cfi_vcall_diff cfi_vcall_diff.cpp
然后运行它:
[email protected]:/src/clang-cfi-showcase# ./cfi_vcall_diff Derived::printMe 55.5 CFI Prevents this control flow 0 Evil::makeAdmin
从上面的输出中可以看出,包含漏洞的程序被利用了——攻击者的有效载荷makeAdmin被执行了。这是完全可能的,因为本机机器代码运行时,没有对函数的参数类型进行任何检查——因此,“恶意”函数会畅行无阻(尽管结果有些奇怪,因为该函数把提供给它的整数误认为是浮点数)。
小结
在本文中,我们对WebAssembly语言及其安全设计进行了简要的概述,同时给出了一个含有类型混淆漏洞的示例代码,然后,介绍了相关工具的配置方法,最后,介绍了使用WebAssembly类型检查阻止漏洞利用的例子。在下篇中,我们将为读者介绍更多精彩的内容。