翻译自:https://medium.com/@danielabloom/binary-exploitation-eli5-part-2-8fd71bf214b9
“ Wait a minute. Using an RX modulator, I might be able to conduct a mainframe cell direct and hack the uplink to the download.” — Hackerman, Kung Fury
本文是白话二进制漏洞攻击方式系列文章的第二部分。
在整篇文章中,我们将介绍:
0x01 - 必备知识:权限和异常处理
0x02 - 防御:Stack Canaries
0x03 - 攻击:格式字符串漏洞
0x04 - 攻击:SEH Overwrites
0x05 - 防御:DEP/NX
让我们开始吧!
随着您越来越多地使用计算机,您可能会经常听到Kernel(内核)和userland(用户态)这两个术语。如果我们尝试搜索内核的定义,我们会发现:
内核是操作系统的核心,可以完全控制系统上的所有内容(处理器,内存,芯片等)。每当程序需要做任何事情(即写入文件,打开网络连接等)时,它都会暂时将对用户空间处理的控制权交给内核。
虽然这个定义让我们对内核的内容有了很好的理解,但让我们快速看看我的第五篇文章Explain Spectre and Meltdown Like I'm 5 的解释:
每当蝙蝠侠和正义联盟的其他成员在与超级罪犯作战时,阿尔弗雷德(蝙蝠侠的管家)(内核)就会控制蝙蝠侠的基地(处理者),这样他能够提供重要信息并帮助正义联盟获胜。当正义联盟击败坏人时,蝙蝠侠解除并收回了阿尔弗雷德的控制权。
为了减轻负担,内核是操作系统为您的应用程序完成所有繁重的工作(即内存管理等)的后端。而另一方面,用户态是常规应用程序运行内存空间。就像蝙蝠侠和正义联盟一样,用户态应用程序在需要特权信息时将控制权移交给内核(阿尔弗雷德)。
这种权限分离的方式在计算机中大量使用。然而,虽然蝙蝠侠和正义联盟只有两类特权,但计算机有4个独立的“戒指”特权。
如您所见,应用程序在最低权限环中运行。但是,显然有时应用程序需要访问某些特权信息或功能(即建立网络连接,内存管理功能等)。在这种情况下,应用程序将使用称为syscalls的特定函数,它们充当不同特权环之间的桥梁。
形容特权戒指的一种简单方法可能是视频游戏中的等级:
为了提高整个游戏中的一般能力(无论是升级护甲或武器,进入不同的世界,还是最终从尖刺的乌龟身上拯救公主),玩家必须通过不同的关卡。如果玩家试图跳过一个级别,他/她就没有足够的力量或资源在他们跳到的关卡中击败BOSS! 他们会失败!
当用户态应用程序尝试访问特权信息时,应用程序将失败并出现异常。这意味着应用程序将崩溃并输出崩溃报告。除了尝试从非特权(用户空间)环境访问特权内存或函数之外,由于许多原因,程序可能都会因异常而失败。例如,如果一个程序试图将某个数据除以0(我们都知道不能这样做),程序将失败并抛出ArithmeticException的异常。
现在,虽然简单地理解程序可能失败的原因很重要,但我们需要讨论程序失败时实际发生的情况。这就是结构化异常处理(SEH)的用武之地。在我们讨论结构化异常处理程序是什么以及它们如何工作之前,让我们以夺宝奇兵为例:
在1981年的电影“夺宝奇兵之法柜奇兵”中,琼斯和他的团队继续寻找失落的约柜方舟。沿着他的路线,琼斯和他的团队遇到了许多陷阱,例如毒镖和巨石,这些陷阱是由秘密的压力板或开关触发的。琼斯和他的团队必须努力生存通过所有这些陷阱,以实现他们的目标。
在夺宝奇兵之法柜奇兵中,琼斯和他的团队必须通过整个地牢和洞穴中设置陷阱。这些陷阱是在琼斯出生前几年设计的,用来保护这次冒险的中心的金色雕像。同样,程序有各自的陷阱,从权限不匹配和访问拒绝到逻辑和算术错误,程序有很多方法可以防止(处理)错误或错误的操作。此外,程序员能够通过函数来实现自定义异常处理程序如try-catch代码块,利用try尝试代码块,再使用catch捕捉处理他们时发生的任何错误或异常。
在许多操作系统中(我们将更具体地讨论Microsoft的结构化异常处理),正好有一个系统来处理最初未被异常处理程序捕获的任何异常。也就是说,如果捕获到错误但未正确处理错误,操作系统将提升异常处理的方式将捕获的异常重定向到单独的结构化异常处理程序。如果异常继续无法正确处理,应用程序将崩溃。
另外需要注意的一点是,当程序因异常而失败时,程序会非常短暂地获得对更高级别的访问权限,因为系统会检索有关崩溃的信息。
在上一篇文章中,我们讨论了堆栈缓冲区溢出以及能够覆盖堆栈上数据的危险(或利益,取决于您在攻击中所处的角色)。多年来,为了防御这些攻击,已经在编译器和操作系统中执行了许多类型的防御。在本节中,我们将讨论Stack Canaries,它是一种非常常见且极其基本的溢出防御形式。
Stack Canaries实际上是参考the canary in a coal mine(煤矿中的金丝雀)命名的。
在1913年左右,John Scott Haldane建议在深矿中使用金丝雀或其他温血动物以检测一氧化碳。理论认为,由于鸟类的身体对一氧化碳比人类的敏感度高得多,如果矿工看到鸟儿不行了,他们就会知道空气被一氧化碳污染了,因此,在受到气体的影响之前就可以离开洞穴了。
就像煤矿中的金丝雀一样,Stack Canaries会警告程序有些事情是不对的,允许程序在任何恶意操作发生之前退出。这是通过在为局部变量分配空间之后在堆栈上放置一个长随机数并在函数返回之前检查长随机数来完成的。如果堆栈缓冲区溢出攻击覆盖堆栈的canary,程序将退出。
虽然Stack Canaries是针对溢出攻击的常用性防御,但它们远非无敌。我们接下来看看一些攻击。
击败Stack Canaries的一种简单的方法是找出它们在堆栈中的位置和随机值是什么,这样你就可以在溢出攻击期间完全覆盖Canaries。但是,正如我之前提到的,Stack Canary值是大的随机数,因此,要“知道”堆栈canary的位置就非常困难。此外,在为应用程序发掘漏洞时,您无法调试(在另一个特殊的程序中运行上述程序并在运行时通过控制其操作,显示数据和信息)这个程序并简单的查看堆栈/手动搜索Canaries。那么我们究竟如何检索这些信息呢?
好吧,这就是格式字符串漏洞的来源。在我们讨论实际漏洞之前,让我们先谈谈格式字符串是什么。
在许多编程语言中(虽然我们将在本文中专门讨论C),但有一个名为printf(或类似的东西)的输出函数。printf是用于格式化和打印数据的函数。要使用此功能,程序员输入:
printf(“格式”,变量)
例如,如果程序员想要打印出整数变量intvar的值,他们只需执行:
printf(“%i”,intvar)
在上面,%i被称为格式说明符,它只是指定要打印的变量的格式。虽然有许多有用的格式说明符,例如%s,它指定字符串值(一般文本,即“hello”,“123”等等(只要是双引号(“”)中的文本))。但是,在这种情况下,对我们来说最重要的两个格式说明符是:
%x - 十六进制值的格式说明符
%n - 一种特殊格式说明符,它允许我们将实际的数据写入变量,而不是从变量中读取数据
你们中的一些人可能知道我要说什么了,但让我们来看一个小例子:
Bob刚刚通过UHB注册了一些网上银行(代表UnHackable Bank)当Bob进入UHB网站时,他看到了“输入你的账号”提示。当Bob输入他的账号时,银行会显示他的账户信息,他可以自由查看并转移他的钱。有一天,鲍勃登录了银行的网站,但当提示“输入你的账号”时,Bob意外的输入了错误的账号。网站没有提示“不正确的帐户”,而是进入了另一个客户的帐户,并能够查看并转移这个客户的钱。
能够显示任意信息是一个巨大的安全漏洞。如果能够显示和操纵任意信息则是一个灾难性的安全漏洞,而格式化字符串漏洞正是如此。
如果用户能够直接将字符串输入到printf语句而不进行任何类型的检查,他们只需输入%x即可显示堆栈上的信息,因为正如我们从上一篇文章中所知,堆栈上存储了变量和其他数据。此外,如果恶意用户继续输入%x,最终可能会泄漏极其敏感的信息,例如异常处理程序指针(重定向进行处理错误的函数的内存地址),甚至是Stack Canaries。
此外,能够从内存中查看任意数据是一个大问题,但如果恶意用户具有对printf调用的完全访问权限,他们也可以使用%n这种格式说明符覆盖数据。这意味着恶意用户可以复制并使用自己想要的值覆盖Stack Canaries,然后执行ret2libc攻击(在第一篇文章中讨论过)或覆盖函数返回指向其他恶意payload的指针。
我们来看一个小例子:
让我们将上图分解为几个部分。
请注意:在上面的示例中堆栈被删除了,并没有显示
首先看代码:
首先,我们设置了两个字符串类型变量inputvar和stackvar 。
接下来,我们通过readuserinput函数读取用户的输入,并将所述输入存储在inputvar变量中。然后,我们使用格式说明符为%s的printf语句并传入inputvar。
上述代码中的漏洞源于用户能够无需检查格式说明符直接影响printf语句。
现在,看看上面的整个攻击图。如您所见,当用户输入常规的,非格式说明符时,输入不会发生任何特殊情况。但是,当用户输入“ %x%x%x”时,程序输出堆栈的内容(请注意:%x以十六进制格式输出堆栈的内容。在此示例中,我只使用基本文本和数字,以便它更容易理解)。此外,如果用户使用%n来覆盖堆栈的cookie和返回指针,他/她将能够将程序重定向到恶意的payload。
与ret2libc攻击非常相似,SEH(结构化异常处理程序)Overwrites是覆盖指向不同函数指针的攻击。但是,虽然ret2libc攻击基于覆盖返回指针以将程序重定向到特定的libc函数,SEH Overwrites通过覆盖异常处理程序指针函数,然后触发异常,以便程序重定向到我们恶意的payload。
你可以想象SEH溢出类似保险欺诈:
Bob拥有一艘有大量保险单的船,保单的价值远远超过了这艘船本身。Bob作为罪犯,他决定试图通过燃气泄漏把船炸飞来欺骗保险公司,这样他就可以索赔保险金。经过几周的策划,Bob炸了他的船。经过一番调查后,保险公司无法明确证明Bob的船燃气泄漏但实际上没有爆炸,因此支付了索赔。
与Bob的保险欺诈非常相似,SEH Overwrite攻击强制异常处理程序指向到恶意用户的payload,而不是正确的异常处理程序。这些攻击可以利用格式字符串漏洞或一般堆栈缓冲区溢出攻击来完成,因为异常处理程序指针也是存储在堆栈中的。要执行此漏洞,恶意代理需要覆盖异常处理程序指针以指向恶意payload,而不是实际的异常处理程序,然后用户需要触发异常。一旦捕获到该异常,它将被重定向到恶意函数,然后恶意函数可以丢弃异常并执行它的payload。
到目前为止,您可能已经意识到不受限制地访问堆栈是一件非常危险的事情。此外,即使Stack Canaries充当防御,恶意用户也可以在格式字符串漏洞的帮助下轻松地重定向程序的执行,甚至可以轻松地将恶意函数和功能添加到程序中。然而,随着我们在这个系列的进展,你会很快发现虽然有很多攻击,但也有很多防御措施。在本节中,我们将谈论Data Execution Prevention(DEP)和Non-Executable memory(NX)。
能够联想到DEP和NX的好方法可能是原来的点格棋游戏。
在点和方框中,两个玩家以点网格开始。在每个回合中,玩家可以绘制一条连接两个点的线。两个玩家会用红色(玩家一)和绿色(玩家二)在点中着色,当画线使一个正方形封闭时,在区域内画一该玩家符号,表示占领。在游戏结束时,哪个玩家占领的区域大就会获胜。
现在,想象一下你脑袋里的那个盒子(或者从wikihow看下面的图片):
我们假设每个正方形都是计算机上的内存区域,而不是点和框的网格。在红色区域是允许数据执行的,绿色区域则不允许数据执行。
这正是DEP和NX的意思。它们只是阻止在不同的内存区域中执行写入内存操作(即恶意代码)的机制。例如,大多数现代程序在堆栈上启用了DEP,因此恶意用户无法通过堆栈执行payload。
在理想的世界中,计算机可以在所有内存使用W^X(write xor execute)策略,但是有几个很大的原因无法这样做。W^X策略是一种内存可以专门写入或可以独占执行的策略,但是不能两者同时进行。由于多种原因所有存储器中部分不能按照这个策略进行工作,这些将在以后的文章中介绍。但是对于某些内存区域例如堆栈,DEP和NX的使用让使用这个策略成为可能。
在本文中,我们讨论了:
0×01.内核和特权环
0x02.异常和结构化异常处理程序
0x03.结构化异常处理程序覆盖攻击
0x04.格式化字符串和printf命令
0x05.格式字符串漏洞和利用
0x06. DEP/NX
我希望这篇文章很有帮助。请密切注意本系列的下一部分,我们将讨论更多的攻击和防御。
如果您还没有开始阅读,请阅读本系列的第一篇文章:
白话二进制漏洞攻击方式第一部分