Cybersecurity

Hacking Ham Radio: WinAPRS – Part 5

Coalfire Cybersecurity Team

May 17, 2022
Blog Images 2022 ham5 tile

Key takeaways:

  • Windows XP is easier to exploit in this instance due to a lack of ASLR.
  • Windows 10 is exploitable after grooming heap memory with malicious radio packets.
  • WinAPRS will likely remain unpatched.

This installment will review and demonstrate functional exploits for WinAPRS on both Windows XP SP3 and Windows 10.

In part four of this series, we built a three-stage shellcode payload to overcome problems encountered due to corrupted stack memory in the WinAPRS process. The shellcode will theoretically spawn a reverse shell and redirect its output to the ham radio’s TNC where it will then be transmitted over the air. The shellcode will then listen for incoming commands from the TNC’s serial port.

This installment will review the final Python exploit code. The exploit will transmit the three-stage shellcode in two separate AX.25 packets. It will then listen for a response from the victim machine and allow the attacker to send commands back over ham radio. We’ll then revisit Windows 10 and find a way to work around the Address Space Layout Randomization (ASLR) protections to build a working exploit for the more modern operating system.

Exploit

Windows XP SP3 Exploit

The Python exploit code is based on a publicly available Python script called send_kiss_frame.py which allows you to generate custom AX.25 packets. The three shellcode stages are assembled separately into Python byte strings and pasted into the final exploit script.

The final payload consists of KISS control characters to begin and end the malicious packet. Then there are AX.25 addressing components to ensure the packet is processed correctly by WinAPRS. The message portion of the APRS packet begins with the stage one shellcode, followed immediately by stage two. The exploit then fills in any gaps with 'A' characters, ensuring the NSEH and SEH address end up in the correct positions. Next the NSEH and SEH addresses are appended. NSEH contains jump code which will instruct the CPU to jump over the SEH address and continue execution.

After the SEH address is additional jump code that will point the CPU to the beginning of the stage one shellcode. Finally, some 'C' characters are appended to the end to ensure the packet is long enough to trigger the overflow.

The exploit sends the first packet immediately and then waits for user input. It takes a few seconds for the exploit to trigger and for WinAPRS to close. The attacker can then press enter to send stage three and then sit back and wait for their shell.

Success! The exploit worked. I now had a functional exploit which allowed me to hack into a Windows XP SP3 computer using only ham radio. This target virtual machine did have an Ethernet connection, but this exploit would still work even if the target was not connected to a conventional network. If the system is running WinAPRS with a KISS TNC, it is still vulnerable to attack.

Windows XP Video Demo

 

Windows 10 Exploit

I hinted in part three that I was able to get this exploit working on Windows 10. It takes an extra step and is less reliable, but it does work. The main problem with exploiting this vulnerability on modern versions of Windows is ASLR. WinAPRS’ program memory contains a NULL byte, which means we can’t point EIP to any address within WinAPRS memory. Since Windows 10 uses ASLR, there's also no reliable address to point the CPU to for a POP, POP, RET instruction in any built-in Windows modules because the base address of Windows’ built-in modules changes with every reboot. There is, however, a way to determine an unreliable address containing our shellcode payload.

I sent a few packets to my WinAPRS victim and searched for them in process memory using WinDbg.

I found them all crammed in next to each other. I noticed that their memory address did not contain a null byte.

I found that this was a 1004kB chunk of heap memory allocated by WinAPRS, apparently to store this packet data.

I found that when WinAPRS restarted, this address changed. Sometimes a lot, sometimes a little bit. It seemed there are a few areas of memory Windows "liked" to start from and then the exact location varied slightly from there. For example, I saw this chunk of memory allocated in address ranges 0x03128000-0x03296000, 0x05d8a000-0x05f7d000, and 0x08102000-08301000. These areas of memory stayed consistent between reboots. This meant that the chosen heap memory address was somewhat predictable.

The 1004kB memory chunk allocated was big enough to overlap large sections of each of those three primary ranges. If I could fill the entire chunk of memory with my own payloads containing POP, POP, RET instructions, I could have a somewhat decent chance of hitting one of them. I tediously ran over 30 tests manually and chose the address 0x03216170. It seemed to have the most overlap based on the data I was able to collect.

The next step was to find a way to fill that heap buffer with my own packets. I wasn't about to send them over the air for every test. That would take hours per attempt. Instead, I wrote Python script that emulated a KISS TNC on Windows and sent KISS packets directly to WinAPRS, bypassing the airwaves to simply prove the concept. Through trial and error, I figured out the most bytes I could fit into my packet and have them all still show up in the buffer. I also discovered that the RET instruction (0xC3) was filtered by WinAPRS and could not be used. I therefore replaced the POP, POP, RET instructions with a JMP [esp+0x08] instruction, which had the same effect without the bad character. The rest of the payload was filled with NOPs (0x90). This is called a “NOP sled”. If my hardcoded heap address hits anywhere in the NOP sled, the CPU will just skip each NOP instruction until it eventually hits my POP, POP, RET equivalent instructions at the end. I also discovered that the buffer could hold about 10,000 packets, so that's how many packets the script sends. This fills up the buffer as much as possible and gives the exploit more chances to hit the JMP [esp+0x08] instruction sequence.

The first step of running the exploit against Windows 10 is to run this heap spray script against the victim. In a real-world attack, you'd have to send these 10,000 packets over the air one after another, blocking the frequency from anyone else. It could work, but it's not very practical and would certainly draw attention. This script simulates that process to save time and to prove the concept.

The Windows 10 exploit then works similarly to the Windows XP exploit, with a few changes. First, there are no structures on the heap that need "fixing" by the shellcode. Also, in Windows 10 I can call most Win32 APIs right from the first payload. In Windows XP, this was not possible due to the corrupted stack memory. However, on Windows 10, I have not found a reliable memory address to obtain a handle to the COM port. This means I don't have a reliable way to access the COM port to send or receive data from the attacking machine. After a lot of trial and error, I found the easiest way to free up the COM port was to simply close the WinAPRS process. This would kill exploit payload though, so I still was stuck using multiple shellcode stages injected into external process like I did with Windows XP.

Another problem is that Windows 10 is a 64-bit operating system. Explorer.exe is a 64-bit process. My shellcode is 32-bit shellcode. This means that the Windows XP technique of injecting the shellcode into explorer.exe will no longer work unless I rewrite it to use 64-bit assembly. Instead of reinventing the wheel, the stage one shellcode now calls CreateProcessA to launch a 32-bit cmd.exe process. Stage one then injects stage two into that process instead of explorer.exe which will then be used to execute stage two. The 32-bit cmd.exe process acts like a container to execute our 32-bit shellcode.

The second and third stage shellcode are almost identical to the Windows XP shellcode, with one simplification. For the Windows 10 shellcode, I don't close the COM port at the end of stage two. Instead, I leave it open, and stage three just uses the same handle that stage two used.

The Windows 10 exploit is less reliable than the XP exploit because it depends on Windows choosing a heap memory address that includes the hardcoded 0x03216170 address in the exploit. If Windows chooses a heap memory location too far away from there, we won’t hit our NOP sled and WinAPRS will simply crash. I've found this technique to be successful approximately one third of the time, probably a bit less. It also requires that the attacker spend a few hours spamming packets at the victim to groom the heap. But it goes to show that an attacker with enough determination can still exploit this vulnerability on a modern operating system.

Windows 10 Video Demo

 

Disclosure

I disclosed this bug and several others to the software authors on December 28, 2020. I wasn’t sure if I would receive a reply since the software hadn’t been updated since 2013, but was surprised to hear back from them almost immediately. I had a great conversation with the author about the bug I found and other security vulnerability categories they were interested in related to a new project they were working on. I even inspired them to search for overflow bugs in their new project! Unfortunately, the author no longer has an environment configured to develop WinAPRS, so the bugs are unlikely to ever be fixed. Luckily, there are many other more modern options for APRS software on Windows, so it is simple to switch to something new.

CVEs were obtained on February 9, 2022.

  • CVE-2022-24702
  • CVE-2022-24701
  • CVE-2022-24700

Coalfire is publicly disclosing this bug in accordance with our vulnerability disclosure policy. Full details can be found here.

Full source code for these exploits can be found here.