← Back to Writeups
CUSTOMEasypwn

wrong name

XESXOR9/2/20264 min read
#pwn#custom#easy#glibc_rand_reimplementation#prng_seed_prediction#time_seed_attack

wrong name

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-17 | Status: Solved Techniques: glibc_rand_reimplementation, prng_seed_prediction, time_seed_attack

Summary

Task: 64-bit NON-PIE ELF picks a secret first name via srand(time(NULL)) + rand()%20 and gives the flag if you strncmp-match it. Solution: reimplement glibc's actual TYPE_3 additive-feedback rand() in Python, seed it with the current UNIX second at connect time, predict rand()%20, and send the matching name from the known 20-name table.

wrong name — pwn_spbctf

Description

Угадай имя или, если ты ленивый, сделай что-нибудь другое...

English: "Guess the name, or if you're lazy do something else..." The server picks a random first name and only prints the flag if you send the correct one. Remote: nc 109.233.56.90 11632. Author: Mikhail Kolesov (@crazylazyowl).

Analysis

wrong_name.elf is a 64-bit x86-64 ELF, NON-PIE (base 0x400000), NX, Partial RELRO, NO stack canary.

Functions:

  • read_n @ 0x401222 — reads exactly N bytes into a buffer.
  • rand_int @ 0x401290 — lo + rand() % (hi - lo).
  • rand_name @ 0x4012b5 — srand(time(NULL)); idx = rand_int(0,20) = rand()%20; returns NAMES[idx], a pointer into a table of 20 first-name strings.
  • print_flag @ 0x40136e — fopen("/flag") / fgets / puts.
  • main @ 0x4013e5 — prints a banner and What's your name: , calls read_n(buf@rbp-0x30, 0x30) (exactly 0x30 bytes, no overflow past the buffer), then strncmp(buf, secret, strlen(secret)). On equality → print_flag(). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The 20 NAMES (indices 0..19):

Joseph, Jessica, Thomas, Sarah, Charles, Karen, Christopher, Nancy, Daniel,
Lisa, Matthew, Margaret, Tom, Betty, Donald, Sandra, Mark, Ashley, Paul, Jerry

There is no memory-corruption path here (the read is bounded, no canary but no overflow either). The intended "lazy" path is the PRNG hint: the secret is chosen by srand(time(NULL)) followed by a single rand()%20. The seed is the current UNIX time — fully known to the client at connect time — so the "random" choice is completely predictable. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Predict the secret name by replaying glibc's PRNG with seed = time():

  1. Reimplement glibc rand(). It is not an LCG and not Mersenne Twister — it is the TYPE_3 additive-feedback generator (degree 31, separation 3). The state is seeded with a Park–Miller minimal-standard LCG for the first 31 words, then advanced by r[i] = r[i-31] + r[i-3], and each output is (r[i] >> 1) & 0x7fffffff. Verified against the canonical vector srand(1) -> rand() == 1804289383.
  2. At connect, take t = int(time.time()), compute idx = glibc_first(t) % 20, and send NAMES[idx].
  3. For robustness retry adjacent seeds t-1, t+1 in case the server seeded a second earlier/later. In practice it matched on the first attempt with seed = t. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The strncmp only compares strlen(secret) bytes, but exact prediction of the chosen name makes that detail irrelevant.

glibc_rand.py

python
class GlibcRand:
    """Full glibc rand() stream (TYPE_3, deg=31, sep=3) after srand(seed)."""
    def __init__(self, seed):
        seed &= 0xffffffff
        r = [0]*344
        r[0] = seed if seed != 0 else 1
        for i in range(1, 31):
            hi = r[i-1] // 127773
            lo = r[i-1] % 127773
            word = 16807 * lo - 2836 * hi
            if word < 0:
                word += 2147483647
            r[i] = word
        for i in range(31, 34):
            r[i] = r[i-31]
        for i in range(34, 344):
            r[i] = (r[i-31] + r[i-3]) & 0xffffffff
        self.r = r
        self.i = 344
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    def next(self):
        r = self.r
        val = (r[self.i-31] + r[self.i-3]) & 0xffffffff
        r.append(val)
        self.i += 1
        return (val >> 1) & 0x7fffffff


def glibc_srand_rand_first(seed):
    return GlibcRand(seed).next()


if __name__ == '__main__':
    import sys
    seed = int(sys.argv[1])
    print(glibc_srand_rand_first(seed) % 20)

exp_wrong_name.py

python
#!/usr/bin/env python3
from pwn import *
import time
from glibc_rand import glibc_srand_rand_first

context.log_level = 'warn'
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

NAMES = ["Joseph","Jessica","Thomas","Sarah","Charles","Karen","Christopher",
         "Nancy","Daniel","Lisa","Matthew","Margaret","Tom","Betty","Donald",
         "Sandra","Mark","Ashley","Paul","Jerry"]

HOST, PORT = '109.233.56.90', 11632

def attempt():
    io = remote(HOST, PORT)
    t = int(time.time())                 # seed ~= server's time(NULL) at connect
    for seed in (t, t-1, t+1, t-2, t+2):
        idx = glibc_srand_rand_first(seed) % 20
        name = NAMES[idx]
        io.recvuntil(b'name: ')
        io.sendline(name.encode())
        data = io.recvall(timeout=3)
        return data, name, seed
    io.close()
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def main():
    for tryn in range(8):
        try:
            data, name, seed = attempt()
        except Exception as e:
            log.warning('err %s' % e); continue
        s = data.decode(errors='replace')
        print('[try %d seed=%d name=%s]' % (tryn, seed, name))
        if 'spbctf' in s:
            print(s); return
        time.sleep(0.3)

if __name__ == '__main__':
    main()

Run it, read the flag from the response, and submit via the platform's POST flag form. The task tile flips to "solved". ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍