The goal of the challenge was to develop a PoC “jailbreak” for the fictional cyOS. Given to players was a collection of files, which included the OS’s ELF loader, both source and compiled. A web service existed for players to upload their binaries, and hope for success. A sample binary was provided, which would successfully run when sent to the web service. A public key and a code signing python script was also provided.
I opened the loader code and studied it for some time. From initial facts provided by the challenge description, I knew this was some kind of ELF loader. After reading the code for a bit, I discovered the following:
- The file to be loaded must have a valid ELF header
- The file must be code signed
- The file must be code signed, which is generated by hashing all the executable pages, taking a hash of all of those hashes, then signing the hash using libsodium
- The file to be sent must be signed with an existing key, which we are only provided the public part of
We can conclude that since we cannot crack the private key (or if we can, within the time of the competition), we have to manipulate the sample file provided.
When the loader is run, the binary is loaded, and all the required memory locations are created with
mmap. The memory is copied over from the binary to the correct location, then the permissions of the section are adjusted with
mprotect. If the section is marked with the executable permission, the following snippet is run:
validate_section_code_signature(char* buff, size_t buff_size, void* main_addr, Elf64_Shdr* section, Elf64_Shdr* code_signature) will ensure the section passed in has been validly signed.
So, every time there is an executable section to be marked as executable, the loader will ensure the
.text section has been validly signed.
Now, read that sentence again. The oversight really sticks out. The issue is that only the
.text section is checked. If there is another executable section under a different name, the loader will happily mark it as executable, since the
.text section is still signed validly.
The second part of this challenge is to determine how we can get our code to be executed. Note, we cannot change the address of the
main symbol, as that is included in the code signing process.
Originally, I had thought to overwrite something like a GOT pointer or overwrite a value in the
.[init/fini]_array. However, I realised that the loader called
main within the executable, not
start, meaning nothing libc related would be set up. Further digging in the provided ‘test’ binary resulted in the discovery of using a syscall to print out a string, rather than using printf.
I thought of past CTF challenges, and thought of the classic trick in binary exploitation where you overwrite a GOT pointer. Except, in this case, we don’t overwrite a GOT pointer in our binary. We overwrite a GOT pointer in the loader binary. I had came across this idea when I ran
checksec on the provided binary, and noticed
PIE wasn’t enabled! We can do this by making a section whose address is the address of a GOT entry, and overwrite it with the address of our own executable memory, and then win!
Now, I had my plan of action:
- Create a section with R-X permissions, make sure it is a) named something unique and b) at a different address to
__text(to prevent any accidents)
- Create a section with RW- permissions, with the address of a GOT entry, and the value of the memory address of the above created section.
Before we start writing an exploit, I wanted to be able to debug the loader file so we can see what is going on. Noting that libsodium needed to be installed,
sudo apt-get install libsodium needed to be ran. Once installed, I could run
./loader. We can test that the test binary works by running
./loader test.signed, and see the message “Welcome to the SecElf challenge! I’m a signed binary that doesn’t really do anything.”
Now we need a way to modify the ELF binary to create new sections. Reading
signer.py, I noticed the use of the
lief module to create the code signature section. Reading a little more, I believed I could use this module to modify the existing binary to run some unsigned code.
First thing I did was to create a basic template for our exploit:
This would open the file, allow us to add our modifications, then write to a new file our modified binary.
If we were to run this file, we get some information about the sections within the binary:
It wasn’t clear at the time what each column represented, but I inferred that the first column was the section name, the 3rd column was the virtual memory address, and the second to last column was the permissions.
Following through with our plan, I began by first creating the section that would contain our executable code:
When ran, we see a new entry in the sections list:
However, we can’t see any permissions attached to it. We want to make sure this mapped memory can be executed. Digging around the
lief documentation, I found that flags could be added to sections. Adding the following lines caused them to appear in the section list:
If we run the loader with the modified binary, we will notice the following:
- The loader will find and allocate memory for our new section
- We will see two codesigning validations (e.g. look for two outputs of the combined hash), compared to the original binary where it was only one validation run.
- When running the loader in gdb, we notice our section being allocated with the correct permissions
Great! Now we can move on to actually creating a way for our code to be executed.
A similar method was used to create the section. Instead of
EXECINSTR, I used
WRITE for the permissions (to make the permissions RW- rather than R-X). The content was the address of
.my_text. However, the main problem was deciding which entry to overwrite. After fiddling around, I concluded
fflush was the best function to overwrite (
fflush comes after the call to the original main function in the binary). So using
readelf -r ./loader, I found the address of the fflush GOT entry, and set it as the VM address of the section:
However, when running the binary with the loader, the loader crashes with a segmentation fault.
Doing some debugging, we notice that there is a bigger problem: the entire GOT is NULLed after the fflush entry, which does contain our desired address. The problem stems off this message in the logs:
For some reason, the section reports a size of
0x1000 bytes, rather than just 8 bytes, the length of the buffer we set as the contents. This was a bit of a pain to debug, and ended up requiring a hacky solution. I noticed if I manually changed the sizes from
0x8, the solution worked. However,
lief didn’t seem to support manually setting/modifying the size. So, I decided to open the patched file, replace the large size with the intended size, then use that as the binary to test.
Yes, I know, a hacky solution. But it works! When we run the loader with the new binary, we see the following error:
Recall that we filled out executable page with breakpoints (
0xcc), so our code was successfully hit! (We can check this by running the loader in gdb, and noticing the breakpoint being hit at
The final step was writing shellcode to be run. From previous CTF challenges, the usual thing would be either start a shell (which we can’t, since we only get stdout and stderr back from the server), or open a file called
flag.txt. After playing around a bit, I found the path to the flag was actually
When writing the shellcode, an issue I came across was finding a way to read the file into a buffer. There were two solutions. Either create a new section, or mark our executable page as writable as well (RWX permissions, which works since, as far as I know, there is no ~W enforcement on X pages in linux). I chose the second option, since it seemed easier to implement. After that, it was a matter of converting the assembly to the raw opcodes, setting that as the contents of the section, then getting the flag!
Final solution (The file
/flag.txt must exist to see the exploit work):
Thanks to Cybears for organising a great CTF at BSides Canberra 2019. The challenges can/will be found at https://gitlab.com/cybears/fall-of-cybeartron.