$ cat writeup.md…
$ cat writeup.md…
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.
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.
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.
Basic triage showed the expected hardened Linux target:
Runtime behavior was simple on the surface:
enter positive integers a,b,c; one per line:a > b > cflag.txtFrom the disassembly, the important control points were:
0xfd600xfe70main near 0x491bThat 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.
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:
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.
0xfd60The 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.
0xfe70The final gate was the helper at 0xfe70.
Its behavior was roughly:
0x110b0The key acceptance fact was discovered at the main call site near 0x491b:
That meant I did not need to reconstruct every single late-stage field equally. I needed the decisive scalar quantity driving the sign.
Several approaches looked promising at first and wasted time:
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.
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 > 0So 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.
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.
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 < 1Converting that rational point back from (x, y) to integer inputs using x = a/b and y = c/b yielded the exact triple below.
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.
The practical solve workflow was:
flag.txt0xfd60F(x, y) = exp(x) - log(y)0xfe70, including its refinement behavior through 0x110b0N / DD in the valid region to reduce the problem to N = 0x = a/b, y = c/bThe 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.
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