Patch Diffing Applied

pexels-instawalli-169789 Photo by InstaWalli on Pexels

In this section we leverage patch diffing via Ghidra Version Tracking to analyze the differences across the previously identified Windows Print Spooler CVEs (1048,1337,17001). We will look at each diff individually using the lens of patch diffing as an attempt to get some clarity as to why it took so many attempts to get it right. As we go, we will try to pull in some of our previous CVE Analysis to get some context for each patch.

gantt

    title Patch diffing sessions comparing N-1,1048,1337,17001
    dateFormat  YYYY-MM-DD
	axisFormat %Y-%m
	
	section Relevant CVEs
	N-1 :a1, 2020-04-14, 2020-05-11 
    CVE-2020-1048 :a2, 2020-05-12, 2020-06-08
	CVE-2020-1337 :a3, 2020-08-11, 2020-09-07
	CVE-2020-17001 :a4, 2020-11-10, 2020-12-07
	
    section Patch Diffing Sessions
    Session1  :l1, 2020-04-14, 2020-06-08 
    Session2 :l2, 2020-06-08, 2020-09-07
	Session3 :l3, 2020-09-07, 2020-12-07

Assuming we already have the prep work done (Ghidra loaded with the files needed for diffing and symbols downloaded), we can get started. It’s time now to create the a Version Tracking session for each diff .


Table of Contents


Workflow


graph TD;

subgraph Prep
	A[Create Session] --> B[Load Binary Version A];
	A --> C[Load Binary Version B];
	B --> D[Pass Preconditions];
	C --> D;
	D --> E[Auto Analyze A/B];
end	
subgraph Evaluation


	E --> F[Choose / Run Correlators];
	F --> G[Generate Associations];
	G --> H[Evaluate Matches];
	H --> I[Accept Matching Functions];
	I --> J[Discover Enough Differences];
	J -- Yes --> K[End];
	J -- No --> F;
end

The prep stage for each session is the same. Find the binaries to diff and load them into Ghidra for analysis. The evaluation takes place in the latter half of the workflow is the meat of the process. For each session we will try to discover all the changes, additions, and deletions. From this information, combined with our CVE analysis context, we will try to make sense of each of the patches.

CVE-2020-1048 Version Tracker - Session 1

gantt

    title Patch diffing sessions comparing N-1,1048,1337,17001
    dateFormat  YYYY-MM-DD
	axisFormat %Y-%m
	
	section Relevant CVEs
	N-1 :a1, 2020-04-14, 2020-05-11 
    CVE-2020-1048 :a2, 2020-05-12, 2020-06-08
	CVE-2020-1337 :a3, 2020-08-11, 2020-09-07
	CVE-2020-17001 :a4, 2020-11-10, 2020-12-07
	
    section Patch Diffing Sessions
    Session1  :crit, l1, 2020-04-14, 2020-06-08 
    Session2 :l2, 2020-06-08, 2020-09-07
	Session3 :l3, 2020-09-07, 2020-12-07

In the first session we will compare a vulnerable localspl.dll (N-1) and the patch for CVE-2020-1048. The prep steps (1 + 2) will be glossed as have been covered in detail in the overview of the Version Tracker Workflow. This section will focus on the running of the correlators and evaluation.

Binaries compared:

  • N-1 –> localspl.dll (6.1.7601.24383)
  • CVE-2020-1048 –> localspl.dll (6.1.7601.24554)

Prep

  1. Setup and Prep Step 1 - Load both binaries into the session. load_vt_session
  2. Auto-analysis Step 2 - Ensure you have symbols loaded prior to analysis. auto-analysis-ghidra-code-browser

Correlators + Evaluation

  1. Run Correlators Step 3
    1. Start with Automatic Version Tracking automatic-version-tracking-icon.
  2. Evaluating Matches Step 4 - The results are pretty good. At least for finding new and deleted functions. After running the automatic correlators, all functions from the N-1 binary were matched with a function in the new binary. This can be derived from the fact that the lower pane Version Tracking Functions Table is empty for the source binary once the “Show Only Unmatched Functions” filter is applied (ie no functions were deleted).

    vt-functions-filter-new-short Left source pane empty after “Show Only Unmatched” filter applied

    From the same filter we can identify two brand new functions in the patched destination binary, that have no match in the vulnerable version.

    vt-functions-filter-new-deleted

    • IsPortNamedPipe
    • IsValidNamedPipeOrCustomPort

    These functions seem quite relevant for CVE-2020-1048 if we go back review our our CVE-2020-1048 Analysis requirements:

    Requirements

    what stars needed to align?

    • User context
      • unprivileged users can add printers (and assign a printer port)
      • ability to assign a printer port to an arbitrary file path.
        • Several APIs are able to do this. Some clients have security checks for the path PortIsValid, some do not. This is a Client Side Port Check Vulnerability.

    We aren’t quite done, as we can’t yet see the changed functions. If you remember, Ghidra has some deficiencies with it’s default correlators. Currently, all the matches contain either a score of 1.0 or 0 (implied matches have a score of 0).

    vt-functions-filter-match-score

  3. Time to Boost The Signal - Once the default correlators have run. It is now time to run one of the additional correlators offered by PatchDiffCorrelator.

    ghidra_correlators_list_pdiff

    Running the Bulk Basic Block Mneumonics Match (File –> Add to Session.. –> Check Bulk Basic Block Mnemonics Match -> Click Through Default Options) produces new matches, and with the default options, will only run the correlator on already accepted matches. More details on which correlator to run, and tips as to which options can be found on the github page or from Boosting The Signal section.

    version-tracker-matches-result-1048-lcmcreateportentry

After filtering out perfect matches, only one change is found. LcmCreatePortEntry is the only row with a score less than 1.0. This is an ideal scenario. Two new functions have been added, and only one function was changed.

Taking a quick look at LcmCreatePortEntry decompilation in both the source and destination code browser, we can see the changes new code.

n-1_1048-vscode-diff Decompilation from Ghidra Code Browser for LcmCreatePortEntry diffed with VScode

A bit easier to simply take a look at the Function Call Tree Results: ghidra-call-tree-diff-n-1-1048

Function Call Tree from both source and destination for LcmCreatePortEntry diffed with VScode

LcmCreatePortEntry has added some extra checks:

  • PortIsValid - which existed in the vulnerable version, but was not called within this function.
  • IsValidNamedPipeOrCustomPort - A new function introduced in the same update as CVE-2020-1048. The other new function IsPortNamedPipe is called within this new one.

CVE Analysis Context

From our CVE-2020-1048 Analysis:

PrintDemon is an elevation of privilege (EoP) vulnerability that exists in the Windows Print Spooler service as it improperly allows arbitrary file writing on the file system

The primary issues with CVE-2020-1048 were:

  1. Client Side Port Check Vulnerability - only checking whether or not a port is valid on the client side (and there was a code path on the server side that didn’t call the check)
  2. Self Impersonation (SYSTEM) - The Spooler impersonated itself when it didn’t have the correct user context (after service restarts).

This patch added the PortIsValid check to LcmCreatePortEntry, which ensures the server checks that the port (think file path to be written to) is valid on the server side as well.

This is confirmed the public report:

If the system is patched, however, this won’t work. Microsoft fixed the vulnerability by now moving the PortIsValid check inside of LcmXcvDataPort. Source

Notice, there are two primary issues with only one addressed.

Session 1 - Patch Diff Summary

New FunctionsDeleted FunctionsChanged Functions
- IsValidNamedPipeOrCustomPort
- IsPortNamedPipe
NoneLcmCreatePortEntry

CVE-2020-1337 Version Tracker - Session2 - Speed Round

gantt

    title Patch diffing sessions comparing N-1,1048,1337,17001
    dateFormat  YYYY-MM-DD
	axisFormat %Y-%m
	
	section Relevant CVEs
	N-1 :a1, 2020-04-14, 2020-05-11 
    CVE-2020-1048 :a2, 2020-05-12, 2020-06-08
	CVE-2020-1337 :a3, 2020-08-11, 2020-09-07
	CVE-2020-17001 :a4, 2020-11-10, 2020-12-07
	
    section Patch Diffing Sessions
    Session1  :l1, 2020-04-14, 2020-06-08 
    Session2 :crit, l2, 2020-06-08, 2020-09-07
	Session3 :l3, 2020-09-07, 2020-12-07
	


Binaries compared:

  • CVE-2020-1048 –> localspl.dll (6.1.7601.24554)
  • CVE-2020-1337 –> localspl.dll (6.1.7601.24559)

For this session the same steps will be applied with a bit less hand holding.

Running Correlators

  1. Run Automatic Version Tracking
  2. Run Bulk Basic Block Mneumonics Match Correlator

session2-version-tracking-cve-2020-1337

Evaluation

Identifying New and Deleted Functions

session2-version-tracking-cve-2020-1337-new-deleted Two new function found. Filter applied to filter out garbage filter names

There were no deleted functions (empty left pane). As for new functions we have:

  • IsPortAlink
  • IsPortANetworkPrinter

Identifying Changed Functions

session2-version-tracking-cve-2020-1337-changed

The were more changed functions this time around. That being said, there seemed to be some analysis confusion in Ghidra. The Bulk PatchDiffCorrelator found changes in the following functions.

ScoreFunc LabelSource SizeDest SizeComment
0.931InitializePrintMonitor2567566No functional differences. Compiler?
0.796LcmStartDocPort825951 
0.762FdiCabNotify596716Added WPP (tracing)
0.619PortIsValid295371 
0.107entry45858Analysis error. Both are calls to __DllMainCRTStartup
Function Call Trees For New Functions

Taking a look at the function call trees for the new functions IsPortAlink and IsPortANetworkPrinter we can quickly see where they have been placed.

isportalink-1337

isportanetwork-1337

They line up exactly with the functions that have changed (LcmStartDocPort and PortIsValid.

CVE Analysis Context

CVE-2020-1337 was a bypass for CVE-2020-1048. The primary issue with the patch for CVE-2020-1048 was that it:

Unfortunately, the patch has two main issues:

  1. Patch leaves the system vulnerable to pre-existing ports.
  2. Even worse, the check of user read/write permissions on the given path is performed only on port creation event. Source

CVE-2020-1337 used a junction directory to exercise a TOCTOU vulnerability that remained after the CVE-2020-1048 patch. The check PortIsValid added in LcmCreatePortEntry for CVE-2020-1048 was only checking the port on creation. After creation of the port, the attacker could change the directory

PS C:\Users\user> New-Item -ItemType Junction -Path "C:\Users\user\userDir\"  -Target "C:\Windows\System32"

Powershell command to create a reparse point

In the CVE-2020-1337 patch the PortIsValid call is made when the port is added (Time of Check) as before (with an add PortIsALink check, hence the change discovered). port-is-valid-calltree

LcmStartDocPort was also updated.

compare-refs-lcmstartdocport-1337 Copy-paste from the Function Call Tree Window diffed in vscode

The LcmStartDocPort call is performed when the port path will be written to (Time of Use). They added several port checks to prevent an invalid port. It seems like they solved the TOCTOU issue. But again, that wasn’t the only issue.

Session 2 - Summary

New FunctionsDeleted FunctionsChanged Functions
- IsPortAlink
- IsPortANetworkPrinter
None- LcmStartDocPort
-PortIsValid

CVE-2020-17001 Version Tracker - Session3

gantt

    title Patch diffing sessions comparing N-1,1048,1337,17001
    dateFormat  YYYY-MM-DD
	axisFormat %Y-%m
	
	section Relevant CVEs
	N-1 :a1, 2020-04-14, 2020-05-11 
    CVE-2020-1048 :a2, 2020-05-12, 2020-06-08
	CVE-2020-1337 :a3, 2020-08-11, 2020-09-07
	CVE-2020-17001 :a4, 2020-11-10, 2020-12-07
	
    section Patch Diffing Sessions
    Session1  :l1, 2020-04-14, 2020-06-08 
    Session2  :l2, 2020-06-08, 2020-09-07
	Session3  :crit, l3, 2020-09-07, 2020-12-07
	


Binaries compared:

  • CVE-2020-1048 –> localspl.dll (6.1.7601.24559)
  • CVE-2020-1337 –> localspl.dll (6.1.7601.24562)

Running Correlators

vt-session3-17001

Evaluation

Identifying New and Deleted Functions

vt-session3-17001-new-deleted Two new function found. Filter applied to filter out garbage filter names

No deleted functions (empty left pane). New functions:

  • IsMissingUNCPortsServicingEnabled
  • IsSpoolerImpersonating
  • IsValidSpoolDirectory

Identifying Changed Functions

vt-session3-17001-changed

ScoreFunc LabelSource SizeDest SizeComment
0.957LcmCreatePortEntry430443 
0.941DebugLibraryMalloc265277Minor changes.
0.922SplSetPrinterDataEx872927Added IsValidSpoolDirectory
0.913PortIsValid371419 
0.909LcmStartDocPort948983Added IsSpoolerImpersonating
0.654DoAddPort276399Added IsMissingUNCPortsServicingEnabled

Adding CVE Analysis Context

CVE-2020-17001 was detailed by James Forshaw in his bug report. He details yet another way to break the path validation by using a UNC path for the port assignment. CVE-2020-17001

For this session we once again have quite a few changed functions. As we recall from CVE-2020-1048, there were two primary issues.

The primary issues with CVE-2020-1048 were:

  1. Client Side Port Check Vulnerability - only checking whether or not a port is valid on the client side (and there was a code path on the server side that didn’t call the check)
  2. Self Impersonation (SYSTEM) - The Spooler impersonated itself when it didn’t have the correct user context (after service restarts).

CVE-2020-17001 is yet another way to circumvent the first issue (port validation). At this point, it is pretty clear that they have added IsSpoolerImpersonating to stop printing to a port at all if spooler is running as SYSTEM (If it is not Impersonating). After several attemps, it seems that they have finally fixed the root cause.

Session 3 - Summary

New FunctionsDeleted FunctionsChanged Functions
- IsMissingUNCPortsServicingEnabled
- IsSpoolerImpersonating
- IsValidSpoolDirectory
None- LcmCreatePortEntry
- SplSetPrinterDataEx
- PortIsValid
- LcmStartDocPort
- DoAddPort

Aside - Patches Are Not Necessarily Atomic

gantt

title Multplie CVEs Per Security Update
dateFormat YYYY-MM-DD
axisFormat %Y-%m

section CVE Release Dates
section 2020-Nov
CVE-2020-17042 :crit, cve16, 2020-11-10, 30d
CVE-2020-17014 :crit, cve17, 2020-11-10, 30d
CVE-2020-17001 :cve18, 2020-11-10, 30d
section 2020-Sep
CVE-2020-1030 :cve22, 2020-09-08, 30d
section 2019-Mar
section 2020-Aug
CVE-2020-1337 :cve19, 2020-08-11, 30d
section 2020-May
CVE-2020-1070 :crit, cve20, 2020-05-12, 30d
CVE-2020-1048 :cve21, 2020-05-12, 30d
section 2020-April
N-1 :n1, 2020-04-14,30d

    section Patch Diffing Sessions
    Session1  :l1, 2020-04-14, 2020-06-08 
    Session2 :l2, 2020-06-08, 2020-09-07
	Session3 :l3, 2020-09-07, 2020-12-07
	

The CVEs anlayzed for each session across security updates were not always the only changes. Taking a look at the CVEs with there security updates, we can see that CVE-2020-1070 was patched alongisde CVE-2020-1048. We did no analysis of CVE-2020-1070 (as we had no public articles or insight into 1070), but based on the function names added, I bet it had something to do with an issue named pipes.

  • IsValidNamedPipeOrCustomPort - A new function introduced in the same update as CVE-2020-1048. The other new function IsPortNamedPipe is called within this new one.

It could be beneficial to try and understand CVE-2020-1070 to understand the vulnerability behind that CVE and learn about it’s vulnerability class. To patch diff in the dark is an excellent way to analyze vulnerabilities that haven’t had as much fanfare or public exposure.

Additionally, besides other CVE updates, there are sometimes feature (vs security) updates that coincide with CVEs. In that case expect more functions to discover and potential non-security changes.

Conclusion

There you have it. Binary truth. It isn’t magic, simply focused. Patch diffing gives you the ability to go and see for yourself what has changed. When the patch is specifically related to security, you can bring out the exact code changes that were made to patch a vulnerability. When you combine the changes made with what you know from CVE analysis, the picture becomes that much clearer. These CVEs were chosen to point out that patches aren’t perfect. It took Microsoft several tries to get it right. Really, until the root cause of a vulnerability is fixed, there always seems to be a workaround. In the next section, we will dive into an introductory lesson for root cause analysis. We will take a high level view, combining what we know from our CVE analysis and patch diffing to help us always seek out the root cause.


CVE North Stars Map


graph TD;

classDef current fill:#00cc66;

F:::current;
A1[N-1] --> |"localspl.dll (6.1.7601.24383)"| F;
A[CVE-2020-1048] --> |"localspl.dll (6.1.7601.24554)"| F;
B[CVE-2020-1337] --> |"localspl.dll (6.1.7601.24559)"| F;
C[CVE-2020-17001] --> |"localspl.dll (6.1.7601.24562)"| F;
G[CVE Analysis] --> F;
F[Patch Diffing + CVE Analysis];


Next section: Root Cause Analysis