# return2libc学习笔记

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2016-03-21T04:36:00Z
Canonical: https://wepostx.com/topics/143

> 乌云历史资料归档
>
> **原始作者：** 路人甲
> **原始编号：** superkieran-wooyundrops:1100
> **原始发布时间：** 2016-03-21 12:36
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

# 0x00 背景介绍

author:万抽抽
r2libc技术是一种缓冲区溢出利用技术，主要用于克服常规缓冲区溢出漏洞利用技术中面临的no stack executable限制(所以后续实验还是需要关闭系统的ASLR，以及堆栈保护)，比如PaX和ExecShield安全策略。该技术主要是通过覆盖栈帧中保存的函数返回地址(eip)，让其定位到libc库中的某个库函数(如，system等)，而不是直接定位到shellcode。然后通过在栈中精心构造该库函数的参数，以便达到类似于执行shellcode的目的。

# 0x01 技术原理

大致技术原理，如图1-1所示：

![原文图片](/media/2016/06/3f4030c35f757fbdc770408ca05178da)

图1-1 r2libc技术原理
简要概括如下：
1) 使用libc库中system函数的地址覆盖掉原本的返回地址；这样原函数返回的时候会转而调用system函数。
获取system函数的返回地址很简单，只需要使用gdb调试目标程序，在main函数下断点，程序运行中断在断点处后，使用p system命令即可:

```text
#!bash >>> p system $1 = {<text variable, no debug info>} 0xb7e56190 <__libc_system>
```
该方法可以获取任意libc函数的地址。
2) 设置system函数返回后的地址，以及为system函数构造我们预定的参数。
难点主要在第2步中system函数相关的栈帧结构的安排上，比如为什么Filler就是Return Address After system，为什么传递给system的参数紧跟在Fillter之后？这就涉及到函数的调用规则。我们知道函数调用在汇编中通过call指令实现，而函数返回则通过ret指令实现。Call指令可以实现多种方式的函数跳转，这里为了简便，暂且只考虑跳转地址在内存中的call指令的实现
CPU在执行call指令时需要进行两步操作：

- 将当前的IP(也就是函数返回地址)入栈，即：push IP;

- 跳转，即： jmp dword ptr 内存单元地址。
CPU在执行ret指令时只需要恢复IP寄存器即可，因此ret指令相当于pop IP。
因此对于正常函数调用而言，其栈帧结构如下图1-2所示：

![原文图片](/media/2016/06/b72c07ca6294cc64a84ab2e87cb5b942)

图1-2 函数调用栈帧结构图
但是由于我们使用system的函数地址替换了原本的IP寄存器，强制执行system函数，破坏了原程序的栈帧分配和释放策略，所以后续的操作必须基于这个被破坏的栈帧结构实现。

### 1.1 为何Filler是函数的返回地址
首先，解释为何Filler是函数的返回地址。这需要我们查看system函数的汇编实现。通过 gdb调试得到如下信息：

![原文图片](/media/2016/06/f11cf6a320770c17e59894341134fb34)

图1-3 system函数汇编实现
正常情况下，我们是通过call指令进行函数调用的，因此在进入到system函数之前，call指令已经通过push IP将其返回地址push到栈帧中了，所以在正常情况下ret指令pop到 IP的数据就是之前call指令push到栈帧的数据，也就是说两者是成对的。但是！！！在我们的漏洞利用中，直接通过覆盖IP地址跳转到了system函数，而并没有经过call调用，也即是没有push IP的操作，但是system函数却照常进行了ret指令的pop IP操作。那么这个ret指令pop到IP的是哪一处地址的数据呢？答案就是Filler!
在程序执行到图1-1中的Saved EIP(即system函数地址)的时候，此时的ESP指向了Filler，然后就转而执行system函数。通过分析system函数的汇编实现可以看出：该函数中对ESP的操作都是成对出现的，如push %ebx与pop %ebx等。所以当执行到ret指令时，ESP还是指向Filler，也就是说ret指令内涵的pop IP操作就是将Filler的数据pop到IP寄存器中！

### 1.2 为何传递给system的参数紧跟在Filler之后
其实通过2.1节的分析，答案已经很明了了。在正常的函数调用中，其栈帧分布如图1-2所示，但是在此漏洞利用中，由于我们强制更改了调用过程，省去了call调用的push IP步骤，因此就造成了Filler变成了EIP。但是，需要注意的是，我们也仅仅是省去了push IP这一步而已，其他步骤与正常函数调用并无区别，所以如果我们将Filler看做是保存的返回地址EIP的话，那么它“之后(相对栈增长方向而言)”的数据就自然而然变成了system函数的参数了。

### 1.3构造"/bin/sh"参数
下面唯一的问题就是如何在栈中构造”/bin/sh”参数了。当然，这里用“构造”一词并不准确，“借用”更合适——我们从内存中搜寻此字符串，然后将该字符串在内存中的起始地址赋值到Filler之后即可。那么如何获取到这个字符串的地址呢？我们知道，LINUX的SHELL环境变量的值一般就是“/bin/sh”，所以我们可以通过gdb调试器，暴力搜索%esp寄存器之后开始的n个字符串，方法如下图1-4所示：

![原文图片](/media/2016/06/b9e4bc4d3fe21e66fb0616c3edaade2e)

图1-4 GDB暴力搜索字符串
但是这种方法比较“非黑客范”，个人觉得gray hat hacking第四版中提及的方法更犀利，该方法先通过dlopen和dlsym获取system之类的函数的地址，再借助于setjmp与longjmp函数以及信号量机制，以system函数的地址为起点从前、后两个方向搜索字符串，详情见书本LAB11-5。
还有一种更简单的方法，详细步骤如下图1-5所示：

![原文图片](/media/2016/06/dd0bf7b7959550b93805a439ac9168db)

图1-5 构造“/bin/sh”参数方法三
只要我们将CHOUCHOU_SH定义为“/bin/sh”即可。可以用这种方式借助环境变量构造任意的字符串。此方法会在后文中大量使用。这里gtenv的源代码如下：

```text
#!cpp #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { char *addr; addr = getenv(argv[1]); printf("%s is located at %p\n", argv[1], addr); return 0; }
```
构造完参数之后，return to libc利用就算告一段落了，但是该方案是基于system函数实现的，且在一次攻击中只执行一个libc函数，局限性较大，另外system函数有一个致命的缺陷就是：有时候我们并不能利用它成功获取root权限。
因为system函数本质上就是通过fork一个子进程，然后该子进程通过系统自带的sh执行system的命令。而在某些系统中，在启动新进程执行sh命令的时候会将它的特权给剔除掉(如果/bin/sh指向zsh，则不会进行权限降低；如果/bin/sh指向bash则会进行权限降低)，这样我们system就无法获取root权限了。
为了解决这个问题，高手们又研发了一种更高级的攻击技术——基于libc的函数调用链攻击。

# 0x02 基于libc的函数调用链攻击原理

回顾图1-1，我们知道system函数执行后，其返回地址是Filler，因此如果我们将需要执行的第二个libc函数的地址放置到Filler上，那么不就构造了一个函数调用链么？详情见图2-1：

![原文图片](/media/2016/06/21b050da614607a414e534bb03613e53)

图2-1基于libc的函数调用链攻击原理图
前面说到，单纯通过system函数不一定能够获取到root权限，但是，如果我们在执行system函数之前，先通过setuid(0)函数将正在运行的程序的SET-UID提升至root权限，然后再通过system函数执行sh就一定能够获取root权限了(关于为什么通过setuid(0)设置SET-UID为root权限之后，system执行sh就能获取root权限，超出了本文的范畴，大家可自行google)。

# 0x03 进阶——使用Execl函数

前文提到，使用system函数必须结合setuid(0)函数才能确保获取root权限，这种处理相对麻烦，好在我们可以直接使用execl函数解决(system是通过fork子进程执行shell，而execl是直接在当前进程中执行shell)。
Execl函数的标准用法如下：Execl("/bin/sh","/bin/sh", NULL)。
这引入了一个新的问题——如何构造‘NULL’参数。因为在输入参数中是不能夹杂NULL字节的，否则会截断我们构造的攻击buffer。所以我们得采用迂回的办法——借助格式化字符串漏洞，在漏洞程序进程的堆栈中构造一个‘NULL’参数，而不是直接输入。

### 3.1 构造‘NULL’参数
格式化字符串漏洞主要是利用printf函数的format参数进行攻击，这里我们需要构造一个特定的format参数以实现在“合适的位置”写入’NULL’参数。
很自然地，我们会想到利用格式化字符串中的‘%#$n’。这里的“#”的具体数值需要根据NULL参数在stack中的offset加以确定。具体的构造方式在后文详细介绍。

### 3.2 构造libc函数调用链
根据我们的攻击逻辑，我们需要完成如下几步：

- 基于格式化字符串漏洞，利用printf函数构造execl函数的第三个参数“NULL”；

- 调用execl函数执行sh；

- 调用exit函数，退出攻击。
因此，我们需要构造如图3-1所示的stack数据：

![原文图片](/media/2016/06/a969db86c34fb7993f1f66fb86b61ccc)

图3-1 构造实施攻击的栈结构
简要概括上图的执行逻辑：

- 先通过printf(“%5$n”)函数将Addr of HERE处字节设置为NULL;

- 执行POP/RET指令，跳转到并执行execl(“/bin/sh”, “/bin/sh”, NULL)函数；

- 执行exit函数，完成攻击。
这里我们详细探讨1、2步的实现原理。
一、为什么用“%5$n”作为参数？
因为Addr of HERE的地址刚好离Addr of ‘%5$n’有5个指针的距离。根据表3-1 Blaess, Grenier, and Raynal提出的magic formula中最后一行的推导公式可以得出以上结论。
表3-1 magic formula

![原文图片](/media/2016/06/54947e98d54f9d6272cb8f7d81320b44)

公式的详细推导见：
[http://www.cgsecurity.org/Articles/SecProg/Art4/](http://www.cgsecurity.org/Articles/SecProg/Art4/)
另外，提及一点注意事项：在Linux shell中，如果使用双引号” ”包含字符串的话，就需要在’$’之前加上转义字符‘\’，如果使用单引号’ ’包含字符串的话，就不需要额外添加转义字符’\’了。详情见：
[http://lspgyy.blog.51cto.com/5264172/1282107](http://lspgyy.blog.51cto.com/5264172/1282107) (值得一看！)
二、POP/RET指令的作用
为什么它能够让程序正确无误地跳转到execl函数并执行呢？首先我们需要知道这里的POP/RET指令是指两条连续的指令：

```text
#!bash pop %ebp ret
```
结合1.1章节的分析，可以知道当执行完printf函数之后，此时的esp指针指向图3-1中的Add of ‘%5$n’处，如图3-2所示：

![原文图片](/media/2016/06/434e17bd5caedd51640d7ae909229edf)

图3-2 执行完printf函数后的ESP指针位置
现在开始执行pop %ebp指令，将Addr of ‘%5$n’放入ebp寄存器中；然后再执行ret指令(即pop IP)，将Addr of execl放入IP寄存器中，这样程序下一步就会转而执行execl函数了，而此时的ESP指针指向Addr of exit，如下图3-3所示：

![原文图片](/media/2016/06/428e3a382099b9d861ef755b7d66c70e)

图3-3 开始执行execl函数之前ESP指针位置
那么我们如何在目标进程的内存中找到这个POP/RET指令呢？这里需要借助metasploits的msfelfscan程序，假设目标程序为target，那么可以通过如下命令获取：

```text
#!bash msfelfscan –p –D target -p, --poppopret，表示Search for pop+pop+ret combinations； -D, --disasm，表示Disassemble the bytes at this address；
```
输出结果如下：

```text
#!bash 0x080484ae pop edi; pop ebp; ret 0x080484ae pop edi 0x080484af pop ebp 0x080484b0 ret
```
现在我们就成功获取了目标程序中POP/RET指令的地址了。
至此利用execl的整个攻击逻辑和关键技术点，就介绍完毕了，下面我们找个例子进行实际操作。

# 0x04 实战演练

含有缓冲区溢出漏洞的程序源代码如下：

```text
#!cpp /*vuln.c*/ #include <stdio.h> /*small buffer vuln prog*/ int main(int argc, char* argv[]) { char buffer[7]; strcpy(buffer, argv[1]); return 0; }
```
我们关闭系统的ASLR保护机制之后，再使用root用户编译：

```text
#!bash # gcc -fno-stack-protector -o vuln vuln.c
```
然后添加SET-UID：# chmod u+s vuln
然后退出root用户，切换到普通用户模式：# exit
现在我们可以开始以普通用户权限进行提权攻击了！整个攻击工作可以分为两个大的步骤：1)确定漏洞buffer的大小；2)开始构建攻击buffer。

### 4.1 确定漏洞buffer大小
为方便快速确定大小，我们可以借助metasploits提供的pattern_create.rb和pattern_offset脚本。前者用于生成特殊的输入数据，后者用于帮助确定指定数据在该输入数据中的偏移值。
pattern_create.rb的使用方式如下：

```text
#!bash Usage: pattern_create.rb length [set a] [set b] [set c]
```
这里我们使用pattern_create.rb 250生成250字节的输入数据:

```text
#!bash Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2A
```
然后使用gdb调试目标程序：

```text
#!bash [email protected] :~/security/flaws/return2libc$ gdb ./vuln -q Reading symbols from ./vuln...(no debugging symbols found)...done. >>> r Aa0……i2A(省略部分输入数据) 回车之后，gdb输出如下错误数据： Program received signal SIGSEGV, Segmentation fault. 0x61413661 in ?? () Traceback (most recent call last): File "<string>", line 701, in lines File "<string>", line 116, in run gdb.error: No function contains program counter for selected frame.
```
显然，目标程序去执行0x61413661的地址时发生错误，这个地址就是被我们使用攻击buffer覆盖后的EIP地址，现在我们使用pattern_offset.rb查找这个地址在攻击buffer中的位置：

```text
#!bash [email protected] :~/security/flaws/return2libc$ pattern_offset.rb 0x61413661 250 [*] Exact match at offset 19
```
好了，现在我们已经确定了用于覆盖的EIP的数据在攻击buffer中的偏移值为19，下面就开始精心构造攻击buffer了。

### 4.2 构建攻击buffer
从前文的分析知道，要使用execl完成基于ret2libc的root提权攻击，我们需要实现搜集如下数据信息：

- printf函数的地址，以及printf函数要使用的字符串’%5$n’的地址；

- execl函数的地址，以及该函数使用的’/bin/sh’字符串的地址；

- exit函数的地址；

- POP/RET 指令块的地址。
Printf 0xb7e63280 Execl 0xb7ecbec0 Exit 0xb7e491e0 ‘%5$n’ 0xbffff2b6 ‘/bin/sh’ 0xbffffa6e POP/RET 0x080484af Addr of NULL 0xbfffefc9+18+4*7=0xbfffeff7

获取了这些数据之后，将它们各自安插到图3-1的合适位置即可构造对应的攻击buffer。

```text
19字节overflow| 0xb7e63280 | 0x080484af | 0xbffff284 | 0xb7ecbec0 | 0xb7e491e0 | 0xbffffeea | 0xbffffeea | 0xbfffeff7
```

# 0x05 对抗ASLR

鉴于ASLR机制只是将既然stack，heap以及共享库文件的起始地址进行了随机化，而文件内部各部分数据之间的偏移值却是固定不变的，所以攻击的整体思路如下：
先泄漏出libc.so某些函数在内存中的地址，然后再利用泄漏出的函数地址根据偏移量计算出system()函数等其他函数的地址，字符串地址类似。
那么怎么才能泄露出Libc库文件的地址呢？考虑到程序本身在内存中的地址并不是随机的，所以只要将返回值设置到程序本身所处的内存段就可以了，当然，前提是我们必须获取目标系统中的libc.so库文件。Linux内存随机化分布图如下图所示：

![原文图片](/media/2016/06/0dcdf4583bc1b6f53ce0f9e9b3ccd69c)

整体攻击思路：
1.观察目标程序，查看其本身使用的、处于Libc库文件的库函数，常见的函数有write, read等；
观察方法很简单：
1) 首先使用objdump –R命令查看目标文件的动态重定位项，也就是常说的GOT表项：

![原文图片](/media/2016/06/f903f4351a151039faee9adab24eaa1c)

2) 然后使用objdump –j .glt –d命令反编译目标文件的plt表：

```text
#!bash Disassembly of section .plt: 08048300 < [email protected] >: 8048300: ff 35 04 a0 04 08 pushl 0x804a004 8048306: ff 25 08 a0 04 08 jmp *0x804a008 804830c: 00 00 add %al,(%eax) ... 08048310 < [email protected] >: 8048310: ff 25 0c a0 04 08 jmp *0x804a00c 8048316: 68 00 00 00 00 push $0x0 804831b: e9 e0 ff ff ff jmp 8048300 <_init+0x30> 08048320 < [email protected] >: 8048320: ff 25 10 a0 04 08 jmp *0x804a010 8048326: 68 08 00 00 00 push $0x8 804832b: e9 d0 ff ff ff jmp 8048300 <_init+0x30> 08048330 < [email protected] >: 8048330: ff 25 14 a0 04 08 jmp *0x804a014 8048336: 68 10 00 00 00 push $0x10 804833b: e9 c0 ff ff ff jmp 8048300 <_init+0x30> 08048340 < [email protected] >: 8048340: ff 25 18 a0 04 08 jmp *0x804a018 8048346: 68 18 00 00 00 push $0x18 804834b: e9 b0 ff ff ff jmp 8048300 <_init+0x30>
```
熟悉linux中elf文件的动态重定位机制都知道，由于linux采用懒绑定机制，所以对于GOT表中的动态重定位符号，在第一次调用的时候got表项会定位到其对应的plt表项中，再由该plt表项转到真正的函数处。第一次执行完之后，系统会将此时GOT表项的位置加以修改，直接指向真正的函数处，而不再指向之前的plt表项。
2.根据第1步的结果确定想要获取的属于Libc的某个函数的地址，这里我们使用write函数。获取方法并不难，从上面对linux动态重定位机制的介绍可以知道，我们只需要按照如下方式做就可以了：首先构造payload执行目标文件的write函数(怎么执行呢？[email protected])，[email protected]，[email protected]的write函数的地址，所以我们再使用write函数打印出该地址即可：payload构造如下：

![原文图片](/media/2016/06/ce0c0207ca976aa37a72a3b96e7473b9)

即执行write(STDOUT, &[email protected], 4)，这样就会将libc中真正的write函数地址打印到标准输出中。这里的vul_func地址可以通过objdump –d获取，当然使用IDA更方便。
3.获取了write的真正地址之后，只需要根据libc中write函数与system函数的相对偏移地址就可以计算出system函数真正的地址，计算公式如下：

```text
#!bash system_real_addr = write_real_addr – ( write_addr_in_libc – system_addr_in_libc)
```
字符串的地址获取方式类似，这里不再细说。
4.执行system(‘/bin/sh’)，payload构造如下：

![原文图片](/media/2016/06/6cca63f9c3d05fd5828dd0031f777250)

# 0x06 参考文献

- [http://stackoverflow.com/questions/1697440/difference-between-system-and-exec-in-linux](http://stackoverflow.com/questions/1697440/difference-between-system-and-exec-in-linux)

- [http://blog.sina.com.cn/s/blog_70dd16910100rdgn.html](http://blog.sina.com.cn/s/blog_70dd16910100rdgn.html)
介绍setuid的两篇好文：

- [http://hubingforever.blog.163.com/blog/static/171040579201372932033844/](http://hubingforever.blog.163.com/blog/static/171040579201372932033844/)

- [http://nba20717zcx.blog.51cto.com/343890/392063](http://nba20717zcx.blog.51cto.com/343890/392063)

- [http://www.cs.berkeley.edu/~daw/papers/setuid-usenix02.pdf](http://www.cs.berkeley.edu/~daw/papers/setuid-usenix02.pdf)

## Replies
