Monday, January 05, 2009

Lessons learned from MS08-005

[ This post has been 80% complete for 1 year. And I swear the cleanup my "todo list" in 2009 :) ]

MS08-005 (KB 942831) is a local privilege escalation bug affecting:
  • IIS 5.0 (Windows 2000)
  • IIS 5.1 (Windows XP)
  • IIS 6.0 (Windows 2003)
  • IIS 7.0 (Vista)
This particular bug caught my attention for several reasons:
  • Local bugs tend to be more easily and reliably exploitable ;
  • The bugfix is very small in size ;
  • This bug made up his way into Vista, despite manual and automated code analysis.
Let's play with Windows XP SP2 version of this bug!

BinDiff-ing

First step is to install the patch, and to recover backuped files. In our case, there is only 1 file (infocomm.dll), that can be found in:
C:\windows\$NtUninstallKB942831$

Second step is to diff both original and patched files. If you happen to have a legit IDA Pro copy, Tenable PatchDiff2 is the best free plugin available out there. Otherwise, you'll have to fall back on eEye Binary Diffing Suite. Screenshots below are taken from BinDiff2.

Only 2 functions were modified by the patch:
int __stdcall CVRootDirMonitorEntry::FileChanged(char *lpString2, int) int __thiscall CVRootDirMonitorEntry::ActOnNotification(unsigned long, unsigned long)

Understanding the change

We will focus on FileChanged() function, in which strlen()/strcpy() operations were fixed (as shown in the graph below). Note: both APIs were inlined.



Code changes can also be spotted using the excellent Hex-Rays decompilation plugin.

Unpatched version:

if ( _strchr(input_filename, '~') )
{
result = ConvertToLongFileName(*((char **)long_filename + 3), input_filename, &FindFileData);
if ( !
result )
return
result;
v7 = _strrchr(input_filename, '\\');
if (
v7 )
{
v9 = v7 - input_filename;
v15 = v7 - input_filename + 1;
v16 = v7 - input_filename + 1;
v15 >>= 2;
memcpy(&v25, input_filename, 4 * v15);
v17 = &input_filename[4 * v15];
v18 = &v25 + 4 * v15;
v19 = v16 & 3;
v8 = FindFileData.cFileName;
memcpy(v18, v17, v19);
do
v20 = *v8++;
while (
v20 );
memcpy(&v26[v9], FindFileData.cFileName, v8 - &FindFileData.cFileName[1] + 1);
input_filename = &v25;
}

Patched version:

if ( _strchr(v4, '~') )
{
result = ConvertToLongFileName(*(char **)(v3 + 12), v4, &FindFileData);
if ( !
result )
return
result;
v7 = _strrchr(v4, '\\');
if (
v7 )
{
v9 = v7 - v4 + 1;
memcpy(v24, v4, v7 - v4 + 1);
v8 = FindFileData.cFileName;
v10 = 261 - v9;
do
v17 = *v8++;
while (
v17 );
if (
v8 - &FindFileData.cFileName[1] + 1 < v10 )
{
v11 = FindFileData.cFileName;
do
v18 = *v11++;
while (
v18 );
v10 = v11 - &FindFileData.cFileName[1] + 1;
}
v3 = v22;
memcpy(&v24[v9], FindFileData.cFileName, v10);
v25 = 0;
v4 = v24;
}


Runtime analysis

At this point, a little bit of runtime analysis using WinDbg could help us to get the whole picture.

Being attached to the inetinfo.exe process (where infocomm.dll is loaded, according to Sysinternals handle utility), we set a breakpoint on FileChanged():

0:016> .reload /f
Reloading current modules ..................................................................
0:016> bp CVRootDirMonitorEntry::FileChanged
0:016> bl

0 e 71ba7ca0 0001 (0001) 0:**** INFOCOMM!CVRootDirMonitorEntry::FileChanged
0:016> g


Given the name of the function under scrutiny, we suspect it will be called during file operations inside the Web root:

echo "hello" > c:\inetpub\wwwroot\test.txt

It worked! Moreover, we can confirm that the first argument passed to FileChanged(), which is of type char* according to debug symbols, is the filename.

Breakpoint 0 hit
eax=00000000 ebx=00000008 ecx=00712430 edx=007238c4 esi=007238a8
edi=007238a8
eip=71ba7ca0 esp=009cfed8 ebp=009cff0c
iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246 INFOCOMM!CVRootDirMonitorEntry::FileChanged:
71ba7ca0 8bff mov edi,edi

0:004> da poi(esp+4)

009cfee4 "test.txt"


Summary of our findings

Not everything makes sense for now, but we have gathered much interesting information during this preliminary analysis phase:
  • CVRootDirMonitorEntry::FileChanged() does (possibly insecure) string manipulation.
  • This function is called whenever a file under the Web root is "touched". The filename is passed as the first argument.
  • The offending code will be reached only if the filename contains the "~" character, and the "\" character (thus being inside a subdirectory).
  • ConvertToLongFileName() will be called in between on the filename.
A bug, really ?

At this point, we need to have a closer look at ConvertToLongFileName() internals.

According to debug symbols, the function prototype is:
int __stdcall ConvertToLongFileName(char *, LPCSTR lpString2, LPWIN32_FIND_DATAA lpFindFileData)

Implementation of this function is trivial: it takes the filename as an argument and uses FindFirstFileA() on it. The corresponding WIN32_FIND_DATA structure is passed back to the caller for future use.

MSDN documentation relative to FindFirstFile() is pretty straightforward. The WIN32_FIND_DATA structure is more interesting:

typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
TCHAR cFileName[MAX_PATH];
TCHAR cAlternateFileName[14];
}
WIN32_FIND_DATA,
*PWIN32_FIND_DATA, *LPWIN32_FIND_DATA;


The caller will copy
FindFileDate.cFileName into a fixed size buffer of 264 bytes. Since MAX_PATH has a value of 260 on Windows platform, this is probably MAX_PATH+1 aligned to a DWORD. Where is the trick ?

Where Unicode comes into play

The trick is called Unicode. Quoting MSDN documentation:
In the Windows API (with some exceptions discussed in the following paragraphs), the maximum length for a path is MAX_PATH, which is defined as 260 characters.
(...)
The Windows API has many functions that also have Unicode versions to permit an extended-length path for a maximum total path length of 32,767 characters.
What about CreateFile ?
The Unicode versions of several functions permit a maximum path length of approximately 32,000 characters composed of components up to 255 characters in length.
Therefore, it is possible to build a very long Unicode path, as long as each path token is less than 255 characters long. There is a little quirk in FindFirstFile() documentation here: cFileName cannot be longer than MAX_PATH, but the full path to this file can go far beyond MAX_PATH.

Do it yourself

Here are the steps to trigger the bug:
  • Using the mkdir command, create a directory inside C:\Inetpub\wwwroot with a long name (200 times 'A', for instance).
  • Using CreateFile("\\?\C:\Inetpub\wwwroot\AAA...AAA\BBB...BBB"), create inside this directory a file with a long name (200 times 'B', for instance). This API call must be Unicode-style, because the resulting full path will be longer than MAX_PATH.
  • Now access this file using its short name, as reported by the dir /x command. In this example, this would be something like echo toto > bbbbbb~1.
Et voilĂ  ! IIS should crash, because it expanded "aaa...aaa\bbbbbb~1" into "aaa...aaa" and "bbb...bbb" strings, that are thereafter concatenated into a stack-based buffer of size MAX_PATH.

Since infocomm.dll has been compiled with /GS option, a stack cookie prevents direct exploitation of this bug. Exploitation on IIS 5 is left as an exercise to the reader ;)

Conclusion

That was a very nice bug to study (even if it ended up in a trivial stack overflow) because it requires good knowledge of Windows internals.

As usual, it would be nice to know "how" this bug has been found by the original author. However, using Unicode filenames breaks so many applications out there that it could have been found by accident ;)

PS. Happy New Year to all readers !

No comments: