[ 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-ingFirst 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 analysisAt 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> gGiven 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.txtIt 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 findingsNot 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 yourselfHere 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 ;)
ConclusionThat 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 !