Throwing Down the Hacking Gauntlet at BSidesTLV
As part of our initiative to give back to the community, Palo Alto Networks sponsored BSidesTLV, and the Prisma Cloud Security Research team supported the conference in our unique way by creating a Capture the Flag (CTF) challenge. Every year, BSidesTLV hosts a CTF, which is a hacking competition where participants attempt to solve as many hacking challenges as possible in a limited time.
To solve our challenge, participants had to find a vulnerability in an application and exploit it while bypassing several exploitation defenses.
Out of the 739 teams that participated in the CTF, only one team solved the challenge.
Started by a group of hackers, security researchers and cyber security enthusiasts eager to share their knowledge and ideas, Security BSides is a global network of non-profit security events taking place anywhere, including BSidesTLV in Tel Aviv.
The Prisma Cloud Security Research Team Challenge
We dubbed our hacking challenge, created for the CTF competition, Intergalactic Communicator. Participants had to exploit a remote application and exfiltrate the file flag.txt to solve the challenge. They were provided with the container image of the server, which serves an x86_64 architectured ELF executable binary, and had to reverse engineer it with the intention of finding and exploiting a vulnerability while overcoming exploitation mitigations, such as ASLR, DEP and stack canary.
The application would receive all the incoming messages, sanitize them into ASCII characters (seeing as sometimes aliens can scramble some weird words) and concatenate them into one transmission to then broadcast intergalactically.
The binary expects to receive packets over a proprietary protocol with two sections – header and data.
0x00 - 0x04: length
0x04 - 0x0c: checksum
0x0c - 0x10: opcode
Length - The first 4 bytes of the header represents the length of the packet without the length value in big endian.
Checksum - The next 8 bytes of the header is a CRC-64 hash of the packet (without the length and checksum values).
Opcode - The next 8 bytes of the header is the opcode value, which is used to indicate which operation should be performed.
The data is encrypted using RC4 with a unique key for each packet, and since RC4 is a symmetric encryption, the key can be used to encrypt and decrypt the data.
The key is calculated by taking the phrase “NotFlag!JustKey!” and XORing each half of the key with the checksum in the header section.
When the application receives a packet, it decrypts the data and validates its integrity by verifying the checksum is correct. It does so by calculating the checksum of the packet and comparing it with the checksum in the header.
The opcode parameter instructs the application what functionality to initiate:
Case 0x1 - Return the number of messages created in a dynamic array.
Case 0x2 - Fetch the requested message and concatenate the message to “Message from earth: %s”.
Case 0x3 - Sanitize the data in the request and append it to the end of the messages array.
Case 0x4 - Clear all of the messages created in the vector.
Case 0x5 - Concatenate all the messages created by the client to a prefix message “Broadcasting to planets…\n” and copy them to the output buffer, which will transmit the messages.
In any other case, the server will simply return the “Welcome to the Intergalactic Communicator\n” message.
We embedded an intentional stack overflow condition in the last opcode case 0x5 where the server will concatenate all the messages in the array and copy them to the output buffer without validating the buffer’s size and the concatenated message’s size.
Looking back at the opcode case 0x2 that will read a message using an index supplied in the data, the posted message will be concatenated with a prefix of “Message from earth: %s” and written to the output buffer using two vsnprintf function calls.
The first function call:
vsnprintf(temp_buffer, 2048, “Message from earth: %s”, message)
The message will be set as the va_args, while the prefix string will be set as the format string. In this case, there is no issue to exploit.
The second function call:
vsnprintf(output_buffer, 2048, temp_buffer)
In this case the temporary buffer from the previous call is introduced as a format string argument, which introduces a format string specifier vulnerability.
The format string specifier vulnerability allows leaking data from the stack as the unsanitized message is being treated as a format string argument. For example, %p could be used to leak 8 bytes from the stack giving us our desired read primitive.
While it is possible to crash the application with the stack overflow vulnerability, there are some constraints and mitigations to overcome before achieving control over the execution flow.
Stack canary is used to detect a stack buffer overflow by placing a value in the memory before the return address in the stack. During exploitation, as buffer overflows overwrite the stack, they will also overwrite the canary, which will be checked later to verify if a buffer overflow has occurred.
The stack canary is randomly generated every time the application is started.
Bypass the Stack Canary
To bypass the stack canary, a memory leak or a read primitive should be found to leak the stack canary and write it in its place. The usage of format string specifiers in the opcode case 0x2 can be used to develop a way to leak data from the application’s memory.
Seeing as vsnprintf will only write 2048 bytes to the buffer and the stack canary is located much farther away, we can use the $ character to specify which argument position to leak from the stack – in our case canary:%485$p will leak the stack canary successfully.
Usually, Format String Specifier vulnerabilities can be used to develop a write primitive using the %n format string, but in this case, the binary was patched to prevent this.
The next restriction is character sanitization.
In the case of opcode 0x3, when data is received prior to being pushed into the messages array, the server will loop the data through a sanitization process to convert non ASCII characters.
As previously mentioned, we can only use ASCII characters. We should also note that any byte bigger than 0x7f is converted with a bitwise AND operator instead of being stripped or replaced with fixed characters. This can prove helpful to send null bytes, which are usually bad characters when dealing with strings.
The character sanitation restriction is an issue because the stack canary is randomly generated when an application is started and they remain unchanged until the application finishes its execution. The stack canary in our case is 7 random bytes followed by a null byte. This means that in many cases an attacker could not be able to override the canary with the right characters as he is restricted.
Bypass the Character Sanitization
Luckily this application is being served with socat. Every new connection starts the application again and serves it until timeout or until the process is killed, which means the stack canary is randomly generated again.
At least once in 4000 requests, a fitting stack canary with ASCII characters and a null byte will be generated. Seeing as the stack canary is randomly generated, this happens quite often and is around the 100-200 requests area.
The last issue is the fact that the least significant byte of the stack canary is a null byte (0x00), which might cause issues with null terminator aware functions. We can overcome this problem by abusing the character sanitization process, seeing as 0x80 will be converted to a null byte in that process and allowing us to introduce null bytes at any location without terminating any function.
The next restriction to bypass is Data Execution Prevention (DEP), which enforces “read”, “write” and “execute” permissions over memory regions. DEP mitigation is used to enforce “read” and “write” permissions over data regions, such as stack or heap memory regions, in an attempt to mitigate buffer overflow exploits from executing due to the lack of the “execute” permission.
Seeing as this is a stack overflow, we will be overwriting a memory area that has only “read” and “write” permissions. Any assembly code written by us won’t be executed due to the DEP mitigation, seeing as this area lacks the execute permission.
We can use a common technique called Return Oriented Programming (ROP) to bypass DEP. In short, we can inject a chain of pointers that will divert the execution flow to small bits of assembly opcodes (called gadgets) from the application and create a syscall that can either execute commands or set the landed memory area as executable and introduce a shellcode for example.
This is where we encounter the last restriction – the Address Space Layout Randomization
(ASLR) mitigation. This mitigation will randomize the middle 28 bits of the application’s base address.
The ASLR will prevent us from using hardcoded pointers from within the application, seeing as the base address will change every time the application is started and require us to leak and determine the application’s base address, then calculate each gadget’s exact pointer using its offset from the base address.
The ASLR restriction is also impacted by the character restriction, seeing as we need the application to have a base address that has only ASCII characters and only uses gadgets that have only ASCII characters.
Luckily the base address is randomly generated with each execution of the application, as previously mentioned, so we can simply leak an address using the format string specifier vulnerability – and if the address doesn’t fit, we can crash the application and try again until both the base address and stack canary have ASCII only characters.
This solar eclipse-like condition happens quite often, and after some testing, it is averaged to 1500-2000 requests required to stumble on this.
Our exploit will start by gathering the information required to bypass all the mitigations, starting with the format string specifier read primitive to leak a pointer from the stack using a %3$p format string. This will be needed to bypass DEP later using the mprotect syscall.
The next pointer required is the binary’s base address.There is a pointer that points to base_address + 0x131f9c on the stack at position 225, so we can use %225$p to leak its address and subtract 0x131f9c from it to get the binary’s base address.
The last thing needed is the stack canary value, which is located at position 485, and we can leak it with a %485$p format string.
To recap – the message used to leak this information looks like this:
We will loop the information leak constantly until we find a stack canary that contains only ASCII characters. Once this requirement is met, we can exploit the buffer overflow while overwriting the stack canary with the correct value and achieve control over the execution of the application.
Now, to bypass the DEP mitigation and execute our shellcode, a ROP chain should be constructed to build a syscall for mprotect. The character sanitizer restriction might be hard to pass, though, seeing as we need to fill the RDI register with a pointer from the stack, which will always contain bad characters that the sanitizer will transform.
An interesting thing is that seeing as the packet is fully decrypted before the opcode is handled means we can encounter a copy of our buffer lower on the stack. That means, if we send a packet with an opcode 0x5 (triggers the buffer overflow) along with data, we will both trigger the buffer overflow and introduce an unsanitized buffer somewhere on the stack, past the return address. If we manage to pivot the stack to this area, we can execute any gadget and any shellcode without character restriction.
To pivot the stack to the unsanitized buffer, any “pop register; ret” gadget can be used 21 times and land exactly on the unsanitized buffer. Our script will leak the base address in a loop until a base address is found that will allow a fitting gadget to be used.
After pivoting the stack with an ASCII “pop” gadget, we can introduce the variables required to call mprotect to the correct registers according to the calling convention. RDI should contain the start of a memory page that we would like to change its permissions. In our case, we will take the leaked stack address and point it at the beginning of the memory page using a bitwise logical AND against 0xffffffffffff000, and then fill RDI with the result.
The RSI register will contain the size of the region. We would like to change its permission, and we can instruct a big value like 0x3000, which will probably cover the area that should contain our shellcode. The RDX register should contain the permission we would like to set – in our case 0x7 should allow for read, write and execute. The last thing needed is to fill RAX with 0xa, which should represent the syscall for mprotect. To execute it, a syscall gadget is introduced, and a jmp rsp gadget is added afterward to jump to our shellcode for execution.
Finally, once we overcome the DEP mitigation, any shellcode can be executed to achieve remote code execution over the server and read the flag.txt file.
Who Solved the Challenge?
A total of 1221 users participated in the CTF across 739 competitive teams. Out of the 739 teams competing, only a single team solved the Intergalactic Communicator challenge, which was also the team that solved most challenges and won the CTF.
See You Next Time
We would like to thank everyone who participated in the CTF and particularly BSidesTLV organizers for a great conference. It was a pleasure to meet up and give back to the community.