Using x64dbg to view the disassembly for the following simple program, hellowindows.exe, I wanted to explore the additional functions beyond my code that the compiler inserted into the binary.
#include <windows.h> int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow ) { MessageBox(NULL, TEXT("Hello, Windows!"), TEXT("Title"), 0); }Compiler: Microsoft Visual C/C++(19.36.33133)[LTCG/C]
The first function encountered at the program entry point was __security_init_cookie, which will be the focus of this article.
sub rsp,28 call <hellowindows.__security_init_cookie> add rsp,28
According to Microsoft's documentation, the security cookie is a randomly generated value that is hidden on the stack before calling functions that are vulnerable to buffer overflow. After the vulnerable function executes, the hidden cookie's integrity is verified. If the hidden cookie has been corrupted, then a buffer overflow has occurred and the program is terminated. __security_init_cookie is responsible for ensuring a randomly generated cookie. The function declaration for __security_init_cookie is
void __security_init_cookie(void);
But wait? Why are 40 bytes being reserved for a function that takes no arguments? Let's dig into the __security_init_cookie function and see how it interacts with this space.
mov qword ptr ss:[rsp+18],rbx push rbp mov rbp,rsp sub rsp,30 mov rax,qword ptr ds:[<__security_cookie>] mov rbx,2B992DDFA232 cmp rax,rbx jne hellowindows.7FF7520C170F and qword ptr ss:[rbp+10],0 lea rcx,qword ptr ss:[rbp+10] call qword ptr ds:[<&GetSystemTimeAsFileTime>] mov rax,qword ptr ss:[rbp+10] mov qword ptr ss:[rbp-10],rax call qword ptr ds:[<&GetCurrentThreadId>] mov eax,eax xor qword ptr ss:[rbp-10],rax call qword ptr ds:[<&GetCurrentProcessId>] mov eax,eax lea rcx,qword ptr ss:[rbp+18] xor qword ptr ss:[rbp-10],rax call qword ptr ds:[<&QueryPerformanceCounter>] mov eax,dword ptr ss:[rbp+18] lea rcx,qword ptr ss:[rbp-10] shl rax,20 xor rax,qword ptr ss:[rbp+18] xor rax,qword ptr ss:[rbp-10] xor rax,rcx mov rcx,FFFFFFFFFFFF and rax,rcx mov rcx,2B992DDFA233 cmp rax,rbx cmove rax,rcx mov qword ptr ds:[<__security_cookie>],rax mov rbx,qword ptr ss:[rsp+50] not rax mov qword ptr ds:[<__security_cookie_complement>],rax add rsp,30 pop rbp ret
Stepping through the function twice (to take both branches), it was observed that 3 QWORDS within that reserved space were used. Here is a representation of the stack immediately after the function prologue
mov qword ptr ss:[rsp+18],rbx push rbp mov rbp,rsp
is called to visualize how the reserved space is (potentially) utilized. Note that the stack offsets are in hexadecimal to be consistant with x64dbg's disassembly.
Offset | Purpose |
---|---|
rbp+8 | Return address |
rbp+10 | GetSystemTimeAsFileTime result |
rbp+18 | QueryPerformanceCounter result |
rbp+20 | Temp storage for rbx |
rbp+28 | Unused |
rbp+30 | Unused |
The GetSystemTimeAsFileTime and QueryPerformanceCounter result space is only used if the function has to generate a new security cookie, which we will discuss later. The rbx storage is always used. rbx is stored at the beginning of the function and restored at the end, just like a prologue/epilogue push/pop. Only 3 of the 5 reserved QWORDS were used. I'm not sure what the additional unused 16 bytes are for. I considered padding, but that would be unnecessary as the utilized space along with the return address already fall on a 16-byte boundary. It's also odd that the caller is seemingly responsible for reserving this space rather than the function allocating its own space as is done with local variables. Microsoft's documentation mentions that a programmer can (and in certain cases, should) manually call __security_init_cookie, but it makes no mention of a requirement to manually allocate storage on the stack.
It remains a mystery to me what the unused reserved 16 bytes are for and also why the caller is responsible for allocating local storage for the function.
The beginning of the function (following the prologue) shows the security cookie being pulled from the data segment and compared against 2B992DDFA232.
mov rax,qword ptr ds:[<__security_cookie>] mov rbx,2B992DDFA232 cmp rax,rbx jne hellowindows.7FF7520C170F
Microsoft's documentation states, "Normally, __security_init_cookie is called by the CRT when it's initialized. If you bypass CRT initialization—for example, if you use /ENTRY to specify an entry-point—then you must call __security_init_cookie yourself. If __security_init_cookie isn't called, the global security cookie is set to a default value, and buffer overrun protection is compromised."
When checking the memory location of <__security_cookie>, I observed that the number stored at that location changed everytime I restarted the program. So it appears that the security cookie is being generated sometime before the program entry point and, thus, before this function is ever called. I have yet to find out where it is actually generated, but in any case, this is clearly not the default security cookie.
When the security cookie is compared to 2B992DDFA232, if found to be not equal, the function jumps to the end for some tidying up. If they are equal, the function then generates a new cookie. So what is the significance of 2B992DDFA232 and why is it an undesirable value for the cookie? Could this be the default cookie the documentation warned us about?
Lets search the binary for 2B992DDFA232 using CFF Explorer (We will actually be searching for 32A2DF2D992B as it is the little-endian representation of 2B992DDFA232). The search shows two occurences: one at 00000A90 and the other at 00002200. Let's find out what segments these addresses are located in. CFF Explorer also provides us with the location of the various segments within the binary.
Name | Raw Address |
---|---|
.text | 00000400 |
.rdata | 00001200 |
.data | 00002200 |
.pdata | 00002400 |
.rsrc | 00002600 |
.reloc | 00002800 |
We can see that the first occurence is within the .text segment. This is the hard-coded immediate value we saw earlier within the instructions. The second occurence is located within the .data segment. In fact, it is at the very start of the .data segment. Interesting. It must be the intial value of a global variable.
x64dbg's memory map shows us that for this execution, these segments have been placed at the following locations:
Address | Info |
---|---|
00007FF7520C1000 | ".text" |
00007FF7520C2000 | ".rdata" |
00007FF7520C3000 | ".data" |
00007FF7520C4000 | ".pdata" |
00007FF7520C5000 | ".rsrc" |
00007FF7520C6000 | ".reloc" |
x64dbg revealed that the address of <__security_cookie> is 00007FF7520C3000, the same address as the start of the .data segment. This is where our mysterious number 2B992DDFA232 was found.
We can conclude that <__security_cookie> is a global variable with an initial value of 2B992DDFA232.
Therefore, our mysterious number, 2B992DDFA232, must be the default cookie which is undesirable as it is vulnerable to attackers per the documentation. The function is checking to make sure that this default cookie has been replaced by a unique cookie and, if not, taking it upon itself to create one.
If the security cookie was found to be the default, the function will use the following instructions to generate a new cookie.
and qword ptr ss:[rbp+10],0 lea rcx,qword ptr ss:[rbp+10] call qword ptr ds:[<&GetSystemTimeAsFileTime>] mov rax,qword ptr ss:[rbp+10] mov qword ptr ss:[rbp-10],rax call qword ptr ds:[<&GetCurrentThreadId>] mov eax,eax xor qword ptr ss:[rbp-10],rax call qword ptr ds:[<&GetCurrentProcessId>] mov eax,eax lea rcx,qword ptr ss:[rbp+18] xor qword ptr ss:[rbp-10],rax call qword ptr ds:[<&QueryPerformanceCounter>] mov eax,dword ptr ss:[rbp+18] lea rcx,qword ptr ss:[rbp-10] shl rax,20 xor rax,qword ptr ss:[rbp+18] xor rax,qword ptr ss:[rbp-10] xor rax,rcx mov rcx,FFFFFFFFFFFF and rax,rcx mov rcx,2B992DDFA233 cmp rax,rbx cmove rax,rcx mov qword ptr ds:[<__security_cookie>],rax
The security cookie looks to be generated by a manipulation of the values returned by four separate system functions. Let's examine them individually.
and qword ptr ss:[rbp+10],0 lea rcx,qword ptr ss:[rbp+10] call qword ptr ds:[<&GetSystemTimeAsFileTime>] mov rax,qword ptr ss:[rbp+10] mov qword ptr ss:[rbp-10],rax
call qword ptr ds:[<&GetCurrentThreadId>] mov eax,eax xor qword ptr ss:[rbp-10],rax
call qword ptr ds:[<&GetCurrentProcessId>] mov eax,eax . . . xor qword ptr ss:[rbp-10],rax
lea rcx,qword ptr ss:[rbp+18] . . . call qword ptr ds:[<&QueryPerformanceCounter>]
The address of the QWORD from the reserved stack space, [rbp+18], is passed as an argument to QueryPerformanceCounter which populates it with a 64-bit value.
mov eax,dword ptr ss:[rbp+18] . . . shl rax,20 xor rax,qword ptr ss:[rbp+18] xor rax,qword ptr ss:[rbp-10]
lea rcx,qword ptr ss:[rbp-10] . . . xor rax,rcx
mov rcx,FFFFFFFFFFFF and rax,rcx mov rcx,2B992DDFA233 cmp rax,rbx cmove rax,rcx mov qword ptr ds:[<__security_cookie>],rax
The purpose of this function is to ensure that the default security cookie (hard-coded into the binary) has been replaced by a randomly generated cookie, generating a new cookie itself, if necessary. It also computes and stores the complement of the security cookie.
Though the function declaration specifies that it takes no arguments, there is an odd stack allocation preceding the function call reserving 5 QWORDs (only 3 of which are used) which is used as temporary local storage within the function.