Hacking Ham Radio: RCE in WinAPRS
In early 2021 I took an Offensive Security course to earn my Offensive Security Exploit Developer certification. This course taught me a lot about exploiting memory corruption vulnerabilities in Windows 32-bit programs. It was also a lot of fun.
Aside from security things, I also dabble in ham radio. Most of my experience with ham radio has been through the use of packet radio and other digital modes. Rather than talking to people over the air using voice, I’m sending and receiving data through a computer over the airwaves. Actually, I originally got my ham license back in 2008 specifically so I could use APRS for a nearspace balloon project.
For years I’ve wondered what kind of vulnerabilities exist in ham radio software. A lot of ham software is old and written by hobbiests so I figured there must be some pretty fun bugs to be found. I also was always fascinated by the idea of obtaining remote code execution (RCE) on a computer over ham radio. RCE bugs are super fun in general, but I had never heard of anyone finding one exploitable over ham radio. In theory, a vulnerable system wouldn’t even need an Ethernet connection. As long as it was hooked up to a radio, it could be exploited. After taking the OSED course I finally had enough experience to feel confident in tackling a research project to specifically look for exploitable RCE vulnerabilities in ham software.
During the summer in 2021, I finally got to work. I started out researching in my spare time, but eventually my employer approved some research and development time for me to work on it using company time! The trade off of being paid to do research is of course that the results of this project are published on Coalfire’s blog as opposed to my own. My original post was broken up into five pieces and published there.
- Hacking Ham Radio: WinAPRS Part 1
- Hacking Ham Radio: WinAPRS Part 2
- Hacking Ham Radio: WinAPRS Part 3
- Hacking Ham Radio: WinAPRS Part 4
- Hacking Ham Radio: WinAPRS Part 5
If you prefer to read the whole thing in one go, you can read my original draft in PDF form here. I’ll summarize the findings below.
I chose to target WinAPRS because:
- It was listed on the APRS website
- I was more familiar with APRS than other packet protocols
- It hadn’t been updated since 2012
- It seemed to be written in C, which meant I could put my new WinDBG and IDA Pro reverse engineering skills to the test and it would be more likely to contain memory corruption vulnerabilities
I dug through the code with IDA Pro and used WinDBG as my debugger. I eventually found that sending an extra long AX.25 packet would cause WinAPRS to crash with an access violation. It turned out there was a loop somewhere that copied my packet from one buffer to another. These two buffers were next to each other in stack memory. As a simplified example, imagine there are two 0x100 byte buffers in stack memory. One at address 0x1000 and one at 0x1100:
0x1000 - 0x10ff : Buffer 1
0x1100 - 0x11ff : Buffer 2
Obviously buffer 1 can only hold 0x100 bytes in this example, beacause buffer 2 exists at 0x1100. In the case of WinAPRS, at some point my packet was copied to buffer 1 without checking my packet size. My packet completely filled buffer 1 and overflowed into buffer 2.
WinAPRS then hit a function which copied my packet from buffer 1 to buffer 2. The function started by copying the byte at 0x1000 (buffer 1) to 0x1100 (buffer 2). When it did this, it overwrote the part of my packet that had overflowed to the 0x1100 position. It then moved on to byte 2, 3, 4, and so on.
This copy loop checked each byte for a NULL byte (0x00) to indicate the end of the string. My payload was too long to fit into buffer 1, so the NULL byte actually ended up somewhere in buffer 2. This copy loop was overwriting buffer 2 with the first 0x100 bytes of my payload, which meant the NULL byte ultimately was overwritten. The result was that WinAPRS never found a NULL byte and just kept copying data in a loop until it hit unallocated memory. Imagine the stack memory looked something like this:
0x1000 - 0x10ff : \0\0\0\0\0\0\0\0... (Buffer 1)
0x1100 - 0x11ff : \0\0\0\0\0\0\0\0... (Buffer 2)
0x1200 - 0x3fff : Random WinAPRS stuff
0x4000 - 0x4007 : Structured Exception Handler
0x4008 - 0xffff : Random stuff
UNALLOCATED
Now imagine if I sent a large payload which consisted of 0x100 A’s and 0x01 B’s. WinAPRS would copy this oversized packet into buffer 1, and it would spill just barely into buffer 2 like this:
0x1000 - 0x10ff : AAAAAAAA... (Buffer 1)
0x1100 - 0x11ff : B\0\0\0\0\0\0\0... (Buffer 2)
0x1200 - 0x3fff : Random WinAPRS stuff
0x4000 - 0x4007 : Structured Exception Handler
0x4008 - 0xffff : Random stuff
UNALLOCATED
The copy loop would then copy the first byte of buffer 1 into buffer 2 like this:
0x1000 - 0x10ff : AAAAAAAA... (Buffer 1)
0x1100 - 0x11ff : A\0\0\0\0\0\0\0... (Buffer 2)
0x1200 - 0x3fff : Random WinAPRS stuff
0x4000 - 0x4007 : Structured Exception Handler
0x4008 - 0xffff : Random stuff
UNALLOCATED
Then the second byte…
0x1000 - 0x10ff : AAAAAAAA... (Buffer 1)
0x1100 - 0x11ff : AA\0\0\0\0\0... (Buffer 2)
0x1200 - 0x3fff : Random WinAPRS stuff
0x4000 - 0x4007 : Structured Exception Handler
0x4008 - 0xffff : Random stuff
UNALLOCATED
You can see now that the NULL byte that was in buffer 2 would be overwritten. The loop therefore never found a NULL byte, so it kept copying whatever was in buffer 1 over and over until it went right through the SEH handler and right passed 0xffff into unallocated space.
0x1000 - 0x10ff : AAAAAAAA... (Buffer 1)
0x1100 - 0x11ff : AAAAAAAA... (Buffer 2)
0x1200 - 0x3fff : AAAAAAAA... (Random WinAPRS stuff)
0x4000 - 0x4007 : AAAAAAAA... (Structured Exception Handler)
0x4008 - 0xffff : AAAAAAAA... (Random stuff)
UNALLOCATED : Access Violation!
This caused an access violation and triggered an exception. You’ll notice that the Structured Exception Handler (SEH) was also overwritten with whatever was in buffer 1. When the exception was triggered, the OS looked at the SEH record to see what memory address the CPU should point to to execute instructions to attempt to handle the exception. This record was overwritten by my payload (AAAA or hexidecimal 0x41414141 in the above example). The CPU would therefore attempt to execute instructions at address 0x41414141. I could therefore point CPU at any memory address I wanted by simply changing those AAAA bytes to an address containing instructions that would allow me to execute my own code. Corelan has a great tutorial on SEH exploits which goes into more detail about how this works.
All of this happens because the NULL byte at the end of my packet/payload gets put into buffer 2 and is therefore overwritten. If the copy loop were to see a NULL byte anywhere, the loop would stop and this exploit would fail. Therefore, my payload could not contain any NULL bytes. This was unfortunate because WinAPRS’ entire memory space started with a NULL byte and WinAPRS did not load any external DLLs. This meant that I could not point the CPU to any known location inside of WinAPRS. Instead, I’d have to point to a operating system DLL. This was another problem because in modern versions of Windows, OS DLL’s all implement Address Space Layour Randomization (ASLR), which means that their address space is randomized each time Windows boots up. There’s no way to predict what address a useful function will have in one of those modules.
I ended up falling back on Windows XP SP3 to build a working proof of concept reverse shell. Windows XP did not have ASLR, so I didn’t have to worry about getting around that mitigation. However, I did come back to Windows 10 later and I managed to get the exploit working. I found that incoming APRS messages were stored in a chunk of heap memory that didn’t include any NULL bytes. The address of this heap memory changed each time WinAPRS was launched, but there were three main ranges of addresses that it always seemed to pick. I found that I could groom the heap by sending about 10,000 packets containing a NOP sled and a POP, POP, RET instruction. This would fill a chunk of heap memory with the instruction I needed to trigger my real payload. I then configured my payload to overwrite the SEH address with an address from that chunk of heap memory. I found that this worked roughly one third of the time. Much less reliable than the Windows XP exploit, but still functional. It also would require sending about 10,000 packets over the course of 2-3 hours… So it’s a bit noisy but it could happen!
There were a bunch of other interesting problems to solve, and I discuss them in more detail in the blog series on Coalfire’s website.
All of my exploit code is available on Coalfire’s GitHub page.