CVE-2019-7629: RCE in an Open Source MUD Client

A few weeks ago I took the Corelan Advanced class and when I came back, I started poking at some open source projects that I personally use. It was a great exercise and I ultimately ended up with my first CVE. I was a little disappointed I didn’t get to use any of my newly acquired heap exploitation skills, but I was still thrilled to find a remotely exploitable 0-day in an open source project.

What is a MUD?

For those of you who don’t know, MUDs (short for “Multi-User Dungeon” or “Multi-User Domain”) were the precursors to modern day massively multiplayer online RPGs such as World of Warcraft. Most MUDs were 100% text-based and could be played using Telnet or a number of custom clients. Players would log in and create a character, roleplay with other players, slay monsters, and complete quests.

Login screen for Legends of the Jedi MUD, one of my personal favorites

Third-party clients, such as WinTin++ shown above, offer several benefits over the standard Telnet client, such as support for scripting, colored text, macros, and other features that make it more convenient to play the game. WinTin++ in particular has a ton of features and happens to be written in C, so it looked like a great first target for my vulnerability research. Turns out I was right!

Triggering the Stack Overflow

I considered patching the TinTin++/WinTin++ source so I could fuzz it with AFL, but it didn’t turn out to be necessary since I found the first crash after about 5 minutes of manual testing. Here is the Python code used to trigger the crash:

In short, we set up a malicious MUD server on port 4000 and send 60,000 “A”s to WinTin++ after it connects. I’m using a Windows 7 SP1 machine and WinTin++ 2.01.6, which was the latest official version at the time. Here is the crash on the Windows client:

First crash with 60,000 A’s

We can see in WinDBG that the crash happened because we tried to dereference 0x41414179, which is obviously an invalid address. We have clearly corrupted something, and using “kb” to print the callstack shows thousands of A’s instead of valid function addresses, so it looks like a basic stack overflow. To help us develop our exploit, let’s use mona.py to look at loaded modules and see if WinTin++ is taking advantage of ASLR and other optional exploit mitigations:

Output of mona’s modules command

We can see that WinTin++ and the cygwin1.dll library that ships with it were compiled without ASLR, and since DEP is opt-in by default on Windows 7, we won’t have to bypass any exploit mitigations to exploit this vulnerability.

Finding the Offset

Looking at the crash, we can see that the application tries to read from an address that was clobbered by our stack overflow and thus terminates with an illegal memory access error. We have overwritten the saved return pointer at EBP, so we will have control of execution once the vulnerable functions returns.

To get started, we’ll use mona to generate a cyclic pattern so we can calculate the offset of our saved return pointer:

We then replace our buffer of A’s in the payload with the cyclic pattern and trigger the crash again. The register state now looks like this:

We find the offset at 33432 using mona’s pattern_offset command:

The Vulnerability

Now that we have the offset of EBP, let’s stop and take a look at the source code to see if we can figure out what is going on that causes the crash in the first place. I used the address of the instruction we are crashing at to locate the function in Binary Ninja. I discovered that the application is crashing in parse.c: do_one_line():

“do_one_line” takes the MUD output (char *line), creates another buffer of 32,000 bytes on the stack, and passes both buffers to “strip_vt102_codes”. The problem here is that *line can be up to 64,000 bytes, as shown in the calling function “process_mud_output”:

Furthermore, if we look at strip_vt102_codes, it does not check the size of either buffer:

So, when the raw MUD output being parsed (*line) is greater than the size of the stripped buffer, we overflow the stripped buffer and start smashing the stack in the caller. Note that we aren’t overwriting strip_vt_102_codes’ return address, even though that’s where the out-of-bounds write happens. *buf is declared in do_one_line, and *pto simply points to it. Because of this, we are overwriting one function’s return address from another function.

“skip_vt102_codes” strips out VT102 escape codes from the buffer before copying it, which originally caused a lot of problems for me when I was trying to call functions in the bundled cygwin1.dll module, since the base address included valid VT102 codes. Another issue that tripped me up for longer than I would like to admit is that we have to null terminate our buffer to exit the while loop in strip_vt102_codes. This must be done so we don’t start clobbering the stack values being checked in the if (!HAS_BIT) statements in do_one_line and segfault before reaching the end of the function.

Finishing the Exploit

Now that we understand the vulnerability, it’s time to build our exploit. We’re going to send 33432 bytes of junk + our shellcode, followed by an address pointing into our nopsled, followed by a null byte. Fortunately, the base address in the tt++ module starts with a null byte, which takes care of null-terminating our buffer and exiting the strip_vt102_codes loop right when we need to. The final exploit looks like this:

And we see our lovely calculator:

Calc.exe proof-of-concept

Note, I did not bother to bypass ASLR and DEP–the application did not opt-in to either of them, so it wasn’t necessary on Windows 7. In theory, ASLR should be trivial to bypass as the application saves function addresses and their corresponding arguments at the beginning of each function to provide debugging information in the event of a crash. If you can find the address in the debug stack, you should have all you need to defeat ASLR. DEP is a little trickier since both the tt++.exe and cygwin1.dll modules have null bytes or VT102 code sequences in their base addresses, so building a ROP chain without mangling and prematurely terminating your buffer could prove difficult.

I contacted the developer, who responded immediately and pushed a fix within a matter of hours.

Disclosure Timeline

  • 2/5/2019: Vulnerability discovered
  • 2/7/2019: Developer contacted
  • 2/7/2019: Developer responded and pushed a fix to the dev branch
  • 2/7/2019: CVE-2019-7629 assigned
  • 2/9/2019: Developer announced official release containing the bugfix
  • 2/19/2019: Vulnerability publicly disclosed