[vsCTF '23] Cosmic Ray v2
An interesting twist on a past CTF challenge.
Last updated
Was this helpful?
An interesting twist on a past CTF challenge.
Last updated
Was this helpful?
This challenge is a twist on the challenge from . The original challenge was a simple buffer overflow, but this version removes the overflow opportunity.
We are provided a binary file, cosmicrayv2
. Running some basic information on the file:
I will use radare2
and Ghidra for the dissection of this challenge.
Checking main()
, the most important information is a call to cosmic_ray()
. Nothing is pushed into rdi
before the function is called, I will venture into this function assuming no arguments are passed.
We'll use Ghidra for the disassembly of this function. We'll clean it up by naming each local variable, hiding the canary check, and removing some unnecessary casting. The result is shown below.
What does this function do for us? This function lets us input a memory address and then flip any bit at that specified address. With this, we hold a lot of power! We can change one bit at one address anywhere in the program's memory.
I started modifying random instructions after the flip to affect the program's flow. I recognized that cosmic_ray()
comes right before main()
, which was a big clue. If I could modify the ret
instruction so that it no longer returned, the program would continue executing through main()
, giving me another run of the binary.
Now that we can run the program infinitely, we can continue to modify single bits until we do something. We can clearly see we must get a shell.
We know we must call system()
. We don't know if ASLR is turned on, but we'll assume it is. This makes Step 1 to leak libc
. From here, we can use the following code as the basis for our next steps:
We dissected in our disassembly this was the following:
This tells us two things:
If we continue to enter valid bits, we will never call exit()
.
If we do put an invalid bit, we can call exit()
on command.
This leads us to the GOT Overwrite exploit: we can overwrite the GOT entry for exit()
with the address of system()
. This will allow us to call system()
with any argument we want.
To make this happen, we need to find a way to load /bin/sh
into rdi
. There already is an instruction to load 0x1
into edi
before the call to exit()
, so we must modify this instruction through consecutive bit-flipping.
A bit of dumb luck managed to carry me through this. While I was stepping through the possibilities of changing the instruction through gdb
, I noticed my input was sitting in rdx
at the time of the mov edi, 1
instruction. This inspired me to change this instruction to mov edi, edx
.
We can now put together our solution. Our solution comes in a few stages:
Flip the return bit to allow infinite runs of the binary
Leak libc
using the program's output
Modify the mov edi, 0x1
instruction
Modify the GOT entry for exit()
to point to system()
Call system()
after passing /bin/sh
as our input.
I chose to write some helper functions to do this. The first function, bit_modify
, takes an address and a bit and modifies that bit number.
The second function takes an address and reads the byte at that address. It does not modify the data there.
The third function is string_modify
, which takes an initial address and does a series of modifications. This is used to change instructions or series of addresses.
We can write the rest of our exploit now that we have these helper functions. Getting the addresses of most of the data here is trivial, so I'll leave it as an exercise to the reader.
Flip the return bit to allow infinite runs of the binary
Leak libc
using the program's output. I leaked exit@got
and then used the provided libc
file to get the offset in libc
.
Modify the mov edi, 0x1
instruction. This is the most difficult part of the exploit. We must find the address of the instruction and then modify it to mov edi, edx
.
Modify the GOT entry for exit()
to point to system()
. We can use the same string_modify
function to do this.
Call system()
after passing /bin/sh
as our input. By passing data that's not a valid bit, it will call exit()
(which is now system()
). The address we chose to write was arbitrary, but it must be a valid address.
If we run this, we get a shell and the flag!
What do I change ret
to? As it is, ret
is c3
in hex, which is 11000011
in binary. I must change the instruction such that it's still valid. Otherwise, the program will crash. Using a , I found that modifying the second bit changed the instruction to 83
, a cmp
instruction. This won't crash the program and will still return to main()
.
More information on the GOT Overwrite exploit can be found here: