6

A very quick background to help you answering my questions :

  • Learning IDA Interactive Disassembler, the old free edition (too expensive for a hobbyist)
  • 15 year Linux Sysadmin & DBA experience
  • Hobbyist coder (C/ASM/Fortran/...)
  • Not very knowledgeable about Windows & Microsoft in general
  • not a security expert, but enough to be a sysadmin

After a lot of random disassembly of stuff here and there i quickly noticed they have a lot of common code. And at the very beginning of a lot of executable there is something that look like this :

lea     eax, [ebp+SystemTimeAsFileTime]
push    eax             ; lpSystemTimeAsFileTime
call    ds:GetSystemTimeAsFileTime
mov     eax, [ebp+SystemTimeAsFileTime.dwHighDateTime]
xor     eax, [ebp+SystemTimeAsFileTime.dwLowDateTime]

It's literally in pretty much every binary and a quickly found that it's something called by microsoft : "Buffer Security Check". And it's added automagically by the compiler using the /GS flag.

It's a stack canary used to detect stack buffer overflow :

This method works by placing a small integer, the value of which is randomly chosen at program start, in memory just before the stack return pointer. Most buffer overflows overwrite memory from lower to higher memory addresses, so in order to overwrite the return pointer (and thus take control of the process) the canary value must also be overwritten. This value is checked to make sure it has not changed before a routine uses the return pointer on the stack

And from the Microsoft documentation :

On functions that the compiler recognizes as subject to buffer overrun problems, the compiler allocates space on the stack before the return address. On function entry, the allocated space is loaded with a security cookie that is computed once at module load. On function exit, and during frame unwinding on 64-bit operating systems, a helper function is called to make sure that the value of the cookie is still the same. A different value indicates that an overwrite of the stack may have occurred. If a different value is detected, the process is terminated.

It's all good and everyone will live happily ever after.

However, i'm confused about the implementation itself. It's a bunch of XOR of : System time, processId, ThreadId, uptime, more time (perf counter) at process creation.

I'm not saying it's a terribly bad implementation (but at first glance i'm thinking it) since it's a lot of XOR and a single wrong bit will screw up the cookie entirely (unless there is an attack i'm not aware of) and brute forcing is 100% prevented (or is it ? dunno) since the process will be terminated when the guess is incorrect and a new cookie generated at next process launch.

Now i have a bunch of question :

  • Why such a weird pseudo-random generation implementation ? Doesn't the OS have a reliable entropy source ? Is it portability related ? Some variant of Windows may not have an entropy source ?
  • isn't processId and threadId known to everyone on the system anyway ? so why use it as entropy source ?
  • It's all time related and launched at process startup, aren't services launch on boot (stuff that need the most protection) guaranteed to have low uptime & perfcounter and a known system time and therefore the weakest entropy ?
  • Why isn't this cookie generation a system call anyway ? It's now unpatchable without recompilation.

Here is my commented disassembly :

push    ebp             ; prolog
mov     ebp, esp        ; prolog
sub     esp, 14h        ; prolog
and     [ebp+SystemTimeAsFileTime.dwLowDateTime], 0
and     [ebp+SystemTimeAsFileTime.dwHighDateTime], 0
mov     eax, __security_cookie
push    esi
push    edi
mov     edi, 0BB40E64Eh ; a global constant thingy for security cookie
mov     esi, 0FFFF0000h
cmp     eax, edi        ; compare local security cookie
                    ; with global security cookie.
                    ; if security_cookie == 0BB40E64Eh then it's not initialized
                    ; and it need to be initialized
jz      short generate_security_cookie

The generation is done only if it compare to a world-wide known constant "0BB40E64Eh". Isn't there a potential attack vector ? couldn't it be value local to the system ?

  • Why bother make the generation conditional ?
  • Is it even a security feature ? or just a debug mean to detect something went wrong with the stack ?

It's a lot of questions, it's my first step in this kind of stuff, but this is so confusing. But mainly : why not just call an OS entropy source ? I'm so confused...

Bonus question : If one have a mean to modify the binary (a single bit in the worldwide known constant ?) couldn't the whole /GS stuff could be bypassed ? and if it's a network open service you now have a service with a buffer overflow potential open to the world, right ?

schroeder
  • 123,438
  • 55
  • 284
  • 319
ker2x
  • 163
  • 4
  • 1
    I have to wonder this too. On Linux, the `AT_RANDOM` auxiliary vector points to 16 bytes of random data which is used for a stack cookie, so there's no fundamental reason this could not be done. – forest Dec 19 '17 at 04:34

1 Answers1

3

The goal of the stack canary is to protect the program from a buffer overflow. The canary is made dynamic, a new one is used on each execution, and, an attacker will only have one attempt for guessing it, as failing it will abort the program.

The source of the canary is quite similar to the one from CryptGenRandom, which is the official way to obtain cryptographically secure numbers in Windows (previous to the introduction of the CNG API), so it isn't bad.

isn't processId and threadId known to everyone on the system anyway ? so why use it as entropy source ?

Not all attacks will come from the local system. A network attacker will have no idea about those. As it is cheap to include them in the computation, they are added for additional entropy.

aren't services launch on boot (stuff that need the most protection) guaranteed to have low uptime & perfcounter (…)?

It's not enough to know that "a low value is used". You need the exact number in order to bypass the canary, and you only have one attempt. I don't think you will be able to guess the canary of any of them.

Why isn't this cookie generation a system call anyway ?

A system call would mean the canary can only be used on a new Windows version. This way, there is no dependency on the OS version. Plus, that would make the generation slower.

Windows random numbers would in general benefit by having a dedicated system call (or another OS interface for entropy, akin to unix /dev/random) for accessing entropy maintained system-wide, rather than the per-process pool created by CryptGenRandom, but it would be of little benefit here.

Why bother make the generation conditional ?

This is a classical check-if-it-is-initialised approach. The global variable for the cookie initially is 0BB40E64Eh. Then, on first use, it sees that a proper canary was not generated, and runs the calculation. This way, no resources are lost calculating the canary unless needed. (A more usual value would be to have used 00000000h, I don't know why 0BB40E64Eh was picked)

Is it even a security feature ? or just a debug mean to detect something went wrong with the stack ?

It serves as a debugging help, but it is a security feature.

Bonus question : If one have a mean to modify the binary (a single bit in the worldwide known constant ?) couldn't the whole /GS stuff could be bypassed ?

Yes. Using a constant canary would allow to bypass the canary checks (that is still harder than not having any canary check, though!)

and if it's a network open service you now have a service with a buffer overflow potential open to the world, right ?

No. You have a service with compiler stack protection disabled. But this service should not be vulnerable to a buffer overflow to begin with!

And you probably wouldn't like the denial of service produced by the failing canary check taking down your service (it is better than code execution, but still bad. This check is a last line of defense).

If you could modify the binary, even if just a bit, you can probably find deadlier targets than disabling the stack canary, such as changing the JE of the password comparison to a JNE :)

Ángel
  • 17,578
  • 3
  • 25
  • 60