[vsCTF '23] Cosmic Ray v2
An interesting twist on a past CTF challenge.
This challenge is a twist on the Cosmic Ray challenge from Sekai CTF '23. The original challenge was a simple buffer overflow, but this version removes the overflow opportunity.
Distribution
We are provided a binary file, cosmicrayv2
. Running some basic information on the file:
Solution
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.
Simply based on the solution of the original Cosmic Ray challenge, we know we must modify an instruction. The original solution modified the canary check instruction to jump if there wasn't a buffer overflow. This won't work because we don't get another input once we're done. We need to find another instruction to modify.
Inspiration
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.
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 x64 Instruction Table, 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()
.
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.
Attack Vector
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.
More information on the GOT Overwrite exploit can be found here: https://cyber.cole-ellis.com/binex/08-got/
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.
How I found the Solution...
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
.
Solution
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 outputModify the
mov edi, 0x1
instructionModify the GOT entry for
exit()
to point tosystem()
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.
You'll notice this function actually does modify the data there but switches it back!
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 leakedexit@got
and then used the providedlibc
file to get the offset inlibc
.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 tomov edi, edx
.Modify the GOT entry for
exit()
to point tosystem()
. We can use the samestring_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 callexit()
(which is nowsystem()
). 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!
Full Exploit
Last updated