Div 2

Category: Crypto / Oracle

Target: nc play.scriptsorcerers.xyz 10294

Flag: scriptCTF{b1n4ry_s34rch_u51ng_d1v1s10n?!!_a2bb991472a6}


TL;DR

The service lets us submit a 128-bit number num and returns int(secret / num) using decimal (50-digit precision). Because both secret and int(num) are 128-bit, the quotient is always 0 or 1, effectively giving us a comparison oracle:

  • Output 1secret ≥ num
  • Output 0secret < num

Do a binary search on [2^127, 2^128−1] (≤128 queries), then submit the final guess.


Challenge Code (given)

import secrets
import decimal
decimal.getcontext().prec = 50
secret =  secrets.randbelow(1 << 127) + (1 << 127) # Choose a 128 bit number
for _ in range(1000):
    print("[1] Provide a number\n[2] Guess the secret number")
    choice = int(input("Choice: "))
    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))
    if choice == 2:
        guess = int(input("Enter secret number: "))
        if guess == secret:
            print(open('flag.txt').read().strip())
        else:
            print("Incorrect!")
        exit(0)

Why the oracle works

  • secret is a 128-bit integer in [2^127, 2^128−1].
  • The check int(fl_num).bit_length() == secret.bit_length() forces our submitted num to also be 128-bit (i.e., num ≥ 2^127).
  • Therefore, secret / num ∈ (0, 2):
    • If num > secret → quotient in (0,1)int(...) = 0
    • If num ≤ secret → quotient in [1,2)int(...) = 1
  • With decimal precision = 50 significant digits and values ~1e38, the smallest positive gap near 1 is ≳ 1/2^127 ≈ 5.9e−39, far above the 1e−50 rounding threshold, so the 0/1 boundary is reliable.

Attack plan

  1. Define search range: lo = 2^127, hi = 2^128−1.
  1. Query midpoints with option [1]:
    • Send mid = (lo + hi + 1) // 2.
    • If output is 1secret ≥ midlo = mid.
    • If output is 0secret < midhi = mid − 1.
  1. After ≤128 steps, lo == hi == secret.
  1. Choose option [2] and submit the found value.

Exploit script

# requirements: pip install pwntools
from pwn import *

HOST, PORT = "play.scriptsorcerers.xyz", 10294

def main():
    r = remote(HOST, PORT)
    r.recvuntil(b"Choice: ")

    def oracle(x: int) -> int:
        r.sendline(b"1")
        r.recvuntil(b"Enter a number:")
        r.sendline(str(x).encode())
        out = r.recvuntil(b"Choice: ")
        # extract the last bare 0/1 on its own line
        lines = [ln.strip() for ln in out.splitlines()]
        vals = [ln for ln in lines if ln in (b"0", b"1")]
        if not vals:
            out2 = r.recvuntil(b"Choice: ")
            lines += [ln.strip() for ln in out2.splitlines()]
            vals = [ln for ln in lines if ln in (b"0", b"1")]
        return int(vals[-1])

    lo  = 1 << 127
    hi  = (1 << 128) - 1

    while lo < hi:
        mid = (lo + hi + 1) // 2
        res = oracle(mid)   # 1 iff secret >= mid
        if res == 1:
            lo = mid
        else:
            hi = mid - 1

    # guess
    r.sendline(b"2")
    r.recvuntil(b"Enter secret number:")
    r.sendline(str(lo).encode())

    print(r.recvall(timeout=2).decode(errors="ignore"))

if __name__ == "__main__":
    main()

Run & proof

$ python3 script.py
[+] Opening connection to play.scriptsorcerers.xyz on port 10294: Done
[+] Receiving all data: Done (57B)
[*] Closed connection to play.scriptsorcerers.xyz port 10294
 scriptCTF{b1n4ry_s34rch_u51ng_d1v1s10n?!!_a2bb991472a6}

Notes & mitigations

  • The vulnerability is exposing a comparison oracle via int(secret / num) under a constrained domain.
  • Mitigations (in real systems):
    • Do not leak comparison results; add noise/dummy operations; or rate-limit queries.
    • Avoid returning floor/rounded divisions that reveal order relations.
    • If comparison must exist, ensure it doesn’t apply to a tight bounded secret domain (or enforce coarse granularity that collapses the ordering).

Key takeaways

  • Any function that deterministically maps (secret, query) to {0,1} based on ordering is a binary search oracle.
  • Precision settings matter, but here the 50-digit decimal precision was more than enough to keep the oracle stable.
  • With 1000 allowed queries and only 128 needed, recovery is guaranteed.