Copy Link
Add to Bookmark
Report
uninformed 07 01
Reducing the Effective Entropy of GS Cookies
skape
mmiller@hick.org
3/2007
1) Foreword
Abstract: This paper describes a technique that can be used to reduce the
effective entropy in a given GS cookie by roughly 15 bits. This reduction is
made possible because GS uses a number of weak entropy sources that can, with
varying degrees of accuracy, be calculated by an attacker. It is important to
note, however, that the ability to calculate the values of these sources for
an arbitrary cookie currently relies on an attacker having local access to the
machine, such as through the local console or through terminal services. This
effectively limits the use of this technique to stack-based local privilege
escalation vulnerabilities. In addition to the general entropy reduction
technique, this paper discusses the amount of effective entropy that exists in
services that automatically start during system boot. It is hypothesized that
these services may have more predictable states of entropy due to the relative
consistency of the boot process. While the techniques described in this paper
do not illustrate a complete break of GS, any inherent weakness can have
disastrous consequences given that GS is a static, compile-time security
solution. It is not possible to simply distribute a patch. Instead,
applications must be recompiled to take advantage of any security
improvements. In that vein, the paper proposes some solutions that could
be applied to address the problems that are outlined.
Thanks: Aaron Portnoy for lending some hardware for sample collection.
Johnny Cache and Richard Johnson for discussions and suggestions.
2) Introduction
Stack-based buffer overflows are generally regarded as one of the most common
and easiest to exploit classes of software vulnerabilities. This prevalence
has lead to the implementation of many security solutions that attempt to
prevent the exploitation of these vulnerabilities. Some of these solutions
include StackGuard[1], ProPolice[2], and Microsoft's /GS compiler switch[5]. The
shared premise of these solutions involves the placement of a cookie, or
canary, between the buffers stored in a stack frame and the stack frame's
return address. The cookie that is placed on the stack is used as a marker to
detect if a buffer overflow has occurred prior to allowing a function to
return. This simple concept can be very effective at making the exploitation
of stack-based buffer overflows unreliable.
The cookie-based approach to detecting stack-based buffer overflows involves
three general steps. First, a cookie that will be inserted into a function's
stack frame must be generated. The approaches taken to generate cookies vary
quite substantially, some having more implications than others. Once a cookie
has been generated, it must be pushed onto the stack in the context of a
function's prologue at execution time. This ensures that the cookie is placed
before the return address (and perhaps other values) on the stack. Finally, a
check must be added to a function's epilogue to make sure that the cookie that
was stored in the stack frame is the value that it was initialized to in the
function prologue. If an overflow of a stack-based buffer occurs, then it's
likely that it will have overwritten the cookie stored after the buffer. When
a mismatch is detected, steps can be taken to securely terminate the process
in a way that will prevent exploitation.
The security of a cookie-based solution hinges on the fact that an attacker
doesn't know, or is unable to generate, the cookie that is stored in a stack
frame. Since it's impossible to guarantee in all situations that an attacker
won't be able to generate the bytes that compose the value of a cookie, it
really all boils down to the cookie being kept secret. If the cookie is not
kept secret, then the presence of a cookie will provide no protection when it
comes to exploiting a stack-based buffer overflow vulnerability.
Additionally, if an attacker can trigger an exploitable condition before the
cookie is checked, then it stands that the cookie will provide no protection.
One example of this might include overwriting a function pointer on the stack
that is called prior to returning from the function.
While the StackGuard and ProPolice implementations are interesting and useful,
the author feels that no implementation is more critical than the one provided
by Microsoft. The reason for this is the simple fact that the vast majority
of all desktops, and a non-trivial number of servers, run applications
compiled with Microsoft's Visual C compiler. Any one weakness found in the
Microsoft's implementation could mean that a large number of applications are
no longer protected against stack-based buffer overflows. In fact, there has
been previous research that has pointed out flaws or limitations in
Microsoft's implementation. For example, David Litchfield pointed out that
even though stack cookies are present, it may still be possible to overwrite
exception registration records on the stack which may be called before the
function actually returns. This discovery was one of the reasons that
Microsoft later introduced SafeSEH (which had its own set of issues)[6].
Similarly, Chris Ren et al from Cigital pointed out the potential implications
of a function pointer being used in the path of the error handler for the case
of a GS cookie mismatch occurring[9]. While not directly related to a particular
flaw or limitation in GS, eEye has described some of the problems that come
when secrets get leaked[3].
Even though these issues and limitations have existed, Microsoft's GS
implementation at the time of this writing is considered by most to be secure.
While this paper will not present a complete break of Microsoft's GS
implementation, it will describe certain quirks and scenarios that may make it
possible to reduce the amount of effective entropy that exists in the cookies
that are generated. As with cryptography, any reduction of the entropy that
exists in the GS cookie effectively makes it so there are fewer unknown
portions of the cookie. This makes the cookie easier to guess by reducing the
total number of possibilities. Beyond this, it is expected that additional
research may find ways to further reduce the amount of entropy beyond that
described in this document. One critical point that must be made is that
since the current GS implementation is statically linked when binaries are
compiled, any flaw that is found in the implementation will require a
recompilation of all binaries affected by it. To help limit the scope, only
the 32-bit version of GS will be analyzed, though it is thought that similar
attacks may exist on the 64-bit version as well.
The structure of this paper is as follows. In chapter 3, a brief description
of the Microsoft's current GS implementation will be given. Chapter 4 will
describe some techniques that may be used to attack this implementation.
Chapter 5 will provide experimental results from using the attacks that are
described in chapter . Chapter 6 will discuss steps that could be taken to
improve the current GS implementation. Finally, chapter 7 will discuss some
areas where future work could be applied to further improve on the techniques
described in this document.
3) Implementation
As was mentioned in the introduction, security solutions that are designed to
protect against stack-based buffer overflows through the use of cookies tend
to involve three distinct steps: cookie generation, prologue modifications,
and epilogue modifications. Microsoft's GS implementation is no different.
This chapter will describe each of these three steps independent of one
another to paint a picture for how GS operates.
3.1) Cookie Generation
Microsoft chose to have the GS implementation generate an image file-specific
cookie. This means that each image file (executable or DLL) will have their
own unique cookie. When used in conjunction with a stack frame, a function
will insert its image file-specific cookie into the stack frame. This will be
covered in more detail in the next section. The actual approach taken to
generate an image file's cookie lives in a compiler inserted routine called
__security_init_cookie. This routine is placed prior to the call to the image
file's actual entry point routine and therefore is one of the first things
executed. By placing it at this point, all of the image file's code will be
protected by the GS cookie.
The guts of the __security_init_cookie routine are actually the most critical part
to understand. At a high-level, this routine will take an XOR'd combination
of the current system time, process identifier, thread identifier, tick count,
and performance counter. The end result of XOR'ing these values together is
what ends up being the image file's security cookie. To understand how this
actually works in more detail, consider the following disassembly from an
application compiled with version 14.00.50727.42 of Microsoft's compiler.
Going straight to the disassembly is the best way to concretely understand the
implementation, especially if one is in search of weaknesses.
Like all functions, the __security_init_cookie function starts with a prologue.
It allocates storage for some local variables and initializes some of them to
zero. It also initializes some registers, specifically edi and ebx which will
be used later on.
.text:00403D58 push ebp
.text:00403D59 mov ebp, esp
.text:00403D5B sub esp, 10h
.text:00403D5E mov eax, __security_cookie
.text:00403D63 and [ebp+SystemTimeAsFileTime.dwLowDateTime], 0
.text:00403D67 and [ebp+SystemTimeAsFileTime.dwHighDateTime], 0
.text:00403D6B push ebx
.text:00403D6C push edi
.text:00403D6D mov edi, 0BB40E64Eh
.text:00403D72 cmp eax, edi
.text:00403D74 mov ebx, 0FFFF0000h
As part of the end of the code above, a comparison between the current
security cookie and a constant 0xbb40e64e is made. Before __security_init_cookie
is called, the global securitycookie is initialized to 0xbb40e64e. The
constant comparison is used to see if the GS cookie has already been
initialized. If the current cookie is equal to the constant, or the high
order two bytes of the current cookie are zero, then a new cookie is
generated. Otherwise, the complement of the current cookie is calculated and
cookie generation is skipped.
.text:00403D79 jz short loc_403D88
.text:00403D7B test eax, ebx
.text:00403D7D jz short loc_403D88
.text:00403D7F not eax
.text:00403D81 mov __security_cookie_complement, eax
.text:00403D86 jmp short loc_403DE8
To generate a new cookie, the function starts by querying the current system
time using GetSystemTimeAsFileTime. The system time as represented by Windows
is a 64-bit integer that measures the system time down to a granularity of 100
nanoseconds. The high order 32-bit integer and the low order 32-bit integer
are XOR'd together to produce the first component of the cookie. Following
that, the current process identifier is queried using GetCurrentProcessId and
then XOR'd as the second component of the cookie. The current thread
identifier is then queried using GetCurrentThreadId and then XOR'd as the
third component of the cookie. The current tick count is queried using
GetTickCount and then XOR'd as the fourth component of the cookie. Finally,
the current performance counter value is queried using
QueryPerformanceCounter. Like system time, this value is also a 64-bit
integer, and its high order 32-bit integer and low order 32-bit integer are
XOR'd as the fifth component of the cookie. Once these XOR operations have
completed, a comparison is made between the newly generated cookie value and
the constant 0xbb40e64e. If the new cookie is not equal to the constant
value, then a second check is made to make sure that the high order two bytes
of the cookie are non-zero. If they are zero, then a 10 bit left shift of the
cookie is performed in order to seed the high order bytes.
.text:00403D89 lea eax, [ebp+SystemTimeAsFileTime]
.text:00403D8C push eax
.text:00403D8D call ds:__imp__GetSystemTimeAsFileTime@4
.text:00403D93 mov esi, [ebp+SystemTimeAsFileTime.dwHighDateTime]
.text:00403D96 xor esi, [ebp+SystemTimeAsFileTime.dwLowDateTime]
.text:00403D99 call ds:__imp__GetCurrentProcessId@0
.text:00403D9F xor esi, eax
.text:00403DA1 call ds:__imp__GetCurrentThreadId@0
.text:00403DA7 xor esi, eax
.text:00403DA9 call ds:__imp__GetTickCount@0
.text:00403DAF xor esi, eax
.text:00403DB1 lea eax, [ebp+PerformanceCount]
.text:00403DB4 push eax
.text:00403DB5 call ds:__imp__QueryPerformanceCounter@4
.text:00403DBB mov eax, dword ptr [ebp+PerformanceCount+4]
.text:00403DBE xor eax, dword ptr [ebp+PerformanceCount]
.text:00403DC1 xor esi, eax
.text:00403DC3 cmp esi, edi
.text:00403DC5 jnz short loc_403DCE
...
.text:00403DCE loc_403DCE:
.text:00403DCE test esi, ebx
.text:00403DD0 jnz short loc_403DD9
.text:00403DD2 mov eax, esi
.text:00403DD4 shl eax, 10h
.text:00403DD7 or esi, eax
Finally, when a valid cookie is generated, it's stored in the image file's
securitycookie. The bit-wise complement of the cookie is also stored in
securitycookiecomplement. The reason for the existence of the complement will
be described later.
.text:00403DD9 mov __security_cookie, esi
.text:00403DDF not esi
.text:00403DE1 mov __security_cookie_complement, esi
.text:00403DE7 pop esi
.text:00403DE8 pop edi
.text:00403DE9 pop ebx
.text:00403DEA leave
.text:00403DEB retn
In simpler terms, the meat of the cookie generation can basically be
summarized through the following pseudo code:
Cookie = SystemTimeHigh
Cookie ^= SystemTimeLow
Cookie ^= ProcessId
Cookie ^= ThreadId
Cookie ^= TickCount
Cookie ^= PerformanceCounterHigh
Cookie ^= PerformanceCounterLow
3.2) Prologue Modifications
In order to make use of the generated cookie, functions must be modified to
insert it into the stack frame at the time that they are called. This does
add some overhead to the call time associated with a function, but its overall
effect is linear with respect to a single invocation. The actual
modifications that are made to a function's prologue typically involve just
three instructions. The cookie that was generated for the image file is XOR'd
with the current value of the frame pointer. This value is then placed in the
current stack frame at a precisely chosen location by the compiler.
.text:0040214B mov eax, __security_cookie
.text:00402150 xor eax, ebp
.text:00402152 mov [ebp+2A8h+var_4], eax
It should be noted that Microsoft has taken great care to refine the way a
stack frame is laid out in the presence of GS. Locally defined pointers,
including function pointers, are placed before statically sized buffers in the
stack frame. Additionally, dangerous input parameters passed to the function,
such as pointers or structures that contain pointers, will have local copies
made that are positioned before statically sized local buffers. The local
copies of these parameters are used instead of those originally passed to the
function. These two changes go a long way toward helping to prevent other
scenarios in which stack-based buffer overflows might be exploited.
3.3) Epilogue Modifications
When a function returns, it must check to make sure that the cookie that was
stored on the stack has not been tampered with. To accomplish this, the
compiler inserts the following instructions into a function's prologue:
.text:00402223 mov ecx, [ebp+2A8h+var_4]
.text:00402229 xor ecx, ebp
.text:0040222B pop esi
.text:0040222C call __security_check_cookie
The value of the cookie that was stored on the stack is moved into ecx and
then XOR'd with the current frame pointer to get it back to the expected
value. Following that, a call is made to securitycheckcookie where the stack
frame's cookie value is passed in the ecx register. The securitycheckcookie
routine is very short and sweet. The passed in cookie value is compared with
the image file's global cookie. If they don't match, reportgsfailure is
called and the process eventually terminates. This is what one would expect
in the case of a buffer overflow scenario. However, if they do match, the
routine simply returns, allowing the calling function to proceed with
execution and cleanup.
.text:0040634B cmp ecx, __security_cookie
.text:00406351 jnz short loc_406355
.text:00406353 rep retn
.text:00406355 loc_406355:
.text:00406355 jmp __report_gsfailure
4) Attacking GS
At the time of this writing, all publicly disclosed attacks against GS that
the author is aware of have relied on getting control of execution before the
cookie is checked or by finding some way to leak the value of the cookie back
to the attacker. Both of these styles of attack are of great interest and
value, but the focus of this paper will be on a different method of attacking
GS. Specifically, this chapter will outline techniques that may be used to
make it easier to guess the value an image file's GS cookie. Two techniques
will be described. The first technique will describe methods for calculating
the values that were used as entropy sources when the cookie was generated.
These calculations are possible in situations where an attacker has local
access to the machine, such as through the console or through terminal
services. The second technique describes the general concept of predictable
ranges of some values that are used in the context of boot start services,
such as lsass.exe. This predictability may make the guessing of a GS cookie
more feasible in both local and remote scenarios.
4.1) Calculating Entropy Sources
The sources used to generate the GS cookie for a given image file are constant
and well-known. They include the current system time, process identifier,
thread identifier, tick count, and performance counter. In light of that
fact, it only makes sense to investigate the amount of effective entropy each
source adds to the cookie. Since it's a requirement that the cookie produced
be secret, the ability to guess a value used in the generation of the cookie
will allow it to be canceled out of the equation. This is true due to the
simple fact that each of the values used to generate the cookie is XOR'd with
each other value (XOR is a commutative operation). The ability to guess
multiple values can make it possible to seriously impact the overall integrity
of the cookie.
While the sources used in the generation of the cookie have long been regarded
as satisfactory, the author has found that the majority of the sources
actually contribute little to no value toward the overall entropy of the
cookie. However, this is currently only true if an attacker has local access
to the machine. Being able to know a GS cookie that was used in a privileged
process would make it possible to exploit a local privilege escalation
vulnerability, for example. There may be some circumstances where the
techniques described in this section could be applied remotely, but for the
purpose of this document, only the local scenario will be considered. The
following subsections will outline methods that can be used to calculate or
deterministically find the specific values that were used when a cookie was
being generated in a particular process context. As a result of this
analysis, it's become clear that the only particular variable source of true
entropy for the GS cookie is the low 17 bits of the performance counter. All
other sources can be reliably calculated, with some margin of error.
For the following subsections, a modified executable named vulnapp.exe was
used to extract the information that was used at the time that a process
executable's GS cookie was generated. In particular, __security_init_cookie was
modified to jump into a function that saves the information used to generate
the cookie. The implementation of this function is shown below for those who
are curious:
//
// The FramePointer is the value of EBP in the context of the
// __security_init_cookie routine. The cookie is the actual,
// resultant cookie value. GSContext is a global array.
//
VOID DumpInformation(
PULONG FramePointer,
ULONG Cookie)
{
GSContext[0] = FramePointer[-3];
GSContext[1] = FramePointer[-4];
GSContext[2] = FramePointer[-1];
GSContext[3] = FramePointer[-2];
GSContext[4] = GetCurrentProcessId();
GSContext[5] = GetCurrentThreadId();
GSContext[6] = GetTickCount();
GSContext[7] = Cookie;
}
4.1.1) System Time
System time is a value that one might regard as challenging to recover. After
all, it seems impossible to get the 100 nanosecond granularity of the system
time that was retrieved when a cookie was being generated. Quite the
contrary, actually. There are a few key points that go into being able to
recover the system time. First, it's a fact that even though the system time
measures granularity in terms of 100 nanosecond intervals, it's really only
updated every 15.625 milliseconds (or 10.1 milliseconds for more modern CPUs).
To many, 15.625 may seem like an odd number, but for those familiar with the
Windows thread scheduler, it can be recognized as the period of the timer
interrupt. For that reason, the current system time is only updated as a
result of the timer interrupt firing. This fact means that the alignment of
the system time that is used when a cookie is generated is known.
Of more interest, though, is the relationship between the system time value
and the creation time value associated with a process or its initial thread.
Since the minimum granularity of the system time is 15.6 or 10.1 milliseconds,
it follows that the granularity of the thread creation time will be the same.
In terms of modern CPUs, 15.6 milliseconds is an eternity and is plenty long
for the processor to execute all instructions from the creation of the thread
to the generation of the security cookie. This fact means that it's
possible to assume that the creation time of a process or thread is the
same as the system time that was used when the cookie was generated. This
assumption doesn't always work, though, and there are indeed cases where
the creation time will not equal the system time that was used. These
situations are usually a result of the thread that creates the cookie not
being immediately scheduled.
Even if this is the case, it would be necessary to be able to obtain the
creation time of an arbitrary process or thread. On the surface, this would
seem impossible because task manager prevents a non-privileged user from
getting the start time of a privileged process.
This is all a deception, though, because there does exist functionality that
is exposed to non-privileged users that can be used to get this information.
One way of getting it is through the use of the native API routine
NtQuerySystemInformation. In this case, the
SystemProcessesAndThreadsInformation system information class is used to query
information about all of the running processes on the system. This
information includes the process name, process creation time, and the creation
time for each thread in each process. While this information class has been
removed in Windows Vista, there are still potential ways of obtaining the
creation time information. For example, an attacker could simply crash the
vulnerable service once (assuming it's not a critical service) and then wait
for it to respawn. Once it respawns, the creation time can be inferred based
on the restart delay of the service. Granted, service restarts are limited
to three times per day in Vista, but crashing it once should cause no major
issues.
Using NtQuerySystemInformation, it's possible to collect some data that can be
used to determine the likelihood that the creation time of a thread will be
equal to the system time that was used when a GS cookie was generated. To
test this, the author used the modified vulnapp.exe executable to extract the
system time at the time that the cookie was generated. Following that, a
separate program was used to collect the creation time information of the
process in question using the native API. The initial thread's creation time
was then compared with the system time to see if they were equal. The
creation time and system time were often equal in a sample of 742 cookies.
Obviously, the data set describing differences is only relevant to a
particular system load. If there are many threads waiting to run during the
time that a process is executed, then it is unlikely that the system time will
equal the process creation time. In a desktop environment, it's probably safe
to assume that the thread will run immediately, but more conclusive evidence
may be necessary.
Given these facts, it is apparent that the complete 64-bit system time value
can be recovered more often than not with a great degree of accuracy just by
simply assuming that thread creation time is the same as the system time
value.
4.1.2) Process and Thread Identifier
The process and thread identifier are arguably the worst sources of entropy
for the GS cookie, at least in the context of a local attack. The two high
order bytes of the process and thread identifiers are almost always zero.
This means they have absolutely no effect on the high order entropy.
Additionally, the process and thread identifier can be determined with 100
percent accuracy in a local context using the same API described in the
previous section on getting the system time. This involves making use of
the NtQuerySystemInformation native API with the
SystemProcessesAndThreadsInformation system information class to get the
process identifier and thread identifier associated with a given process
executable.
The end result, obviously, is that the process and thread identifier can be
determined with great accuracy. The one exception to this rule would be
Windows Vista, but, as was mentioned before, alternative methods of obtaining
the process and thread identifier may exist.
4.1.3) Tick Count
The tick count is, for all intents and purposes, simply another measure of
time. When the GetTickCount API routine is called, the number of ticks is
multiplied by the tick count multiplier. This multiplication effectively
translates the number of ticks to the number of milliseconds that the system
has been up. If one can safely assume that the that the system time used to
generate the cookie was the same as the thread creation time, then the tick
count at the time that the cookie was generated can simply be calculated using
the thread creation time. The creation time isn't enough, though. Since the
GetTickCount value measures the number of milliseconds that have occurred
since boot, the actual uptime of the system has to be determined.
To determine the system uptime, a non-privileged user can again make use of
the NtQuerySystemInformation native API, this time with the
SystemTimeOfDayInformation system information class. This query returns the
time that the system was booted as a 64-bit integer measured in 100 nanosecond
intervals, just like the thread creation time. To calculate the system uptime
in milliseconds, it's as simple as subtracting the boot time from the creation
time and then dividing by 10000 to convert from 100 nanosecond intervals to 1
millisecond intervals:
EstTickCount = (CreationTime - BootTime) / 10000
Some experimentation shows that this calculation is pretty accurate, but some
quantity is lost in translation. From what the author has observed, a
constant scaling factor of 0x4e, or 78 milliseconds, needs to be added to the
result of this calculation. The source of this constant is as of yet unknown,
but it appears to be a required constant. This results in the actual equation
being:
EstTickCount = [(CreationTime - BootTime) / 10000] + 78
The end result is that the tick count can be calculated with a great degree of
accuracy. If the system time calculation is off, then that will directly
affect the calculation of the tick count.
4.1.4) Performance Counter
Of the four entropy sources discussed so far, the performance counter is the
only one that really presents a challenge. The purpose of the performance
counter is to describe the total number of cycles that have executed. On the
outside, the performance counter would seem impossible to reliably determine.
After all, how could one possibly determine the precise number of cycles that
had occurred as a cookie was being generated? The answer, of course, comes
down to the fact that the performance counter itself is, for all intents and
purposes, just another measure of time. Windows provides two interesting
user-mode APIs that deal with the performance counter. The first,
QueryPerformanceCounter, is used to ask the kernel to read the current value
of the performance counter[8]. The result of this query is stored in the 64-bit
output parameter that the caller provides. The second API is
QueryPerformanceFrequency. This routine is interesting because it returns a
value that describes the amount that the performance counter will change in
one second[7]. Documentation indicates that the frequency cannot change while
the system is booted.
Using the existing knowledge about the uptime of the system and the
calculation that can be performed to convert between the performance counter
value and seconds, it is possible to fairly accurately guess what the
performance counter was at the time that the cookie was generated. Granted,
this method is more fuzzy than the previously described methods, as
experimental results have shown a large degree of fluctuation in the lower 17
bits. Those results will be discussed in more detail in chapter . The actual
equation that can be used to generate the estimated performance counter is to
take the uptime, as measured in 100 nanosecond intervals, and multiply it by
the performance frequency divided by 10000000, which converts the frequency
from a measure of 1 second to 100 nanosecond:
EstPerfCounter = UpTime x (PerfFreq / 10000000)
In a fashion similar to tick count, a constant scaling factor of -165000 was
determined through experimentation. This seems to produce more accurate
results in some of the 24 low bits. Based on this calculation, it's possible
to accurately determine the entire 32-bit high order integer and the first 15
bits of the 32-bit low order integer. Of course, if the system time estimate
is wrong, then that directly effects this calculation.
4.1.5) Frame Pointer
While the frame pointer does not influence an image file's global cookie, it
does influence a stack frame's version of the cookie. For that reason, the
frame pointer must be considered as an overall contributor to the effective
entropy of the cookie. With the exception of Windows Vista, the frame pointer
should be a deterministic value that could be deduced at the time that a
vulnerability is triggered. As such, the frame pointer should be considered a
known value for the majority of stack-based buffer overflows. Granted, in
multi-threaded applications, it may be more challenging to accurately guess
the value of the frame pointer.
In the Windows Vista environment, the compile-time GS implementation gets a
boost in security due to the introduction of ASLR. This helps to ensure that
the frame pointer is actually an unknown quantity. However, it doesn't
introduce equal entropy in all bits. In particular, octet 4, and potentially
octet 3, may have predictable values due to the way that the randomization is
applied to dynamic memory allocations. In order to prevent fragmentation of
the address space, Vista's ASLR implementation attempts to ensure that stack
regions are still allocated low in the address space. This has the side
effect of ensuring that a non-trivial number of bits in the frame pointer will
be predictable. Additionally, while Vista's ASLR implementation makes an
effort to shift the lower bits of the stack pointer, there may still be some
bits that are always predictable in octet 2.
4.2) Predictability of Entropy Sources in Boot Start Services
A second attack that could be used against GS involves attacking services that
start early on when the system is booted. These services may experience more
predictable states of entropy due to the fact that the amount of time it takes
to boot up and the order in which tasks are performed is fairly, though not
entirely, consistent. This insight may make it possible to estimate the value
of entropy sources remotely.
To better understand this type of attack, the author collected 742 samples
that were taken from a custom service that was set to automatically start
during boot on a Windows XP SP2 installation. This service was simply
designed to log the state used at the time that the GS cookie was being
generated. While a sampling of the GS cookie state applied to lsass.exe would
have been more ideal, it wasn't worth the headache of having to patch a
critical system service. Perhaps the reader may find it interesting to
collect this data on their own. From the samples that were taken, the
following diagrams show the likelihood of each individual bit being set for
each of the different entropy sources.
Overall, there are a number of predictable bits in things like the high
32-bits of both the system time and the performance counter, the process
identifier, the thread identifier, and the tick count. The sources that are
largely unpredictable are the low 32-bits of the system time and the
performance counter. However, if it were possible to come up with a way to
discover the boot time (or uptime) of the system remotely, it might be
possible to infer a good portion of the low 32-bits of the system time. This
would then directly impact the ability to estimate things like the tick count
and performance counters.
5) Experimental Results
This chapter describes some of the initial results that were collected using a
utility developed by the author named gencookie.exe. This utility attempts to
calculate the value of the cookie that was generated for the executable image
associated with an arbitrary process, such as lsass.exe. While the results of
this utility were limited to attempting to calculate the cookie of a process'
executable, the techniques described in previous chapters are nonetheless
applicable to the cookies generated in the context of dependent DLLs. The
results described in this chapter illustrate the tool's ability to accurately
obtain specific bits within the different components that compose the cookie,
including specific bits of the cookie itself. This helps to paint a picture
of the amount of true entropy that is reduced through the techniques described
in this document.
The data set that was used to calculate the overall results included 5001
samples which were collected from a single machine. The samples were
collected through a few simple steps. First, a program called vulnapp.exe
that was compiled with /GS was modified to have its __security_init_cookie routine
save information about the cookie that was generated and the values that
contributed to its generation. Following that, the gencookie.exe utility was
launched against the running process in an attempt to calculate vulnapp.exe's
GS cookie. A comparison between the expected and actual value of each
component was then saved. These steps were repeated 5001 times. The author
would be interested in hearing about independent validation of the findings
presented in this chapter.
The following sections describe the bit-level predictability of each of the
components that are used to generate the GS cookie, including the overall
predictability of the bits of the GS cookie itself.
5.1) System Time
The system time component was highly predictable. The high 32-bit bits of the
system time were predicted with 100 percent accuracy. The low 32-bit bits on
the other hand were predicted with only 77 percent accuracy (3878 times). The
reason for this discrepancy has to do with the thread scheduling scenario
described in subsection . Even still, these results indicate that it is
likely that the entire system time value can be accurately calculated.
5.2) Process and Thread Identifier
The process and thread identifier were successfully calculated 100 percent of
the time using the approach outlined in section .
5.3) Tick Count
The tick count was accurately calculated 67 percent of the time (3396 times).
The reason for this lower rate of success is due in large part to the fact
that the tick count is calculated in relation to the estimated system time
value. As such, if an incorrect system time value is determined, the tick
count itself will be directly influenced. This should account for at least 23
percent of the inaccuracies judging from how often the system time was
inaccurately estimated. The remaining 10 percent of the inaccuracies is as of
yet undetermined, but it is most likely related to the an improper
interpretation of the constant scaling factor that is applied to the tick
count. In any case, it is expected that only a few bits are actually affected
in the remaining 10 percent of cases.
5.4) Performance Counter
The high 32-bits of the performance counter were successfully estimated 100
percent of the time. The low 32-bits, on the other hand, show the greatest
degree of volatility when compared to the other components. The high order 15
bits of the low 32-bits show a bias in terms of accuracy that is not a 50/50
split. The remaining 17 bits were all guessed correctly roughly 50 percent of
the time. This makes the low 17 bits the only truly effective source of
entropy in the performance counter since there is no bias shown in relation to
the estimated versus actual values. Indeed, this is not enough to prove that
there aren't observable patterns in the low 17 bits, but it is enough to show
that the gencookie.exe utility was not effective in estimating them. Figures
and show the percent accuracy for the high and low order 32-bits.
This discrepancy actually requires a more detailed explanation. In reality,
the estimates made by the gencookie.exe utility are actually not as far off as
one might think based on the percent accuracy of each bit as described in the
diagrams. Instead, the estimates are, on average, off by only 105,000. This
average difference is what leads to the lower 17 bits being so volatile. One
thing that's interesting about the difference between the estimated and actual
performance counter is that there appears to be a time oriented trend related
to how far off the estimates are. Due to the way that the samples were taken,
it's safe to assume that each sample is roughly equivalent to one second worth
of time passing (due to a sleep between sample collection). Further study of
this apparent relationship may yield better results in terms of estimating the
lower 17 bits of the low 32 bits of the performance counter. This is left for
future research.
5.5) Cookie
The cookie itself was never actually guessed during the course of sample
collection. The reason for this is tightly linked with the current inability
to accurately determine the lower 17 bits of the low 32 bits of the
performance counter. Comparing the percent accuracy of the cookie bits with
the percent accuracy of the low 32 bits of the performance counter yields a
very close match.
6) Improvements
Based on the results described in chapter , the author feels that there is
plenty of room for improvement in the way that GS cookies are currently
generated. It's clear that there is a need to ensure that there are 32 bits
of true entropy in the cookie. The following sections outline some potential
solutions to the entropy issue described in this document.
6.1) Better Entropy Sources
Perhaps the most obvious solution would be to simply improve the set of
entropy sources used to generate the cookie. In particular, the use of
sources with greater degrees of entropy, especially in the high order bits,
would be of great benefit. The challenge, however, is locating sources that
are easy to interact with and require very little overhead. For example, it's
not really feasible to have the GS cookie generator rely on the crypto API due
to the simple fact that this would introduce a dependency on the crypto API in
any application that was compiled with /GS. As this document has hopefully
shown, it's also a requirement that any additional entropy sources be
challenging to estimate externally at a future point in time.
Even though this is a viable solution, the author is not presently aware of
any additional entropy sources that would meet all three requirements. For
this reason, the author feels that this approach alone is insufficient to
solve the problem. If entropy sources are found which meet these
requirements, the author would love to hear about them.
6.2) Seeding High Order Bits
A more immediate solution to the problem at hand would involve simply ensuring
that the predictable high order bits are seeded with less predictable values.
However, additional entropy sources would be required in order to implement
this properly. At present, the only major source of entropy found in the GS
cookie is the low order bits of the performance counter. It would not be
sufficient to simply shift the low order bits of the performance counter into
the high order. Doing so would add absolutely no value by itself because it
would have no effect on the amount of true entropy in the cookie.
6.3) External Cookie Generation
An alternative solution that could combine the effects of the first two
solutions would be to change the GS implementation to generate the cookie
external to the binary itself. One of the most dangerous aspects of the GS
implementation is that it is statically linked and therefore would require a
recompilation of all affected binaries in the event that a weakness is found.
This fact alone should be scary. To help address both this problem and the
problem of weak entropy sources, it makes sense to consider a more dynamic
approach.
One example of a dynamic approach would be to have the GS implementation issue
a call into a kernel-mode routine that is responsible for generating GS
cookies. One place that this support could be added is in
NtQuerySystemInformation, though it's likely that a better place may exist.
Regardless of the specific routine, this approach would have the benefit of
moving the code used to generate the cookie out of the statically linked stub
that is inserted by the compiler. If any weakness were to be found in the
kernel-mode routine that generates the cookie, Microsoft could issue a patch
that would immediately affect all applications compiled to use GS. This would
solve some of the concerns relating to the static nature of GS.
Perhaps even better, this approach would grant greater flexibility to the
entropy sources that could be used in the generation of the cookie. Since the
routine would exist in kernel-mode, it would have the benefit of being able to
access additional sources of entropy that may be challenging or clumsy to
interact with from user-mode (though the counterpoint could certainly be made
as well). The kernel-mode routine could also accumulate entropy over time and
feed that back into the cookie, whereas the statically linked implementation
has no context with which to accumulate entropy. The accumulation of state
can also do more harm than good. It would be disingenuous to not admit that
this approach could also have its own set of problems. A poorly implemented
version of this solution might make it possible for a user to eliminate all
entropy by issuing a non-trivial number of calls to the kernel-mode routine.
There may be additional consequences that have not yet been perceived.
The impact on performance is also a big point of concern for any potential
change to the cookie generation path. At a high-level, a transition into
kernel-mode would seem concerning in terms of the amount of overhead that
might be added. However, it's important to note that the current
implementation of GS already transitions into kernel-mode to obtain some of
it's information. Specifically, performance counter information is obtained
through the system call NtQueryPerformanceCounter. Even more, this system
call results in an in operation on an I/O port that is used to query the
current performance counter.
Another important consideration is backward compatibility. If Microsoft were
to implement this solution, it would be necessary for applications compiled
with the new support to still be able benefit from GS on older platforms that
don't support the new kernel interface. To allow for backward compatibility,
Microsoft could implement a combination of all three solutions, whereby better
entropy sources and seeding of high order bits are used as a fallback in the
event that the kernel-mode interface is not present.
As it turns out, Microsoft does indeed have a mechanism that could allow them
to create a patch that would affect the majority of the binaries compiled with
recent versions of GS. This functionality is provided by exposing the address
of an image file's security cookie in its the load config data directory.
When the dynamic loader (ntdll) loads an image file, it checks to see if the
security cookie address in the load config data directory is non-NULL. If
it's not NULL, the loader proceeds to store the process-wide GS cookie in the
module-specific's GS cookie location. In this way, the __security_init_cookie
routine that's called by the image file's entry point effectively becomes a
no-operation because the cookie will have already been initialized. This
manner of setting the GS cookie for image files provides Microsoft with much
more flexibility. Rather than having to update all binaries compiled with GS,
Microsoft can simply update a single binary (ntdll.dll) if improvements need
to be made to the cookie generation algorithm. The following output shows a
sample of dumpbin /loadconfig on kernel32.dll:
Microsoft (R) COFF/PE Dumper Version 8.00.50727.42
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file c:\windows\system32\kernel32.dll
File Type: DLL
Section contains the following load config:
00000048 size
0 time date stamp
...
7C8836CC Security Cookie
7) Future Work
There is still additional work that can be done to further refine the
techniques described in this document. This chapter outlines some of the
major items that could be followed up on.
7.1) Improving Performance Counter Estimates
One area in particular that the author feels could benefit from further
research has to do with refining the technique used to calculate the
performance counter. A more thorough analysis of the apparent association
between time and the lower 17 bits of the performance counter is necessary.
This analysis would directly affect the ability to recover more cookie state
information, since the entropy of the lower 17 bits of the performance counter
is one of the only things standing in the way of obtaining the entire cookie.
7.2) Remote Attacks
The ability to apply the techniques described in this document in a remote
scenario would obviously increase the severity of the problem. In order to do
this, an attacker would need the ability to either infer or be able to
calculate some of the key elements that are used in the generation of a
cookie. This would rely on being able to determine things like the process
creation time, the process and thread identifier, and the system uptime. With
these values, it should be possible to predict the state of the cookie with
similar degrees of accuracy. Of course, methods of obtaining this information
remotely are not obvious.
One point of consideration that should be made is that even if it's not
possible to directly determine some of this information, it may be possible to
infer it. For instance, consider a scenario where a vulnerability in a
service is exposed remotely. There's nothing to stop an attacker from causing
the service to crash. In most cases, the service will restart at some
predefined point (such as 30 seconds after the crash). Using this approach,
an attacker could infer the creation time of the process based on the time
that the crash was generated. This isn't fool proof, but it should be
possible to get fairly close.
Determining process and thread identifier could be tricky, especially if the
system has been up for some time. The author is not aware of a general
purpose technique that could be used to determine this information remotely.
Fortunately, the process and thread identifier have very little effect on high
order bits.
The system uptime is an interesting one. In the past, there have been
techniques that could be used to estimate the uptime of the system through the
use of TCP timestamps and other network protocol anomalies. At the time of
this writing, the author is not aware of how prevalent or useful these
techniques are against modern operating systems. Should they still be
effective, they would represent a particularly useful way of obtaining a
system's uptime. If an attacker can obtain both the creation time of the
process and the uptime of the system, it's possible to calculate the tick
count and performance counter values with varying degrees of accuracy.
The performance counter will still pose a great challenge in the remote
scenario. The reliance on the performance frequency shouldn't be seen as an
unknown quantity. As far as the author is aware, the performance frequency on
modern processors is generally 3579545, though there may be certain power
situations that would cause it to be different.
It is also important to note that the current attack assumes that the load
time for an image that has a GS cookie is equivalent to the initial thread's
creation time. For example, if a DLL were loaded much later in process
execution, such as through instantiating a COM object in Internet Explorer, it
would not be possible to assume that initial thread creation time is equal to
the system time that was obtained when the DLL's GS cookie was generated.
This brings about an interesting point for the remote scenario, however. If
an attacker can control the time at which a DLL is loaded, it may be possible
for them to infer the value of system time that is used without even having to
directly query it. One example of this would be in the context of internet
explorer, where the client's date and time functionality might be abused to
obtain this information.
8) Conclusion
The ability to reduce the amount of effective entropy in a GS cookie can
improve an attacker's chances of guessing the cookie. This paper has
described two techniques that may be used to calculate or infer the values of
certain bits in a GS cookie. The first approach involves a local attacker's
ability to collect information that makes it possible to calculate, with
pretty good accuracy, the values of the entropy sources that were used at the
time that a cookie was generated. The second approach describes the potential
for abusing the limited entropy associated with boot start services.
While the results shown in this paper do not represent a complete break of GS,
they do hint toward a general weakness in the way that GS cookies are
generated. This is particularly serious given the fact that GS is a compile
time solution. If the techniques described in this document are refined, or
new and improved techniques are identified, a complete break of GS would
require the recompilation of all affected binaries. The implications of this
should be obvious. The ability to reliably predict the value of a GS cookie
would effectively nullify any benefits that GS adds. It would mean that all
stack-based buffer overflows would immediately become exploitable.
To help contribute to the improvement of GS, a few different solutions were
described that could either partially or wholly address some of the weakness
that were identified. The most interesting of these solutions involves
modifying the GS implementation to make use of a external cookie generator,
such as the kernel. Going this route would ensure that any weaknesses found
in the cookie generation algorithm could be simply addressed through a patch
to the kernel. This is much more reasonable than expecting all existing GS
enabled binaries to be recompiled.
It's unclear whether the techniques presented in this paper will have any
appreciable effect on future exploits. Only time will tell.
References
[1] Cowan, Crispin et al. StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks.
http://www.usenix.org/publications/library/proceedings/sec98/full_papers/cowan/cowan_html/cowan.html; accessed 3/18/2007.
[2] Etoh, Hiroaki. GCC extension for protecting applications from stack-smashing attacks.
http://www.research.ibm.com/trl/projects/security/ssp/; accessed 3/18/2007.
[3] eEye. Memory Retrieval Vulnerabilities.
http://research.eeye.com/html/Papers/download/eeyeMRV-Oct2006.pdf; accessed 3/18/2007.
[4] Litchfield, David. Defeating the Stack Based Buffer Overflow Prevention Mechanism of Microsoft Windows 2003 Server
http://www.nextgenss.com/papers/defeating-w2k3-stack-protection.pdf; accessed 3/18/2007.
[5] Microsoft Corporation. /GS (Buffer Security Check).
http://msdn2.microsoft.com/en-us/library/8dbf701c(VS.80).aspx; accessed 3/18/2007.
[6] Microsoft Corporation. /SAFESEH (Image has Safe Exception Handlers).
http://msdn2.microsoft.com/en-us/library/9a89h429(VS.80).aspx; accessed 3/18/2007.
[7] Microsoft Corporation. QueryPerformanceFrequency Function.
http://msdn2.microsoft.com/en-us/library/ms644905.aspx; accessed 3/18/2007
[8] Microsoft Corporation. QueryPerformanceCounter Function.
http://msdn2.microsoft.com/en-us/library/ms644904.aspx; accessed 3/18/2007
[9] Ren, Chris et al. Microsoft Compiler Flaw Technical Note
http://www.cigital.com/news/index.php?pg=art&artid=70; accessed 3/18/2007.
[10] Whitehouse, Ollie. Analysis of GS protections in Windows Vista
http://www.symantec.com/avcenter/reference/GS_Protections_in_Vista.pdf; accessed 3/20/2007.