In part one of this series on vulnerability research in ham radio software, we discussed ham radio and digital communications via packet radio. We reviewed some relevant packet radio protocols such as AX.25, APRS, and KISS. We then chose WinAPRS as our target application. In this installment we will cover reverse engineering components of WinAPRS using WinDbg and IDA Pro to look for exploitable memory corruption vulnerabilities along our configured code path. We’ll also modify open-source packet radio software to aid in fuzzing WinAPRS’ KISS TNC input to hit paydirt. Though first, we must configure WinAPRS to work with our specific hardware setup.
- Radio: Icom IC-207
- TNC: Kantronics KPC 9612+ in KISS mode
- Operating System: Windows XP SP3 and Windows 10
- Software: WinAPRS v2.9.0
I found that I had to make a few configuration changes to get WinAPRS working properly with my hardware. First was my callsign.
Then the serial COM port. In my test virtual machine (VM) with my KISS TNC this was COM3 at 9600 baud.
Then I had to enable KISS mode.
Once it was setup and working, I was receiving APRS packets and plotting station locations on the basic included map.
Reversing the Binary
Next, I disassembled the executable in IDA Pro and attached WinDbg to the running process. The first thing I did was to load the narly WinDbg extension and check to see if WinAPRS had any protections in place. It had no protections at all. I also found that it loaded no external DLLs.
The next thing I needed to do was figure out where the data was entering into WinAPRS from the TNC. Normally, I'd find some Win32 API calls to RECV (TCP) or RECVFROM (UDP) and check there, but in this case the data was coming in from a serial port. The target system could theoretically not even be connected to Ethernet. I did some research on Win32 APIs related to serial communications and found that COM ports in Windows are basically treated as files (https://www.xanthium.in/Serial-Port-Programming-using-Win32-API). You first open a handle to the port and then make a call to ReadFile. I used IDA to search for calls to ReadFile and found this one:
I set a breakpoint on this call and waited for an APRS packet to come in through the radio. The breakpoint triggered, which was the first clue I was on the right track. I checked the parameters on the stack to find the address of lpBuffer.
I stepped over the ReadFile call and checked the contents of the buffer to find it was loaded with packet data.
This indicated that I found the correct call to ReadFile and could now trace through the code to hopefully find vulnerable code paths. My first thought was to see if the lpBuffer was susceptible to an overflow. To answer this question, I needed to know how big lpBuffer was, and where the nNumberOfBytesToRead parameter came from. If I could craft a packet that set nNumberOfBytesToRead to a value that was larger than the lpBuffer size, then I might be able to overflow the buffer and gain control of EIP. I started with the lpBuffer size.
I inspected the buffer before the call to ReadFile and found it had been filled with 0xCC characters.
I kept moving down the stack until I found the end of the 0xCC characters.
Some quick math showed that the buffer was 2052 bytes large.
The value for the nNumberOfBytesToRead parameter seemed to come from a variable called Stat.cbInQue.
According to Microsoft documentation (https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-comstat), cbInQue represents the number of bytes received by the serial provider but not yet read by a ReadFile operation. This seemed like good news because it meant that value for the nNumberOfBytesToRead parameter would be set by how much data was sent by the TNC. The assembly code didn't seem to have any checks in place to ensure the data wasn't greater than 2052 bytes long, which meant it was likely vulnerable to a basic overflow. I would just have to send a packet that was greater than 2052 bytes. But how to do that?
The one TNC I had was being used for the receiving station. I needed a way to transmit a packet from a second station that met the specific conditions to trigger the overflow. I did have a Kenwood TH-G71 handheld transceiver with audio jacks for an external speaker and microphone. I studied the radios pinouts in the manual and found that if I hooked up the radio's external mic and speaker ports to a computer, it would automatically key up the radio and start transmitting whatever audio the computer was outputting. It wasn't an ideal solution but would work if I could find a way to build custom AX.25 packets and convert them to an audio file I could play on command.
I investigated the open-source software Direwolf, which is normally used as a soundcard-based radio modem interface. I found that it came with a utility called “gen_packets” that allows you to generate an audio file representing a customized AX.25 packet. This was exactly what I needed, but it had a built-in packet size limitation. I studied the source code and found a way to modify it to allow me to make much larger packets. First, I modified ax25_pad.c:
Then I made a change to gen_packets.c.
After compiling Direwolf, I was able to generate custom packets up to 3000 bytes in size.
I started by generating a packet greater than 2052 bytes, but interestingly after transmitting it over the air, nothing seemed to happen. I couldn't get the ReadFile breakpoint to even trigger. I tried a bunch of packets of varying sizes, and I found that smaller packets seemed to trigger the breakpoint fine, but anything over 1024 bytes long didn't. I connected to my TNC with putty and found that any packets over 1024 bytes were simply not being sent to the PC over the serial port. The KPC 9612+ was limiting the packet size to 1024 bytes. From my research, it does not seem that AX.25 or the KISS protocols have this size limitation. I suspect that the TNC itself has some memory limitation or just a hard-coded size limit for whatever reason. This meant that I wasn't going to be able to overflow the buffer using this hardware.
However, all was not lost. I found that when I generated a packet with 1000 A's, I got an access violation. And after attempting to continue execution, I had somehow gained control of EIP.
Some simple fuzzing had paid off! The CPU’s EIP register points to the memory address of the CPU instruction that is about to be executed. In this case, it now pointed to the address 0x41414141. 0x41 is the hexadecimal equivalent of an ASCII ‘A’ character. My payload contained 1000 A’s, so it was likely that EIP now pointed to a location controllable from my payload. If this were true, I could theoretically modify my payload to contain some other characters that could point EIP almost anywhere. If I could find a way to point EIP to a memory location that contained my own shellcode, the CPU would then execute my own instructions and I could gain remote code execution on the victim machine. But how did this happen? My payload only contained 1000 A’s, which was smaller than the lpBuffer variable size. I didn’t expect this result. There must be some other vulnerability further along in the code.
In Part 3 of the Hacking Ham Radio blog series, we’ll track down the source of this bug and figure out how it led to control of the EIP register. We’ll also discover some limitations in acceptable address values we can place in EIP. We’ll have to make some compromises to get around this problem and continue forward to our goal of obtaining a reverse shell using ham radio.