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
1
⇔secret ≥ num
- Output
0
⇔secret < 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 submittednum
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
- If
- 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
- Define search range:
lo = 2^127
,hi = 2^128−1
.
- Query midpoints with option [1]:
- Send
mid = (lo + hi + 1) // 2
.
- If output is
1
⇒secret ≥ mid
⇒lo = mid
.
- If output is
0
⇒secret < mid
⇒hi = mid − 1
.
- Send
- After ≤128 steps,
lo == hi == secret
.
- 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.