Home

2024-02-10

__security_init_cookie


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]
Linker: Microsoft Linker(14.36.33133)
Tool: Visual Studio(2022 version 17.6)

*Note that the disassembly used from x64dbg present all numbers in hexadecimal without any prefix or suffix such as "0x" or "h".

A Mysterious Stack Reservation


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.

Checking the Security Cookie


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.

Generating the Security Cookie


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
  1. The QWORD from the reserved stack space, [rbp+10], is zeroed-out.
  2. The address to that QWORD is then passed as an argument to GetSystemTimeAsFileTime, which populates that QWORD with the current system time as a 64-bit number.
  3. That same value is then copied to local variable [rbp-10].
call qword ptr ds:[<&GetCurrentThreadId>]
mov eax,eax
xor qword ptr ss:[rbp-10],rax
  1. GetCurrentThreadId is called which returns a 32-bit value to eax.
  2. Zero-out the top 32-bits of rax? (Redundant as GetCurrentThreadId writing to eax will already do that)
  3. Xor this return value with the previous result at local variable [rbp-10] and store it there.
call qword ptr ds:[<&GetCurrentProcessId>]
mov eax,eax
.
.
.
xor qword ptr ss:[rbp-10],rax
  1. GetCurrentProcessId is called which returns a 32-bit value to eax.
  2. Zero-out the top 32-bits of rax? (Redundant as GetCurrentProcessId writing to eax will already do that)
  3. Xor this return value with the running result at local variable [rbp-10] and store it there.
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]
  1. Move the lower 32-bits from the previous result stored at the reserved stack space, [rbp+18], into eax
  2. Shift those lower 32 bits in rax to the upper 32 bits
  3. Xor this result with the original value at reserved stack space, [rbp+18], and store in rax
  4. Xor this result with the local variable [rbp-10] and store in rax
lea rcx,qword ptr ss:[rbp-10]
.
.
.
xor rax,rcx
  1. Load local variable, [rbp-10], into rcx
  2. Xor rax with rcx, store in rax
mov rcx,FFFFFFFFFFFF
and rax,rcx
mov rcx,2B992DDFA233
cmp rax,rbx
cmove rax,rcx
mov qword ptr ds:[<__security_cookie>],rax
  1. Zero-out the upper 2 bytes of rax
  2. Load 2B992DDFA233 (the default security cookie + 1) into rcx
  3. If rax equals rbx(2B992DDFA232), move rcx(2B992DDFA233) into rax
  4. Store the value in rax as the new security cookie

Summary


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.