In part two of this series, we reviewed our WinAPRS software and hardware configuration. We then began reverse engineering WinAPRS and fuzzing it for vulnerabilities using modified open-source software. Finally, we identified a potentially exploitable vulnerability. This installment will dig into that vulnerability to discover how it provided us control over the CPU’s EIP register. We’ll then discover some limitations and make some compromises to continue toward our goal of obtaining a reverse shell over ham radio.
Tracing the Bug
After sending a packet radio payload containing 1,000 A’s, WinAPRS crashed with an access violation. After attempting to continue execution, I had somehow gained control over the CPU’s EIP register.
WinDbg revealed the CPU instruction which caused the violation.
I used IDA Pro to view that code block and found the following function:
I checked the call stack after the crash and found that it was corrupted with data from my payload (0x41 characters).
I also checked the Structured Exception Handler (SEH) chain and found that it also had been corrupted.
My 1,000-byte packet had somehow managed to overwrite the SEH chain. Then an exception was triggered, and the CPU jumped to the address of the SEH handler, which had been overwritten by 0x41414141. Since I controlled the 0x41414141 payload, this indicated that I should be able to point the CPU to any memory address I wanted and gain code execution. Things were looking up.
To figure out what was happening, I set a breakpoint just before the lodsb loop at 0x0055990C. I then sent the 1,000-byte packet. Tracing through the code, I found that ESI and EDI eventually were set as such:
The loop would copy one byte from ESI (source) over to the address pointed to by EDI (destination). It would loop until it found a NULL byte. This makes sense since C strings are generally NULL terminated. I checked the data in ESI and found that it contained my packet contents.
I then checked EDI and found that it also contained a portion of the packet.
That's when I noticed that the memory address of EDI was close to ESI, but higher up. I did some quick math and found that it was 880 bytes higher.
I went back and checked the original access violation and found that it was triggered when the stosb operation tried to write to an unallocated memory address.
It seemed that my packet was larger than the application was expecting. It copied 1,000 bytes into a source buffer that apparently could only hold 880 bytes. As a result, it overflowed into the destination buffer. This meant that the source buffer did not contain a NULL byte to terminate the string, because the NULL byte was placed in the destination buffer. The loop then copied 880 bytes of my payload into the destination, which overwrote the final 120 bytes of the original payload, including the NULL byte. Since this function didn't find a NULL terminator, it just continued copying in a loop forever. The original destination buffer now became the source buffer, and the bytes were then copied to memory just after the original destination buffer.
Effectively, it just continued overwriting memory with the first 880 bytes of my payload infinitely until it overwrote the SEH handler and finally attempted to write to unallocated memory space and triggered the exception. This was good news for me. I now had a way to trigger an exception, and at the same time I could overwrite the SEH handler, which would allow me to point the CPU to some other location in memory and potentially gain code execution. My next step was to figure out the exact offset in my payload that would overwrite the address of the SEH handler.
I used msf-pattern_create to generate a 1,000-character-long string with no repeating characters.
I then generated a packet with that data.
I transmitted the packet, causing a crash in WinAPRS. I then checked the value of EIP.
I then used msf-pattern_offset to find the exact offset to overwrite EIP.
I built a new packet to see if I could overwrite EIP with BBBB (0x42424242).
I transmitted the packet and found that EIP was overwritten with 0x42424242 (BBBB) as expected.
This meant that I could control EIP and send execution to any address I wanted. The typical way you would exploit this kind of vulnerability is to locate a memory address containing the instruction sequence: POP r32, POP r32, RET (PPR). This POPs two DWORDs off the stack, leaving the address of the next SEH handler on top. The RET instruction then tells the CPU to return to that address and start executing the code there. Since I have control over that address, I can include a few instructions to JMP to my shellcode on the stack and ideally do something fun like gain shell command execution. For a much more detailed explanation of SEH-based exploits, refer to this excellent article from corelan: https://www.corelan.be/index.php/2009/07/25/writing-buffer-overflow-exploits-a-quick-and-basic-tutorial-part-3-seh/
It turned out I couldn't send execution to just any address. There were two limitations. The address could not contain a NULL byte (0x00). If I included any NULL bytes in my payload, the copy loop would see it as the end of the source buffer string and stop copying. This would prevent the infinite loop, the SEH overwrite, and the access violation. Game over. To gain control of EIP, I needed the loop to continue copying indefinitely. Therefore, any address I used must not contain a NULL byte. Also, my payload could not contain the bytes 0xC0 and 0xDB. These are KISS protocol control characters that have special meaning in a KISS packet. If my payload included either character, WinAPRS would interpret that as the end of my packet and cut the payload short, causing the same problems noted above.
Looking back at the output from narly, the entire memory space of WinAPRS contains a NULL byte.
The stack space also contained a NULL byte, so there was really no place to transfer execution. All other loaded modules were operating system modules and included Address Space Layout Randomization (ASLR). ASLR means that the addresses for each module will change every time Windows is rebooted. There is therefore no way to hardcode an address to jump to in any of those modules. I would need a way to leak a memory address back to me as the attacker somehow over ham radio to calculate the address to a PPR instruction.
Microsoft first implemented ASLR in Windows Vista, although they have improved it since then. Just for fun, I decided to fall back on good old Windows XP SP3. Targeting Windows XP would allow me to locate a reliable memory address for a POP, POP, RET instruction, which would permit code execution. Once I had that, I could work on a custom shellcode payload to provide a reverse shell via ham radio, which was what I really wanted to accomplish with this project. From there, I could explore other options with Windows 10 to see if I could get anywhere (spoiler: I did! We’ll revisit Windows 10 later in this series).
I set up a Windows XP SP3 virtual machine and copied over WinAPRS and the installers for all my tools. Using WinDbg and narly, I checked the protections on all loaded modules.
Excellent. None of them had ASLR enabled, though most of them had SafeSEH turned on. SafeSEH is a different protection mechanism that attempts to prevent this exact kind of SEH-based buffer overflow exploit. Modules compiled with SafeSEH enabled have a table that includes a list of all valid exception handlers for the module. When an exception is triggered, Windows will check to see if the handler address falls within a memory range related to a loaded module. If so, it will check to see if the handler is valid for that module by checking the table. If it's not valid, it won't execute the handler. If the handler's address is outside of the address range of a loaded module, then the handler is not checked and SafeSEH is effectively bypassed.
Corelan's mona.py plugin for Immunity debugger has a built-in function called "jseh" that will search for SEH gadgets in memory that exists outside of any loaded modules. I fired up immunity debugger and used mona to search for such a gadget.
It found 42 gadgets, but unfortunately, they all required a JMP to either EAX or EBX. In this case, those registers did not contain an address to my shellcode, so none of these were going to work. I looked at the Mona source code to figure out what exactly it was looking for, thinking maybe there were some other gadgets that would work that weren't included in the search. I found that, interestingly, Mona was not searching for POP, POP, RET at all. It was looking for other gadgets instead. I added 16 variations of POP, POP, RET to the list.
Then I ran the script again.
Bingo. I had a few candidates at the bottom of the list. I chose the last one and modified my payload to use it as the SEH handler address.
I executed the payload and found that the SEH handler now pointed to a POP, POP, RET instruction (red outlines below). I set a breakpoint on the Next SEH handler address and when I continued code execution, the breakpoint was triggered (green outlines below). This indicated that I successfully pointed EIP back to my payload.
At this point, EIP was pointing at the NSEH handler address. This gave me only four bytes of CPU instructions to work with, because just after NSEH is the SEH address, which points to the POP, POP, RET instructions. I had to fill NSEH with instructions that would point EIP at the beginning of my payload. I checked the memory around EIP and found that the start of my payload was just 101 bytes ahead of NSEH. My payload included 783 A's, which meant I could get 783 bytes worth of shellcode if I could find a way to point EIP to that spot.
Around this time during the project, I got my hands on a Raspberry Pi with a TNC-Pi hat (https://www.tnc-x.com/TNCPi.htm). I made a custom cable to hook it up to my Kenwood handheld transceiver and was up and running quickly.
Now, instead of using the Direwolf audio generation tool, I could send raw packet data to the Pi’s serial port. This would make it easier to develop a working exploit. I found a python script that allowed me to build custom AX.25 packets:
I made a simple test packet to validate that it worked.
The exception handler address was overwritten with 0x42424242 (BBBB), which is what was expected. It worked. This script would become the basis of my exploit code. I knew it wouldn't be as simple as dropping an msfvenom-generated shellcode payload into the exploit, because Metasploit doesn't come with a KISS TNC reverse shell payload. Why would it? I was going to have to write a custom payload. This turned out to be much trickier than I expected.
The next installment in this series will review a three-stage shellcode payload that overcomes problems I encountered when attempting to call many useful Win32 APIs. We will walk through the assembly shellcode step by step, explaining the purpose of each bit of code. The ultimate goal of the shellcode will be to open an interactive command prompt reverse shell over ham radio.