CVE-2019-12181 patch analysis

TL;DR

The patch in Serv-U FTP server version 15.1.7 that fixes my vulnerability (CVE-2019-12181), does so properly. Continue reading to for a walkthrough of the patch analysis. This blog post depends on knowledge and context from this blog post, please read it before continuing.

Motivation

I was told by a smart and trusted @yoavalon that failed patches are a norm in our industry, and I should therefore ensure the vulnerability I found is properly fixed in the latest allegedly safe version of the program.

Potentially Inadequate Fixes

It is possible (and depending on the security mindset of the company, maybe even probable) to unsuccessfully fix a vulnerability or even introduce a new vulnerability in a patch. For example, if some filtering logic is added to block malicious input from the user, it is worth ensuring the filter can’t be bypassed.

Analysis Process

The first thing I did was check if my initial POC code worked on the patched Serv-U 15.1.7. Thankfully it didn’t. This means that at the very least the Serv-U programmers gave the vulnerability some attention. Now I just need to make sure there isn’t a way to bypass the patch and that the patch doesn’t introduce a new vulnerability. With both the un-patched Serv-U 15.1.6 and the patched Serv-U 15.1.7 binaries at my disposal, I began searching for the exact location of the vulnerability’s fix.

Initial Strategy

The classic methodology for learning more about the delta between two executables is called binary diffing. In binary diffing, a smart algorithm analyzes each function in each of the executables and through heuristics such as, a function referencing the same unique string in both of the executables, the algorithm knows to match equal functions. To learn more about binary diffing view this slideshow.

In my research I used a binary diffing tool called Diaphora since it’s free and easy to integrate into IDA 7. Diaphora intelligently analyses each function of each executable and gives each function a “Match Ratio” between 0 to 1. The Match Ratio reflects how close the closest function in the other executable is to the current function in the current executable. A score of 1 shows a perfect match, and a score of 0 shows absolutely no match.

Since I was searching for a security patch, I was either looking for a new function added with logic to parse out malicious input (for CVE-2019-12181, that logic would be to filter out anything that isn’t the program’s filename), or I was looking for a function that changed by adding filtering logic to it directly (instead of putting the logic in a separate function). If a function changed, for example by logic being added to it, the change would be reflected in a Match Ratio less than 1.

Diaphora Partial Function Matches
Partial Function Matches found by Diaphora. Notice the range of the Match Ratio labeled "Ratio" varies and is relatively close to 1.
Diaphora Unreliable Function Matches
Unreliable Function Matches found by Diaphora.  Notice the range of the Match Ratio labeled "Ratio" varies and is relatively close to 0.

After looking at Diaphora’s results above, I realized too many functions changed for me to review them each manually. In addition, looking at the release notes we can see more was added than just the patch to CVE-2019-12181, thus explaining all the changes. After I realized using binary diffing will be more time consuming than I had hoped for, I decided to take a new approach to the challenge.

Winning Strategy

I took a step back and realized:

  • The only way to correctly fix this vulnerability (CVE-2019-12181) is to not depend on the user for the path of the current executable running

Which gave me 2 theories about where the program is now getting the path of the executable:

  1. The path is a hard-coded value in the program
  2. The path is calculated during run-time through a system call

Theory #1

Disproving theory #1 was as simple as looking for the path “/usr/local/Serv-U/Serv-U” in the program and finding 0 results.

Zero occurences of string in IDA Pro search
Zero results when searching for "/usr/local/Serv\-U/Serv\-U" in the program

Having 0 occurences of the program path in the program fairly strongly proves that the path isn’t hard-coded in the code, and is instead generated at runtime.

Theory #2

Proving or disproving theory #2 was more complex. However, I was able to simplify the problem by only looking at certain code. For our purposes we don’t care about the grand majority of the code in the program: we only care about the previously vulnerable code.

Trying to prove or disprove theory #2 by finding a new system call added to the patched program, I ran both the patched and un-patched version with strace and its -c flag to get a log of the system calls called. After saving the log to a file, I used BeyondCompare to look at the differences between the two files.

strace system call output comparison between patched and unpatched server
System calls executed by the program before and after the update

As we can see in the picture above, there are a lot of differences (indicated by the red lines). The results weren’t as clean as I had hoped. BeyondCompare was (rightfully) showing a lot of changes between the files because of different values in columns such as “% time” or “seconds” which aren’t relevant to us. I cleaned up the table by leaving only the function names and compared the results.

cleaned strace system call output comparison between patched and unpatched server
System calls executed by the program before and after the update (cleaned)

Viewing the cleaned results in the picture above, we can easily see there has been one new system call added to the code: readlink! This fits perfectly with Theory #2: that a system call has been added to get the program’s file path. Lets use IDA Pro to see how and where readlink is being used.

IDA Pro decompilation output showing readlink call
IDA Pro decompilation showing call to `readlink` (line 25)

In the decompiled code above, we see that in line 25 (highlighted in red) readlink is used to read where the symbolic link /proc/self/exe points, and saves the path into the variable buf. Next, in line 31 (highlighted in red) we see the path is saved into the singleton variable representing the server.

Since /proc/self/exe is a symbolic link provided by the kernel pointing to the current executable reading the link, reading this link is an excellent and safe way to get the current path of the file running. And, if indeed the variable buf initialized from /proc/self/exe is later used to reference the current file (in contrast to before the patch where argv[0] was used), then CVE-2019-12181 is fully and correctly patched!

After more reversing, debugging, and playing around with the variable buf, I can confirm the data from readlink("/proc/self/exe") is indeed the variable used to reference the current file when calling system(), thus

Conclusion

  • The patch for CVE-2019-12181 adequately patches the vulnerability
  • It’s fun to use advanced tools such as Diaphora for complex tasks such as binary diffing, but sometimes a much more efficient and simple approach should be used.