4th Feb 2003 [SBWID-5966]
COMMAND
Preventing buffer exploits discussion
SYSTEMS AFFECTED
Windows platforms
PROBLEM
- see below -
SOLUTION
David Litchfield [[email protected]] suggests :
http://www.ngssoftware.com/
Defeating Exploits
******************
The ideas in this "paper" present a method for defeating exploits; not
the actual vulnerability. Before getting to the details let's consider
slammer (again).
What made slammer so successful? The overriding factor that made
slammer so successful was it's ability to spread. What made it's spread
a foregone conclusion was the fact that every vulnerable SQL
Server/MSDE had a "jmp esp" instruction at address 0x42B0C9DC. This was
the address that was used to gain control of the SQL Server's path of
execution to a point where the worm's payload, the "arbitrary code",
would be executed.
This address is in a dynamic link library (DLL) , sqlsort.dll which has
an "image base" of 0x42AE0000.
Every image file, DLL or executable, has an "Image Base" and this base
is the preferred location where the file should be loaded into memory
by the Windows Loader. [I don't want to digress, here, as to what
happens if there's a conflict. See the references at the end.] Now if
this Image Base on one particular system had been 0x42AF0000 then the
worm would have failed to infect this particular box; the "jmp esp"
instruction that should've been at 0x42B0C9DC on this system would be
found at 0x42B1C9DC so the worm would have been off target. The SQL
Server running on this system, whilst still being "vulnerable" to the
buffer overflow vulnerability would have been invulnerable to this
worm. Sure - the SQL Server may have crashed - but it would not have
been compromised.
It's like sickle cell. Someone born with the gene that causes sickle
cell anaemia, a blood disorder that affects many people of a West
African origin, or carriers of the gene, sickle cell trait, do not
suffer from the ill affects of malaria, a disease caused by a parasite
and most commonly spread by mosquitoes. Whilst someone with sickle cell
trait can still catch malaria, the gene mutates the haemoglobin in
their blood in such a way that they are invulnerable to the
debilitating side effects and syptoms of the disease such as mental
confusion, coma and death. There is an obvious evolutionary advantage
to sickle cell trait; remember that the evolution of the species cares
not about how long a person lives, only that they live long enough to
pass on their genes. (Those with anaemia may suffer from crises,
periods of acute pain so the trade off is somewhat questionable.)
In areas where malaria is a common cause of death, being a carrier of
the sick cell gene can help ensure that this person lives long enough
to have progeny. This is Darwinian natural selection in progress.
Rebasing
********
The problem with operating systems is that they all have pretty much
the same "genetic code" which makes each and every one of them
vulnerable to a new exploit. So we need to make them different and this
can be achieved through rebasing. Rebasing is the process of changing
the Image Base of an image file. By doing this the DLL/EXE is loaded
into a different location in the virtual address space.
Going back to Slammer, had I have rebased sqlsort.dll giving it a new
base of 0x41410000 my box would have been invulnerable to the worm. If
another worm were written, though, that used an address that contained
a "jmp esp" instruction in kernel32.dll then I would be vulnerable. So
I rebase kernel32.dll. But then another worm uses another DLL so I
rebase that one, too. Eventually I've rebased all of the DLLs used by
SQL Server mutating it's "genetic code", making it considerably
different to any other SQL Server install on the planet. In fact if I
rebase every DLL on my system and every executable then I can make my
box almost invulnerable to a given exploit, past, present or future.
It's not that my box is invulnerable to a buffer overflow vulnerability
- it's just invulnerable to the exploits for it. To gain control of a
system protected in such a way would require that the author of the
exploit know the location of loaded DLLs.
So how easy is it to rebase DLLs and executables? Very. Microsoft have
provided a function to do this, ReBaseImage(), exported by
imagehlp.dll. If you rebase an image the new base must be on a 64K
boundary - i.e. if the image base mod 64000 !=0 the base is not valid.
The only other problem is Windows File Protection. Once you've rebased
a copy of the DLL you need to copy the new DLL over the old one but
Windows File Protection won't allow you to do this. To get around the
problem use the MoveFileEx and specifying the
MOVEFILE_DELAY_UNTIL_REBOOT flag. Doing this will add a registry value,
"PendingFileRenameOperations" to
HKLM\System\CurrentControlSet\Control\Session Manager\. You then need
to add another DWORD value "AllowProtectedRenames" and set it to 1.
Then restart the system. On reboot the new DLLs, with their new image
bases, will be loaded. For example - here is sample output of listdlls
after kernel32.dll and ws2_32.dll have been rebased.
Copyright (C) 1997-2000 Mark Russinovich
http://www.sysinternals.com
----------------------------------------------------------------------------
--
WINLOGON.EXE pid: 208
Command line: winlogon.exe
Base Size Version Path
0x01000000 0x2e000 \??\C:\WINNT\system32\winlogon.exe
0x77f80000 0x7b000 5.00.2195.5400 C:\WINNT\System32\ntdll.dll
0x78000000 0x46000 6.01.9359.0000 C:\WINNT\system32\MSVCRT.dll
0x4a4a0000 0xb1000 5.00.2195.6011 C:\WINNT\system32\KERNEL32.dll
0x77db0000 0x5b000 5.00.2195.5992 C:\WINNT\system32\ADVAPI32.dll
0x77d30000 0x71000 5.00.2195.5419 C:\WINNT\system32\RPCRT4.dll
0x54530000 0x13000 5.00.2195.4874 C:\WINNT\system32\WS2_32.dll
..
..
Now all the way through this I've been saying things like "almost
invulnerable" etc. Here's the reason. For some vulnerabilities it may
be sufficient to overwrite a saved return address, function pointer or
whatever by only a few bytes. For example assume a saved return address
is 0x44784500 and at address 0x44784536 is a "jmp ebx" instruction and
ebx points to our code. Then we only need to overwrite the saved return
address by 1 byte - with 0x36. So knowledge of the DLL load address is
not needed. However, this scenario is going to happen so infrequently
(if ever) that it does not detract from the idea of rebasing your
system. There may other ways to bypass this method.
Some ideas to further help prevent exploits from working.
Use addresses such as 0x**000000 or 0x00**0000 for the new image base.
With there being a NULL in much of the image's address space this will
help. (This of course won't make a difference with unicode overflows)
Ensure at least one (core) DLL has a base of 0x00119400 . This will
ensure that a common stack location 0x00120000 has been assigned
forcing the OS to chose another location for the stack. You get the
idea.
MSDN Info ReBaseImage()
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/debug/base/rebaseimage.asp
MoveFileEx()
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/base/movefileex.asp
Update (05 Februrary 2003)
======
Thomas [[email protected]] comments :
--snip--
Rebasing everything is something you're not very likely to achieve.
Hardly any commercial software has executables which still contain
valid relocation information -- which means that you can rebase all
DLL's as much as you want, the main EXE (which is always mapped at
0x00400000 and cannot be remapped) will be present & can be used
for exploitation. Unless you rebase the complete address space you
remain vulnerable. Furthermore, rebasing might not be sufficient, as
there's
less than 32k different bases -- if the service restarts cleanly brute
force is definitely an option. So you need full randomization.
Heap corruptions allow an attacker to write arbitrary data to arbitrary
locations -- so he can patch his own "jmp ebx" or whatever to whereever
he wishes. Unless you implement something PaX-like for
writable/executable pages, you're still vulnerable. And the majority of
all buffer overruns _are_ heap corruptions.
Oh, and there's always the static mapping of the TEB's under Windows.
So the solution you're proposing
a) Will only work against a small subset of all
closed-source-applications (those with relocatable main .exe)
b) Will even then only protect you against vanilla stack smashes, and offer 0
protection against heap corruptions or format string bugs
c) Will be suspectible to brute-force attacks on your address space
(which cannot be more complex than 2^15 ... hardly a "hard"
task)
-Also-
Someone points out :
There is a tool called "ReBase" shipped with Visual C++ and Visual
C++.NET.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/tools/perfutil_2z39.asp
" Rebase is a command-line tool that you can use to specify the base
addresses for the DLLs that your application uses. "
" Alternatively, you can use the ReBaseImage function. "
Update (06 February 2003)
======
Riley Hassell [[email protected]], Security Research Associate of eEye
Digital Security argues :
So the course of this talk with most likely go into generating a
totally dynamic address space and once again, end in another
theoretical solution, to an overly complex problem.
Defeating Rebasing
-------------------------------------
Many operating systems with fault handling features and refined
multitasking, reference address spaces with segments to permit these
features and aid general performance. The majority of this behavior can
be studied by following process creation and task switching. Start from
the user API and step through until the entry point of the executable
is reached.
TEB: Thread Environment Block
TIB: Thread Information Block
PEB: Process Environment Block
The TEB/TIB fs:[] segment references originated in the OS2 days and
have since passed down into 9x client systems, and of course, Windows
NT. During the process creation the PEB and TIB are initialized into
the new virt and can be referenced by the fs segment. Modifying the
address space referenced by fs and how fs is setup is possible... but
by the time you're done you're designing a new operating system.
As Ryan might say... It's all data ;) The data referenced in this delta
can be referenced during a ret. You just need to find a set of bytes
that forms the needed instruction. You may be able to modify this arena
in a way that you can insert your own instructions. Maybe some of the
TLS storage can be controlled by supplying malformed sizes in your
exploit session... ;)
Note: PEB locking pointers can be overwritten with format bugs and
control structure based heap overflows.
7FFDF000 00010000
7FFDF004 FFFFFFFF
7FFDF008 01000000 <- Executable image base ;)
7FFDF00C 00071E90
7FFDF010 00020000
7FFDF014 00000000
7FFDF018 00070000
7FFDF01C 77FCF170 <- PEB fast Lock entry point
7FFDF020 77F8313C <- PEB lock entry point
7FFDF024 77F8316D <- PEB unlock entry point
Brett Moore wrote me several months ago with a very interesting exploit
concept using multiple writes. The first write you insert your needed
instruction into writeable memory somewhere. The second write you
overwrite some writeable entry point or hook, with the address of the
inserted instruction.
There's not much out there if you're interested in learning about the
TIB and the PEB. If you really want to understand these structures and
general loading behavior, learn Polish and Russian, then hit up some
VX'er archives. If you end up talking to any of them, tell them that
somebody is trying to stop exploits by rebasing dll's :)
Rebasing... There's a reason why relocation sections exist. While doing
your own relocations is possible, the design of such a system is
extremely, and I'll say again, extremely complex. Just differentiating
all the instructions from data is a fairly painful process. Maybe the
ETCH guys did this at one point but as far as I know this has been a
big hurdle in image modifcation for quite some time.
Michal Zalewski provided some great examples of issue's you'll run
into:
MZ> Also, what if I wanted to pass a value 4325404 (0x42001c) to this
MZ> function, and it is not a pointer, only looks this way? For example,some
MZ> FOO_ASYNC flag is defined as 0x400000, FOO_LOCK as 0x020000, and
voila,OR
MZ> them and you have "a pointer".
MZ> In other cases, say, with register calls, it is getting even nastier,
MZ> because even if, one way or another, you managed to find out how every
MZ> single function is going to use its parameters (not likely), register
MZ> calls are still black magic.
GetProcAddress ACL's...
>
> It is possible to intercept every call to GetProcAddress and determine
> whether or not the call should be authorized based on a predetermined list
> of known valid callers (runtime call stack analysis).
Simply rewrite a micro GetProcAddress. GetProcAddress is basically an
overstuffed RVA engine. This is defeated by "The payload brings the
tools concept". Most userland hooking schemes can also easily be
bypassed by using direct gates "Ex: Interupts Gates". You could also
intercept a thread that has the neccesary privelege by snagging a hook
in it's path.
> This list of authorized callers must be constructed through the use of
forensic profiling
> tools in the case of other people's binaries, but can be constructed with
> the help of additional API calls in the case of one's own code. Call a
> profiling/tracing API before calling GetProcAddress. After compilation but
> before deployment to production boxes you simply execute the code in
profile
> mode to generate a list of authorized callers. This list is then
configured
> as a static security setting adhered to by the security layer that sits
> between GetProcAddress and the rest of the virtual world.
Who's an authorized caller?
Someone who has a "safe" caller address on the stack....
If a the attacker start's offering instructions to your CPU... kiss
your ass goodbye. Research AV/VX trends from the late 80's and early
90's.
Update (08 Februrary 2003)
======
Alex Fedotov proposed the following PEB/TEB data structures :
http://groups.google.com/groups?hl=en&lr=&ie=UTF-8&oe=UTF-8&safe=off&q=_NT_TEB&btnG=Google+Search
typedef struct _CURDIR
{
UNICODE_STRING DosPath;
PVOID Handle;
} CURDIR, *PCURDIR;
typedef struct RTL_DRIVE_LETTER_CURDIR
{
USHORT Flags;
USHORT Length;
ULONG TimeStamp;
UNICODE_STRING DosPath;
} RTL_DRIVE_LETTER_CURDIR, *PRTL_DRIVE_LETTER_CURDIR;
typedef struct _PEB_FREE_BLOCK
{
struct _PEB_FREE_BLOCK* Next;
ULONG Size;
} PEB_FREE_BLOCK, *PPEB_FREE_BLOCK;
/* RTL_USER_PROCESS_PARAMETERS.Flags */
#define PPF_NORMALIZED (1)
typedef struct _RTL_USER_PROCESS_PARAMETERS
{
ULONG MaximumLength; // 00h
ULONG Length; // 04h
ULONG Flags; // 08h
ULONG DebugFlags; // 0Ch
PVOID ConsoleHandle; // 10h
ULONG ConsoleFlags; // 14h
HANDLE InputHandle; // 18h
HANDLE OutputHandle; // 1Ch
HANDLE ErrorHandle; // 20h
CURDIR CurrentDirectory; // 24h
UNICODE_STRING DllPath; // 30h
UNICODE_STRING ImagePathName; // 38h
UNICODE_STRING CommandLine; // 40h
PWSTR Environment; // 48h
ULONG StartingX; // 4Ch
ULONG StartingY; // 50h
ULONG CountX; // 54h
ULONG CountY; // 58h
ULONG CountCharsX; // 5Ch
ULONG CountCharsY; // 60h
ULONG FillAttribute; // 64h
ULONG WindowFlags; // 68h
ULONG ShowWindowFlags; // 6Ch
UNICODE_STRING WindowTitle; // 70h
UNICODE_STRING DesktopInfo; // 78h
UNICODE_STRING ShellInfo; // 80h
UNICODE_STRING RuntimeInfo; // 88h
RTL_DRIVE_LETTER_CURDIR DLCurrentDirectory[0x20]; // 90h
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
#define PEB_BASE (0x7FFDF000)
typedef struct _PEB_LDR_DATA
{
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
typedef VOID STDCALL (*PPEBLOCKROUTINE)(PVOID);
typedef struct _PEB
{
UCHAR InheritedAddressSpace; // 00h
UCHAR ReadImageFileExecOptions; // 01h
UCHAR BeingDebugged; // 02h
UCHAR Spare; // 03h
PVOID Mutant; // 04h
PVOID ImageBaseAddress; // 08h
PPEB_LDR_DATA Ldr; // 0Ch
PRTL_USER_PROCESS_PARAMETERS ProcessParameters; // 10h
PVOID SubSystemData; // 14h
PVOID ProcessHeap; // 18h
PVOID FastPebLock; // 1Ch
PPEBLOCKROUTINE FastPebLockRoutine; // 20h
PPEBLOCKROUTINE FastPebUnlockRoutine; // 24h
ULONG EnvironmentUpdateCount; // 28h
PVOID* KernelCallbackTable; // 2Ch
PVOID EventLogSection; // 30h
PVOID EventLog; // 34h
PPEB_FREE_BLOCK FreeList; // 38h
ULONG TlsExpansionCounter; // 3Ch
PVOID TlsBitmap; // 40h
ULONG TlsBitmapBits[0x2]; // 44h
PVOID ReadOnlySharedMemoryBase; // 4Ch
PVOID ReadOnlySharedMemoryHeap; // 50h
PVOID* ReadOnlyStaticServerData; // 54h
PVOID AnsiCodePageData; // 58h
PVOID OemCodePageData; // 5Ch
PVOID UnicodeCaseTableData; // 60h
ULONG NumberOfProcessors; // 64h
ULONG NtGlobalFlag; // 68h
UCHAR Spare2[0x4]; // 6Ch
LARGE_INTEGER CriticalSectionTimeout; // 70h
ULONG HeapSegmentReserve; // 78h
ULONG HeapSegmentCommit; // 7Ch
ULONG HeapDeCommitTotalFreeThreshold; // 80h
ULONG HeapDeCommitFreeBlockThreshold; // 84h
ULONG NumberOfHeaps; // 88h
ULONG MaximumNumberOfHeaps; // 8Ch
PVOID** ProcessHeaps; // 90h
PVOID GdiSharedHandleTable; // 94h
PVOID ProcessStarterHelper; // 98h
PVOID GdiDCAttributeList; // 9Ch
PVOID LoaderLock; // A0h
ULONG OSMajorVersion; // A4h
ULONG OSMinorVersion; // A8h
ULONG OSBuildNumber; // ACh
ULONG OSPlatformId; // B0h
ULONG ImageSubSystem; // B4h
ULONG ImageSubSystemMajorVersion; // B8h
ULONG ImageSubSystemMinorVersion; // C0h
ULONG GdiHandleBuffer[0x22]; // C4h
PVOID ProcessWindowStation; // ???
} PEB, *PPEB;
typedef struct _GDI_TEB_BATCH
{
ULONG Offset;
ULONG HDC;
ULONG Buffer[0x136];
} GDI_TEB_BATCH, *PGDI_TEB_BATCH;
typedef struct _NT_TEB
{
NT_TIB Tib; // 00h
PVOID EnvironmentPointer; // 1Ch
CLIENT_ID Cid; // 20h
PVOID ActiveRpcInfo; // 28h
PVOID ThreadLocalStoragePointer; // 2Ch
PPEB Peb; // 30h
ULONG LastErrorValue; // 34h
ULONG CountOfOwnedCriticalSections; // 38h
PVOID CsrClientThread; // 3Ch
PVOID Win32ThreadInfo; // 40h
ULONG Win32ClientInfo[0x1F]; // 44h
PVOID WOW32Reserved; // C0h
ULONG CurrentLocale; // C4h
ULONG FpSoftwareStatusRegister; // C8h
PVOID SystemReserved1[0x36]; // CCh
PVOID Spare1; // 1A4h
LONG ExceptionCode; // 1A8h
ULONG SpareBytes1[0x28]; // 1ACh
PVOID SystemReserved2[0xA]; // 1D4h
GDI_TEB_BATCH GdiTebBatch; // 1FCh
ULONG gdiRgn; // 6DCh
ULONG gdiPen; // 6E0h
ULONG gdiBrush; // 6E4h
CLIENT_ID RealClientId; // 6E8h
PVOID GdiCachedProcessHandle; // 6F0h
ULONG GdiClientPID; // 6F4h
ULONG GdiClientTID; // 6F8h
PVOID GdiThreadLocaleInfo; // 6FCh
PVOID UserReserved[5]; // 700h
PVOID glDispatchTable[0x118]; // 714h
ULONG glReserved1[0x1A]; // B74h
PVOID glReserved2; // BDCh
PVOID glSectionInfo; // BE0h
PVOID glSection; // BE4h
PVOID glTable; // BE8h
PVOID glCurrentRC; // BECh
PVOID glContext; // BF0h
NTSTATUS LastStatusValue; // BF4h
UNICODE_STRING StaticUnicodeString; // BF8h
WCHAR StaticUnicodeBuffer[0x105]; // C00h
PVOID DeallocationStack; // E0Ch
PVOID TlsSlots[0x40]; // E10h
LIST_ENTRY TlsLinks; // F10h
PVOID Vdm; // F18h
PVOID ReservedForNtRpc; // F1Ch
PVOID DbgSsReserved[0x2]; // F20h
ULONG HardErrorDisabled; // F28h
PVOID Instrumentation[0x10]; // F2Ch
PVOID WinSockData; // F6Ch
ULONG GdiBatchCount; // F70h
ULONG Spare2; // F74h
ULONG Spare3; // F78h
ULONG Spare4; // F7Ch
PVOID ReservedForOle; // F80h
ULONG WaitingOnLoaderLock; // F84h
PVOID StackCommit; // F88h
PVOID StackCommitMax; // F8Ch
PVOID StackReserve; // F90h
PVOID MessageQueue; // ???
} NT_TEB, *PNT_TEB;
Update (11 Februrary 2003)
======
In Peter Huang whitepaper of compiler security optimization :
http://members.rogers.com/yinrong/articles/Prevent.htm
For the past few days, I have done a few experiments and some research
on ways to prevent the buffer overflow exploitation. I believe the
following compiler option (if implemented and used) should make the
exploitation of stack buffer overflow by "jmp esp" method impossible
(as far as I know) to execute a piece of malicious code on Intel-Inside
PC. The buffer vulnerability, if exists, still overflows to be trapped
by this mechanism if being pumped too much.
For a function like:
Void testVoid (void)
{
do something here and the stack overflows such as sprintf in ssnetlib.dll
in SQL 2000 server (pre-SP3)
}
The compiler inserts the following opcodes before "ret".
Mov ecx, [esp]
Cmp word ptr [ecx], 0FFE4h ; check for "jmp esp"
Jnz realRet
Mov byte ptr [esp+4], 0cch ; stack has been overflowed and function
; will breakpoint (no need to go on anyway)
RealRet:
ret
For a function like:
Void testParametered(int I, int i2, int i3 ...)
{
do something here and the stack overflows such as sprintf in ssnetlib.dll
in SQL 2000 server (pre-SP3)
}
The compiler inserts the following opcode before "ret"
Mov byte ptr[esp+4], 0cch ; if return address is not overwritten,
; then this will be discarded by opcode such
as
; "add esp, xx" down the execution path.
; Otherwise, the exploitation code injected
onto the stack,
; which happens before this move, will
; cause a breakpoint trap to happen due to the
; "jmp esp" method exploited by the buffer
overflow.
Ret
Normally, both above functions continue normally. However, if the stack
is overflowed due to some software bugs under exploitation attack, then
the mechanism listed above will generate breakpoint trap and the
execution of malicious code is stopped.
Unfortunately, it will definitely increase the executable image size
and some runtime hit (should not be much because of the cache lines are
still valid etc.). Some might think that this creates some false
illusion of security. However, it does dam the overflow to what has
been overflowed and prevent the overflow from spreading so that we do
not have to hear others say:" dam it".