We describe a method to exploit a Windows Nday vulnerability to escape the Adobe sandbox. This vulnerability is assigned CVE-2021-31199 and it is present in multiple Windows 10 versions. The vulnerability is an out-of-bounds write due to an integer overflow in a Microsoft Cryptographic Provider library, rsaenh.dll
.
Microsoft Cryptographic Provider is a set of libraries that implement common cryptographic algorithms. It contains the following libraries:
dssenh.dll
– Algorithms to exchange keys using Diffie-Hellman or to sign/verify data using DSA.rsaenh.dll
– Algorithms to work with RSA.basecsp.dll
– Algorithms to work with smart cards.These providers are abstracted away by API in the CryptSP.dll
library, which acts as an interface that developers are expected to use. Each call to the API expects an HCRYPTPROV
object as argument. Depending on certains fields in this object, CryptSP.dll
redirects the code flow to the right provider. We will describe the HCRYPTPROV
object in more detail when describing the exploitation of the vulnerability.
Both Adobe Acrobat and Acrobat Reader run in Protected Mode by default. The protected mode is a feature that allows opening and displaying PDF files in a restricted process, a sandbox. The restricted process cannot access resources directly. Restrictions are imposed upon actions such as accessing the file system and spawning processes. A sandbox makes achieving arbitrary code execution on a compromised system harder.
Adobe Acrobat and Acrobat Reader use two processes when running in Protected Mode:
When the sandbox needs to execute actions that cannot be directly executed, it emits a request to the broker through a well defined IPC protocol. The broker services such requests only after ensuring that they satisfy a configured policy.
The communication between broker and sandbox happens via a shared memory that acts like a message channel. It informs the other side that a message is ready to be processed or that a message has been processed and the response is ready to be read.
On startup the broker initializes a shared memory of size 2MB and initializes the event handlers. Both, the event handlers and the shared memory are duplicated and written into the sandbox process via WriteProcessMemory()
.
When the sandbox needs to access a resource, it prepares the message in the shared memory and emits a signal to inform the broker. On the other side, once the broker receives the signal it starts processing the message and emits a signal to the sandbox when the message processing is complete.
The elements involved in the Sandbox-Broker communication are as follows:
In summary, when the sandbox process cross-calls a broker-exposed resource, it locks the channel, serializes the request and pings the broker. Finally it waits for broker and reads the result.
The vulnerability occurs in the rsaenh.dll:ImportOpaqueBlob()
function when a crafted opaque key blob is imported. This routine is reached from the Crypto Provider interface by calling CryptSP:CryptImportKey()
that leads to a call to the CPImportKey()
function, which is exposed by the Crypto Provider.
// rsaenh.dll
__int64 __fastcall CPImportKey(
__int64 hcryptprov,
char *key_to_imp,
unsigned int key_len,
__int64 HCRYPT_KEY_PUB,
unsigned int flags,
_QWORD *HCRYPT_KEY_OUT)
{
[Truncated]
v7 = key_len;
v9 = hcryptprov;
*(_QWORD *)v116 = hcryptprov;
NewKey = 1359;
[Truncated]
v12 = 0i64;
if ( key_len
&& key_len = key_len )
{
if ( (unsigned int)VerifyStackAvailable() )
{
[1]
v13 = (unsigned int)(v7 + 8) + 15i64;
if ( v13 = (unsigned int)v7 )
{
v39 = (char *)((__int64 (__fastcall *)(_QWORD))g_pfnAllocate)((unsigned int)(v7 + 8));
v12 = v39;
[Truncated]
}
[Truncated]
goto LABEL_14;
}
[Truncated]
LABEL_14:
[2]
memcpy_0(v12, key_to_imp, v7);
v15 = 1;
v107 = 1;
v9 = *(_QWORD *)v116;
[Truncated]
[3]
v18 = NTLCheckList(v9, 0);
v113 = (const void **)v18;
[Truncated]
[4]
if ( v12[1] != 2 )
{
[Truncated]
}
if ( v16 == 6 )
{
[Truncated]
}
switch ( v16 )
{
case 1:
[Truncated]
case 7:
[Truncated]
case 8:
[Truncated]
case 9:
[5]
NewKey = ImportOpaqueBlob(v19, (uint8_t *)v12, v7, HCRYPT_KEY_OUT);
if ( !NewKey )
goto LABEL_30;
v40 = WPP_GLOBAL_Control;
if ( WPP_GLOBAL_Control == &WPP_GLOBAL_Control || (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) == 0 )
goto LABEL_64;
v41 = 210;
goto LABEL_78;
case 11:
[Truncated]
case 12:
[Truncated]
default:
[Truncated]
}
}
Before reaching ImportOpaqueBlob()
at [5], the key to import is allocated on the stack or on the heap according to the available stack space at [1]. The key to import is copied, at [2], into the new memory allocated; the public key struct version member is expected to be 2. The HCRYPTPROV
object pointer is decrypted at [3], and then at [4] the key version is checked to be equal to 2. Finally a switch case on the type field of the key to import leads to executing ImportOpaqueBlob()
at [5]. This occurs if and only if the type member is equal to OPAQUEKEYBLOB
(0x9).
The OPAQUEKEYBLOB
indicates that the key is a session key (as opposed to public/private keys).
__int64 __fastcall ImportOpaqueBlob(__int64 a1, uint8_t *key_, unsigned int len_, unsigned __int64 *out_phkey)
{
[Truncated]
*out_phkey = 0i64;
v8 = 0xC0;
[6]
if ( len_ < 0x70 )
{
[Truncated]
return v10;
}
[7]
v13 = *((_DWORD *)key_ + 5); // read 4 bytes from (uint8_t*)key + 0x14
if ( v13 )
v8 = v13 + 0xC8;
v14 = *((_DWORD *)key_ + 4); // read 4 bytes from (uint8_t*)key + 0x10
if ( v14 )
v8 += v14 + 8;
v15 = (char *)ContAlloc(v8);
v16 = v15;
if ( v15 )
{
memset_0(v15, 0, v8);
[Truncated]
v17 = *((unsigned int *)key_ + 4); // key + 0x10
[Truncated]
v18 = v17 + 0x70;
v19 = *((_DWORD *)key_ + 5);
if ( v19 )
v18 = v19 + v17 + 0x70;
[8]
if ( len_ >= v18 ) // key + 0x10
{
if ( (_DWORD)v17 )
{
[9]
*((_QWORD *)v16 + 3) = v16 + 0xC8;
memcpy_0(v16 + 0xC8, key_ + 0x70, v17);
}
[Truncated]
}
else
{
[Truncated]
}
if ( v16 )
FreeNewKey(v16);
return v10;
}
[Truncated]
return v10;
}
In order to reach the vulnerable code, it is required that the key to import has more than 0x70 bytes [6]. The vulnerability occurs due to an integer overflow that happens at [7] due to a lack of checking the values at addresses (unsigned int)((uint8_t*)key + 0x14)
and (unsigned int)((uint8_t*)key + 0x10)
. For example if one of these members is set to 0xffffffff
, an integer overflow occurs. The vulnerability is triggered when the memcpy()
routine is called to copy (unsigned int)((uint8_t*)key + 0x10)
bytes from key + 0x70
into v16 + 0xc8
at [9].
An example of an opaque blob that triggers the vulnerability is the following: if key + 0x10
is set to 0x120
and key + 0x14
equals 0xffffff00
, then it leads to allocating 0x120 + 0xffffff00 + 0xc8 + 0x08 = 0xf0
bytes of buffer, into which 0x120 bytes are copied. The integer overflow allows bypassing a weak check, at [8], which requires the key length to be greater than: 0x120 + 0xffffff00 + 0x70 = 0x90
.
The goal of exploiting this vulnerability is to escape the Adobe sandbox in order to execute arbitrary code on the system with the privileges of the broker process. It is assumed that code execution is already possible in the Adobe sandbox.
The Adobe broker exposes cross-calls such as CryptImportKey()
to the sandboxed process. The vulnerability can be triggered by importing a crafted key into the Crypto Provider Service, implemented in rsaenh.dll
. The vulnerability yields an out-of-bounds write primitive in the broker, which can be easily used to corrupt function pointers. However, Adobe Reader enables a large number of security features including ASLR and Control Flow Guard (CFG), which effectively prevent ROP chains from being used directly to gain control of the execution flow.
The exploitation strategy described in this section involves bypassing CFG by abusing a certain design element of the Microsoft Crypto Provider. In particular, the interface that redirects code flow according to function pointers stored in the HCRYPTPROV
object.
HCRYPTPROV
is the object instantiated and used by CryptSP.dll
to dispatch calls to the right provider. It can be instantiated via the CryptAcquireContext()
API, that returns an instantiated HCRYPTPROV
object.
HCRYPTPROV
is a basic C structure containing function pointers to the provider exposed routine. In this way, calling CryptSP.dll:CryptImportKey()
executes HCRYPTPROV->FunctionPointer()
that corresponds to provider.dll:CPImportKey()
.
The HCRYPTPROV
data structure is shown below:
Offset Length (bytes) Field Description
--------- -------------- -------------------- ----------------------------------------------
0x00 8 CPAcquireContext Function pointer exposed by Crypto Provider
0x08 8 CPReleaseContext Function pointer exposed by Crypto Provider
0x10 8 CPGenKey Function pointer exposed by Crypto Provider
0x18 8 CPDeriveKey Function pointer exposed by Crypto Provider
0x20 8 CPDestroyKey Function pointer exposed by Crypto Provider
0x28 8 CPSetKeyParam Function pointer exposed by Crypto Provider
0x30 8 CPGetKeyParam Function pointer exposed by Crypto Provider
0x38 8 CPExportKey Function pointer exposed by Crypto Provider
0x40 8 CPImportKey Function pointer exposed by Crypto Provider
0x48 8 CPEncrypt Function pointer exposed by Crypto Provider
0x50 8 CPDecrypt Function pointer exposed by Crypto Provider
0x58 8 CPCreateHash Function pointer exposed by Crypto Provider
0x60 8 CPHashData Function pointer exposed by Crypto Provider
0x68 8 CPHashSessionKey Function pointer exposed by Crypto Provider
0x70 8 CPDestroyHash Function pointer exposed by Crypto Provider
0x78 8 CPSignHash Function pointer exposed by Crypto Provider
0x80 8 CPVerifySignature Function pointer exposed by Crypto Provider
0x88 8 CPGenRandom Function pointer exposed by Crypto Provider
0x90 8 CPGetUserKey Function pointer exposed by Crypto Provider
0x98 8 CPSetProvParam Function pointer exposed by Crypto Provider
0xa0 8 CPGetProvParam Function pointer exposed by Crypto Provider
0xa8 8 CPSetHashParam Function pointer exposed by Crypto Provider
0xb0 8 CPGetHashParam Function pointer exposed by Crypto Provider
0xb8 8 Unknown Unknown
0xc0 8 CPDuplicateKey Function pointer exposed by Crypto Provider
0xc8 8 CPDuplicateHash Function pointer exposed by Crypto Provider
0xd0 8 Unknown Unknown
0xd8 8 CryptoProviderHANDLE Crypto Provider library base address
0xe0 8 EncryptedCryptoProvObj Crypto Provider object's encrypted pointer
0xe8 4 Const Val Constant value set to 0x11111111
0xec 4 Const Val Constant value set to 0x1
0xf0 4 Const Val Constant value set to 0x1
When a CryptSP.dll
API is invoked the HCRYPTPROV
object is used to dispatch the flow to the right provider routine. At offset 0xe0
the HCRYPTPROV
object contains the real provider object that is used internally in the provider routines. When CryptSP.dll
dispatches the call to the provider it passes the real provider object contained at HCRYPTOPROV + 0xe0
as the first argument.
// CryptSP.dll
BOOL __stdcall CryptImportKey(
HCRYPTPROV hProv,
const BYTE *pbData,
DWORD dwDataLen,
HCRYPTKEY hPubKey,
DWORD dwFlags,
HCRYPTKEY *phKey)
{
[Truncated]
[1]
if ( (*(__int64 (__fastcall **)(_QWORD, const BYTE *, _QWORD, __int64, DWORD, HCRYPTKEY *))(hProv + 0x40))(
*(_QWORD *)(hProv + 0xE0),
pbData,
dwDataLen,
v13,
dwFlags,
phKey) )
{
if ( (dwFlags & 8) == 0 )
{
v9[11] = *phKey;
*phKey = (HCRYPTKEY)v9;
v9[10] = hProv;
*((_DWORD *)v9 + 24) = 572662306;
}
v8 = 1;
}
[Truncated]
}
At [1], we see an example how CryptSP.dll
dispatches the code to the provider:CPImportKey()
routine.
The Crypto Providers’ interface uses the HCRYPTPROV
object to redirect the execution flow to the right Crypto Provider API. When the interface redirects the execution flow it sets the encrypted pointer located at HCRYPTPROV + 0xe0
as the first argument. Therefore, by overwriting the function pointer and the encrypted pointer, an attacker can redirect the execution flow while controlling the first argument.
The Adobe Acrobat broker provides the CryptGenRandom()
cross-call to the sandbox. If the CPGenRandom()
function pointer has been overwritten with a function having a predictable return value different from the return value of the original CryptGenRandom()
function, then it is possible to determine that a HCRYPTPROV
object has been overwritten.
For example, if a pointer to the absolute value function, ntdll!abs
, is used to override the CPGenRandom()
function pointer, the broker executes abs(HCRYPTPROV + 0xe0)
instead of CPGenRandom()
. Therefore, by setting a known value at HCRYPTPROV + 0xe0
, this cross-call can be abused by an attacker to identify whether the HCRYPTPROV
object has been overwritten by checking if its return value is abs(<known value>)
.
The Adobe Acrobat broker provides the CryptReleaseContext()
cross-call to the sandbox. This cross-call ends up calling CPReleaseContext(HCRYPTPROV + 0xe0, 0)
. By overwriting the CPReleaseContext()
function pointer in HCRYPTPROV
with WinExec()
and by overwriting HCRYPTPROV + 0xe0
with a previously corrupted HCRYPTPROV
object, one can execute WinExec
with an arbitrary lpCmdLine
argument, thereby executing arbitrary commands.
In the following we describe the shared memory structure and more specifically how arguments for cross-calls are stored. The layout of the shared memory structure is relevant when the integer overflow is used to overwrite the function pointers in contiguous HCRYPTPROV
objects.
The share memory structure is shown below:
Field Description
-------------------- --------------------------------------------------------------
Shared Memory Header Contains main shared memory information like channel numbers.
Channel 0 Header Contains main channel information like Event handles.
Channel 1 Header Contains main channel information like Event handles.
...
Channel N Header Contains main channel information like Event handles.
Channel 0 Channel memory zone, where the request/response is written.
Channel 1 Channel memory zone, where the request/response is written.
...
Channel N Channel memory zone, where the request/response is written.
The shared memory main header is shown below:
Offset Length (bytes) Field Description
--------- -------------- -------------------- -------------------------------------------
0x00 0x04 Channel number Contains the number of channels available.
0x04 0x04 Unknown Unknown
0x08 0x08 Mutant HANDLE Unknown
The channel main header data structure is shown below. The offsets are relative to the channel main header.
Offset Length (bytes) Field Description
--------- -------------- -------------------- -------------------------------------------
0x00 0x08 Channel offset Offset to the channel memory region for
storing/reading request relative to share
memory base address.
0x08 0x08 Channel state Value representing the state of the channel:
1 Free, 2 in use by sandbox, 3 in use by broker.
0x10 0x08 Event Ping Handle Event used by sandbox to signal broker that
there is a request in the channel.
0x18 0x08 Event Pong Handle Event used by broker to signal sandbox that
there is a response in the channel.
0x20 0x08 Event Handle Unknown
Since the shared memory is 2MB and the header is 0x10 bytes long and every channel header is 0x28 bytes long, every channel takes (2MB - 0x10 - N*0x28) / N bytes
.
The shared memory channels are used to store the serialization of the cross-call input parameters and return values. Every channel memory region, located at shared_memory_address + channel_main_header[i].channel_offset
, is implemented as the following data structure:
Offset Length (bytes) Field Description
--------- -------------- -------------------- -----------------------------------------------
0x00 0x04 Tag ID Tag ID is used by the broker to dispatch the
request to the exposed cross-call.
0x04 0x04 In Out Boolean, if set the broker copy-back in the
channel the content of the arguments after the
cross-call. It is used when parameters are
output parameters, e.g. GetCurrentDirectory().
0x08 0x08 Unknown Unknown
0x10 0x04 Error Code Windows Last Error set by the broker after the
cross-call to inform sandbox about error status.
0x14 0x04 Status Broker sets to 1 if the cross-call has been
executed otherwise it sets to 0.
0x18 0x08 HANDLE Handle returned by the cross call
0x20 0x04 Return Code Exposed cross-call return value.
0x24 0x3c Unknown Unknown
0x60 0x04 Args Number Number of argument present in the cross-call
emitted by the sandbox.
0x64 0x04 Unknown Unknown
0x68 Variable Arguments Data structure representing every argument
[ Truncated ]
At most 20 arguments can be set for a request but only the required arguments need to be specified. It means that if the cross-call requires two arguments then Args Number
will be set to 2 and the Arguments
data structure contains two elements of the Argument
type. Every argument uses the following data structure:
Offset Length (bytes) Field Description
--------- -------------- -------------------- --------------------------------------------------
0x0 4 Argument type Integer representing the argument type.
0x4 4 Argument offset Offset relative to the channel address, i.e. Tag
ID address, used to localize the argument value in the channel self
0x8 4 Argument size The argument's size.
Each of the argument data structures must be followed by another one that contains only the offset field filled with an offset greater than the last valid argument’s offset plus its own size, i.e argument[n].offset + argument[n].size + 1
. Therefore, if a cross-call needs two arguments then three arguments must be set: two representing the valid arguments to pass to the cross-call and the third set to where the arguments end.
Shown below is an example of the arguments in a two-argument cross-call:
Offset Length (bytes) Field
--------- -------------- --------------------
0x68 4 Argument 0 type
0x6c 4 Argument 0 offset
0x70 4 Argument 0 size
0x74 4 Argument 1 type
0x78 4 Argument 1 offset
0x7c 4 Argument 1 size
0x80 4 Not Used
0x84 4 Argument 1 offset + Argument 1 size + 1: 0x90 + N + M + 1
0x8c 4 Not Used
0x90 N Argument 0 value
0x90 + N M Argument 1 value
An Argument
can be one of the following types:
Argument type Argument name Description
-------------- ------------------ -------------------------------------------------------------
0x01 WCHAR String Specify a wide string.
0x02 DWORD Specify an int 32 bits argument.
0x04 QWORD Specify an int 64 bits argument.
0x05 INPTR Specify an input pointer, already instantiated on the broker.
0x06 INOUTPTR Specify an argument treated like a pointer in the cross-call
handler. It is used as input or output, i.e. return to the
sandbox a broker valid memory pointer.
0x07 ASCII String Specify an ascii string argument.
0x08 0x18 Bytes struct Specify a structure long 0x18 bytes.
When an argument is of the INOUTPTR
type (intended to be used for all non-primitive data types), then the cross-call handler treats it in the following way:
INOUT
cross-call type is true then the pointer address is copied back to the sandbox.The exploit consists of the following phases:
CryptAcquireContext()
N times in order to allocate multiple heap chunks of 0x100 bytes. The broker’s heap layout after the spray is shown below.HCRYPTPTROV
object is passed as a parameter to CryptAcquireContext()
the pointer must be returned to the sandbox in order to allow using it for operations with Crypto Providers in the broker context. Because of this feature it is possible to find contiguous HCRYPTPROV
objects.CryptImportKey()
multiple times with a maliciously crafted key. It is expected that the key overflows into the next chunk, i.e. an HCRYPTPROV
object. The overflow overwrites the initial bytes of the HCRYPTPROV
object with a command string, CPGenRandom()
with the address of ntdll!abs,
and HCRYPTPROV + 0xe0
with a known value.CryptGenRandom()
. If it returns the known value then ntdll!abs()
has been executed and the overwritten object has been found.CryptImportKey()
multiple times with a maliciously crafted key. It is expected that the key overflows the next chunk, i.e. an HCRYPTPROV
object. The overflow overwrites CPReleaseContext()
with kernel32:WinExec()
, CPGenRandom()
with the address of ntdll!abs
, and HCRYPTPROV + 0xe0
with the pointer to the object found in step 5.CryptGenRandom()
. If it returns the absolute value of the pointer found in step 5 then ntdll!abs()
has been executed and the overwritten object has been found.CryptReleaseContext()
on the HCRYPTPROV
object found in step 7 to trigger WinExec()
.Wrapping Up
We hope you enjoyed reading this. If you are hungry for more make sure to check our other blog posts.