I was thinking that if not WinDbg was able to break into the kernel32's entry point function, maybe I could insert the OP-code for debug break in the executable myself? When investigating the possibilities, I learned how to ensure an executable (PE file) is signed, and this is the subject for this post. I'm using WinDbg, Hex Edit and sigcheck on my Windows Vista 32-bit machine.
Well, it all started when I was investigating the order of the execution of entry point functions during the application loading and startup. Ntdll.dll was always loaded first, then kernel32.dll. According to WinDbg, the entry point function for ntdll.dll was 0, meaning ntdll.dll has no entry point function. However, kernel32.dll has an entry point function, but it was executed (and finished) before the first chance exception.
Was it possible to insert 0xCC (op-code for debug break) inside the kernel32.dll's entry point function? If it would have been possible, then WinDbg could break in the entry point function.
While editing the kernel32.dll's entry point function in Hex Edit by Commercial Software Pty, I did not want to overwrite any existing code, I just wanted to insert the 0xCC in the beginning of the entry point function. So when I was about to insert the 0xCC code in the kernel32.dll entry point function, using the Insert Mode, I realized this would of course not work. Since the Insert Mode would have moved the complete .text section 2 bytes, no code that referenced to a certain position in the file would work anymore!
Okay, this was not the only issue. Let's say it was possible to insert 0xCC in the .text section without corrupting anything else, then I needed to ensure this copy of kernel32.dll was loaded into the memory for an application (and not the original one in system32 folder). Normally it is possible to ensure a certain version of a DLL is loaded for an application, by placing the DLL in the application's folder. The DLL search process will first search the current folder for the DLL before proceeding the search. However, this is not the case for kernel32.dll. Even though I've placed a copy of kernel32.dll in the application folder, the loader will not load that copy, the loader will load the correct one from the system32 folder. The reason for this is because kernel32.dll is a well-known DLL, meaning it will always search in the system32 folder.
Well, enough about DLL loading and searching. I was still determined to edit a PE file in some way, but apparently it was not possible to do so in the .text section without corrupt the PE file. However, there are other sections in a PE file that was not affected in the same way.
In my previous posts, I've done some research on notepad.exe, I will continue to do so in this post.
Opening notepad.exe in Hex Edit, we can see part of the .rsrc section below.
![]() |
Hex Edit - notepad.exe |
We can see the string "All rights reserved" above. Before doing any modifications, I've made a copy of notepad.exe to my test folder. Using Hex Edit, I have edit the string to "all rights reserved" (using Overwrite mode).
![]() |
Hex Edit - notepad.exe is modified |
At this point, I was starting to think if Windows could recognize that the PE file has been modified? The answer is of course yes! Notepad.exe is signed to ensure that the copy is the original one. If we would found a copy of notepad.exe which is unsigned, we know that this has been modified (and maybe risky to execute).
Using the tool sigcheck by Sysinternals, we can verify if the PE file is signed.
Below I've used sigcheck on the original notepad.exe (from the system32 folder), it is clearly signed.
![]() |
Sigcheck - notepad.exe is signed |
When using sigcheck on the modified notepad.exe (in my test folder), we see that it is unsigned. In other words, sigcheck can easily find PE files which has been modified.
![]() |
Sigcheck - notepad.exe is modified and unsigned |
Before digging into sigchecks implementation, I will just present some basic knowledge about signing.
A PE file can be signed in two ways, either by adding an embedded digital signature or by using security catalog files (.CAT files).
By opening the properties dialog of a file, we can see if a file has an embedded digital signature. If the tab "Digital signatures" is present, we know that the file has an embedded signature, an example of this is shown below. Note that my language is in Swedish, it says "Digitala signaturer", this is translated in english to "Digital signatures".
![]() |
Properties dialog - Embedded signing |
Notepad.exe is also signed (according to sigcheck), but has no "Digital signatures" tab in the properties dialog.
![]() |
Properties dialog - notepad.exe is not embedded sign |
![]() |
catroot folders |
![]() |
Sigcheck- showing the .CAT filename for notepad.exe |
![]() |
Finding the .CAT file |
![]() |
Showing the security catalog |
![]() |
Sigcheck - Showing hashes |
So how does sigcheck determine if a PE file is signed?
I must admit, I've spent a lot of time together with my friend WinDbg when debugging sigcheck together with the original/modified version of notepad.exe. I've stepped through the machine code several times, made some notes about the executed functions and branching until I got a clue what was going on. Then I got into detail of the important functions (reading MSDN), their return values and the consequences.
Before going into some detail of the executed functions, I summarize my conclusions in a short version below.
To verify if notepad.exe is signed, the WinVerifyTrust function is used. This function resides in wintrust.dll. First sigcheck tries to verify the signing from the file i.e. notepad.exe (using WinVerifyTrust). Then sigcheck calls WinVerifyTrust again, but this time only to verify the hash (not actual signing). Both these calls fails, since WinVerifyTrust returns a non zero value. The execution moves on to CryptCATAdminCalcHashFromFileHandle, which calculates the hash from notepad.exe. This hash is then passed to the function CryptCATAdminEnumCatalogFromHash, which returns a HCATINFO. HCATINFO is valid if a catalog context could be found from the hash, which is true in this case. So the execution continues by calling WinVerifyTrust again, this time by verify notepad.exe using security catalogs. This time WinVerifyTrust succeeds and returns a zero value meaning sigcheck could verify that notepad.exe was signed. Finally another call is made the WinVerifyTrust to clean up the memory.
This was the short version, if you are up for it, here follows the long version.
We start by Open the executable (sigcheck.exe) and defines the arguments and start directory.
![]() |
WinDbg - Setting up debugging for sigcheck.exe |
The WinVerifyTrust function is well documented on MSDN. When it returns 0, we know that verification was OK. The parameters in WinVerifyTrust is repeated below for convenience;
LONG WINAPI WinVerifyTrust( _In_ HWND hWnd, _In_ GUID *pgActionID, _In_ LPVOID pWVTData );
![]() |
WinDbg - Setting breakpoints |
hWnd = 0,
GUID = 0x0044d8f0 {00AAC56B-CD44-11d0-8CC2-00C04FC295EE}
pWVTData = 0x0012A18C
![]() |
WinDbg - WinVerifyTrust |
The input pWVTData = 0x0012A18C does not tell us much, but when dumping the memory, and comparing to the WIN_TRUST data structure, we can get more information.
The WIN_TRUST data structure is well documented on MSDN as well, and is given below for convenience.
![]() |
WINTRUST_DATA structure |
cbStruct = 0x30;
pPolicyCallbackData = 0x00;
pSIPClientData = 0x00;
dwUIChoice =0x02;
fdwRevocationChecks = 0x40;
dwUnionChoice = 0x01;
pFile = 0x0012a178;
dwStateAction = 0x01; //WTD_STATEACTION_VERIFY
hWVTStateData = 0x00;
pwszURLReference = 0x00;
dwProvFlags = 0x00;
dwUIContext = 0x00;
pSignatureSettings = 0x00;
As we can see, the dwUnionChoice is 0x01, which corresponds to WTD_CHOICE_FILE meaning, "Use the file pointed to by pFile" according to MSDN.
pFile in this case is 0x0012a178.
The pFile member is a pointer to a WINTRUST_FILE_INFO struct, which is defined at MSDN, but is given here for convenience.
typedef struct WINTRUST_FILE_INFO_ { DWORD cbStruct; LPCWSTR pcwszFilePath; HANDLE hFile; GUID *pgKnownSubject; } WINTRUST_FILE_INFO, *PWINTRUCT_FILE_INFO;
Dumping memory at 0x0012a178 gives us;
![]() |
WinDbg - Memory dump of WINTRUST_FILE_INFO |
cbStruct = 0x10;
pcwszFilePath = 0x0012bf00;
hFile = 0x00;
*pgKnownSubject = 0x00;
Looking into memory at location 0x0012bf00, we see that the filename is notepad.exe.
![]() |
WinDbg - Memory showing the WINTRUST_FILE_INFO member pcwszFilePath |
When stepping through WinVerifyTrust, this call fails, it returns 0x800b0100.
![]() |
WinDbg - return value from WinVerifyTrust |
This particular error code corresponds to TRUST_E_NOSIGNATURE according to some research I've made on the net.
Stepping further in WinDbg, we encounter the next breakpoint in WinVerifyTrust with the arguments according to below.
hWnd = 0,
GUID = 0x0044d8f0 {00AAC56B-CD44-11d0-8CC2-00C04FC295EE}
pWVTData = 0x0012A18C
![]() |
WinDbg - WinVerifyTrust |
However, when dumping the pWVTData, we can see that one member differ from the previous call. This is marked in the red rectangle above.
From the dumped memory and the definition above, we can conclude following;
cbStruct = 0x30;
pPolicyCallbackData = 0x00;
pSIPClientData = 0x00;
dwUIChoice =0x02;
fdwRevocationChecks = 0x40;
dwUnionChoice = 0x01;
pFile = 0x0012a178;
dwStateAction = 0x01; //WTD_STATEACTION_VERIFY
hWVTStateData = 0x00;
pwszURLReference = 0x00;
dwProvFlags = 0x00000200; //WTD_HASH_ONLY_FLAG
dwUIContext = 0x00;
pSignatureSettings = 0x00;
According to MSDN, the value of dwProvFlags corresponds to WTD_HASH_ONLY_FLAG, which means "Only the hash is verified.".
So from this second break, we can conclude that WinVerifyTrust was trying to verify the signature from the file in a slightly different way than the first call.
When stepping through WinVerifyTrust, this call fails, it returns 0x800b0100.
![]() |
WinDbg - return value from WinVerifyTrust |
Stepping further in WinDbg, we encounter the next breakpoint.
This time, WinDbg breaks in CryptCATAdminCalcHashFromFileHandle. This function is documented on MSDN and its parameters is given below for convenience.
BOOL CryptCATAdminCalcHashFromFileHandle( _In_ HANDLE hFile, _Inout_ DWORD *pcbHash, _In_ BYTE *pbHash, _In_ DWORD dwFlags );
Below, we have just finished CryptCATAdminCalcHashFromFileHandle, and dumped the memory to show the calculated hash.
![]() |
WinDbg - CryptCATAdminCalcHashFromFileHandle |
According to MSDN, CryptCATAdminEnumCatalogFromHash parameters is defined as below.
HCATINFO CryptCATAdminEnumCatalogFromHash( _In_ HCATADMIN hCatAdmin, _In_ BYTE *pbHash, _In_ DWORD cbHash, _In_ DWORD dwFlags, _In_ HCATINFO *phPrevCatInfo );
![]() |
WinDbg - CryptCATAdminEnumCatalogFromHash |
Stepping further and execute CryptCATAdminEnumCatalogFromHash, we see that this function returns a non zero value, i.e. we have successfully found a HCATINFO.
![]() |
WinDbg - return value from CryptCATAdminEnumCatalogFromHash |
Since we found a valid HCATINFO, sigcheck proceeds and make another call to WinVerifyTrust, this time according to the arguments below.
hWnd = 0,
GUID = 0x0044d8e0 {00AAC56B-CD44-11d0-8CC2-00C04FC295EE}
pWVTData = 0x0012A18C
![]() |
WinDbg - WinVerifyTrust |
The GUID, is the same as in the previous call to WinVerifyTrust, as well as the pWVTData pointer.
However, when dumping the pWVTData, we can see that some member differ from the previous calls. This is marked in the red rectangle above.
From the dumped memory and the definition above, we can conclude following;
cbStruct = 0x30;
pPolicyCallbackData = 0x00;
pSIPClientData = 0x00;
dwUIChoice =0x02;
fdwRevocationChecks = 0x01;
dwUnionChoice = 0x02;
pCatalog = 0x00129E70;
dwStateAction = 0x01; //WTD_STATEACTION_VERIFY
hWVTStateData = 0x00;
pwszURLReference = 0x00;
dwProvFlags = 0x00;
dwUIContext = 0x00;
pSignatureSettings = 0x00;
As we can see, the dwUnionChoice is 0x02, which corresponds to WTD_CHOICE_CATALOG, meaning, "Use the catalog pointed to by pCatalog" according to MSDN.
pCatalog in this case is 0x00129E70.
The pCatalog member is a pointer to a WINTRUST_CATALOG_INFO struct, which is defined at MSDN, but is given here for convenience.
typedef struct WINTRUST_CATALOG_INFO_ { DWORD cbStruct; DWORD dwCatalogVersion; LPCWSTR pcwszCatalogFilePath; LPCWSTR pcwszMemberTag; LPCWSTR pcwszMemberFilePath; HANDLE hMemberFile; _Field_size(cbCalculatedFileHash)BYTE *pbCalculatedFileHash; DWORD cbCalculatedFileHash; PCCTL_CONTEXT pcCatalogContext; HCATADMIN hCatAdmin; } WINTRUST_CATALOG_INFO, *PWINTRUST_CATALOG_INFO;
Dumping memory at 0x00129e70 gives us;
![]() |
WinDbg - Memory dump of WINTRUST_CATALOG_INFO |
cbStruct = 0x24;
dwCatalogVersion = 0x00;
pcwszCatalogFilePath = 0x00129f3c;
pcwszMemberTag = 0x00129ea4;
pcwszMemberFilePath = 0x0012bf00;
hMemberFile = 0x00;
*pbCalculatedFileHash = 0x0012a1e8;
cbCalculatedFileHash = 0x00000014;
pcCatalogContext = 0x00;
hCatAdmin = 0x0012a358;
From previous call, we already know that 0x0012bf00 corresponds to the filename notepad.exe.
Looking into memory at location 0x00129f3c, we see that the pcwszCatalogFilePath ;
![]() |
WinDbg - Memory showing the member pcwszCatalogFilePath |
Looking into memory at location 0x0012a1e8, we see that the *pbCalculatedFileHash;
![]() |
WinDbg - Memory showing the member pbCalculatedFileHash |
When stepping through WinVerifyTrust, this call succeeds, it returns 0.
![]() |
WinDbg - return value from WinVerifyTrust |
Stepping further in WinDbg, we encounter the final breakpoint.
The fourth time, WinVerifyTrust is called using;
hWnd = 0,
GUID = 0x0044d8f0 {00AAC56B-CD44-11d0-8CC2-00C04FC295EE}
pWVTData = 0x0012A18C
![]() |
WinDbg - WinVerifyTrust |
From the dumped memory and the definition above, we can conclude following;
cbStruct = 0x30;
pPolicyCallbackData = 0x00;
pSIPClientData = 0x00;
dwUIChoice =0x02;
fdwRevocationChecks = 0x01;
dwUnionChoice = 0x02;
pCatalog = 0x00129E70;
dwStateAction = 0x02; //WTD_STATE_ACTION_CLOSE
hWVTStateData = 0x001924c0;
pwszURLReference = 0x00;
dwProvFlags = 0x00;
dwUIContext = 0x00;
pSignatureSettings = 0x00;
Above, dwStateAction differ, 0x02 corresponds to WTD_STATE_ACTION_CLOSE ,which means "Free the hWVTStateData member previously allocated with the WTD_STATEACTION_VERIFY action. This action must be specified for every use of the WTD_STATEACTION_VERIFY action." according to MSDN.
Also hWVTStateData differ, in previous calls it was 0x00, in this call it is 0x001924c0, which is the handle to the state data.
This call to WinVerifyTrust is probably not to do the verification, it is a call to make sure we clean up the memory.
Let's checkout the contents of the pCatalog.
Dumping memory at 0x00129e70 gives us;
![]() |
WinDbg - Memory dump of WINTRUST_CATALOG_INFO |
The valid contents of pCatalog has probably been removed, and it is not important in this call, since we only want to clean up the memory. As we can see in the screenshot, we don't care about the return value (eax). Immediately after the WinVerifyTrust function, a xor eax, eax instruction is executed, meaning we loose (and don't care) about the return value.
So we can conclude from this four calls that, the first one tries to verify the file, like the second call. The difference is just that the second call only tries only to verify to hash. Both calls fails. The third one verifies the file from the catalogs, which is successful. The last one just clean up the memory.
Before ending this post, I will do a similar procedure with the notepad.exe which is modified and check out the difference.
Do you remember? In the modified version of notepad.exe, I replaced the a to an A, which shown again below.
![]() |
Hex Edit - notepad.exe |
![]() |
Hex Edit - notepad.exe is modified |
![]() |
WinDbg - Setting breakpoints |
The difference appears when we call CryptCATAdminCalcHashFromFileHandle.
![]() |
WinDbg - CryptCATAdminCalcHashFromFileHandle |
This hash is later passed to CryptCATAdminEnumCatalogFromHash (just as when we were verifying the original notepad.exe), which does not find an security catalog
![]() |
WinDbg - return value from CryptCATAdminEnumCatalogFromHash |
In this case, the return value is NULL, which means we don't find any security catalog for this calculated hash. Further, since the we don't find any security catalog, we don't proceed with any further calls to WinVerifyTrust as when we were verifying the original notepad.exe
This became a long post, and I must confess I've spent several hours of debugging sigcheck. However, I've learned a lot. I also learned that there are a lot more to learn.
You are welcome to leave comments, complaints or questions!
Incredibly useful, many thanks!
ReplyDeleteGreat writeup! I was wondering if you knew how sigcheck dumps all of the hashes from a catalog when running with -d argument? Appreciate any info you might have!
ReplyDeletegood writeup.
ReplyDeleteJust a few pointers that helped me:
1. WinVerifyTrust is __fastcall which on x64 means that the GUID will be in rdx and the pWVTData param will be in r8.
2. if you load symbols for ole32 you can visually display WINTRUST_DATA using dt WINTRUST_DATA @r8 (or in your case dt WINTRUST_DATA 0x0012A18C) instead of parsing it manually.
cheers!
I have a question regarding debugging WinTrustVerify, do you think it is possible to understand what checks it does other than calculating file hash? For example, if any check is done on the certificate used for signing the file?
ReplyDelete