Version: 0.99

帮长者处理的一篇文章,对翻译质量很不满意,受制于水平有限,只有日后再来填坑了。
Kyr1os正在翻译这篇文章,目前已经基本完工了。只剩下一些主观的后记内容没有翻。
当前版本的翻译还存在一些问题,因为有些部分我对原作者意思的理解可能出了一些偏差。
如果对某些翻译有不同的看法欢迎在评论区指正。
万分感谢。

原文链接

http://phrack.org/issues/67/9.html

0.介绍

现在 ,Windows CRT会默认禁用%n [0]。并且glibc的FORTIFY_SOURCE补丁提供了保护机制使漏洞不可能被利用。
虽然是格式字符串并不是非常少见,它们的威胁等级已经被降级到了infoleak。但是,格式字符串的伟大之处在于它们提供了读写权限。他们是漏洞利用的“spork”(既能当勺子也能当叉子的餐具)。 ASLR?PIE? NX?对格式化字符串漏洞来说这些都不是问题。

2000年左右,每个人都在寻找格式字符串。因为当时一切都很脆弱。查看链接中的TESO文章,那里面记录了一些现在看来很荒谬的事情。比如用locales就可以在内核上随便乱打。 [1]。但到了今天,那些日子已经过去太久了。除非你是攻击教育网站,才能用locale bug来弹出PMOS技术的root shell。

几个月前,一些有趣的事发生了。一个名叫Ronald Volgers[2]的人利用CUPS lppasswd在Ubuntu和Debian中发送了root setuid来提权。骚操作!Locale bugs,666!

不幸的是,最开始提到的补丁使得格式化字符串的利用变得不太可能。

详细的说,FORTIFY_SOURCE提供了两种对抗格式化字符串的机制。

  1)包含%n的格式化字符串不能位于程序内存中的可写地址。

  2)当使用位置参数时,必须使用范围内的所有参数。所以如果要使用%7$x,你必须同时使用1,2,3,4,5和6。

但是,FORTIFY_SOURCE补丁并没有真正完工,(-
你问我为什么?因为glibc的代码真的很奇怪,令我惊讶的是,有些人哪怕在世界各地旅行的时候都会因为glibc而倍受好评,尽管那时他可能并没有在写glibc。实际上,任何有身份的人都不会向大众承认自己写了glibc的一部分内容,不要误会,glibc让我的电脑跑的很好。但是这代码难看到如果这是你女票你都不会向父母介绍她,如果你知道我在说什么的话。(Kyr1os: 灵魂翻译上线)

你接下来要阅读的内容比写格式字符串要困难得多,但是比写glibc本身要容易一些。 (由于编译困难,Glibc二进制文件非常适合ELF VX)。阅读本文的前提是理解格式字符串的利用。如果你需要复习一下格式化字符串,你可以在这里[3]找到相关资料。如果你从来没有使用过格式字符串,请通过“rebel”来寻找文章[6]。

所以让我们开始吧。

1.Glibc的FORTIFY_SOURCE


警告:本文的剩余部分含有的GLIBC代码可能引起读者的瘙痒,呕吐,黑痣或永久性视力损失
所有被压过的GLIBC代码由原作者编写。


“%49150u %4849$hn %1$*269158540$x %1$*13996$x %1073741824$d”

你以前看过一个这样的格式字符串吗? 这样写就使位置参数看起来不怎么显眼对吧?

所以为什么格式化字符串变成了这个鬼样子?

对于一个编译时带了’-D_FORTIFY_SOURCE = 2’,并使用至少为-O2的优化级别的二进制文件来说。 这可能是因为编译器通过补丁实现的。

此时会有以下情况。

首先,对printf等的调用在你编译的二进制文件中被更换成__*_ chk,第一个参数是flag:在这个例子里flag是1。

A.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

这个函数将FILE *结构体中的_IO_FLAGS2_FORTIFY位设置为ON以启用FORTIFY检查。 这是个聪明的办法,因为当进入危险函数时,_IO_FLAGS2_FORTIFY位将始终被切换。禁用这个机制是件比较麻烦的事情。 但归根结底这还并不能保证任何形式的安全。

在libio / libio.h下,定义了以下辅助标志:

禁用整个标志缓冲区不应该太麻烦,但如果文件指针在mode参数中用’m’打开,可能会导致一些冲突。

精明的读者会想了解vsnprintf这样不需要文件指针的函数。OK,glibc是这样操作的。 文件指针在堆栈上的通过回调函数写入缓冲区而不是文件描述符。 然后这个文件指针会被传给vfprintf。

现在,在使用_IO_FLAGS2_FORTIFY位的情况下,我们启用了两个保护。

B. Protection #1
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

如果包含%n的格式字符串payload位于堆栈或BSS段或DATA段的可写区域中,则此补丁就会报错。除了DoS之外,这个补丁会使格式化字符串变得非常安全。

C. Protection #2
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

这个第二个补丁的效果是默认情况下所有的arg_types都设置为-1。 如果有参数未被处理,它们将保留为-1。 所以相当于:

%4$x

会无效但是

%4$x %2$x %1$x %3x

就是可以的。

说实话,我真的不认为这是一个巨大的安全改进,它带来麻烦可能还要更多一点。 它并没有真正阻止很多混乱。也许他们想防止人们利用8个字符的格式字符串,因为这些字符串在真实场景中是非常常见的。

2.绕过FORTIFY_SOURCE

现在,如果你注意到了,在1-A中的alloca函数片段里有:nargs:variable,这是计算出的最大参数数。 如果fmt str有简单的参数,那么该值就是这个数字。 但是,如果fmt str使用宽度参数或位置参数(通常在其他格式字符串文章中称为直接参数),那么这些参数也被归于最大值:nargs:value

就像下面这个示例字符串
%x %x %x %13981938$x

13981938是:nargs:value传给1-C中的alloca函数

别太激动了 这对于通用控制是不够的。 不幸的是,我们不能像[4]中那样进行相同的堆栈移位,因为我们处于初始堆栈帧分配的上下文中。在函数的结尾处,基址寄存器将被用来折叠堆栈,从而减少伴随堆栈移位而产生的内存浪费。
对许多架构的C编译器也是如此。 他们几乎都用基址寄存器来实现一些简单的堆栈清理,所以alloca函数本身很难攻击。

我们使用必须利用的分配的内存的操作取而代之。比如整数溢出可用于触发各种内存侵入。

另一件事要做的事是使用alloca将栈转移到堆中,这因为这些memset操作而变得有些困难。

但我们确实有失去的状态。但新的机会总会从状态的损失中出现。我们在未定义的地方。嘿爸爸妈妈我们在这儿!(Kyr1os: 啥玩意儿啊?咋回事儿啊?这可咋翻啊?)

本文将使用一个这样的侵入机会绕过FORTIFY_SOURCE。也许会指出其他的存在,但可能比这个更难用。

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

任意4字节NUL覆盖。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
File: stdio-common/vfprintf.c

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

关键代码:
args_type [ATTACKER_OFFSET] = 0x00000000;

说明:
需要上述代码来处理传入的宽度参数
参数。

例如:

        %1$*269096872$ X

有效利用:
         //specs[cnt].width_arg = 269096872
         args_type [specs [cnt] .width_arg] = 0

..如果您的堆栈设置正确就可以关闭stdout FILE结构体中的_IO_FLAGS2_FORTIFY位。

请记住:args_type:是在1-C上面的alloca片段中分配给堆栈的缓冲区之一,是一个整数类型的数组。

这里要当心,当nargs设置的很大时,alloca会出问题。这可能会将堆栈移动到未映射的空间。在此之后下一个push或call将导致崩溃,导致无法正常运行。

所以对:nargs:的关键限制如下:

1)堆栈必须移动到某处正确的位置
2)memset不能开到未映射的页。

满足这两个约束的最简单的方法是使用整数溢出。
由于没有限制检查发生在:nargs:,你可以人为地将堆栈保持在这样的位置:%1073741824 $。没有说明符,你可以用$符号结束它。第二个约定也是满足的,因为memset的调用的参数为0。

如果您查看程序,那么allocas的详细信息会更复杂一些。为了我们的目的,他们大致上做:

当截断为32位整数时,1073471824 * 4恰好为0。 1073471824的这个和其他因素证明是足够用于侧重分配限制。

这可以得到任意的4字节NUL覆盖。


我们可以通过清除文件流指针中的IO_FLAGS2_FORTIFY位来禁用第一个补丁(1-B)。通常它将是文件流指针上启用的唯一标志。在另外一个bit例如_IO_FLAGS2_MMAP被设置的情况下,当文件流指针关闭时可能会出现一些冲突。这可能影响可利用性。

现在我们将重新讨论补丁的第二部分。它使格式字符串漏洞的利用变得很难受。在发现参数中的“漏洞”时,nargs上的循环必须尽早终止以避免断言(assert)和libc_fatal。对于漏洞,像(1-C)中的代码,它检查:args_type:值是否为-1。请记住,fortify source补丁程序不会让您访问%5$x而不访问%1$? %2$? %3$? 和 %4?.在这种情况下,“漏洞”就是这个意思。

如果堆栈正确对齐,那么该循环的终止可能巧合地发生。当:nargs:设置为0时,循环会超出alloca创建的缓冲区的范围,并自行终止,前提是:nargs:由编译器存储在栈中。如果它没有做到这一点,一个assert()statemet将被触发,防止利用。

或者,我们可以重复使用4个字节NUL写入,可以用来稳妥地绕过循环

一个成功的绕过实例然后可以由两个简单的步骤执行。

  1.关闭IO_FORTIFY_SOURCE位以允许来自可写地址的%n
  2.设置nargs = 0跳过填充值循环。

请注意,通过#2绕过循环需要我们进一步挖掘堆栈以找到我们的用户输入,因为这个循环负责填充args_value数组。 如果你曾试图通过截断一个指针并重用它作为glibc上的目标来利用一个格式字符串,那么你可能因为那个args_value数组而失败了。(Kyr1os: 也许我的理解出了一些偏差,这里并没有理解的很清楚)

3.exp

在标准的phrack风格中,我们将首先在测试二进制文件上做这个,然后用真实世界的二进制文件来反驳任何对学术倾向的指责,比如思想实验。

A.虚拟测试程序

注意:ASLR被禁用,程序有一个可执行的堆栈。

哦不,可怕的格式化字符串保护正在使一切受到伤害。

ENABLE POWER MORPHING LINUX SHARING COMMUNITY POWER


好的让我们记住下面的过程。
1.禁用fortify source
2.设置nargs = 0
3.享受%n

首先,让我们弄清楚在我们的系统中,任意4字节NUL写入的位置。 我们会选择一些荒谬的目标,如%1$*269168516$。 如果它不会崩溃,继续增加约20000。

所以我们会发送以下作为我们的测试payload。 第一部分应该触发写入NUL。 第二部分保持堆栈。

%1$*269168516$x %1073741824$

fortify source bit将位于s = 0x29f4e0处的文件流指针的内部。

     stdout->_flags2 |= _IO_FLAGS2_FORTIFY;

在这个目标机器上,恰好是@ + 60

0x29f51c <_IO_2_1_stdout_+60>: 0x00000004

由于这里的操作是相对取址,ASLR不是太大的问题,只要你找到偏移量(YMMV)。

这是等式
$ecx + $eax*4 should = 0x29f51c

(gdb) p/d ((0x10029f51c-$ecx) & 0xffffffff)/4
$11 = 269145003

从0开始计数,所以给payload加1。

p/d (0xbffeadcc-$ecx)/4+1
= 472 for me

同时禁用 :nargs: 和 fortify source : [%*472$ %1$*269145004$%1073741824$]

OK,这不是真的。 这取决于你的缓冲区看起来像什么。
例如,如果您尝试执行:
%49150u %4847$hn %*472$ %1$*269145004$ %1073741824$

前两个参数将导致堆栈移位,必须根据$hn偏移量的大小重新计算值。这比较麻烦但还是必须要做。 接下来的任务是想办法劫持控制流。

在写入%n之后,一些东西将被马上释放掉。

我们可以覆盖指向栈指针的高16位。
(0x001b->0xbfff).
写入地址: 0x29f01a

传出这个值的一种方法是使用命令行参数。

%49150u %4847$hn %13996$ %1$269158528$ %1073741824

所以在经过一番操作之后,你会得到一个很大的改进就是通过工具或紧密映射堆栈来自动化。

B.真实环境中的利用

CUPS locale()漏洞。

Ronald Volgers注意到lppasswd使用了用户指定的语言环境。 有几个发行版(debian,ubuntu,fedora?)发行了lppasswd setuid root。 这简直棒死了。

要利用它,只需将LOCALEDIR导出到$LOCALEDIR/C/cups_C.po为lppasswd中的各种printfs保存格式化字符串的位置。

这个漏洞的利用存在某些限制。首先,它不是交互的。也就是说,格式字符串不能用于信息泄露来绕过ASLR。 第二个限制是lppasswd创建一个LOCK文件,所以任何利用必须是高度可靠的。幸运的是,第二个限制可以用资源限制来绕过。

测试程序和lppasswd之间还有一个重要的区别。 libcups中的漏洞由vsnprintf触发。
从内部来看,vsnprintf在堆栈上创建一个伪文件流指针,然后将其传递给vfprintf。
文件流指针是glibc在堆栈上分配的格式字符串结构的固定偏移量,这对绕过ASLR是有帮助的。

libcups中有漏洞的函数如下所示。

禁用ASLR后,最好的选择是停在返回地址之后。 在vfprintf的调用栈中:

第二个返回地址非常适合利用。 第二个参数指向用户输入。 覆盖保存的返回地址时,这非常有用。
地址可以指向&system或__libc_system或do_system,旧的第二个参数将成为系统的参数。

在上面的资源限制设置代码中,环境充满了指向返回地址的指针:: int jmp = 0xbffdc66c;

第一部分执行一个命令。 接下来的4个参数是填充,但删除它们需要重新计算一些东西。

这两个写操作会通过覆盖保存的返回地址的最低和最高有效半字节来把控制流重定向到系统。

%14263u %7352$hn %27249u %7353$hn

最后一个部分用来绕过FORTIFY_SOURCE:
%1$*14951$x %1$*14620$x %1073741824$.

总的来说这是个比较麻烦的事情。

//C.TODO- ASLR

//D.Afterword

Reference

[0] http://msdn.microsoft.com/en-us/library/ms175782.aspx
[1] http://www.securityfocus.com/bid/1634
[2] http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-0393
[3] http://www.phrack.org/issues.html?issue=59&id=7&mode=txt
[4] http://www.phrack.org/issues.html?issue=63&id=14
[5] http://althing.cs.dartmouth.edu/local/formats-teso.html
[6] http://www.loko.nu/formatstring/format_string.htm