$ cat writeup.md…
$ cat writeup.md…
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
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 keyThe script performs AES-256-CBC decryption:
ad24426047b0ffb03b679773664838462a6f00bdcaf0589dd1748e9ed5c568601edc87d974894f9dd9b98cc35535145c494eb0af84c8f78d440a033c91c7de62d506d8cabdc2a10138b95139bbe60e89
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 }
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.
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.
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
At line 133,286:
eWXtk*Oe%j5cof7Od08Gd 30 Bitcoins! , HTB{REDACTED}rand() behaviorunsigned intLD_PRELOAD you can substitute time() and make the binary generate passwords for specific timestamps$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar