I've been attending Blackhat USA 2019 and watched a presentation by Amit Klein and Itzik Kotler on Windows Process Injection techniques. While I didn't learn anything new from the presentation that you couldn't from just reading Hexacorn's blog it was interesting to see them document what techniques worked against Code Integrity Guard (CIG) and what did not. CIG if you don't know, is Microsoft's term for blocking non-MS signed DLLs from being loaded into a process. If CIG is enabled on a process then you can load an arbitrary DLL not signed by Microsoft, instead you'll have to do some sort of shellcode or ROP.
During the presentation I was waiting for the punchline of a technique which bypasses CIG to load an arbitrary DLL, but it never arrived. I'm guessing the researchers don't bother to read my blog posts *sigh*, such as this one on injecting code into a Protected Processes though abusing the KnownDll mechanism. This would also work to bypass CIG if injecting from an external process not under CIG (or Device Guard). All the ways of hijacking the Known DLL loader that I've documented rely on knowing the location of Known DLL handle in NTDLL's data section. That's useful when you have little control over the target process and only an arbitrary read/write primitive. For user-mode code injection you're likely to be able to do anything to the process.
Writing a new handle value does have draw backs if you're thinking about it from a generic code injection perspective. Firstly the location of the handle can (and does) change depending on the version of NTDLL and secondly if you access and write memory of another process you might as well call your binary malware.exe. Of course writing to memory is not the only way to hijack Known DLLs, you can achieve the same thing with only Duplicate Handle access on the process, which is probably slightly less suspicious.
How can we do this without modifying the handle value? There's 3 key observations we can make that only require Duplicate Handle access:
- We can find the existing handle value of the KnownDlls directory by duplicating handles from a process to another and querying for the name.
- We can close a handle in another process by specifying DUPLICATE_CLOSE_SOURCE to DuplicateHandle.
- The kernel's handle allocator will reuse the handle values so we can replace the original handle with a different object through brute force.
Let's go through how this works in practice. I'm going to show some snippets of PowerShell which use my NtObjectManager module. I'm not going to provide a full end-to-end proof-of-concept however for various reasons.
Step 1: Bring up a process to inject into, the Known DLLs handle is created during the initial loader process before the process entry point is called, so the process must run at least that long. Once we know the Process ID of the process to inject into we can dump all handles in the process and look for anything with the NT type of Directory. Each directory handle can then be duplicated into the current process and inspected. If the name of the directory is "\KnownDlls" we've found our target. In PowerShell we can use my Get-NtHandle cmdlet to dump the handle table, this doesn't require opening the process itself. To get the name we only need PROCESS_DUP_HANDLE access to the target. Here's a basic PS function to get the handle value:
$id = $(Get-Process notepad).Id
$hs = Get-NtHandle -ProcessId $id -ObjectTypes Directory
foreach($h in $hs) {
if ($h.Name -eq '\KnownDlls') {
$handle = $h.Handle
break
break
}
}
Step 2: Create an empty object directory and insert into it a named image section object. The name of the section needs to match the name of the system32 DLL we want to hijack. The file backing the section is obviously the DLL you want loaded into the process. Again some code, assuming you've already created the directory
$dir = New-NtDirectory
$sect = Use-NtObject($f = Get-NtFile -Path "\??\c:\dir\fake.dll") {
New-NtSection -File $f -SectionAttributes Image `
-Root $dir -Path "blah.dll" -Protection Execute
}
}
Step 3: Close the original Known DLLs handle. Again this only needs Duplicate Handle access. At this point you probably also want to suspend the process to ensure something doesn't execute and allocate the handle over the top of your now closed handle. Of course if you suspend the process you'll need a bit more access.
$proc = Get-NtProcess -ProcessId $id -Access DupHandle
Copy-NtObject -SourceHandle $handle -SourceProcess $proc `
-CloseSource
Step 4: Repeatedly duplicate the fake Known DLLs directory you created in step 2 until you get the same handle value as you identified in step 1. If the process is suspended this shouldn't take more than a few tries at worst.
$i = 0
while($i -lt 1000) {
$h = Copy-NtObject -DestinationProcess $proc -Object $dir
if ($h -eq $Handle) {
break
}
$i++
}
Step 5: Everything is now setup. The final step is you'll need to get a new library loaded from system32 inside the process. There's a number of possible techniques for this. You could go old-skool and create a new thread in process calling LoadLibrary. Or you could identify a DLL which you know the process will load in response to a UI or RPC action. For example opening a file in Notepad will spawn the explorer open dialog which pulls in ALOT of new DLLs. Be creative, at least if you don't want to open the process with anything above Duplicate Handle access.
The question you might be asking is, "Do any AV/Host Detection tools catch this trick?". Honestly I don't know, nor do I care. However it has some things going for it:
The question you might be asking is, "Do any AV/Host Detection tools catch this trick?". Honestly I don't know, nor do I care. However it has some things going for it:
- It doesn't requiring reading or writing memory from the target process.
- Inline hooks on LoadLibrary/LdrLoadDll will just see loading a system32 DLL unless they also then query for the mapped file name after the operation has completed.
- It bypasses CIG, so anyone thinking that'll prevent injection will be surprised.
You could probably make it even more convert, but I'm not going to do so. As I've noted before I'm also not going to write a proof-of-concept or write a tool to do this, you can do it yourself.