reversefreemedium

Wayback

HackTheBox

Two files are provided: - `V1` — ELF 64-bit LSB PIE executable (password generator, compiled from Wayback.cpp with GCC 14.2.1, not stripped) - `decrypt.py` — Python script for AES-CBC decryption with user-provided key

$ ls tags/ techniques/
prng_seed_bruteforceaes_cbc_decryptionstatic_reverse_engineeringinteger_overflow_arithmetic

Wayback — HackTheBox

Description

A man named Michael Tanz bought 30 bitcoin in 2013 and stored it in his hardware wallet. He set the password for his hardware wallet through a password generator named "V1". He remembers that his password is 20 characters long, and consisted of only alphanumeric characters and symbols. Michael however is not exactly sure of the date he generated the password - he knows it was between the 10th and the 11th of December 2013. Can you crack the password and help him recover his bitcoin?

Two files are provided:

  • V1 — ELF 64-bit LSB PIE executable (password generator, compiled from Wayback.cpp with GCC 14.2.1, not stripped)
  • decrypt.py — Python script for AES-CBC decryption with user-provided key

Analysis

decrypt.py

The script performs AES-256-CBC decryption:

  • Key — user input, left-padded with null bytes to 32 bytes
  • IV — first 16 bytes of the encrypted blob
  • Ciphertext — remaining part after IV
  • PKCS7 padding
  • Encrypted hex:
ad24426047b0ffb03b679773664838462a6f00bdcaf0589dd1748e9ed5c568601edc87d974894f9dd9b98cc35535145c494eb0af84c8f78d440a033c91c7de62d506d8cabdc2a10138b95139bbe60e89

Reversing the V1 binary

Static analysis of the ELF binary revealed a password generator with the following algorithm:

Function: generate_password(int length, bool include_sym, bool include_num)

Character set (72 characters):

abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*_+0123456789

Formed by concatenation: lowercase + uppercase + symbols (if enabled) + digits (if enabled).

Seed calculation — from time(NULL)localtime(), then a custom formula with 32-bit arithmetic and overflow:

struct tm* tm = localtime(&t); int seed = tm->tm_mday * 1000000 + tm->tm_min * 100 + tm->tm_sec + tm->tm_hour * 10000 + (tm->tm_year + 1900) * 0x540BE400 // year * 1,410,065,408 + (tm->tm_mon + 1) * 0x5F5E100; // month(1-12) * 100,000,000 srand((unsigned)seed);

Password generation loop:

for (int i = 0; i < length; i++) { int r = rand(); password[i] = charset[r % charset_len]; // charset_len = 72 }

Vulnerability

The PRNG seed is fully deterministic based on timestamp. Knowing the approximate date range (December 10-11, 2013), we get only 172,800 possible seeds (2 days × 24 hours × 60 minutes × 60 seconds).

Critical nuance: rand() in glibc and macOS are implemented differently. Bruteforce MUST be executed on Linux (glibc), otherwise the generated passwords will be incorrect.

Solution

Step 1: Bruteforce all possible seeds

C program to generate all candidates, compiled and run in a Docker container with glibc:

#include <stdio.h> #include <stdlib.h> #include <string.h> const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*_+0123456789"; int main() { int charset_len = strlen(charset); // 72 int year = 2013; int month = 12; for (int day = 10; day <= 11; day++) { for (int hour = 0; hour < 24; hour++) { for (int min = 0; min < 60; min++) { for (int sec = 0; sec < 60; sec++) { unsigned int seed = (unsigned int)( day * 1000000 + min * 100 + sec + hour * 10000 + year * (unsigned int)0x540BE400 + month * (unsigned int)0x5F5E100 ); srand(seed); char password[21]; for (int i = 0; i < 20; i++) { password[i] = charset[rand() % charset_len]; } password[20] = '\0'; printf("%s\n", password); } } } } return 0; }

Compilation and execution via Docker:

docker run --rm -v $(pwd):/work -w /work gcc:14 bash -c \ "gcc -O2 -o bruteforce bruteforce.c && ./bruteforce > passwords.txt"

Result: 172,800 candidate passwords.

Step 2: Decrypt with each candidate

Python script iterates through all passwords, attempting AES-CBC decryption with PKCS7 unpadding as an oracle:

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import padding encrypted_message = bytes.fromhex( 'ad24426047b0ffb03b679773664838462a6f00bdcaf0589dd1748e9ed5c568601edc87d974894f9dd9b98cc35535145c494eb0af84c8f78d440a033c91c7de62d506d8cabdc2a10138b95139bbe60e89' ) with open("passwords.txt", "r") as f: for i, line in enumerate(f): password = line.strip() try: key = password.encode().ljust(32, b'\x00') iv = encrypted_message[:16] ciphertext = encrypted_message[16:] cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) decryptor = cipher.decryptor() decrypted_padded = decryptor.update(ciphertext) + decryptor.finalize() unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() decrypted = unpadder.update(decrypted_padded) + unpadder.finalize() print(f"FOUND! Password: {password}") print(f"Decrypted: {decrypted.decode()}") break except Exception: continue

Result

At line 133,286:

  • Password: eWXtk*Oe%j5cof7Od08G
  • Decrypted message: d 30 Bitcoins! , HTB{T1me_0n_the_B1t5_1386784885}
  • Timestamp: 1386784885 = Dec 11, 2013 13:01:25 UTC

Lessons

  1. glibc vs macOS rand() — critical difference. Same seed produces different sequences. Always use Docker with Linux to reproduce glibc rand() behavior
  2. 32-bit overflow in seed — the formula uses multiplication by large constants (0x540BE400, 0x5F5E100), causing integer overflow. Need to exactly reproduce the arithmetic with unsigned int
  3. PKCS7 padding as oracle — with wrong key, AES-CBC decryption produces garbage, and PKCS7 unpadding fails with an error. This allows automatic filtering of incorrect passwords
  4. 172K search space is trivial — even in Python, bruteforce takes seconds. Time-based seeds from dates give a very small search space
  5. Static reversing is sufficient — no need to run the binary, just understand the seed algorithm and generation through Ghidra/radare2

Alternative Approaches

  • Running V1 with time() substitution — via LD_PRELOAD you can substitute time() and make the binary generate passwords for specific timestamps
  • Angr/symbolic execution — automatic analysis of the seed formula
  • Hashcat/John — if there was a password hash instead of AES encryption

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups