HTB FlagCasino writeup - Pez1181/CTF GitHub Wiki
Challenge Type: Reversing / Pwn
Vulnerability: Predictable PRNG
Bypassed Protection: Logic check onrand()
Flag: HTB{r4nd_1s_v3ry_pr3d1ct4bl3}
Weβre given a 64-bit ELF binary called casino
.
checksec --file=casino
RELRO STACK CANARY NX PIE
Partial RELRO No canary found NX enabled PIE enabled
Key observations:
- NX is enabled, so stack shellcode is a no-go.
- No canary or full RELRO β but irrelevant for this challenge.
- PIE is enabled, but weβll handle it with runtime analysis.
The binary is not stripped, which means symbols like main
are still present β very helpful.
Hereβs the key logic decompiled from main()
:
char local_d;
uint local_c = 0;
while (local_c < 0x1d) {
scanf(" %c", &local_d);
srand((int)local_d);
int result = rand();
if (result != check[local_c]) {
exit(-2); // Wrong input: you're out
}
puts("[* CORRECT *]");
local_c++;
}
Each input character:
- Gets passed into
srand()
- The first
rand()
value is compared against a hardcoded secret in thecheck[]
array - If any
rand()
doesn't match, the game exits
The player must enter 29 "lucky" bytes to pass.
This is a classic predictable PRNG flaw.
Because srand()
is seeded from a single byte input, and we're comparing the first result of rand()
, we can brute-force each of the 29 expected values.
But first, we need the check[]
table.
Disassembling main()
showed us this:
lea rdx,[rip+0x2e63] # 0x555555558080 <check>
mov edx,DWORD PTR [rdx + rcx*1] ; loads check[local_c]
We dumped it in GDB:
x/29dw 0x555555558080
Got:
608905406, 183990277, 286129175, 128959393, 1795081523, 1322670498, 868603056, 677741240,
1127757600, 89789692, 421093279, 1127757600, 1662292864, 1633333913, 1795081523, 1819267000,
1127757600, 255697463, 1795081523, 1633333913, 677741240, 89789692, 988039572, 114810857,
1322670498, 214780621, 1473834340, 1633333913, 585743402
This is the full check[]
array β each number is what rand()
must return when seeded with your input.
To find the correct input for each check value, we brute-forced the seed:
from ctypes import CDLL
libc = CDLL("libc.so.6")
check = [
608905406, 183990277, 286129175, 128959393, 1795081523, 1322670498, 868603056, 677741240,
1127757600, 89789692, 421093279, 1127757600, 1662292864, 1633333913, 1795081523, 1819267000,
1127757600, 255697463, 1795081523, 1633333913, 677741240, 89789692, 988039572, 114810857,
1322670498, 214780621, 1473834340, 1633333913, 585743402
]
answers = []
for target in check:
for seed in range(0x00, 0x100):
libc.srand(seed)
if libc.rand() == target:
answers.append(seed)
break
else:
answers.append(None)
print("Winning input:")
print("".join(chr(c) for c in answers if c is not None))
This finds the seed (your input byte) that causes rand()
to match each check[i]
.
The script returns:
HTB{C4ching_m0ney-MoN3y_MONEEEEEY}
It's the flag, so we don't need to go any further, but feeding this into the binary:
echo 'HTB{C4ching_m0ney-MoN3y_MONEEEEEY}' | ./casino
...passes all 29 checks and prints "correct" for each one.
- This challenge is a great reminder: PRNG β security.
-
srand()
with low-entropy or guessable input makes systems trivially predictable. - Always use a cryptographically secure RNG for security-related logic (e.g.
/dev/urandom
,arc4random
, or modern crypto libs).
HTB{C4ching_m0ney-MoN3y_MONEEEEEY}