Lesser Less

CyberGame.SK cybergame sk, forensics, reverse engineering writeup covering Lesser Less with analysis, solution steps, and final recovery notes.

By 0xG10D | Last updated: May 2, 2026 | 6 minutes read
Medium #ctf#cybergame-sk#forensics#reverse-engineering#malware-analysis#boot2root

1. Challenge Overview

Challenge Name: Lesser less Points: 471 Category: Reverse Engineering / Binary Analysis Provided File: less

The challenge gives a lightweight version of the Linux less command. At first glance, it behaves like a terminal pager, but analysis shows that it contains hidden functionality that reconstructs and executes a secret command.

The goal is to reverse the binary logic and recover the hidden flag.


2. Reconnaissance and Initial Observations

First, inspect the file type:

file less

Output:

less: ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped

Important details:

64-bit ELF binary
PIE enabled
Dynamically linked
Not stripped

Because the binary is not stripped, function names are still available.

Check symbols:

nm -C less | grep -E "decode|execute|sha|TARGET|main"

Interesting output:

0000000000007020 d TARGET_HASHES
00000000000039de T decode_phrase_from_file
0000000000003be8 T execute_phrase_command
0000000000003c1f T main
00000000000036f1 t sha256_hex

This immediately reveals suspicious logic:

decode_phrase_from_file()
execute_phrase_command()
TARGET_HASHES
sha256_hex()
system()

Check strings:

strings -a less | grep -E "^[0-9a-f]{64}$"

There are exactly 40 SHA-256 hashes inside the binary.


3. Technical Analysis

The binary behaves like a fake less clone, but before entering pager mode, it performs hidden decoding.

The important function is:

decode_phrase_from_file(filename, output_buffer, output_size);

From disassembly, the logic is:

for each target_hash in TARGET_HASHES:
    for each adjacent 2-byte chunk in input_file:
        hash = sha256(chunk)

        if hash == target_hash:
            append those 2 bytes into output_buffer
            break

Then the decoded phrase is passed into:

execute_phrase_command(output_buffer);

The execute_phrase_command() function does this:

system(output_buffer);

So the binary:

  1. Reads the user-provided file.

  2. Searches for 40 required 2-byte chunks.

  3. Reconstructs an 80-byte shell command.

  4. Executes that command using system().

The hidden command is not stored directly in plaintext. Instead, each 2-byte block is protected by SHA-256.

However, this is weak because each unknown block is only 2 bytes.

Total brute-force space:

2 bytes = 16 bits = 65536 possibilities

So every hash can be cracked instantly offline.


4. Root Cause / Vulnerability

The main weakness is weak hash-based obfuscation.

The binary uses SHA-256, but only hashes 2-byte chunks. SHA-256 itself is not broken, but the input space is tiny.

This means the attacker can brute-force every possible 2-byte value:

00 00
00 01
00 02
...
ff ff

For each pair, compute SHA-256 and compare it with the target hashes.

The second issue is the use of:

system(decoded_phrase);

This creates a hidden command execution path. In this challenge, the command contains the flag inside a shell comment.


5. Exploitation Plan

The exploitation strategy is:

  1. Extract all 64-character SHA-256 hashes from the binary.

  2. Generate all possible 2-byte values.

  3. Compute SHA-256 for each 2-byte value.

  4. Match each target hash to its original 2-byte plaintext.

  5. Rebuild the hidden command in order.

  6. Extract the flag from the recovered command.

No memory corruption is needed. This is a pure reverse-engineering and brute-force recovery challenge.


6. Proof of Concept

A minimal proof is to brute-force one hash:

import hashlib

target = "1eb85f4d6a3234ce7acb8c51c75930f12e952517e2e389914a6ca8f89a881a0d"

for i in range(65536):
    pair = i.to_bytes(2, "big")
    if hashlib.sha256(pair).hexdigest() == target:
        print(pair)
        break

This proves the hashes are reversible because the original input space is only 2 bytes.

After cracking all 40 hashes, the recovered command is:

echo 'where is the flag?' > flag.txt # SK-CERT{l99k1n6_f0r_h1dd3n_func710n4l17y}

The flag is hidden after #, which makes it a shell comment.


7. Full Python Exploit / Solver

Save this as solve.py:

#!/usr/bin/env python3
import hashlib
import re
import sys
from pathlib import Path


def extract_hashes(binary_data: bytes) -> list[str]:
    """
    Extract 64-character lowercase hexadecimal SHA-256 strings from the binary.
    Duplicates are removed while preserving order.
    """
    raw_hashes = re.findall(rb"[0-9a-f]{64}", binary_data)

    hashes = []
    seen = set()

    for h in raw_hashes:
        decoded = h.decode()

        if decoded not in seen:
            seen.add(decoded)
            hashes.append(decoded)

    return hashes


def build_sha256_lookup() -> dict[str, bytes]:
    """
    Build a lookup table of:
        sha256(two_bytes) -> original two_bytes

    Since the input size is only 2 bytes, there are only 65536 possibilities.
    """
    lookup = {}

    for value in range(65536):
        pair = value.to_bytes(2, "big")
        digest = hashlib.sha256(pair).hexdigest()
        lookup[digest] = pair

    return lookup


def recover_command(target_hashes: list[str], lookup: dict[str, bytes]) -> bytes:
    """
    Recover the hidden command by resolving each target SHA-256 hash
    back to its original 2-byte chunk.
    """
    recovered = b""

    for index, target_hash in enumerate(target_hashes):
        if target_hash not in lookup:
            raise ValueError(f"Could not recover hash #{index}: {target_hash}")

        recovered += lookup[target_hash]

    return recovered


def extract_flag(command: bytes) -> str | None:
    """
    Extract the SK-CERT flag from the recovered command.
    """
    match = re.search(rb"SK-CERT\{[^}]+\}", command)

    if not match:
        return None

    return match.group().decode(errors="replace")


def main():
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} ./less")
        sys.exit(1)

    binary_path = Path(sys.argv[1])

    if not binary_path.exists():
        print(f"[-] File not found: {binary_path}")
        sys.exit(1)

    binary_data = binary_path.read_bytes()

    print("[+] Extracting SHA-256 hashes from binary...")
    target_hashes = extract_hashes(binary_data)

    if not target_hashes:
        print("[-] No SHA-256 hashes found.")
        sys.exit(1)

    print(f"[+] Found {len(target_hashes)} unique SHA-256 hashes")

    print("[+] Building 2-byte SHA-256 lookup table...")
    lookup = build_sha256_lookup()

    print("[+] Recovering hidden command...")
    command = recover_command(target_hashes, lookup)

    print("\n[+] Recovered command:")
    print(command.decode(errors="replace"))

    flag = extract_flag(command)

    if flag:
        print("\n[+] Flag recovered:")
        print(flag)
    else:
        print("\n[-] Flag pattern was not found in the recovered command.")
        sys.exit(1)


if __name__ == "__main__":
    main()

8. Running the Solver

No external Python packages are required.

python3 solve.py ./less

Expected output:

[+] Extracting SHA-256 hashes from binary...
[+] Found 40 unique SHA-256 hashes
[+] Building 2-byte SHA-256 lookup table...
[+] Recovering hidden command...

[+] Recovered command:
echo 'where is the flag?' > flag.txt # SK-CERT{l99k1n6_f0r_h1dd3n_func710n4l17y}

[+] Flag recovered:
SK-CERT{l99k1n6_f0r_h1dd3n_func710n4l17y}

9. Commands to Reproduce

Full command sequence:

chmod +x less

file less

nm -C less | grep -E "decode|execute|sha|TARGET|main"

strings -a less | grep -E "^[0-9a-f]{64}$"

python3 solve.py ./less

Optional proof that the binary executes the decoded command:

python3 - <<'PY'
cmd = b"echo 'where is the flag?' > flag.txt # SK-CERT{l99k1n6_f0r_h1dd3n_func710n4l17y}"
open("trigger.txt", "wb").write(cmd)
PY

./less trigger.txt
cat flag.txt

Output:

where is the flag?

The flag does not appear in flag.txt because it is placed after #, making it a shell comment.


10. Final Flag Extraction Explanation

The binary hides a shell command by splitting it into 40 two-byte chunks and storing only the SHA-256 hash of each chunk.

Because each chunk is only 2 bytes, we can brute-force every possible chunk and reconstruct the full command.

Recovered command:

echo 'where is the flag?' > flag.txt # SK-CERT{l99k1n6_f0r_h1dd3n_func710n4l17y}

The shell ignores everything after #, but the flag is still visible in the recovered command.

Final flag:

SK-CERT{l99k1n6_f0r_h1dd3n_func710n4l17y}

Authorized security practice only. These notes are for lab, CTF, and explicitly permitted environments.