CyberGame.sk
Lesser Less
CyberGame.SK cybergame sk, forensics, reverse engineering writeup covering Lesser Less with analysis, solution steps, and final recovery notes.
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:
-
Reads the user-provided file.
-
Searches for 40 required 2-byte chunks.
-
Reconstructs an 80-byte shell command.
-
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:
-
Extract all 64-character SHA-256 hashes from the binary.
-
Generate all possible 2-byte values.
-
Compute SHA-256 for each 2-byte value.
-
Match each target hash to its original 2-byte plaintext.
-
Rebuild the hidden command in order.
-
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}