r2uwu2s-resort
Last updated
Was this helpful?
Last updated
Was this helpful?
r2uwu2s-resort
was the easy pwn challenge from this year's diceCTF. While I didn't look much at this challenge during the CTF (as it was released later on, and I was focused on ), I did look at it after the CTF and locked_room, and solved it, so here's my writeup for it.
It wouldn't be a diceCTF challenge if it were missing any proections (except FORTIFY, but who actually cares about that). We're also given source code:
So basically just a int8_t
(i.e. a one byte value).
The program ends when either all the bunnies die, or we do.
Even though we have PIE enabled, the challenge decides to give us a PIE leak for free anyways, how nice of them! why they simply didn't just disable PIE is beyond me.
The bug here is fairly obvious: no bounds check on which bunny we decide to attack. This means we can subtract "random" values from arbitrary int8_t
s on the stack, however since we can't control the output of rand()
, how do we control this OOB?
If we can't control the values we subtract, then the next best thing is to be able to predict them, so when a useful value comes along, we can use it for a specific write to some index. Thankfully, rand
isn't as random as you may expect.
rand
is a pseudo-random generator, which means the numbers aren't truly random, and just appear to be so. These tend to work by applying mathematical operations to a current state to yield a new one, so they're deterministic, and they need a starting seed value to determine an initial state. Importantly, this means if we know the initial seed, we can predict all the generated numbers.
rand
uses srand
to seed it, and a common pattern is doing srand(time(NULL))
at the start of programs, which seeds based on the current time, however there is no call to srand
at all in the program!
By default, rand
is seeded with 0
, so we can generate all the attacks:
Another limitation to get around is our hp
. hp
starts at 100, and goes down by 1 every turn, so we'd only have roughly 100 writes. Since we don't have controlled writes, many of those 100 won't be useful to us, at least not easily, plus the address randomization potentially decreasing reliability of finding the necessary writes.
So what if we used our OOB to hit hp
?
Unfortunately no good, because hp
is stored in the register r12
!
However, it turns out we actually don't need to worry about this at all! If we take a closer look at how hp
is defined, we see that it's actually unsigned: uint8_t hp = 100;
. And since the check is for hp < 0
, NOT hp <= 0
like with the bunnies, this will NEVER pass.
In fact, the compiler recognises this, and just removes this check entirely:
Since we have only a PIE leak (no libc), we can only write PIE addresses, so our attack options are limited. Ideally we'd want to attack the return address, which already has a libc address: __libc_start_call_main+128
.
While we don't have a libc leak, we do have a subtract primitive as opposed to just a write one, so we can use this to our advantage!
We don't need to know all the bits of the address, we just need to know the offset from the address to where we'd want to jump, most likely a one_gadget
, then we can subtract from each byte to effectively subtract that offset!
This isn't perfect, as this won't account for possible carrying that may be needed, depending on the libc base, but it will work well enough in practice.
So, what one_gadget
s do we have?
And this is the state of the registers on returning from main
:
The biggest thing to note is that none of these gadgets will work, because rbp-0xXX
needs to be writable, and rbp=1
. So if we're going to use a one_gadget
, we will need to change rbp
to some writable address, like a PIE one. We can do this by writing to the stack, as rbp
is saved at the start of main
, and restored at the end along with r12-r15
and rbx
.
However with many of these gadgets, we'll need to fix other constraints too, except for 2 of them:
While r10 == NULL
happens by chance (probably from some system call), we can see that rax == NULL
comes from the return 0;
. So all these gadgets need is a valid, writable rbp
, which we can use PIE+0x4100
for.
So the plan is:
Overwrite saved rbp to point to PIE+0x4100
.
Overwrite return address to point to a one_gadget
.
Kill the 3 bunnies to return, and trigger the one_gadget
.
Now let's implement the attack:
Since our write primitive is a subtraction, we either need to know the current value of what we're overwriting, or set it to 0 first using DOM CLOBBERING
(which we don't need to use as we know the values).
So what gen_subs
does is generate a list of what values should be subtracted from each index, and we generate this for the one_gadget
and saved rbp
writes.
Then do_subs
will generate the payload to do these, in no particular order, as these writes don't have an effect until we return. If we have an attack that's useless to us, we send it to an irrelevant index, like -1
.
dice{clearing_the_dust_with_the_power_of_segmentation_fault_core_dumped_ae1f9557}
The program seems to be a kind of turn based singler player game, where we have to defeat 3 bunnies (it's mean , but anything for a flag) by selecting one each turn, then we do a "random" (more on this later) amount of damage to it, or have a ¼ chance to kill it insantly (i.e reduce its .hp
to 0). Each bunny is of type dustbunny
, which is just:
Finally we need to return, so then we kill the 3 bunnies (RIP ) by bringing their hp to 0 or lower.
resort
hp = 100
hp -= 1
one_gadget libc.so.6
main
r10 == NULL
rax == NULL
PIE+0x4100