Thinking like the designer: finding the intended flaw
The hardest CTF challenges have no memory-corruption and no injection - just a system whose logic can be bent. Threat modeling from the attacker's chair: trust boundaries, assumptions, and the questions that find a logic bug.
Some challenges have no exploit in the usual sense. No overflow, no injection, no missing auth check you can point at. The code is clean. And yet there is a way to get the flag, because the system was designed to do something and you can make it do something adjacent that the designer did not intend. These are logic bugs, and they are the hardest and most satisfying category precisely because there is no signature to grep for - you have to understand the design well enough to find where its assumptions break.
The counterintuitive tool for this is a defensive one. Threat modeling - the discipline security engineers use to find flaws in their own designs before shipping - is exactly the lens that finds logic bugs, because a logic bug is just a threat the designer failed to model. Reading a system the way its author should have is how you find what they missed.
Draw the trust boundaries
Every system has lines across which trust changes: between the browser and the server, between one service and another, between user input and a query, between an unauthenticated and an authenticated request. A trust boundary is where data crosses from something the system does not control into something it does. Bugs live on these lines, because they are where the designer had to decide what to verify - and verification is what gets forgotten.
Sketch the system as boxes (components) and arrows (data flowing between them), and mark every arrow that crosses a boundary. For each crossing, ask the load-bearing question: what does the receiving side assume about this data, and can I violate that assumption? A price sent from the client and trusted by the server, a user id in a token that is decoded but not re-checked, a filename that is validated on upload but not on retrieval - each is a boundary where an assumption is doing work it should not.
Interrogate the assumptions
A logic bug is a broken assumption, so the technique is to enumerate the assumptions and test each one. A structured way to generate them is to walk a threat taxonomy across each component. STRIDE is the classic: for each part of the system, ask whether you can Spoof identity, Tamper with data, Repudiate an action, disclose Information, deny Service, or Elevate privilege. Most CTF logic bugs are the S, T, and E of that list.
| Assumption the design makes | The question that breaks it |
|---|---|
| This value arrives in a valid range | What if it is negative? Zero? Enormous? (A negative quantity refunds money.) |
| This sequence of steps happens in order | What if I do step 3 before step 2, or step 2 twice? (Race conditions, state-machine skips.) |
| This id belongs to the requester | What if I use someone else's id? (Access control.) |
| This operation happens once | What if I fire it a hundred times in parallel? (Double-spend, TOCTOU.) |
| This input is one of the values my UI offers | What if I send a value the UI never shows? (Unlisted enum, admin state.) |
| These two checks see the same data | What if the data changes between them? (Time-of-check to time-of-use.) |
The right-hand column is a reusable attack generator. Point it at each trust-boundary crossing you drew, and the questions produce candidate exploits mechanically - which is how you find logic bugs systematically instead of hoping for inspiration.
The classes that recur
- Negative and boundary numbers. Quantities, balances, and indices that were assumed non-negative. Buying -1 of an item, transferring a negative amount, an integer that wraps.
- Order-of-operations and state machines. Reaching a checkout without paying, resetting a password you skipped a verification step for, replaying a one-time action.
- Race conditions. Two requests that each pass a check because neither has committed yet - redeeming one coupon twice, withdrawing the same balance in parallel.
- Confused identity. The system verifies you are *someone* but not that you are the *right* someone - the account-takeover flavour of access control.
- Trusting derived data. A total the client computed, a signature over fields the server does not re-check, a redirect target that was validated in the wrong place.
The method
- Map the system: components as boxes, data flows as arrows.
- Mark every arrow that crosses a trust boundary - especially anything from the client.
- For each crossing, state what the receiver assumes about the data.
- Walk STRIDE (mostly spoof, tamper, elevate) and the assumption-breaking questions against each.
- Test the candidate that breaks the most load-bearing assumption first.
- Keep the assumption list written down - the bug is the one you have not falsified yet.