Skip to content
All posts
miscrevised January 9, 20265 min read

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

  1. browser

    entirely under the attacker's control

  2. API

    the boundary - everything is checked here or nowhere

  3. service

    assumes the API validated it

  4. database

    assumes everything upstream was honest

Every design review starts by drawing this and asking one question per arrow: what does the thing on the right believe about what arrives from the left, and who guaranteed it?

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 makesThe question that breaks it
This value arrives in a valid rangeWhat if it is negative? Zero? Enormous? (A negative quantity refunds money.)
This sequence of steps happens in orderWhat if I do step 3 before step 2, or step 2 twice? (Race conditions, state-machine skips.)
This id belongs to the requesterWhat if I use someone else's id? (Access control.)
This operation happens onceWhat if I fire it a hundred times in parallel? (Double-spend, TOCTOU.)
This input is one of the values my UI offersWhat if I send a value the UI never shows? (Unlisted enum, admin state.)
These two checks see the same dataWhat 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.
  • Instructions arriving as data. Anything that reads attacker-controlled text and then acts on it, which is exactly the boundary prompt injection crosses - and, where the state is money, the boundary smart contract challenges are built around.

The method

  1. Map the system

    Components as boxes, data flows as arrows. On paper, badly, in two minutes.

  2. Mark the crossings

    Every arrow that crosses a trust boundary - especially anything coming from the client.

  3. State the assumption

    For each crossing: what does the receiver believe about this data?

  4. Walk STRIDE

    Mostly spoof, tamper, elevate - against each assumption in turn.

  5. Test the load-bearing one first

    The candidate that breaks the most of the design if it is false.

  6. Keep the list

    The bug is the assumption you have not falsified yet.