The following report is adapted from e-mail sent to Noah Groth, Andrew Schulman, Richard Smith at 03:31 on Saturday 21st August 1999. In summary, it is asserted that:
Beyond the preceding introduction and one remark to follow, I have not attempted any speculation here regarding America Online or Microsoft or any other party involved in the allegations or in supposed confirmations. For now, I just say that I am unimpressed to varying depths with all of them.
[We need some standard terminology and perhaps also a description in tables or pictures rather than words.]
From the AIM clients perspective, much of the protocol centres on packets that begin with a 6-byte header. The first byte is necessarily 2Ah. The second byte is a packet type. There is then a word that serves as a serial number for the packet, and finally a word that gives the size, in bytes, of the packets contents.
Of concern here are packets of type 02h. For these, the packet header is followed by a SNAC header, generally of 10 bytes, being 3 words and a dword. For our purposes, the first two words may be taken as defining a SNAC type. The SNAC header is followed immediately by SNAC data, the format of which varies with the SNAC type. (The name SNAC is taken from the names of functions exported by OSCORE.DLL.)
Of concern here are SNAC headers whose first two words are 0001h and 0013h. For these, the SNAC data consists of a word followed optionally by data bundles. That first word is a bundle type. Each bundle that follows consists of two words plus some data. The first word is the data type. The second word gives the size, in bytes, of the data that follows.
In all the above, bytes have non-Intel ordering within words and dwords.
The AIM module named PROTO.OCM has a C-language function that asks WinSock for data and then examines that data for packets beginning with a recognised 6-byte packet header. Let us name this function GetPackets.
When the GetPackets function is satisfied that it has a 6-byte packet header, it calls a subfunction that is to analyse the packets contents. Let us name this subfunction ProcessPacket.
When the ProcessPacket function sees that the packet type is 02h, it assumes that the packets contents begin with a SNAC header. (This sort of assumption is fairly typical of the coding. The packet header is checked fairly carefully for validity, but the packets further contents are pretty much assumed to be correctly formed.) If the first two words of the SNAC header are 0001h and 0013h, then further handling is passed to yet another subfunction. Let us name this subfunction Proto_13h.
The Proto_13h function assumes that the SNAC data is in the form sketched above. It assumes the presence of at least one word of SNAC data, described above as the bundle type. The function then proceeds to the bundles. For each bundle, at least two words are simply assumed to be present. The second word, no matter how implausible its value, is then taken to be the size of the data that follows. The data in a bundle is of interest to the Proto_13h function only if the data type is 000Bh. In this case, the function saves the data in a 0100h-byte buffer on the stack (and appends a null byte).
For reference and verifiability, the following table gives the addresses of the functions described above for each of the PROTO.OCM versions studied. Addresses are given on the assumption that PROTO.OCM is loaded at its preferred base address 11080000.
2.0 2.1 3.0 GetPackets 1108452D 1108453B 11084A46 ProcessPacket 11084299 110842A7 1108478E Proto_13h 110841F8 11084206 11084560
It is instructive to look at this sequence of calls in terms of its effect on the stack. We start where GetPackets prepares its call to ProcessPacket.
==== ESP when call to ProcessPacket prepared ====
4 arguments to ProcessPacket, irrelevant on return to GetPacket
====
return address from ProcessPacket to GetPacket
====
EBP on entry to ProcessPacket
==== ebp while inside ProcessPacket ====
44h bytes of local variables for ProcessPacket
====
3 arguments to Proto_13h, irrelevant on return to ProcessPacket
====
return address from Proto_13h to ProcessPacket
====
EBP on entry to Proto_13h
==== ebp while inside Proto_13h ====
08h bytes of local variables for Proto_13h
----
0100h-byte local buffer for Proto_13h
====
ESI on entry to Proto_13h
----
EDI on entry to Proto_13h
==== ESP when bundle data read from packet to buffer ====
The ordinary path of return from Proto_13h would start with instructions:
pop edi pop esi leave ret
which brings execution back into ProcessPacket at the instruction immediately after the call to Proto_13h. The esi, edi and ebp registers are restored to the values they had immediately before the call to Proto_13h. The ProcessPacket function then removes the 3 Proto_13h arguments by executing something such as
add esp,0Ch
Note that this usual path relies on two dwords to be correctly preserved on the stack during the execution of Proto_13h and of any subfunctions. These dwords are the ones described above as EBP on entry to Proto_13h and return address from Proto_13h to ProcessPacket. It is possible to return without knowing these dwords, but in exchange, one must know how much space to allow for the 3 arguments to Proto_13h and for the 44h bytes of local variables for ProcessPacket.
The following instructions produce exactly the same result as above
pop edi pop esi leave add esp,10h ; omit RET and skip 3 arguments to Proto_13h mov ebp,esp add ebp,44h ; skip local variables for ProcessPacket
They leave the ProcessPacket function ready to return in its ordinary way to GetPackets.
Suppose we do not want to return immediately to GetPackets. Suppose instead that we want to execute some different C-language function, passing it three arguments. While the stack and registers are set up for a return to GetPackets, we could do the following, were we able to just execute extra instructions. Note especially that ebp points to the place marked above as ebp while inside ProcessPacket.
mov [ebp+10h],arg3 mov [ebp+0Ch],arg2 mov [ebp+08h],arg1 leave jmp different_function
As far as the different function is concerned, it has 3 arguments on the stack and looks set to return to GetPackets. This different function will do whatever its work may be. When it is done, it returns to GetPackets. As far as GetPackets is concerned, it has just been returned to from ProcessPacket.
Lets collect the instructions that return from Proto_13h to GetPackets by way of 1) not assuming preservation of an ebp and a return address on the Proto_13h stack and 2) executing some different function along the way.
pop edi \ pop esi - usual clean up in Proto_13h leave /
add esp,10h mov ebp,esp add ebp,44h mov [ebp+10h],arg3 mov [ebp+0Ch],arg2 mov [ebp+08h],arg1 leave jmp different_function
Please remember this last set for later.
You may ask what is this concern about not assuming preservation of an ebp and return address on the Proto_13h stack.
Look back carefully at the description of how the Proto_13h function processes the SNAC data. See that for data type 000Bh, the data that follows gets copied to a 0100h-byte buffer on the Proto_13h stack. However, the amount of data to copy is not limited to 0100h. The function copies as many bytes as seem to be declared by the SNAC data (plus a null byte, as if to terminate a string). If the SNAC data declares there to be 0110h bytes or more (including that null byte), then both the stacked ebp and the stacked return address get overwritten. They get corrupted.
This bug is remarkably common. [There is much scope for editorial comment here.]
[Insert history of communication to Richard Smith from Phil Bucking.]
It has been observed that some AIM clients receive packets with packet type 02h, SNAC header beginning with 0001h and 0013h, and then SNAC data with bundle type 00FFh followed by the one bundle with data type 000Bh, data size 0118h and then 0118h bytes of data.
This packet triggers the buffer overflow bug, corrupting two dwords of the Proto_13h functions local variables, the stacked EBP and return address and two of the three Proto_13h arguments.
Despite this corruption, the AIM client appears to continue normally.
Lets look at those 0118h bytes.
0000 83 C4 10 4F 8D 94 24 E4-FE FF FF 8B EC 03 AA F8 0010 00 00 00 90 90 90 90 8B-82 F0 00 00 00 8B 00 89 0020 82 4E 00 00 00 8B 4D 04-03 8A F4 00 00 00 8D 82 0030 42 00 00 00 89 45 10 B8-10 00 00 00 89 45 0C C9 0040 FF E1 00 01 00 20 00 00-00 00 00 00 00 04 00 00 0050 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 0060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 0070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 0080 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 0090 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 00A0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 00B0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 00C0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 00D0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 00E0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 00F0 19 10 08 11 29 EC FF FF-44 00 00 00 00 00 00 00 0100 FF 00 00 00 08 01 00 00-00 00 00 00 90 47 40 00 0110 F8 E9 EA FE FF FF 00 00
They begin with 42h bytes that Phil Bucking described as valid (and coherent) assembler. At least after the above analysis, they may seem familiar:
add esp,10h dec edi lea edx,[esp-0000011Ch] mov ebp,esp add ebp,[edx+000000F8h] nop nop nop nop mov eax,[edx+000000F0h] mov eax,[eax] mov [edx+4Fh],eax mov ecx,[ebp+04h] add ecx,[edx+000000F4h] lea eax,[edx+42h] mov [ebp+10h],eax mov eax,10h mov [ebp+0Ch],eax leave jmp ecx add esp,10h mov ebp,esp add ebp,44h mov [ebp+10h],arg3 mov [ebp+0Ch],arg2 leave jmp different_function
Let us assume that the matching of the packets instructions with ours is no coincidence! Then the third instruction aligns edx to the start of the 0118h bytes. See then that the dword at offset F8h in those bytes has the value 00000044h, completing our match.
At the end of the sequence, we can see that our different function is prepared by taking the return address from ProcessPacket to GetPacket and adding the dword from offset F4h in the bytes. The value of this dword is FFFFEC29h, which is -13D7h. In observed versions of PROTO.OCM, adding this to the return address from ProcessPacket to GetPacket gives us the address of a function that PROTO.OCM uses for sending SNAC data, duly wrapped up with a packet header. Let us name that function SendSNACData.
Inspection of PROTO.OCM shows that this SendSNACData function takes three arguments. The first is the same sort of handle that is passed as the first of the ProcessPacket arguments. The second and third are respectively the length and address of the SNAC (both header and the data that follows the header).
Looking again at the code sequence from the Phil Bucking packet, we see that SendSNACData will get the same first argument as was passed to ProcessPacket, but that its second and third arguments will be respectively 10h and an address 42h bytes into the 0118h bytes.
Thus, executing this code as an alternate return from Proto_13h will have the effect of sending a SNAC (with packet header). The SNAC header and data will total 10h bytes and be taken from offset 42h in the original 0118h bytes. Thus, 0001h, 0020h, 0000h, 00000000h for the SNAC header, and 0004h and then the four bytes 8Bh, 44h, 24h, 04h for the data.
That last dword is not supplied in the original 0118h bytes but is prepared by instructions in the code sequence. Specifically, the dword supplied at offset F0h in the 0118h bytes is interpreted as an address from which to take a dword for insertion at offset 4Eh, where it becomes that last dword in the sent SNAC data. Note that the dword at offset F0h is 11081019h. This is an address very near the start of the PROTO.OCM code segment. The instruction there is
mov eax,[esp+04h]
with opcodes 8Bh, 44h, 24h, 04h.
The above discussion explains how receipt of the Phil Bucking packet triggers the buffer overflow bug but lets PROTO.OCM resume execution instead of crashing. It also explains how PROTO.OCM responds to the Phil Bucking packet by sending a packet whose SNAC header begins with 0001h, 0020hand it explains the slight difference between the SNAC header and data as sent and the bytes at offset 42h as originally received in the 0118h bytes.
All that is left is to explain how the code at the start of those 0118h bytes ever gets to execute.
The return address on the Proto_13h stack will have been corrupted from offset 010Ch in the 0118h bytes. When the Proto_13h function attempts its usual return, execution will instead go to the address that was supplied at offset 010Ch in the 0118h bytes. Were the buffer overflow an ordinary bug, with the corrupt return address being an essentially random value, the typical consequence would be a CPU exception and the termination of the AIM process.
To get the Phil Bucking packet to execute, the corrupt return address must be prepared specially. Note that the bytes immediately after offset 010Ch are a CLC and then a JMP back to the start of the 0118h bytes. Given this, the simplest way to get from the Proto_13h functions RET instruction to the start of the 0118h bytes is for there to be a CALL ESP instruction at the corrupt return address.
The CALL ESP instruction is represented by the 2-byte opcode FFh, D4h. All that is required for AOL to execute the downloaded Phil Bucking packet on the AOL clients machine is that AOL know of an address, whether code or data, at which the presence of the two bytes FFh and D4h is certain.
The most certain addresses in the address space of the AIM process are those of AIM.EXE itself, which will be loaded with a base address of 00400000. The resource section of AIM.EXE has some 20 occurrences of the 2-byte sequence FFh D4h. In the Phil Bucking packet, the corrupt return address is 00404790. For version 2.0.912 of the AIM client software, but not for version 2.1.1236, this corrupt return address is indeed that of a 2-byte sequence FFh D4h in the AIM.EXE resource section. .
This page was created on 23rd August 1999.
Copyright © 1999. Geoff Chappell. All rights reserved.
[Home][Programming Samples][Application Notes][Security Notes][Editorial][Consultation][Contacts]