Finding Unicorns: When the C++ Compiler Writes the Vuln
February 28, 2019 | Simon ZuckerbraunIt’s entirely commonplace for vulnerabilities to arise from C++ programming errors. It’s a rarity, though, when a programmer writes a correct C++ program and the compiler transforms it into object code containing a vulnerability. That’s what I experienced this past October, though, when a tool I wrote crashed and I discovered that the fault lay with the Visual C++ compiler. Microsoft addressed our vulnerability report as CVE-2019-0546, though, as we will explain, it is still not fully patched.
Where I Found the Unicorn
I was writing some instrumentation for an x86 module that had been compiled with a Borland toolchain. The instrumentation framework expected a callback function that would perform the task of calling the original function within the target module. The target function’s calling convention was incompatible with Microsoft Visual C++, so my callback needed to contain custom __asm
code. To streamline matters, I defined the callback as a lambda, like this:
The lambda defines a callback function that has one parameter, specifying the address of the original function. The callback copies the parameters to the original function (m, s
) from captured variables and places them in registers as expected by the original function. (Note that the first parameter goes into @eax
. This is not a Microsoft-compatible calling convention, hence the need for __asm
.) Next, it calls the original function. Finally, it copies the return value of the original function from @eax
into captured variable r
.
The compiler accepted this code without complaint, but weirdly, the compiled code did not work as expected. The instructions generated did not reference the correct stack locations for the captured variables. When reading from the captured variables, incorrect stack locations were accessed, potentially leaking sensitive stack data. When writing the captured variable r
, the write went to an improper location on the stack, potentially corrupting data or control flow.
The bug is triggered by a lambda expression meeting these two conditions:
- The lambda has an implicit capture, either by reference or by copy.
- The lambda contains an
__asm
block.
The PoC
Soon I put together a self-contained PoC. This is for Visual Studio 2015, and is intended to be compiled for the Release x86 configuration:
Note that the lambda correctly accesses variable x
, because x
is a global and not a stack-based variable. However, when it writes into variable y
, it writes to the wrong stack address and corrupts the @ebp
value on the frame. When control returns to main
, @ebp
contains the corrupted value of 0xdeadbeef
.
Here’s the resulting crash:
Visual Studio 2017 is also affected.
The Patch
Interestingly, although this bug impacts both Visual Studio 2015 and Visual Studio 2017 (and possibly other versions that we have not tested), Microsoft only released patches for Visual Studio 2017. The vulnerability shown above is still present today in the latest update of Visual Studio 2015 (version 14.0.25431.01 Update 3). When asked about this decision, Microsoft stated,
“This specific report, CVE-2019-0546, is about disallowing inline assembly inside C++ lambda bodies. The said vulnerability is about downloading and running untrusted code, which has always existed in all releases prior VS2017 Update 9 that supported lambdas. The scenario is not common coding practice and considering we've always had this in all our prior releases and have not seen any evidences of exploit it would not make sense to port the change as hotfix from 15.9 to prior VS releases.”
Also, I find it a bit amusing that Microsoft’s fix for Visual Studio 2017 was to remove support for __asm
blocks within lambdas. Now, if you try to compile code such as the above PoC on Visual Studio 2017, you get the following compiler error:
So, I am now the proud owner of both a CVE in the Visual C++ compiler as well as a brand new CXXXX compiler error. :)
While this may be an uncommon scenario, we still believe it worth patching and hope Microsoft reconsiders in the future. It should also be noted that while Microsoft rated this bug as Moderate, other bugs in Visual Studio have received Important severity ratings. If you’re still on the fence about deploying this update, we would consider it Important since it could allow for attacker-controlled code to execute at the level of the logged on user.
Conclusion
It’s long been recognized as a theoretical possibility [PDF] that a compiler could surreptitiously introduce a backdoor or vulnerable behavior into software at compile time. In practice, it is exceedingly rare to hear of a compiler introducing a vulnerability into 100% correct, non-malicious code, whether maliciously or, as in this case, as a result of a compiler bug. On an extraordinary day out in the wilds, though, you might find a unicorn.
You can find me on Twitter at @HexKitchen, and follow the team for the latest in exploit techniques and security patches.