ScriptCTF 2025 Writeups Series – Div 2

Category: Misc

scriptCTF is a 48-hour jeopardy style CTF for hackers to test their skills against creative and innovative CTF challenges. We have mostly beginner friendly challenges, with a few hard ones. scriptCTF makes CTFs fun and approachable for all skill levels! This is hosted by ScriptSorcerers and some members of n00bzUnit3d (as a replacement of n00bzCTF).

When we examine the chall.py file given to us by the challenge, the goal is to find a secret 128-bit number. A flag is also displayed when guessed correctly.

Code Analysis

secret = secrets.randbelow(1 << 127) + (1 << 127)
  • Here, a 128-bit secret number is chosen.
  • 1 << 127 = 2^127.
  • randbelow (1 << 127) = [0, 2^127 − 1] a random number between them.
  • When added, the number is in the range [2^127, 2^128 − 1] → a number exactly 128 bits long.

A structure that works like an infinite loop:

for _ in range(1000):
    print("[1] Provide a number\n[2] Guess the secret number")
    choice = int(input("Choice: "))

The user is presented with two options:

  1. Enter a number, and the program will return a hint.
  2. Try to guess the secret number.

Option 1: Give a number

if choice == 1:
    num = input('Enter a number: ')
    fl_num = decimal.Decimal(num)
    assert int(fl_num).bit_length() == secret.bit_length()
    div = secret / fl_num
    print(int(div))
  • A number is received from the user (num). It is processed like a high-precision float with a decimal.
  • assert int(fl_num).bit_length() == secret.bit_length():
  • The bit length (approximate size) of the number provided by the user must be the same as the secret. In other words, the number must also be 128-bit.
  • div = secret / fl_num → the secret number is divided by the user’s number.
  • print(int(div)) → the integer part of the division is given to the user.

This actually gives you information about the secret.

  • If num < secret → partition ≥ 1 will be output.
  • If num ≈ secret → partition ≈ 1 will be output.
  • If num > secret → partition 0 will be output.

And if you choose the numbers wisely, you can gradually narrow down the secret.

Option 2: Guess

if choice == 2:
    guess = int(input("Enter secret number: "))
    if guess == secret:
        print(open('flag.txt').read().strip())
    else:
        print("Incorrect!")
    exit(0)
  • It takes the user’s secret guess.
  • If correct, it displays the contents of flag.txt.
  • If incorrect, it displays “Incorrect!” and exits.

Solution Hypothesis

A 128-bit secret number is given. Direct guessing is impossible (2^128 possibilities). But thanks to the division oracle function, we can narrow down the secret by using the secret / num information.

This is solved using logic similar to binary search:

  • If we give numbers like 2^127 as num,
  • We learn the range of the secret from the resulting quotient.
  • By narrowing down the range, we finally find the secret.

Code Analysis

In this solving code,

  • Establishing a connection → The script either connects to the remote server (--host, --port) or runs the binary locally (--bin). It reads the output from the menu and sends commands.
  • Oracle query → A 128-bit number is sent with the [1] Provide a number option. The returned result is int(secret / num) → This can only be 0 or 1.
    • 1 → Our number is less than/equal to the secret.
    • 0 → Our number is greater than the secret.
  • Binary search → Initially, the secret’s range is [2^127, 2^128 - 1].
    • The middle number (mid) is selected and requested from Oracle.
    • The range is narrowed down based on the result.
    • This process reaches the exact secret in ~128 steps.
  • Receiving a flag → The found secret is sent with the [2] Guess the secret number option, and if true, a flag is returned.

And, access the flag by running the code with this command

python oracle_bs.py --host play.scriptsorcerers.xyz --port 10040 --verbose

FLAG: scriptCTF{b1n4ry_s34rch_u51ng_d1v1s10n?!!}