HTB FlagCasino writeup - Pez1181/CTF GitHub Wiki

🎰 Flagcasino - HTB CTF Writeup

Challenge Type: Reversing / Pwn
Vulnerability: Predictable PRNG
Bypassed Protection: Logic check on rand()
Flag: HTB{r4nd_1s_v3ry_pr3d1ct4bl3}


πŸ•΅οΈ Recon

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.


πŸ“– Vulnerability Analysis

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 the check[] array
  • If any rand() doesn't match, the game exits

The player must enter 29 "lucky" bytes to pass.


πŸ”Ž Strategy: Reverse the PRNG

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.


🧼 Dumping 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.


🧠 Exploit Script

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].


🧾 Output

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.


πŸ”š Final Notes

  • 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).

🏁 Flag

HTB{C4ching_m0ney-MoN3y_MONEEEEEY}

⚠️ **GitHub.com Fallback** ⚠️