nuclear_codes
umdctf
Task: a stripped PIE ELF asks for three positive integers with a > b > c and validates them through MPFR/Arb-style interval arithmetic. Solution: reverse the interval wrappers, simplify the final comparison to a rational cubic, find a rational point in the valid region via an elliptic-curve transform, and submit the resulting exact integer triple.
$ ls tags/ techniques/
nuclear_codes — UMDCTF
Description
No separate organizer description was present in the provided workspace files.
English summary: the challenge provides a stripped ELF service reachable at nc challs.umdctf.io 30301. It asks for three positive integers a, b, and c with a > b > c, performs a large amount of high-precision interval-style arithmetic, and prints the flag only if the final comparison result is negative.
Challenge summary
This solve ended up being a reverse-engineering problem that turned into a math problem.
The binary did not hide the success path: main eventually opens flag.txt. The hard part was understanding what the huge wall of helper calls was actually computing. After dynamic reversing, the repeated wrapper at 0xfd60 and the final compare helper at 0xfe70 were enough to model the computation, collapse the first decisive scalar component into a rational function, and turn the acceptance condition into finding a rational point on a cubic. A birational map to an elliptic curve produced a point in the valid region x > 1, 0 < y < 1, which converted back into the exact winning integer triple.
Recon
Basic triage showed the expected hardened Linux target:
- ELF64 PIE
- NX enabled
- stack canary present
- Full RELRO
Runtime behavior was simple on the surface:
- print
enter positive integers a,b,c; one per line: - read three decimal strings
- require all three to be positive integers
- require
a > b > c - run a very long arithmetic pipeline
- on success, open
flag.txt
From the disassembly, the important control points were:
- repeated arithmetic helper around
0xfd60 - final comparison helper around
0xfe70 - call site in
mainnear0x491b
That was already enough to focus the work. This was not a memory-corruption task and not a parser bug. The whole challenge was about reconstructing the validator.
Reverse engineering
Input handling and object layout
The program reads the three integers as strings, pads them into fixed decimal buffers, and feeds them into a family of helpers that initialize and manipulate 64-byte objects. Those objects behaved like MPFR/Arb-style high-precision interval or ball values rather than plain integers or doubles.
The main hints were:
- object sizes stayed constant at 64 bytes
- helpers repeatedly normalized and reordered endpoint-like fields
- the program sometimes refined uncertain comparisons instead of deciding immediately
- the arithmetic layer accepted enormous decimal integers without overflow concerns
So the right abstraction was: the program parses a, b, and c into interval-like high-precision numbers and then composes them through many wrappers.
Recovering the repeated wrapper at 0xfd60
The large repeated helper at 0xfd60 looked intimidating statically because of all the interval bookkeeping. The useful breakthrough came from dynamic tracing and treating it as a black-box monotone transform.
Across many traced calls, the wrapper consistently matched a high-level combine of the form:
F(x, y) = exp(x) - log(y)
That was not guessed from thin air. The interval growth, monotonicity, and repeated composition patterns all lined up with applying transcendental functions to interval inputs and then combining the resulting endpoints conservatively.
Once that model was in place, the giant validation chain became understandable: it was building nested expressions over a, b, and c using interval-safe versions of exp, log, and related arithmetic.
Understanding the final compare at 0xfe70
The final gate was the helper at 0xfe70.
Its behavior was roughly:
- take two 64-byte interval-like objects
- normalize or sort endpoint fields
- if the current enclosure is too loose, call a refinement helper at
0x110b0 - return a sign-like comparison result
The key acceptance fact was discovered at the main call site near 0x491b:
- the program accepts only when the final compare result is negative
That meant I did not need to reconstruct every single late-stage field equally. I needed the decisive scalar quantity driving the sign.
Key dead ends
Several approaches looked promising at first and wasted time:
- Direct brute force: impossible once it became clear the accepted values were astronomically large.
- Treating everything as ordinary floating-point math: this misses why the compare helper sometimes refines instead of deciding.
- Trying to reverse every helper in full detail before simplifying the expression: too slow and unnecessary.
- Looking for a parser or integer-overflow bug in the decimal input path: there was none; the validator was intentionally math-heavy.
The successful path was to recover the semantics of the recurring wrappers, identify the exact final sign test, and then simplify the decisive expression algebraically.
Mathematical derivation
After unwinding the relevant wrappers, the first scalar component of the final left-hand object simplified to:
lhs = N(a,b,c) / D(a,b,c)
with
N = -8a^3 - 7a^2b + 19a^2c + 7ab^2 + abc - 10ac^2 - 8b^2c + 8bc^2 - c^3
and
D = (a-c)(2a-b)(a+b-c)
Under the required input ordering a > b > c > 0, every factor in D is positive:
a - c > 02a - b > 0a + b - c > 0
So the sign of lhs is controlled entirely by the sign of N(a,b,c).
The exact winning condition was found by forcing:
N(a,b,c) = 0
This places the solution on a rational cubic. In practice, hitting the numerator exactly at zero was ideal because it parked the interval comparison on the acceptance boundary in the correct direction after the binary's refinement logic.
Normalizing to a cubic
Set:
x = a / b y = c / b
Then N = 0 becomes the affine cubic:
-8x^3 - 7x^2 + 19x^2 y + 7x + x y - 10x y^2 - 8y + 8y^2 - y^3 = 0
The input constraints translate to the region:
x > 1 0 < y < 1
At this stage the problem was cleanly separated from the reverse engineering: find a rational point on this cubic lying inside that region.
Solving the cubic via an elliptic-curve transform
The cubic has the rational point:
(-7, -4)
That is enough to birationally transform the cubic into an elliptic curve. After mapping to Weierstrass form, I used group-law search over rational points and then mapped candidate points back to the original (x, y) model.
Most rational points are useless because they land outside the required inequalities. The winning point was the one that satisfied:
x > 10 < y < 1
Converting that rational point back from (x, y) to integer inputs using x = a/b and y = c/b yielded the exact triple below.
Exact winning input triple
a = 20260869859883222379931520298326390700152988332214525711323500132179943287700005601210288797153868533207131302477269470450828233936557 b = 19042526617180316524139256060457587477079898033904404413796747301621619442196095529358289579194164508926431543186710461288793130962534 c = 16792202595167632657252829598515092665938697948983181195334779924033054964579874761568657321835642556483381729386998074921169204991087
This is the exact winning input triple. It satisfies a > b > c > 0, lies on the rational cubic N = 0, and passes both local and remote validation.
Final solve method
The practical solve workflow was:
- reverse the input handling and locate the success path opening
flag.txt - identify the repeated interval wrapper at
0xfd60 - model that wrapper dynamically as a monotone transcendental combine
F(x, y) = exp(x) - log(y) - recover the semantics of the final compare at
0xfe70, including its refinement behavior through0x110b0 - algebraically simplify the decisive scalar component to
N / D - use the positivity of
Din the valid region to reduce the problem toN = 0 - normalize to the cubic in
x = a/b,y = c/b - map the cubic to an elliptic curve, search rational points, and convert back to integers
- send the resulting triple to the service
Solve script
The script below submits the exact winning triple locally or remotely.
#!/usr/bin/env python3 from pwn import process, remote A = "20260869859883222379931520298326390700152988332214525711323500132179943287700005601210288797153868533207131302477269470450828233936557" B = "19042526617180316524139256060457587477079898033904404413796747301621619442196095529358289579194164508926431543186710461288793130962534" C = "16792202595167632657252829598515092665938697948983181195334779924033054964579874761568657321835642556483381729386998074921169204991087" def solve(io): io.sendlineafter(b"one per line:\n", A.encode()) io.sendline(B.encode()) io.sendline(C.encode()) print(io.recvall(timeout=5).decode(errors="replace")) if __name__ == "__main__": # Local: # io = process("./main") # Remote: io = remote("challs.umdctf.io", 30301) solve(io)
If you want a tiny verification-only snippet for the algebraic condition, this checks that the numerator is exactly zero:
#!/usr/bin/env python3 A = 20260869859883222379931520298326390700152988332214525711323500132179943287700005601210288797153868533207131302477269470450828233936557 B = 19042526617180316524139256060457587477079898033904404413796747301621619442196095529358289579194164508926431543186710461288793130962534 C = 16792202595167632657252829598515092665938697948983181195334779924033054964579874761568657321835642556483381729386998074921169204991087 N = ( -8*A**3 - 7*A**2*B + 19*A**2*C + 7*A*B**2 + A*B*C - 10*A*C**2 - 8*B**2*C + 8*B*C**2 - C**3 ) print(N)
That prints 0.
Verification
Local execution with the triple above prints the flag.
Remote execution returns:
UMDCTF{ok_i_got_the_nuclear_codes_now_where_do_i_enter_them????}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [reverse][free]roulette— umdctf
- [reverse][Pro]Basic— spbctf
- [reverse][Pro]Reverse Me— taipanbyte
- [pwn][free]ipv8— umdctf
- [reverse][free]cf madness— pingctf2026