Authenticode is a Microsoft code signing technology designed to guarantee the origin and integrity of an application. The core principle of its integrity verification system is code immutability. In other words, once an application is signed, its code cannot change without breaking the envelope integrity. This way, the user is guaranteed that the only code they are executing is the one created by the software publisher that signed it.
Portable executable is probably the most prolific application format used for code distribution. It can be found on a wide variety of hardware and software platforms. That’s why it is no wonder that many systems, other than its native Windows, end up supporting it in one way or another. On Linux, and its various distributions, it can be found both in the firmware and the user mode layer. As UEFI drivers for former, and as .NET Mono applications for latter.
Regardless of how the format got onto the platform, it is usually accompanied by its digital signing system, Authenticode, whose entire parsing and integrity validation procedures need to be completely reimplemented for portability. A daunting task by itself --- only amplified by the lack of proper documentation.
Both Authenticode and the executable format it is signing are extremely complex in their design. Mistakes and oversights in their parsing are, therefore, very common. And it is no wonder that certain edge cases even get mishandled the exact same way across different projects. Breaking the Authenticode security model is a cautionary tale of handling malformations.
A typical portable executable file consists of headers followed by an arbitrary number of sections and extra data appended after them. This extra appended data is called an overlay, and it is not a part of the image once the file is loaded in memory. Overlay is where the Authenticode signature resides when the executable image is signed. And it’s usually the last entry within a structured portable executable file.
Authenticode integrity validation is, therefore, a function of hashing the content which precedes the signature. Only two very small regions are excluded from this check. Checksum of the portable executable (a four-byte region), and the information about the Authenticode signature (an eight-byte structure containing its location and size). Neither of those regions have anything to do with the application code.
But what if they did? If it was possible to misappropriate one of those regions, it would be possible to change the code of the application after it is signed.
Authenticode signature location is perfect for such misuse. Its eight byte region describes the physical location of the signature within the file. The first four bytes define its offset, and the last four its size. This same combination of the fields can be found within the section header entry, where they describe the physical location of the section within the file. But, within the section header, these fields are in the inverse order. Here’s how these two structures look like side by side.
typedef struct _IMAGE_SECTION_HEADER { BYTE Name[8]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; |
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; |
Portable executable header structures defined by winnt.h
If overlapped, this pair of header fields would point to two different parts of the file, with their sum being the same. Since the sum of these values is the same, the regions would always overlap. And because the sections are loaded from file on disk, this overlap would map the certificate in memory. This would break the assumption of Authenticode being an object present only on disk, and never in memory.
Making these fields overlap is a little more difficult. What’s important to know at this point is how portable executable structures are laid out in memory. This ordering is illustrated by the following graph.
The key part is where the section table structure is located. While there are a few ways to calculate its location, the following one is used by the official SDK headers.
#define IMAGE_FIRST_SECTION( ntheader ) ((PIMAGE_SECTION_HEADER) ((ULONG_PTR)(ntheader) + FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) + ((ntheader))->FileHeader.SizeOfOptionalHeader )) |
\ |
The formula takes into consideration that the optional portable executable headers are, well, optional. Allowing for their expansion, the previous structure, file headers, defines their size. With the intent of having a standard way to expand this structure, the expectation is that the value SizeOfOptionalHeaders is always greater than the minimum defined by the standard. However, that isn’t checked by the formula above. It simply expects that the section table is always after the optional headers.
Fiddling with the math a bit, it becomes obvious that for the section headers and the security data directory to overlap perfectly, the value which the SizeOfOptionalHeaders field should have is 0x70. That positions the section and the Authenticode signature location pointers in the described inverse order. And, since this region is skipped during integrity validation, its content is arbitrary.
During the preparation phase, for this Authenticode integrity validation bypass, a simple portable executable file is created. It has a single section with no physical content. Its security directory and section headers overlap. Its image size is sufficient to hold the certificate content after the image is signed. Its code and dependency information are placed within the region of the file that doesn’t yet exist, but will after the file is signed. All of which would make it look like the following image.
And that is the key to breaking the integrity validation, placing the code within the region not covered by integrity checks. Which is exactly what the region after the Authenticode signature is. Authenticode allows itself to be expanded in order to support more advanced code signing features such as cross and dual signing, both of which are features designed to enhance the overall security of the code signing process.
However, arbitrary code and data can be placed within that region. The tricky part is getting that code and data loaded into memory. And that is exactly what we’ve been able to do with the procedure described above.
The following image shows both the original and the modified files with their Authenticode signature validation responses in two open source tools.
osslsigncode authenticode integrity validation
chktrust authenticode integrity validation
Both of these open source tools are victims of improper documentation. There are some unwritten rules when it comes to authenticode integrity validation, prerequisites for which are header malformation checks. Those prohibit overlapping, required by this attack, to occur in the first place. Such signature gets discarded as invalid, which is why Microsoft’s authenticode validation fails to find it even within these specially crafted files.
Unfortunately lack of such header malformation checks is very common. It is, therefore, more than likely that other projects that implement their own Authenticode validation suffer from the same shortcomings. It can be easily checked by the developers and maintainers of such project.
While interesting, this attack isn’t very practical. Because there’s a preparation phase involved, no existing Authenticode signature can be modified without breaking the envelope integrity. Every application signed before this publication remains just as secure as it ever was. Following the best development practices and secure development lifecycle procedures will still result in organizations publishing safe code.
ReversingLabs solutions understand the dangers of extended Authenticode signatures, and they actively prohibit whitelisting when such cases are encountered. More importantly, they alert the developers that such behavior introduces risk, and that it should be avoided. Those notifications are just a small piece of a larger picture in which ReversingLabs solutions enhance secure code development.
Code signing is here to stay, and doing it properly is more important than ever.
Affected products:
osslsigncode
chktrust (.NET Mono toolchain)
Disclosure timeline:
06/24/2019 - Found no contact for osslsigncode (dead project)
06/24/2019 - .NET Core plans on rewriting chktrust before inclusion
Demonstration files:
mono.signed.A.exe - 47B70A4C484C6A00D186B4028B735B3DA24A8EAC53FCB94A73E52BAB091624EA
mono.signed.B.exe - 087F77C03B2B8080B00FFA2543D908B500F209698831789A319117339FE18E6F
oss.signed.A.exe - DA86BBEFBCB4EB8BD68E98C4AFEF9AE4B0742DEBF153681EB6682B3995E00F8E
oss.signed.B.exe - DEC054A93242308E5A9C4D36ED11D35CD1788AC31E6D75853FAAA3C6E94B536F
Keep learning
- Find the best building blocks for your next app with RL's Spectra Assure Community, where you can quickly search the latest safe packages on npm, PyPI and RubyGems.
- Get up to speed on securing AI/ML systems and software with our Special Report. Plus, see the Webinar: The MLephant in the Room.
- Learn about complex binary analysis and why it is critical to software supply chain security in our Special Report. Plus: Take a deep dive with RL's white paper.
Explore RL's Spectra suite: Spectra Assure for software supply chain security, Spectra Detect for scalable file analysis, Spectra Analyze for malware analysis and threat hunting, and Spectra Intelligence for reputation data and intelligence.