├── .gitignore ├── .gitmodules ├── Get-InjectedThreadEx.cpp ├── Get-InjectedThreadEx.png ├── Get-InjectedThreadEx.ps1 ├── Get-InjectedThreadEx ├── CfgBitMap.cpp ├── Get-InjectedThreadEx.h ├── Get-InjectedThreadEx.sln ├── Get-InjectedThreadEx.vcxproj ├── Get-InjectedThreadEx.vcxproj.filters ├── Memory.cpp ├── Process.cpp ├── StackClimb.cpp ├── Symbol.cpp └── Unwind.cpp ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Get-InjectedThreadEx/zydis"] 2 | path = Get-InjectedThreadEx/zydis 3 | url = https://github.com/zyantific/zydis 4 | -------------------------------------------------------------------------------- /Get-InjectedThreadEx.cpp: -------------------------------------------------------------------------------- 1 | // Get-InjectedThreadEx.cpp : A C++ implementation of Get-InjectedThreadEx.ps1 2 | // 3 | // .SYNOPSIS 4 | // 5 | // Looks for threads that were created as a result of code injection. 6 | // 7 | // .DESCRIPTION 8 | // 9 | // Memory resident malware (fileless malware) often uses a form of memory injection to get code execution. 10 | // Get-InjectedThreadEx looks at each running thread to determine if it is the result of memory injection. 11 | // 12 | // Win32StartAddress 13 | // 14 | // original 15 | // - not MEM_IMAGE 16 | // new 17 | // - MEM_IMAGE and Win32StartAddress is on a private (modified) page 18 | // - MEM_IMAGE and x64 dll and Win32StartAddress is CFG violation or suppressed export 19 | // - MEM_IMAGE and Win32StartAddress is in a suspicious module 20 | // - MEM_IMAGE and x64 and Win32StartAddress is unexpected prolog 21 | // - MEM_IMAGE and Win32StartAddress is preceded by unexpected bytes 22 | // - MEM_IMAGE and x64 and Win32StartAddress wraps non-MEM_IMAGE start address 23 | // 24 | // KNOWN LIMITATIONS: 25 | // - Only detects suspicious thread creations - not hijacks of existing threads. 26 | // - Some WoW64 support not implemented. 27 | 28 | #include "Get-InjectedThreadEx/Get-InjectedThreadEx.h" 29 | 30 | BOOL ScanThread(HANDLE hProcess, BOOL bIsDotNet, PSS_THREAD_ENTRY& thread, std::string& symbol, std::vector& detections) { 31 | if (thread.Flags & PSS_THREAD_FLAGS_TERMINATED) 32 | return FALSE; 33 | 34 | if (!GetNearestSymbol(hProcess, thread.Win32StartAddress, symbol, true)) 35 | return FALSE; 36 | 37 | if (thread.ContextRecord && thread.ContextRecord->Dr6) 38 | detections.push_back("hw_breakpoint"); 39 | 40 | MEMORY_BASIC_INFORMATION mbi{}; 41 | if (!VirtualQueryEx(hProcess, thread.Win32StartAddress, &mbi, sizeof(mbi))) 42 | return FALSE; 43 | 44 | if (MEM_IMAGE != mbi.Type && MEM_COMMIT == mbi.State) { 45 | detections.push_back("PRIVATE"); 46 | return TRUE; 47 | } 48 | 49 | // Has our MEM_IMAGE Win32StartAddress been (naively) hooked? 50 | // https://blog.redbluepurple.io/offensive-research/bypassing-injection-detection//creating-the-thread 51 | // Note - checking against bytes on disk after the fact won't help with false positives 52 | // as the hook can easily be removed after thread start. 53 | // Detection gap - the hook could easily be deeper, potentially even in a subsequent call. :-( 54 | // Microsoft-Windows-Threat-Intelligence ETW events should detect this more robustly. 55 | PSAPI_WORKING_SET_EX_INFORMATION pwsei{}; 56 | pwsei.VirtualAddress = thread.Win32StartAddress; 57 | if (K32QueryWorkingSetEx(hProcess, &pwsei, sizeof(pwsei)) && !pwsei.VirtualAttributes.Shared) { 58 | // I'm slightly worried about security vendor hooks landing on the same page 59 | // as ntdll!TppWorkerThread and causing a false positive flood. 60 | // So ignore ntdll modifications if we've been hooked too. 61 | static bool s_edrFalsePositive = symbol.starts_with("ntdll.dll!") && 62 | K32QueryWorkingSetEx(GetCurrentProcess(), &pwsei, sizeof(pwsei)) && !pwsei.VirtualAttributes.Shared; 63 | if(!s_edrFalsePositive) 64 | detections.push_back("private_image"); 65 | } 66 | 67 | 68 | ////////////////////////////////////////////////////////////////////////////////////////////////// 69 | // Check for suspcious CFG BitMap states in our local pristine copy of the x64 bitmap 70 | // Notes - executable CFG bitmaps are not shared - only library (dll) ones. 71 | // - only 16-bytes aligned addresses, as this is a SetProcessValidCallTargets() requirement. 72 | ULONG cfgBits; 73 | if (InSystemImageRange(thread.Win32StartAddress) && 0 == ((ULONG_PTR)thread.Win32StartAddress & 0xF) && 74 | GetCfgBitsForAddress(thread.Win32StartAddress, &cfgBits) && 0 == cfgBits) 75 | { 76 | detections.push_back("cfg_invalid"); 77 | } 78 | 79 | 80 | ////////////////////////////////////////////////////////////////////////////////////////////////// 81 | // Suspicious start modules 82 | 83 | std::wstring mappedPath; 84 | // The file path assocated with Win32StartAddressModule 85 | if (!GetMappedFileNameAsDosPath(hProcess, thread.Win32StartAddress, mappedPath)) 86 | return FALSE; 87 | 88 | // There are no valid thread entry points (that I know of) in many Win32 modules. 89 | const std::array modulesWithoutThreadEntrypoints = { 90 | "kernel32", "kernelbase", "user32", "advapi32", 91 | "psapi", "dbghelp", "imagehlp", "powrprof", 92 | "verifier", "setupapi", "rpcrt4" }; // ...and many more 93 | const auto startModule = std::filesystem::path(mappedPath).stem().string(); 94 | for (const auto& module : modulesWithoutThreadEntrypoints) 95 | if (startModule == module) { 96 | (void)GetNearestSymbolWithPdb(hProcess, thread.Win32StartAddress, symbol); 97 | detections.push_back("unexpected(" + startModule + ")"); 98 | } 99 | 100 | // kernel32!LoadLibrary 101 | // And, even if there are, LoadLibrary is always a suspicious start address. 102 | static auto hKernel32 = GetModuleHandleW(L"kernel32.dll"); 103 | static auto pLoadLibraryW = GetProcAddress(hKernel32, "LoadLibraryW"); 104 | static auto pLoadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA"); 105 | if (pLoadLibraryA == thread.Win32StartAddress || pLoadLibraryW == thread.Win32StartAddress) 106 | detections.push_back("unexpected(" + symbol + ")"); 107 | 108 | // ntdll.dll but not a known entrypoint. 109 | // These are the only valid thread entry points in ntdll that I know of. 110 | static const std::array ntdllThreadEntryPoints = { 111 | GetSymbolAddress("ntdll!TppWorkerThread"), 112 | GetSymbolAddress("ntdll!EtwpLogger"), 113 | GetSymbolAddress("ntdll!DbgUiRemoteBreakin"), 114 | GetSymbolAddress("ntdll!RtlpQueryProcessDebugInformationRemote") 115 | }; 116 | 117 | if (mappedPath.ends_with(L"\\System32\\ntdll.dll")) { 118 | auto bValidNtdllEntry = false; 119 | for (const auto& address : ntdllThreadEntryPoints) 120 | bValidNtdllEntry |= address == thread.Win32StartAddress; 121 | 122 | if (!bValidNtdllEntry) { 123 | detections.push_back("unexpected(" + symbol + ")"); 124 | } 125 | } 126 | 127 | ////////////////////////////////////////////////////////////////////////////////////////////////// 128 | // Common setup for the disassembler 129 | constexpr auto MAX_INSN_LENGTH = 11ull; // theoretically 15, but empirically lower 130 | ZydisDecoder decoder; 131 | ZydisDecodedInstruction instruction; 132 | // WoW64 can be inferred from the TEB address. 133 | const bool bIsWow64 = (ULONG_PTR)thread.TebBaseAddress < 0x80000000; 134 | if (bIsWow64) 135 | (void)ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_COMPAT_32, ZYDIS_STACK_WIDTH_32); 136 | else 137 | (void)ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64); 138 | 139 | ////////////////////////////////////////////////////////////////////////////////////////////////// 140 | // Check the bytes immmediately after Win32StartAddress 141 | // They must be a function entrypoint. 142 | // ... but almost anything is a valid entrypoint! 143 | // x64 prologs have more structure (albiet mostly by convention) - so we'll stick to those. 144 | // See https://learn.microsoft.com/en-us/cpp/build/prolog-and-epilog 145 | // 146 | // Note - the loader ignores AddressOfEntry in CLR assemblies so we need to ignore them too. 147 | auto bIsDotNetProcessEntrypoint = bIsDotNet && mappedPath.ends_with(L".exe"); 148 | 149 | if (!bIsWow64 && !bIsDotNetProcessEntrypoint) { 150 | std::string startBytes; 151 | constexpr auto MAX_PROLOG_SIZE = 64; 152 | startBytes.resize(MAX_PROLOG_SIZE); 153 | if (!ReadProcessMemorySafely(hProcess, thread.Win32StartAddress, startBytes, mbi)) 154 | return FALSE; 155 | 156 | auto i = 0; 157 | bool bValidInstruction = true; 158 | ZyanU64 instructionPointer = (ZyanU64)thread.Win32StartAddress; // track this to calculate relative targets 159 | auto framePointer = ZYDIS_REGISTER_RSP; 160 | ZydisDecoderContext ctx{}; 161 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; 162 | ZydisRegisterContext registers{}; 163 | std::string originalBytes; // if we follow a jump keep original bytes 164 | 165 | const auto IsStackOperation = [&]() -> bool { 166 | return ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) && 167 | ZYDIS_OPERAND_TYPE_REGISTER == operands[0].type && ZYDIS_REGISTER_RSP == operands[0].reg.value; 168 | 169 | }; 170 | 171 | const auto IsSaveRegisterOperation = [&]() -> bool { 172 | return ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) && 173 | ZYDIS_OPERAND_TYPE_MEMORY == operands[0].type && (ZYDIS_REGISTER_RSP == operands[0].mem.base || framePointer == operands[0].mem.base) && 174 | ZYDIS_OPERAND_TYPE_REGISTER == operands[1].type; 175 | }; 176 | 177 | const auto IsFramePointerOperation = [&]() { 178 | const auto bIsFP = ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) && 179 | ZYDIS_OPERAND_TYPE_REGISTER == operands[0].type && 180 | ((ZYDIS_OPERAND_TYPE_REGISTER == operands[1].type && (ZYDIS_REGISTER_RSP == operands[1].reg.value || framePointer == operands[1].reg.value)) || 181 | (ZYDIS_OPERAND_TYPE_MEMORY == operands[1].type && (ZYDIS_REGISTER_RSP == operands[1].mem.base || framePointer == operands[1].mem.base))); 182 | if (bIsFP) 183 | framePointer = operands[0].reg.value; 184 | return bIsFP; 185 | }; 186 | 187 | const auto IsRegDestination = [&](ZydisRegister reg) -> bool { 188 | return ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) && 189 | ZYDIS_OPERAND_TYPE_REGISTER == operands[0].type && reg == operands[0].reg.value; 190 | }; 191 | 192 | const auto IsRegSource = [&](ZydisRegister reg) -> bool { 193 | return ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) && 194 | ((ZYDIS_OPERAND_TYPE_REGISTER == operands[1].type && reg == operands[1].reg.value) || 195 | (ZYDIS_OPERAND_TYPE_MEMORY == operands[1].type && reg == operands[1].mem.base)); 196 | }; 197 | 198 | const auto SaveRegister = [&](ZydisRegister reg) -> void { 199 | ZyanU64 regValue = 0; 200 | registers.values[reg] = 0; 201 | // Note - assumes prior call to IsRegDestination 202 | if (ZYDIS_OPERAND_TYPE_REGISTER == operands[0].type && reg == operands[0].reg.value && 203 | ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&instruction, &operands[1], instructionPointer, ®Value))) 204 | { 205 | registers.values[reg] = regValue; 206 | } 207 | }; 208 | 209 | bool bThreadParameterInRax = false; 210 | bool bCallThreadParameter = false; 211 | bool bRaxSetLastInstruction = false; 212 | int nFollowedJumps = 0; 213 | constexpr auto MAX_JUMPS = 3; 214 | const auto FollowJump = [&]() -> bool { 215 | ZyanU64 jmpTarget = 0; 216 | if (nFollowedJumps < MAX_JUMPS && 217 | ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) && 218 | ZYAN_SUCCESS(ZydisCalcAbsoluteAddressEx(&instruction, &operands[0], instructionPointer, ®isters, &jmpTarget)) && 219 | 0 != jmpTarget) 220 | { 221 | // If the jump target isn't an immediate then we need to read it from the calculated address. 222 | if ((ZYDIS_OPERAND_TYPE_MEMORY == operands[0].type && ZYDIS_REGISTER_RIP == operands[0].mem.base)) 223 | { 224 | // If RCX has been mixed into RAX then we can't follow the jump. 225 | // Such start addresses are useful proxy call functions for adversaries. 226 | if (bRaxSetLastInstruction && bThreadParameterInRax) { 227 | bCallThreadParameter = true; 228 | return FALSE; 229 | } 230 | 231 | // This looks like a CFG check. 232 | // mov RAX, indirect-call 233 | // jmp __guard_dispatch_icall_fptr 234 | // The CFG thunk will transfer execution to RAX on success - so just jump to RAX now. 235 | if (bRaxSetLastInstruction && InSystemImageRange((PVOID)jmpTarget)) 236 | jmpTarget = registers.values[ZYDIS_REGISTER_RAX]; 237 | 238 | // Read the jump target 239 | if (!ReadProcessMemorySafely(hProcess, (PVOID)jmpTarget, &jmpTarget)) 240 | return FALSE; 241 | } 242 | 243 | std::string jmpBytes = std::move(startBytes); 244 | startBytes.resize(MAX_PROLOG_SIZE); 245 | if (ReadProcessMemorySafely(hProcess, (PVOID)jmpTarget, startBytes)) { 246 | 247 | jmpBytes.resize(i + instruction.length); 248 | originalBytes += ToHex(jmpBytes) + "|"; 249 | 250 | // reset loop for new bytes 251 | instructionPointer = jmpTarget; 252 | instruction.length = 0; 253 | i = 0; 254 | return TRUE; 255 | } 256 | startBytes = std::move(originalBytes); 257 | } 258 | return FALSE; 259 | }; 260 | 261 | bool bPrologStarted = false; 262 | bool bPrologFinished = false; 263 | bool bRaxSet = false; 264 | bool bTestRcx = false; 265 | for (i = 0; bValidInstruction && !bPrologFinished && i <= startBytes.size() - MAX_INSN_LENGTH; i += instruction.length) { 266 | bValidInstruction = ZYAN_SUCCESS(ZydisDecoderDecodeInstruction(&decoder, &ctx, startBytes.data() + i, startBytes.length() - i, &instruction)); 267 | bRaxSetLastInstruction = bRaxSet; 268 | bRaxSet = false; 269 | switch (instruction.mnemonic) { 270 | case ZYDIS_MNEMONIC_PUSH: 271 | // push nonvolatile 272 | bPrologStarted = true; 273 | break; 274 | case ZYDIS_MNEMONIC_MOV: 275 | // mov [RSP+n], nonvolatile 276 | if (IsSaveRegisterOperation()) 277 | bPrologStarted = true; 278 | // mov frame-pointer, RSP 279 | else if (IsFramePointerOperation()) 280 | bPrologStarted = true; 281 | // mov RAX, fixed-allocation-size 282 | // mov RAX, indirect-call-target 283 | else if (IsRegDestination(ZYDIS_REGISTER_RAX)) { 284 | SaveRegister(ZYDIS_REGISTER_RAX); 285 | bRaxSet = true; 286 | bThreadParameterInRax |= IsRegSource(ZYDIS_REGISTER_RCX); 287 | } 288 | else if (IsRegDestination(ZYDIS_REGISTER_EAX)) 289 | bRaxSet = true; 290 | // sometimes stub functions reorder parameters or set static values 291 | // mov FastcallParamReg, * 292 | else 293 | bValidInstruction = IsRegDestination(ZYDIS_REGISTER_RCX) || IsRegDestination(ZYDIS_REGISTER_RDX) || IsRegDestination(ZYDIS_REGISTER_R8) || IsRegDestination(ZYDIS_REGISTER_R9); 294 | break; 295 | case ZYDIS_MNEMONIC_CALL: 296 | // call __chkstk() is the only call allowed in a prolog 297 | // It uses a special calling convention. 298 | bValidInstruction = bRaxSetLastInstruction; 299 | break; 300 | case ZYDIS_MNEMONIC_LEA: 301 | // lea frame-pointer, [RSP-n] 302 | if (IsFramePointerOperation()) 303 | bValidInstruction = true; 304 | // lea RAX,[RIP+n] 305 | else if (IsRegDestination(ZYDIS_REGISTER_RAX)) { 306 | SaveRegister(ZYDIS_REGISTER_RAX); 307 | bRaxSet = true; 308 | } 309 | // Some "functions" are just stubs around other functions with 310 | // one (or more) fixed parameters. 311 | // lea RCX, [n] - set first parameter. 312 | else 313 | bValidInstruction = IsRegDestination(ZYDIS_REGISTER_RCX); 314 | break; 315 | case ZYDIS_MNEMONIC_SUB: // prolog delimiter 316 | bPrologFinished = IsStackOperation(); 317 | break; 318 | case ZYDIS_MNEMONIC_TEST: 319 | // test RCX, RCX - is the first parameter NULL? 320 | // Checking for a non-NULL parameter and bailing early is 321 | // a common optimisation. 322 | bTestRcx = IsRegSource(ZYDIS_REGISTER_RCX) && IsRegDestination(ZYDIS_REGISTER_RCX); 323 | break; 324 | case ZYDIS_MNEMONIC_JZ: 325 | // test RCX, RCX 326 | // jz early-exit - don't follow 327 | bValidInstruction = bTestRcx; 328 | break; 329 | case ZYDIS_MNEMONIC_JNZ: 330 | // test RCX, RCX 331 | // jnz true-entry-point - follow 332 | bValidInstruction = bTestRcx; 333 | if (!FollowJump()) 334 | bPrologFinished = bTestRcx; 335 | break; 336 | case ZYDIS_MNEMONIC_JMP: 337 | // Some functions start with a short jmp to provide hotpatch space. 338 | // jmp n - follow 339 | bPrologFinished = FollowJump(); 340 | break; 341 | default: 342 | bValidInstruction = false; 343 | } 344 | instructionPointer += instruction.length; 345 | } 346 | startBytes.resize(i); 347 | if (bCallThreadParameter) 348 | detections.push_back(std::string("proxy_call(" + originalBytes + ToHex(startBytes) + ")")); 349 | else if (!bPrologFinished) 350 | detections.push_back(std::string("prolog(" + originalBytes + ToHex(startBytes) + ")")); 351 | } 352 | 353 | ////////////////////////////////////////////////////////////////////////////////////////////////// 354 | // Check the bytes immmediately before Win32StartAddress 355 | // The byte preceding a function prolog is typically a return, or filler byte. 356 | // False positives can occur if data was included in a code section. This was 357 | // common in older compilers... 358 | std::string tailBytes; 359 | tailBytes.resize(std::min(MAX_INSN_LENGTH, (ULONG_PTR)thread.Win32StartAddress - (ULONG_PTR)mbi.AllocationBase)); 360 | if (!ReadProcessMemorySafely(hProcess, (PVOID)((ULONG_PTR)thread.Win32StartAddress - tailBytes.size()), tailBytes)) 361 | return FALSE; 362 | 363 | 364 | // False positives can occur if data was included in a code section. This was common in older compilers... 365 | // ...and also in new compilers that support XFG. In this case, the 8-byte XFG hash is immediately before. 366 | // https://blog.quarkslab.com/how-the-msvc-compiler-generates-xfg-function-prototype-hashes.html 367 | const auto& tailbytesEnd = tailBytes.data() + tailBytes.size(); 368 | auto bIsValidTail = tailBytes.size() >= sizeof(UINT64) && 369 | IsValidXfgHash(*(UINT64*)(tailbytesEnd - sizeof(UINT64))); 370 | 371 | // The byte preceding a function prolog is typically a return, or filler byte. 372 | bIsValidTail |= tailBytes.empty() || '\x00' == tailBytes.back(); // NUL filled. 373 | for (auto i = 1; !bIsValidTail && i <= tailBytes.size(); i++) { 374 | if (!ZYAN_SUCCESS(ZydisDecoderDecodeInstruction(&decoder, NULL, tailbytesEnd - i, i, &instruction)) || instruction.length != i) 375 | continue; 376 | switch (instruction.mnemonic) { 377 | // valid basic block end instructions 378 | case ZYDIS_MNEMONIC_CALL: 379 | case ZYDIS_MNEMONIC_JMP: 380 | case ZYDIS_MNEMONIC_RET: 381 | // valid alignment filler instructions 382 | case ZYDIS_MNEMONIC_NOP: 383 | case ZYDIS_MNEMONIC_INT3: 384 | bIsValidTail = true;; 385 | } 386 | } 387 | 388 | if (!bIsValidTail) 389 | detections.push_back(std::string("tail(" + ToHex(tailBytes) + ")")); 390 | 391 | ////////////////////////////////////////////////////////////////////////////////////////////////// 392 | // Check for suspicious call stacks 393 | // [expected] ntdll!RtlUserThreadStart -> kernel32!BaseThreadInitThunk -> Win32StartAddress 394 | // https://www.trustedsec.com/blog/avoiding-get-injectedthread-for-internal-thread-creation/ 395 | // 396 | 397 | if (bIsWow64) 398 | return TRUE; // TODO(jdu) Implement x86 stack climbing? 399 | 400 | // The TIB is the first element of the TEB. Read the TIB to determine the stack limits. 401 | NT_TIB64 tib; 402 | if (!ReadProcessMemory(hProcess, thread.TebBaseAddress, &tib, sizeof(tib), NULL)) 403 | return FALSE; 404 | 405 | // Determine the consumed stack size (and check for stack pivoting such as ROP). 406 | const auto stackPointer = thread.ContextRecord ? thread.ContextRecord->Rsp : tib.StackLimit; 407 | if (stackPointer > tib.StackBase || stackPointer < tib.StackLimit) { 408 | detections.push_back("stack_pivot"); 409 | return TRUE; 410 | } 411 | 412 | // Read the (partial) base of stack contents - 1.5 pages seems sufficient given current stack randomisation 413 | PVOID stackBuffer[0x1800 / sizeof(PVOID)]; 414 | const auto stackReadLength = std::min(sizeof(stackBuffer), (tib.StackBase - stackPointer) & ~0xF); 415 | if (!ReadProcessMemory(hProcess, (PVOID)(tib.StackBase - stackReadLength), stackBuffer, stackReadLength, NULL)) 416 | return FALSE; 417 | 418 | 419 | // Search the stack bottom up for the (probable) initial return addresses of the first 3+2 frames. 420 | // Note - x64 stack frames are 16-byte aligned. 421 | std::vector callStackFrames; 422 | bool bCallStackDetection = false; 423 | const auto stackBufferCount = stackReadLength / sizeof(PVOID); 424 | if (!StackClimb64(hProcess, stackBuffer, stackBufferCount, callStackFrames, &bCallStackDetection)) 425 | return FALSE; 426 | 427 | // If the thread has been hijacked, then the return address alignment might be off. 428 | // Search the skipped offsets this time. 429 | if(0 == callStackFrames.size() && !StackClimb64(hProcess, stackBuffer, stackBufferCount, callStackFrames, &bCallStackDetection, 1)) 430 | return FALSE; 431 | 432 | // Not enough stack frames discovered yet - append RIP 433 | if (!bCallStackDetection && thread.ContextRecord && callStackFrames.size() < MIN_FRAMES) { 434 | 435 | if (!VirtualQueryEx(hProcess, (PVOID)thread.ContextRecord->Rip, &mbi, sizeof(mbi))) 436 | return FALSE; 437 | 438 | if (!IsExecutable(mbi)) 439 | LogError("pid:%d, tid:%d RIP:%llx is not executable", thread.ProcessId, thread.ThreadId, thread.ContextRecord->Rip); 440 | 441 | if (MEM_IMAGE != mbi.Type) { 442 | callStackFrames.push_back("PRIVATE"); 443 | bCallStackDetection = true; 444 | } 445 | } 446 | 447 | std::string callStackSummary; 448 | callStackSummary.reserve(callStackFrames.size() * 16); 449 | for (const auto& entry : callStackFrames) { 450 | if (entry == "ntdll.dll!RtlUserThreadStart" || entry == "kernel32.dll!BaseThreadInitThunk") 451 | continue; // skip common frames 452 | if (!callStackSummary.empty()) 453 | callStackSummary += "|"; 454 | callStackSummary += entry.substr(0, entry.find_first_of('<')); // trim type information 455 | } 456 | 457 | if (bCallStackDetection) 458 | if(callStackFrames.size() < 4) 459 | detections.push_back("spoof(" + callStackSummary + ")"); 460 | else 461 | detections.push_back("wrapper(" + callStackSummary + ")"); 462 | 463 | return TRUE; 464 | } 465 | 466 | struct FalsePositive { 467 | const std::wstring ProcessName; 468 | const std::string Symbol; 469 | const size_t Count; 470 | }; 471 | 472 | BOOL IsKnownFalsePositive(const HANDLE hProcess, const PROCESSENTRY32 &processEntry, const PSS_THREAD_ENTRY thread, std::string &symbol, const std::vector &detections) { 473 | 474 | static const std::array falsePositives = { { 475 | { L"dwm.exe", "dwmcore.dll!CMit::RunInputThreadStatic", 1 }, 476 | { L"vctip.exe", "vctip.exe!CorExeMain", 1 } 477 | } }; 478 | 479 | (void)GetNearestSymbolWithPdb(hProcess, thread.Win32StartAddress, symbol); 480 | 481 | BOOL bIsFalsePositive = FALSE; 482 | for (const auto &fp : falsePositives) 483 | bIsFalsePositive |= processEntry.szExeFile == fp.ProcessName && symbol == fp.Symbol && detections.size() == fp.Count; 484 | 485 | return bIsFalsePositive; 486 | } 487 | 488 | int main(int, char* []) { 489 | if (!IsElevated()) 490 | LogError("WARNING Not running as Administrator"); 491 | 492 | BOOLEAN _; 493 | if(!NT_SUCCESS(RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &_))) 494 | LogError("WARNING RtlAdjustPrivilege(DEBUG) failed"); 495 | 496 | const auto tsScanStarted = GetTickCount64(); 497 | UINT32 nProcessesTotal = 0; 498 | UINT32 nProcessesScanned = 0; 499 | UINT32 nThreadsScanned = 0; 500 | 501 | //////////////////////////////////////////////////////////////////////////////// 502 | // Scan each process 503 | HANDLE hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 504 | if (hProcessSnapshot == INVALID_HANDLE_VALUE) { 505 | LogError("CreateToolhelp32Snapshot(PROCESS) failed. LastError:%d", GetLastError()); 506 | return 0; 507 | } 508 | 509 | PROCESSENTRY32 processEntry{}; 510 | processEntry.dwSize = sizeof(PROCESSENTRY32); 511 | if (!Process32First(hProcessSnapshot, &processEntry)) { 512 | LogError("Process32First failed. LastError:%d", GetLastError()); 513 | return 0; 514 | } 515 | 516 | do { 517 | if (processEntry.th32ProcessID <= 4) 518 | continue; // skip Idle and System 519 | 520 | nProcessesTotal++; 521 | HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processEntry.th32ProcessID); 522 | if (NULL == hProcess) 523 | continue; // skip process - Access is Denied, or process stopped 524 | 525 | auto bIsDotNet = IsDotNet(hProcess); 526 | 527 | ////////////////////////////////////////////////////////////////////////////////////////// 528 | // Scan all threads in the process 529 | HPSS hThreadSnapshot = NULL; 530 | HPSSWALK hWalk = NULL; 531 | const auto captureFlags = PSS_CAPTURE_THREADS | PSS_CAPTURE_THREAD_CONTEXT; 532 | const auto contextFlags = CONTEXT_CONTROL | CONTEXT_DEBUG_REGISTERS; 533 | if (S_OK == PssCaptureSnapshot(hProcess, captureFlags, contextFlags, &hThreadSnapshot) && S_OK == PssWalkMarkerCreate(NULL, &hWalk)) { 534 | nProcessesScanned++; 535 | PSS_THREAD_ENTRY thread; 536 | while (S_OK == PssWalkSnapshot(hThreadSnapshot, PSS_WALK_THREADS, hWalk, &thread, sizeof(thread))) { 537 | std::string symbol; 538 | std::vector detections; 539 | (void)ScanThread(hProcess, bIsDotNet, thread, symbol, detections); 540 | nThreadsScanned++; 541 | 542 | if (detections.size() > 0 && !IsKnownFalsePositive(hProcess, processEntry, thread, symbol, detections)) { 543 | (void)GetNearestSymbolWithPdb(hProcess, thread.Win32StartAddress, symbol, true); 544 | Log("ProcessName : %S", processEntry.szExeFile); 545 | Log("pid:tid : %d:%d", thread.ProcessId, thread.ThreadId); 546 | Log("Win32StartAddress : %s", symbol.c_str()); 547 | Log("Detections :"); 548 | for (const auto& detection : detections) 549 | Log(" - %s", detection.c_str()); 550 | Log(""); 551 | } 552 | } 553 | } 554 | 555 | if (NULL != hProcess) 556 | (void)CloseHandle(hProcess); 557 | if (NULL != hThreadSnapshot) 558 | (void)PssFreeSnapshot(GetCurrentProcess(), hThreadSnapshot); 559 | if (NULL != hWalk) 560 | (void)PssWalkMarkerFree(hWalk); 561 | } while (Process32Next(hProcessSnapshot, &processEntry)); 562 | 563 | if (INVALID_HANDLE_VALUE != hProcessSnapshot) 564 | (void)CloseHandle(hProcessSnapshot); 565 | 566 | Log("Scanned %d threads in %d (of %d) processes in %.2f seconds", nThreadsScanned, nProcessesScanned, nProcessesTotal, (GetTickCount64() - tsScanStarted) / 1000.0); 567 | 568 | return 0; 569 | } 570 | -------------------------------------------------------------------------------- /Get-InjectedThreadEx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdu2600/Get-InjectedThreadEx/edbff70fc286a3f1c32c6249b3b913d84d70259b/Get-InjectedThreadEx.png -------------------------------------------------------------------------------- /Get-InjectedThreadEx.ps1: -------------------------------------------------------------------------------- 1 | function Get-InjectedThreadEx 2 | { 3 | <# 4 | 5 | .SYNOPSIS 6 | 7 | Looks for threads that were created as a result of code injection. 8 | 9 | .DESCRIPTION 10 | 11 | Memory resident malware (fileless malware) often uses a form of memory injection to get code execution. Get-InjectedThread looks at each running thread to determine if it is the result of memory injection. 12 | 13 | Common memory injection techniques that *can* be caught using this method include: 14 | - Classic Injection (OpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread) 15 | - Reflective DLL Injection 16 | - Memory Module 17 | 18 | NOTE: Nothing in security is a silver bullet. An attacker could modify their tactics to avoid detection using this methodology. 19 | 20 | KNOWN LIMITATIONS: 21 | - PowerShell 2 is not supported - no bitwise arithemtic shift operators. 22 | - 32-bit Windows support not implemented. 23 | - Limited WoW64 support. 24 | - Slow - uses a single thread. 25 | 26 | .PARAMETER Aggressive 27 | Enables additional scans that have higher false positive rates. 28 | 29 | .PARAMETER ProcessId 30 | Only scans the specified pid. 31 | 32 | .PARAMETER Brief 33 | Limits output to process name, pid, tid, Win32StartAddress module and detections only. 34 | 35 | .NOTES 36 | 37 | Authors - Jared Atkinson (@jaredcatkinson) 38 | - Joe Desimone (@dez_) 39 | - John Uhlmann (@jdu2600) 40 | 41 | 42 | .EXAMPLE 43 | 44 | PS > Get-InjectedThreadEx 45 | 46 | ProcessName : ThreadStart.exe 47 | ProcessId : 7784 48 | Wow64 : False 49 | Path : C:\Users\tester\Desktop\ThreadStart.exe 50 | KernelPath : C:\Users\tester\Desktop\ThreadStart.exe 51 | CommandLine : "C:\Users\tester\Desktop\ThreadStart.exe" 52 | PathMismatch : False 53 | ProcessIntegrity : MEDIUM_MANDATORY_LEVEL 54 | ProcessPrivilege : SeChangeNotifyPrivilege 55 | ProcessLogonId : 999 56 | ProcessSecurityIdentifier : S-1-5-21-386661145-2656271985-3844047388-1001 57 | ProcessUserName : DESKTOP-HMTGQ0R\SYSTEM 58 | ProcessLogonSessionStartTime : 3/15/2017 5:45:38 PM 59 | ProcessLogonType : System 60 | ProcessAuthenticationPackage : NTLM 61 | ThreadId : 14512 62 | BasePriority : 8 63 | IsUniqueThreadToken : False 64 | ThreadIntegrity : 65 | ThreadPrivilege : 66 | AdditionalThreadPrivilege : 67 | ThreadLogonId : 68 | ThreadSecurityIdentifier : 69 | ThreadUserName : \ 70 | ThreadLogonSessionStartTime : 71 | ThreadLogonType : 72 | ThreadAuthenticationPackage : 73 | AllocatedMemoryProtection : PAGE_EXECUTE_READWRITE 74 | MemoryProtection : PAGE_EXECUTE_READWRITE 75 | MemoryState : MEM_COMMIT 76 | MemoryType : MEM_PRIVATE 77 | Win32StartAddress : 430000 78 | Win32StartAddressModule : 79 | Win32StartAddressModuleSigned : False 80 | Win32StartAddressPrivate : True 81 | Size : 4096 82 | TailBytes : 90909090909090909090909090909090 83 | StartBytes : 558bec5356578b7d086a008b5f1083671000ff15c4c9595a8bf085f6780f8bcfe82f85f5ff8bf0ff15c8c9595a5653ff 84 | Detections : {MEM_PRIVATE} 85 | 86 | #> 87 | 88 | [CmdletBinding()] 89 | param 90 | ( 91 | [Parameter()] 92 | [Switch]$Aggressive, 93 | 94 | [Parameter()] 95 | [Switch]$Brief, 96 | 97 | [Parameter()] 98 | [UInt32]$ProcessId 99 | ) 100 | 101 | if(![Environment]::Is64BitProcess) 102 | { 103 | Write-Warning "32-bit not currently supported." 104 | } 105 | 106 | $WindowsVersion = [Int]((Get-WmiObject Win32_OperatingSystem).version -split '\.')[0] 107 | 108 | # Cache for signature checks 109 | $AuthenticodeSignatures = @{} 110 | 111 | $CfgBitMapAddress = GetCfgBitMapAddress 112 | 113 | # Construct a list of ntdll thread entry points 114 | $NtdllRegex = '^[A-Z]:\\Windows\\Sys(tem32|WOW64)\\ntdll\.dll$' 115 | $NtdllThreads64 = @() 116 | # [1] ntdll!RtlpQueryProcessDebugInformationRemote is exported - look it up. 117 | $NtdllThreads64 += GetProcAddress -ModuleName "ntdll.dll" -ProcName "RtlpQueryProcessDebugInformationRemote" 118 | # [2] ntdll!DbgUiRemoteBreakin is exported - look it up. 119 | $NtdllThreads64 += GetProcAddress -ModuleName "ntdll.dll" -ProcName "DbgUiRemoteBreakin" 120 | # For the non-exported entry points, we check the Win32StartAddress of threads we trust. 121 | # [3] ntdll!TppWorkerThread is already used by PowerShell :-) 122 | # [4] ntdll!EtwpLogger is not exported, but is spawned in processes that use a Private ETW Logging Session 123 | # https://docs.microsoft.com/en-us/windows/win32/etw/configuring-and-starting-a-private-logger-session 124 | # Note - the PowerShell ETW CmdLets don't fully support private sessions. 125 | # This means that we need to need start it asynchronously (-AsJob) or wait for a timeout. 126 | # We also we can't stop it. 127 | try 128 | { 129 | $EVENT_TRACE_PRIVATE_LOGGER_MODE = 0x800 130 | $Random = [System.IO.Path]::GetRandomFileName() 131 | $Job = New-EtwTraceSession -Name "GetInjectedThreadEx_$($Random)" -LogFileMode $EVENT_TRACE_PRIVATE_LOGGER_MODE -LocalFilePath "$($ENV:Temp)\GetInjectedThreadEx-$($Random).etl" -AsJob 132 | Start-Sleep -Milliseconds 500 133 | } 134 | catch 135 | { 136 | Write-Warning "New-EtwTraceSession not found. Can't find ntdll!EtwpLogger." 137 | } 138 | # Loop over our process's threads to find the valid ntdll threat start adddresses 139 | $hProcess = OpenProcess -ProcessId $PID -DesiredAccess PROCESS_ALL_ACCESS -InheritHandle $false 140 | foreach ($Thread in (Get-Process -Id $PID).Threads) 141 | { 142 | $hThread = OpenThread -ThreadId $Thread.Id -DesiredAccess THREAD_ALL_ACCESS 143 | $Win32StartAddress = NtQueryInformationThread_Win32StartAddress -ThreadHandle $hThread 144 | $StartAddressModule = GetMappedFileName -ProcessHandle $hProcess -Address $Win32StartAddress 145 | if($StartAddressModule -match $NtdllRegex -and $NtdllThreads64 -notcontains $Win32StartAddress) 146 | { 147 | $NtdllThreads64 += $Win32StartAddress 148 | } 149 | } 150 | if($NtdllThreads64.Length -ne 4) 151 | { 152 | Write-Warning "Failed to enumerate all valid ntdll thread start addresses." 153 | } 154 | 155 | $LoadLibrary = @() 156 | $LoadLibrary += GetProcAddress -ModuleName "kernel32.dll" -ProcName "LoadLibraryA" 157 | $LoadLibrary += GetProcAddress -ModuleName "kernel32.dll" -ProcName "LoadLibraryW" 158 | 159 | # Now enumerate all threads for all processes and check for injection characteristics 160 | $Processes = if($ProcessId) { Get-Process -Id $ProcessId } else {Get-Process} 161 | foreach($Process in $Processes) 162 | { 163 | if($Process.Id -eq 0 -or $Process.Id -eq 4) 164 | { 165 | continue # skip Idle and System 166 | } 167 | 168 | $hProcess = OpenProcess -ProcessId $Process.Id -DesiredAccess PROCESS_ALL_ACCESS -InheritHandle $false 169 | if($hProcess -eq 0) 170 | { 171 | continue # skip process - Access is Denied 172 | } 173 | 174 | Write-Verbose -Message "Checking $($Process.Name) [$($Process.Id)] for injection" 175 | 176 | # Collect per-process information 177 | $IsWow64Process = IsWow64Process -ProcessHandle $hProcess 178 | $WmiProcess = Get-WmiObject Win32_Process -Filter "ProcessId = '$($Process.Id)'" 179 | $ProcessKernelPath = QueryFullProcessImageName -ProcessHandle $hProcess 180 | if(-not $ProcessKernelPath) 181 | { 182 | continue # process has stopped 183 | } 184 | $PathMismatch = $Process.Path.ToLower() -ne $ProcessKernelPath.ToLower() 185 | 186 | if(-not $AuthenticodeSignatures.ContainsKey($ProcessKernelPath)) 187 | { 188 | $AuthenticodeSignatures[$ProcessKernelPath] = Get-AuthenticodeSignature -FilePath $ProcessKernelPath 189 | } 190 | $ProcessModuleSigned = $AuthenticodeSignatures[$ProcessKernelPath].Status -eq 'Valid' 191 | 192 | $hProcessToken = OpenProcessToken -ProcessHandle $hProcess -DesiredAccess TOKEN_QUERY 193 | if($hProcessToken -ne 0) 194 | { 195 | $ProcessSID = GetTokenInformation -TokenHandle $hProcessToken -TokenInformationClass 1 196 | $ProcessPrivs = GetTokenInformation -TokenHandle $hProcessToken -TokenInformationClass 3 197 | $ProcessLogonSession = GetTokenInformation -TokenHandle $hProcessToken -TokenInformationClass 17 198 | $ProcessIntegrity = GetTokenInformation -TokenHandle $hProcessToken -TokenInformationClass 25 199 | } 200 | 201 | # Now loop over this process's threads 202 | foreach ($thread in $Process.Threads) 203 | { 204 | $hThread = OpenThread -ThreadId $Thread.Id -DesiredAccess THREAD_ALL_ACCESS 205 | if ($hThread -eq 0) 206 | { 207 | continue # skip thread - Access is Denied 208 | } 209 | 210 | # Win32StartAddress memory information 211 | $Win32StartAddress = NtQueryInformationThread_Win32StartAddress -ThreadHandle $hThread 212 | $MemoryBasicInfo = VirtualQueryEx -ProcessHandle $hProcess -BaseAddress $Win32StartAddress 213 | $AllocatedMemoryProtection = $MemoryBasicInfo.AllocationProtect -as $MemProtection 214 | $MemoryProtection = $MemoryBasicInfo.Protect -as $MemProtection 215 | $MemoryState = $MemoryBasicInfo.State -as $MemState 216 | $MemoryType = $MemoryBasicInfo.Type -as $MemType 217 | 218 | # Win32StartAddress module information 219 | $StartAddressModuleSigned = $false 220 | if($MemoryType -eq $MemType::MEM_IMAGE) 221 | { 222 | $StartAddressModule = GetMappedFileName -ProcessHandle $hProcess -Address $Win32StartAddress 223 | if(-not $AuthenticodeSignatures.ContainsKey($StartAddressModule)) 224 | { 225 | $AuthenticodeSignatures[$StartAddressModule] = Get-AuthenticodeSignature -FilePath $StartAddressModule 226 | } 227 | $AuthenticodeSignature = $AuthenticodeSignatures[$StartAddressModule] 228 | $StartAddressModuleSigned = $AuthenticodeSignature.Status -eq 'Valid' 229 | Write-Verbose -Message " * Thread Id: [$($Thread.Id)] $($StartAddressModule) signed:$($StartAddressModuleSigned)" 230 | } 231 | else 232 | { 233 | Write-Verbose -Message " * Thread Id: [$($Thread.Id)] $($MemoryType)" 234 | } 235 | 236 | # check if thread has unique token 237 | $IsUniqueThreadToken = $false 238 | $ThreadSID = "" 239 | $ThreadPrivs = "" 240 | $ThreadLogonSession = "" 241 | $ThreadIntegrity = "" 242 | $NewPrivileges = "" 243 | try 244 | { 245 | $hThreadToken = OpenThreadToken -ThreadHandle $hThread -DesiredAccess TOKEN_QUERY 246 | 247 | if ($hThreadToken -ne 0) 248 | { 249 | $ThreadSID = GetTokenInformation -TokenHandle $hThreadToken -TokenInformationClass 1 250 | $ThreadPrivs = GetTokenInformation -TokenHandle $hThreadToken -TokenInformationClass 3 251 | $ThreadLogonSession = GetTokenInformation -TokenHandle $hThreadToken -TokenInformationClass 17 252 | $ThreadIntegrity = GetTokenInformation -TokenHandle $hThreadToken -TokenInformationClass 25 253 | $IsUniqueThreadToken = $true 254 | } 255 | } 256 | catch {} 257 | 258 | $Detections = @() 259 | ################################################################################################# 260 | # Suspicious thread heuristics 261 | ################################################################################################# 262 | # original 263 | # - not MEM_IMAGE 264 | # new 265 | # - MEM_IMAGE and x64 and Win32StartAddress is unexpected prolog 266 | # - MEM_IMAGE and Win32StartAddress is on a private (modified) page 267 | # - MEM_IMAGE and dll and Win32StartAddress entry in CFG BitMap is on a private (modified) page 268 | # - MEM_IMAGE and dll and Win32StartAddress is CFG violation or suppressed export 269 | # - MEM_IMAGE and Win32StartAddress is in a suspicious module 270 | # - MEM_IMAGE and x64 and Win32StartAddress wraps non-MEM_IMAGE start address 271 | # - MEM_IMAGE and Win32StartAddress is preceded by unexpected byte (-Aggressive only) 272 | # - MEM_IMAGE and x64 and Win32StartAddress is missing from call stack (-Aggressive only) 273 | # - MEM_IMAGE and x64 and Win32StartAddress is not 16-byte aligned (-Aggressive only) 274 | # - Thread is impersonating SYSTEM 275 | # - Thread is sleeping (enrichment only) 276 | ################################################################################################# 277 | 278 | if ($MemoryState -eq $MemState::MEM_COMMIT) 279 | { 280 | $StartBytesLength = [math]::Min([Int64]48, $MemoryBasicInfo.BaseAddress.ToUInt64() + $MemoryBasicInfo.RegionSize.ToUInt64() - $Win32StartAddress.ToInt64()) 281 | $Buffer = ReadProcessMemory -ProcessHandle $hProcess -BaseAddress $Win32StartAddress -Size $StartBytesLength 282 | $StartBytes = New-Object -TypeName System.Text.StringBuilder 283 | $StartBytes.Capacity = $StartBytesLength 284 | ForEach ($Byte in $Buffer) { $StartBytes.AppendFormat("{0:x2}", $Byte) | Out-Null } 285 | $StartBytes = $StartBytes.ToString() 286 | 287 | $TailBytesLength = [math]::Min([Int64]16, $Win32StartAddress.ToInt64() - $MemoryBasicInfo.BaseAddress.ToUInt64()) 288 | $Buffer = ReadProcessMemory -ProcessHandle $hProcess -BaseAddress ($Win32StartAddress.ToInt64() - $TailBytesLength) -Size $TailBytesLength 289 | $TailBytes = New-Object -TypeName System.Text.StringBuilder 290 | $TailBytes.Capacity = $TailBytesLength 291 | ForEach ($Byte in $Buffer) { $TailBytes.AppendFormat("{0:x2}", $Byte) | Out-Null } 292 | $TailBytes = $TailBytes.ToString() 293 | 294 | # All threads not starting in a MEM_IMAGE region are suspicious 295 | if ($MemoryType -ne $MemType::MEM_IMAGE) 296 | { 297 | $Detections += $MemoryType 298 | } 299 | 300 | # Any x64 threads not starting with a valid Windows x64 prolog are suspicious 301 | # In lieu of a dissassembler in PowerShell we approximate with a regex :-( 302 | $x64PrologRegex = '^(' + 303 | '(488d0[5d]........)?' + # lea rax,[rip+nnnn] 304 | '(eb0.(90){3,14})?' + # hot patch space 305 | '(488bc4|4c8bdc)?' + # stack pointer - rax|r11 306 | '(4[8-9c]89(....|[3-7][4c]24..))*' + # save registers in shadow space 307 | '((5|fff|4(0|1)5)[0-7])*' + # push registers 308 | '(488bec|4889e5)?' + # stack pointer - rbp 309 | '(488d6c24..)?' + # lea rbp,[rsp+n] 310 | '(488dac24........)?' + # lea rbp,[rsp+nnnn] 311 | '(488d68..)?' + # lea rbp,[rax+n] 312 | '(488da8........)?' + # lea rbp,[rax+nnnn] 313 | '(498d6b..)?' + # lea rbp,[r11+n] 314 | '(498dab........)?' + # lea rbp,[r11+nnnn] 315 | '(488(1|3)ec' + # sub rsp,n 316 | '|b8........e8........482be0)' + # mov rax; call; sub rsp, rax 317 | '|4885c90f8[4-5]........(e9........cc|b8........c3)' + # test rcx,rcx; j[n]e nnnn; [jmp nnnn | mov eax, ret] 318 | '|(488d0[5d]........)?(488b..(..)?)*(48)?(e9|ff25)' + # (mov ... ) jmp 319 | '|4d5a90000300000004000000ffff0000b8000000000000004000000000000000' + # PE Header -> CLR Assembly with AddressOfEntryPoint=0 320 | ')' 321 | # TODO(jdu) - update with more variants? Or is the approach simply too unreliable? 322 | if ((-not $IsWow64Process) -and 323 | ($StartBytes -notmatch $x64PrologRegex)) 324 | { 325 | $Detections += 'prolog' 326 | } 327 | 328 | $x86PrologRegex = '^(' + 329 | '(8bff)?(6690)?' + # 2-byte nop 330 | '55(8bec|89e5)' + # stack pointer 331 | '|(..)+8[13]ec' + # sub esp,nnnn 332 | '|(6a..|(68|b8)........)*e8' + # call 333 | '|e9|ff25' + # jmp 334 | '|4d5a90000300000004000000ffff0000b8000000000000004000000000000000' + # CLR Assembly 335 | ')' 336 | if ($IsWow64Process -and 337 | ($StartBytes -notmatch $x86PrologRegex)) 338 | { 339 | $Detections += 'prolog' 340 | } 341 | 342 | # Has our MEM_IMAGE Win32StartAddress been (naively) hooked? 343 | # https://blog.redbluepurple.io/offensive-research/bypassing-injection-detection#creating-the-thread 344 | # Note - checking against bytes on disk after the fact won't help with false positives 345 | # as the hook can easily be removed after thread start. 346 | # Detection gap - the hook could easily be deeper, potentially even in a subsequent call. :-( 347 | # Microsoft-Windows-Threat-Intelligence ETW events should detect this more robustly. 348 | $PrivatePage = IsWorkingSetPage -ProcessHandle $hProcess -Address $Win32StartAddress 349 | if (($MemoryType -eq $MemType::MEM_IMAGE) -and 350 | $PrivatePage) 351 | { 352 | $Detections += 'hooked' 353 | } 354 | 355 | # Check for suspcious CFG BitMap states 356 | if ((-not $IsWow64Process) -and # TODO(jdu) Wow64 support not implemented 357 | ([IntPtr]::Zero -ne $CfgBitMapAddress) -and 358 | ($MemoryType -eq $MemType::MEM_IMAGE)) 359 | { 360 | $Detections += (CfgDetections -pCfgBitMap $CfgBitMapAddress -ProcessHandle $hProcess -Address $Win32StartAddress) 361 | } 362 | 363 | ### Suspicious start modules 364 | 365 | # unsigned module in signed process - e.g. dll sideloading 366 | if (($WindowsVersion -ge 10) -and $ProcessModuleSigned -and -not $StartAddressModuleSigned) 367 | { 368 | $Detections += 'unsigned' 369 | } 370 | 371 | # There are no valid thread entry points (that I know of) in many Win32 modules. 372 | $ModulesWithoutThreadEntries = @( 373 | ('^[A-Z]:\\Windows\\Sys(tem32|WOW64)\\kernel32\.dll$', 'kernel32'), 374 | ('^[A-Z]:\\Windows\\Sys(tem32|WOW64)\\kernelbase\.dll$', 'kernelbase'), 375 | ('^[A-Z]:\\Windows\\Sys(tem32|WOW64)\\user32\.dll$', 'user32'), 376 | ('^[A-Z]:\\Windows\\Sys(tem32|WOW64)\\advapi32\.dll$', 'advapi32') 377 | # ... and many more 378 | ); 379 | foreach ($Module in $ModulesWithoutThreadEntries) 380 | { 381 | if ($StartAddressModule -match $Module[0]) 382 | { 383 | $Detections += $Module[1] 384 | break 385 | } 386 | } 387 | 388 | # kernel32!LoadLibrary 389 | # And, even if there are, LoadLibrary is always a suspicious start address. 390 | if ($LoadLibrary -contains $Win32StartAddress) 391 | { 392 | $Detections += 'LoadLibrary' 393 | } 394 | 395 | # ntdll.dll but not - 396 | # * ntdll!TppWorkerThread 397 | # * ntdll!EtwpLogger 398 | # * ntdll!DbgUiRemoteBreakin 399 | # * ntdll!RtlpQueryProcessDebugInformationRemote 400 | # These are the only valid thread entry points in ntdll that I know of. 401 | if ((-not $IsWow64Process) -and 402 | ($NtdllThreads64.Length -eq 4) -and 403 | ($StartAddressModule -match $NtdllRegex) -and 404 | ($NtdllThreads64 -notcontains $Win32StartAddress)) 405 | { 406 | $Detections += 'ntdll' 407 | } 408 | 409 | # Is SYSTEM being impersonated? 410 | if (($ProcessSID -ne "S-1-5-18") -and ($ThreadSID -eq "S-1-5-18")) 411 | { 412 | $Detections += 'SYSTEM impersonation' 413 | } 414 | 415 | # Check for suspicious call stacks 416 | # https://www.trustedsec.com/blog/avoiding-get-injectedthread-for-internal-thread-creation/ 417 | 418 | $WrapperRegex = '^[A-Z]:\\Windows\\Sys(tem32|WOW64)\\((msvcr[t0-9]+|ucrtbase)d?|SHCore|Shlwapi)\.dll$' 419 | if ((-not $IsWow64Process) -and # TODO(jdu) Wow64 support not implemented 420 | ($StartAddressModule -match $WrapperRegex) -or # Always perform if a known wrapper module. 421 | ($StartAddressModule -match $NtdllRegex) -or # Always perform if ntdll. 422 | $Aggressive -or 423 | ($Detections.Length -ne 0)) 424 | { 425 | $Detections += (CallStackDetections -ProcessHandle $hProcess -ThreadHandle $hThread -StartAddressModule $StartAddressModule -Aggressive $Aggressive) 426 | } 427 | 428 | # The byte preceding a function prolog is typically a return, or filler byte. 429 | # False positives can occur if data was included in a code section. This was 430 | # common in older compilers. 431 | # In practice, this has a medium FP rate - so don't check by default. 432 | $x64EpilogFillerRegex = '(00|90|c3|cc|(e8|e9|ff25)........|eb..|^)$' 433 | if (($Aggressive -or ($Detections.Length -ne 0)) -and 434 | ($TailBytes -notmatch $x64EpilogFillerRegex)) 435 | { 436 | $Detections += 'tail' 437 | } 438 | 439 | # Modern CPUs load instructions in 16-byte lines. So, for performance, compilers may want to 440 | # ensure that the maximum number of useful bytes will be loaded. This is either 16 or the 441 | # number of bytes modulo 16 until the end of the first call (or absolute jmp) instruction. 442 | # 443 | # Any start address not aligned as such is a potential MEM_IMAGE trampoline gadget such 444 | # as 'jmp rcx' 445 | # https://blog.xpnsec.com/undersanding-and-evading-get-injectedthread/ 446 | # 447 | # In practice, this has a high FP rate - so don't check by default. 448 | $EarlyCallRegex = '^(..)*?(e8|ff15)' 449 | $ImmediateJumpRegex = '^(e9|(48)?ff25)' 450 | if (($Aggressive -or ($Detections.Length -ne 0)) -and 451 | (($Win32StartAddress.ToInt64() -band 0xF) -ne 0) -and 452 | # If < Windows 10 then also allow 4-byte alignments 453 | (($WindowsVersion -ge 10) -or (($Win32StartAddress.ToInt64() -band 3) -ne 0))) 454 | { 455 | if ($StartBytes -match $EarlyCallRegex) 456 | { 457 | # Calulate the distance to the end of the call modulo 16 458 | # This calculation isn't perfect - we did a rough regex match, not an exact decompilation... 459 | $BytesNeeded = (($matches[0].Length / 2) -band 0xF) + 4 460 | $BytesLoaded = 16 - ($Win32StartAddress.ToInt64() -band 0xF) 461 | if ($BytesLoaded -lt $BytesNeeded) 462 | { 463 | $Detections += 'alignment' 464 | } 465 | } 466 | elseif ($StartBytes -notmatch $ImmediateJumpRegex) 467 | { 468 | $Detections += 'alignment' 469 | } 470 | } 471 | 472 | # Definitely not a smoking gun on its own, but obfuscate-and-sleep approaches are becoming popular. 473 | if (($Detections.Length -ne 0) -and 474 | ($null -ne $Thread.WaitReason) -and 475 | ($Thread.WaitReason.ToString() -eq 'ExecutionDelay')) 476 | { 477 | $Detections += "sleep" 478 | } 479 | 480 | if ($Detections.Length -ne 0) 481 | { 482 | $ThreadDetail = New-Object PSObject 483 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ProcessName -Value $WmiProcess.Name 484 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ProcessId -Value $WmiProcess.ProcessId 485 | if (-not $Brief) 486 | { 487 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name Wow64 -Value $IsWow64Process 488 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name Path -Value $WmiProcess.Path 489 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name KernelPath -Value $ProcessKernelPath 490 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name CommandLine -Value $WmiProcess.CommandLine 491 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name PathMismatch -Value $PathMismatch 492 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ProcessIntegrity -Value $ProcessIntegrity 493 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ProcessPrivilege -Value $ProcessPrivs 494 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ProcessLogonId -Value $ProcessLogonSession.LogonId 495 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ProcessSecurityIdentifier -Value $ProcessSID 496 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ProcessUserName -Value "$($ProcessLogonSession.Domain)\$($ProcessLogonSession.UserName)" 497 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ProcessLogonSessionStartTime -Value $ProcessLogonSession.StartTime 498 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ProcessLogonType -Value $ProcessLogonSession.LogonType 499 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ProcessAuthenticationPackage -Value $ProcessLogonSession.AuthenticationPackage 500 | } 501 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ThreadId -Value $Thread.Id 502 | if (-not $Brief) 503 | { 504 | $ThreadDetail | Add-Member -MemberType NoteProperty -Name ThreadStartTime -Value $Thread.StartTime 505 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name BasePriority -Value $Thread.BasePriority 506 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name WaitReason -Value $Thread.WaitReason 507 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name IsUniqueThreadToken -Value $IsUniqueThreadToken 508 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ThreadIntegrity -Value $ThreadIntegrity 509 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ThreadPrivilege -Value $ThreadPrivs 510 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name AdditionalThreadPrivilege -Value $NewPrivileges 511 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ThreadLogonId -Value $ThreadLogonSession.LogonId 512 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ThreadSecurityIdentifier -Value $ThreadSID 513 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ThreadUserName -Value "$($ThreadLogonSession.Domain)\$($ThreadLogonSession.UserName)" 514 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ThreadLogonSessionStartTime -Value $ThreadLogonSession.StartTime 515 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ThreadLogonType -Value $ThreadLogonSession.LogonType 516 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name ThreadAuthenticationPackage -Value $ThreadLogonSession.AuthenticationPackage 517 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name AllocatedMemoryProtection -Value $AllocatedMemoryProtection 518 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name MemoryProtection -Value $MemoryProtection 519 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name MemoryState -Value $MemoryState 520 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name MemoryType -Value $MemoryType 521 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name Win32StartAddress -Value $Win32StartAddress.ToString('x') 522 | } 523 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name Win32StartAddressModule -Value $StartAddressModule 524 | if (-not $Brief) 525 | { 526 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name Win32StartAddressModuleSigned -Value $StartAddressModuleSigned 527 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name Win32StartAddressPrivate -Value $PrivatePage 528 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name Size -Value $MemoryBasicInfo.RegionSize 529 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name TailBytes -Value $TailBytes 530 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name StartBytes -Value $StartBytes 531 | } 532 | $ThreadDetail | Add-Member -MemberType Noteproperty -Name Detections -Value $Detections 533 | Write-Output $ThreadDetail 534 | } 535 | } 536 | CloseHandle($hThread) 537 | } 538 | CloseHandle($hProcess) 539 | } 540 | } 541 | 542 | function GetCfgBitMapAddress 543 | { 544 | <# 545 | .SYNOPSIS 546 | 547 | Returns the address of ntdll!LdrSystemDllInitBlock.CfgBitMap, or Zero if CFG is not supported. 548 | 549 | .DESCRIPTION 550 | 551 | .NOTES 552 | 553 | Author - John Uhlmann (@jdu2600) 554 | 555 | .LINK 556 | 557 | .EXAMPLE 558 | #> 559 | 560 | # Find non-exported ntdll!LdrSystemDllInitBlock.CfgBitMap 561 | # 180033520 ntdll!LdrControlFlowGuardEnforced 562 | # 180033520 48833d80be140000 CMP qword ptr[LdrSystemDllInitBlock.CfgBitMap], 0x0 563 | $LdrControlFlowGuardEnforced = GetProcAddress -ModuleName "ntdll.dll" -ProcName "LdrControlFlowGuardEnforced" 564 | if ($LdrControlFlowGuardEnforced -eq 0) 565 | { 566 | return [IntPtr]::Zero # CFG not supported on this platform 567 | } 568 | 569 | $Offset = [System.Runtime.InteropServices.Marshal]::ReadInt32($LdrControlFlowGuardEnforced.ToInt64() + 3) 570 | $pCfgBitMap = $LdrControlFlowGuardEnforced.ToInt64() + 8 + $Offset 571 | 572 | # Read the value of the CFG BitMap address in our CFG-Enabled PowerShell process 573 | $CfgBitMap = [System.Runtime.InteropServices.Marshal]::ReadIntPtr($pCfgBitMap) 574 | if ($CfgBitMap -eq [IntPtr]::Zero) 575 | { 576 | Write-Warning "CFG BitMap address not found at 0x$($CfgBitmap.ToString('x'))" 577 | return [IntPtr]::Zero 578 | } 579 | 580 | # Validate the CFG BitMap address 581 | $CurrentProcess = [IntPtr](-1) 582 | $MemoryBasicInfo = VirtualQueryEx -ProcessHandle $CurrentProcess -BaseAddress $CfgBitMap 583 | if ($MemoryBasicInfo.AllocationBase -ne [UIntPtr]([UInt64]$CfgBitMap.ToInt64())) 584 | { 585 | Write-Warning "CFG BitMap address not valid at 0x$($CfgBitmap.ToString('x'))" 586 | return [IntPtr]::Zero 587 | } 588 | 589 | return [IntPtr]$pCfgBitmap 590 | } 591 | 592 | function CfgDetections 593 | { 594 | <# 595 | .SYNOPSIS 596 | 597 | Checks the CFG BitMap for anomalies related to the given Address. 598 | 599 | .DESCRIPTION 600 | 601 | .PARAMETER pCfgBitMap 602 | 603 | The address of ntdll!LdrSystemDllInitBlock.CfgBitMap 604 | 605 | .PARAMETER ProcessHandle 606 | 607 | A read handle to the target process. 608 | 609 | .PARAMETER Address 610 | 611 | The address to check. 612 | 613 | .NOTES 614 | 615 | Author - John Uhlmann (@jdu2600) 616 | 617 | .LINK 618 | 619 | .EXAMPLE 620 | #> 621 | param 622 | ( 623 | [Parameter(Mandatory = $true)] 624 | [IntPtr] 625 | $pCfgBitMap, 626 | 627 | [Parameter(Mandatory = $true)] 628 | [IntPtr] 629 | $ProcessHandle, 630 | 631 | [Parameter(Mandatory = $true)] 632 | [IntPtr] 633 | $Address 634 | 635 | ) 636 | 637 | $Detections = @() 638 | 639 | # Read the location of the CFG BitMap address in our process 640 | $Buffer = ReadProcessMemory -ProcessHandle $ProcessHandle -BaseAddress $pCfgBitmap -Size $([IntPtr]::Size) 641 | $CfgBitmap = if ([IntPtr]::Size -eq 8) {[System.BitConverter]::ToInt64($Buffer, 0)} else {[System.BitConverter]::ToInt32($Buffer, 0)} 642 | if($CfgBitmap -eq 0) 643 | { 644 | return # CFG is not enabled 645 | } 646 | 647 | # Validate the CFG BitMap 648 | $MemoryBasicInfo = VirtualQueryEx -ProcessHandle $ProcessHandle -BaseAddress $CfgBitmap 649 | if($MemoryBasicInfo.AllocationBase -ne [UIntPtr]([UInt64]$CfgBitmap)) 650 | { 651 | Write-Warning "CFG BitMap address not found at 0x$($CfgBitmap.ToString('x'))" 652 | return 653 | } 654 | 655 | # TODO(jdu) - implement bitwise shift operations to support PowerShell 2. 656 | # Perhaps https://github.com/vrimkus/PoSh2.0-BitShifting 657 | 658 | # Find the CFG entry for target address 659 | $CfgIndexShift = if ([IntPtr]::Size -eq 8) { 9 } else { 8 } 660 | $pCfgEntry = $CfgBitmap + ($Address.ToInt64() -shr $CfgIndexShift) * [IntPtr]::Size 661 | $MemoryBasicInfo = VirtualQueryEx -ProcessHandle $ProcessHandle -BaseAddress $pCfgEntry 662 | if (($MemoryBasicInfo.State -ne $MemState::MEM_COMMIT) -or 663 | ($MemoryBasicInfo.Type -ne $MemType::MEM_MAPPED) -or 664 | ($MemoryBasicInfo.Protect -eq $MemProtect::PAGE_NOACCESS)) 665 | { 666 | Write-Warning "Invalid CFG Entry for 0x$($Address.ToString('x'))" 667 | return 668 | } 669 | 670 | if (IsWorkingSetPage -ProcessHandle $ProcessHandle -Address $pCfgEntry) 671 | { 672 | $AddressModule = GetMappedFileName -ProcessHandle $ProcessHandle -Address $Address 673 | $ProcessExecutable = QueryFullProcessImageName -ProcessHandle $hProcess 674 | 675 | # executable CFG bitmaps are not shared - only library (dll) ones. 676 | # https://www.trendmicro.com/en_us/research/16/j/control-flow-guard-improvements-windows-10-anniversary-update.html 677 | # The original Microsoft Edge modifies its CFG bitmap 678 | if(($AddressModule -notmatch '\.exe$') -and 679 | ($ProcessExecutable -notmatch '^[A-Z]:\\Windows\\.*\\MicrosoftEdge(CP|SH)?\.exe$')) 680 | { 681 | $Detections += "cfg_modified" 682 | } 683 | } 684 | 685 | $Buffer = ReadProcessMemory -ProcessHandle $ProcessHandle -BaseAddress $pCfgEntry -Size $([IntPtr]::Size) 686 | $CfgEntry = if ([IntPtr]::Size -eq 8) { [System.BitConverter]::ToInt64($Buffer, 0) } else { [System.BitConverter]::ToInt32($Buffer, 0) } 687 | 688 | # Check the relevant bits for address in this entry 689 | $CfgOffsetMask = (([IntPtr]::Size -shl 3) - 2) 690 | $BitPairOffset = ($Address.ToInt64() -shr 3) -band $CfgOffsetMask 691 | $BitPair = ($CfgEntry -shr $BitPairOffset) -band 3 692 | # 00 - no address in this range is a valid target 693 | # 01 - the only valid target is 16-byte aligned 694 | # 10 - this range contains an export-suppressed target 695 | # 11 - all addresses in this range are valid. 696 | 697 | # export suppressed CFG addresses are suspicious thread start addresses 698 | if ($BitPair -eq 2) 699 | { 700 | $Detections += 'cfg_export_suppressed' 701 | } 702 | 703 | # Was CFG bypassed? 704 | elseif (($Address.ToInt64() -band 0xF) -eq 0) 705 | { 706 | # 16-byte aligned check 707 | if (($BitPair -band 1) -eq 0) 708 | { 709 | $Detections += 'cfg' 710 | } 711 | } 712 | elseif ($BitPair -ne 3) 713 | { 714 | $Detections += 'cfg' 715 | } 716 | 717 | Write-Output $Detections 718 | } 719 | 720 | function CallStackDetections 721 | { 722 | <# 723 | .SYNOPSIS 724 | 725 | Checks the bottom of the thread's stack for suspicious return addresses. 726 | 727 | .DESCRIPTION 728 | 729 | .PARAMETER ProcessHandle 730 | 731 | .PARAMETER ThreadHandle 732 | 733 | .PARAMETER StartAddressModule 734 | 735 | .PARAMETER Aggressive 736 | 737 | .NOTES 738 | 739 | Author - John Uhlmann (@jdu2600) 740 | 741 | .LINK 742 | 743 | .EXAMPLE 744 | #> 745 | 746 | param 747 | ( 748 | [Parameter(Mandatory = $true)] 749 | [IntPtr] 750 | $ProcessHandle, 751 | 752 | [Parameter(Mandatory = $true)] 753 | [IntPtr] 754 | $ThreadHandle, 755 | 756 | [Parameter(Mandatory = $true)] 757 | [String] 758 | $StartAddressModule, 759 | 760 | [Parameter(Mandatory = $true)] 761 | [Boolean] 762 | $Aggressive 763 | ) 764 | 765 | <# 766 | (func ntdll NtQueryInformationThread ([UInt32]) @( 767 | [IntPtr], #_In_ HANDLE ThreadHandle, 768 | [Int32], #_In_ THREADINFOCLASS ThreadInformationClass, 769 | [THREAD_BASIC_INFORMATION].MakeByRefType(), #_Inout_ PVOID ThreadInformation, 770 | [Int32], #_In_ ULONG ThreadInformationLength, 771 | [IntPtr] #_Out_opt_ PULONG ReturnLength 772 | )) 773 | #> 774 | 775 | $WrapperRegex = '^[A-Z]:\\Windows\\Sys(tem32|WOW64)\\((msvcr[t0-9]+|ucrtbase)d?|SHCore|Shlwapi)\.dll$' 776 | 777 | # TODO(jdu) Handle 32-bit thread stacks... 778 | 779 | # 1. Query the THREAD_BASIC_INFORMATION to determine the location of the Thread Environment Block (TEB) 780 | $ThreadBasicInfo = [Activator]::CreateInstance($THREAD_BASIC_INFORMATION) 781 | $NtStatus = $Ntdll::NtQueryInformationThread($ThreadHandle, 0, [Ref]$ThreadBasicInfo, $THREAD_BASIC_INFORMATION::GetSize(), [IntPtr]::Zero) 782 | if ($NtStatus -ne 0) 783 | { 784 | $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 785 | throw "NtQueryInformationThread Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 786 | } 787 | 788 | if($ThreadBasicInfo.TebBaseAddress -eq 0) 789 | { 790 | return 791 | } 792 | 793 | # 2. The TIB is the first elemenet of the TEB. Read the TIB to determine the stack limits. 794 | $Buffer = ReadProcessMemory -ProcessHandle $ProcessHandle -BaseAddress $ThreadBasicInfo.TebBaseAddress -Size $TIB64::GetSize() 795 | $TibPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($TIB64::GetSize()) 796 | [System.Runtime.InteropServices.Marshal]::Copy($Buffer, 0, $TibPtr, $TIB64::GetSize()) 797 | $Tib = $TibPtr -as $TIB64 798 | 799 | # 3. Read the (partial) stack contents 800 | $StackReadLength = [math]::Min([Int64]0x1000, $Tib.StackBase.ToInt64() - $Tib.StackLimit.ToInt64()) 801 | $StackBuffer = ReadProcessMemory -ProcessHandle $ProcessHandle -BaseAddress ($Tib.StackBase.ToInt64() - $StackReadLength) -Size $StackReadLength 802 | 803 | # 4. Search the stack bottom up for the (probable) initial return addresses of the first 5 frames. 804 | # [expected] ntdll!RtlUserThreadStart -> kernel32!BaseThreadInitThunk -> Win32StartAddress 805 | # Note - at this stack depth it is unlikely, but not impossible, that we encounter a false positive return address on the stack. 806 | $RspBuffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal([IntPtr]::Size) 807 | $Detections = @() 808 | $Unbacked = $false 809 | $ReturnModules = @() 810 | # Our return addresses are only probable as we're not stack walking. Collect up to 5 in case of false positives. 811 | $MaxFrameCount = 5 812 | # x64 stack frames are 16-byte aligned, and return addresses are 8-byte aligned. 813 | for ($i = 8; ($ReturnModules.Count -lt $MaxFrameCount) -and ($i -lt $StackReadLength); $i += 16) 814 | { 815 | [System.Runtime.InteropServices.Marshal]::Copy($StackBuffer, ($StackReadLength - $i), $RspBuffer, [IntPtr]::Size) 816 | $CandidateRsp = [System.Runtime.InteropServices.Marshal]::ReadInt64($RspBuffer) 817 | if ($CandidateRsp -ne 0) 818 | { 819 | $MemoryBasicInfo = VirtualQueryEx -ProcessHandle $ProcessHandle -BaseAddress $CandidateRsp 820 | if ($MemoryBasicInfo.State -eq $MemState::MEM_COMMIT -and 821 | ($MemoryBasicInfo.Protect -eq $MemProtection::PAGE_EXECUTE -or 822 | $MemoryBasicInfo.Protect -eq $MemProtection::PAGE_EXECUTE_READ -or 823 | $MemoryBasicInfo.Protect -eq $MemProtection::PAGE_EXECUTE_READWRITE -or 824 | $MemoryBasicInfo.Protect -eq $MemProtection::PAGE_EXECUTE_WRITECOPY)) 825 | { 826 | if ($MemoryBasicInfo.Type -eq $MemType::MEM_IMAGE) 827 | { 828 | $CandidateRspModule = GetMappedFileName -ProcessHandle $hProcess -Address $CandidateRsp 829 | if($CandidateRspModule -eq $StartAddressModule) 830 | { 831 | # StartAddressModule found - stop searching (or after next frame) 832 | $MaxFrameCount = if($Aggressive -or ($StartAddressModule -match $WrapperRegex)) {[math]::Min($MaxFrameCount, $ReturnModules.Count + 2)} else {$ReturnModules.Count} 833 | } 834 | elseif(IsWorkingSetPage -ProcessHandle $hProcess -Address $CandidateRsp) 835 | { 836 | $Detections += "hooked(" + [System.IO.Path]::GetFileNameWithoutExtension($CandidateRspModule) + ")" 837 | } 838 | } 839 | else 840 | { 841 | $CandidateRspModule = $MemoryBasicInfo.Type -as $MemType 842 | $Unbacked = $true 843 | # Unbacked found - stop searching after next frame 844 | $MaxFrameCount = [math]::Min($MaxFrameCount, $ReturnModules.Count + 2) 845 | } 846 | 847 | Write-Verbose -Message " * Stack [0x$($CandidateRsp.ToString('x'))] +0x$($i.ToString('x')): $($CandidateRspModule) " 848 | 849 | if (($ReturnModules.Count -eq 0) -or ($ReturnModules[$ReturnModules.Count-1] -ne $CandidateRspModule)) 850 | { 851 | 852 | $ReturnModules += $CandidateRspModule; 853 | 854 | if (($ReturnModules.Count -le 2) -and ($CandidateRspModule -match "^[A-Z]:\\Windows\\System32\\(ntdll|kernel32)\.dll$")) 855 | { 856 | $i += 32 # skip parameter shadow space - this helps with FPs 857 | } 858 | } 859 | } 860 | } 861 | } 862 | [System.Runtime.InteropServices.Marshal]::FreeHGlobal($TibPtr) 863 | [System.Runtime.InteropServices.Marshal]::FreeHGlobal($RspBuffer) 864 | 865 | if($ReturnModules.Count -eq 0) 866 | { 867 | return 868 | } 869 | 870 | # 5. Validate the initial inferred call stack frames 871 | $StackSummary = (($ReturnModules | ForEach-Object { [System.IO.Path]::GetFileNameWithoutExtension($_) }) -join ';').Replace("ntdll;kernel32;", "") 872 | 873 | # Has the thread been hijacked before Win32StartAddress was called? 874 | if ($Aggressive -and 875 | # Our "call stack" is a rough approximation - and could cause false positives. 876 | # Also, the Win32StartAddress function could be a Tail Call Optimized (TCO). 877 | ($ReturnModules -notcontains $StartAddressModule) -and 878 | # .NET executables always intially jump to the CLR runtime startup shim mscoree!_CorExeMain. 879 | ($ReturnModules[2] -notmatch "^[A-Z]:\\Windows\\Sys(tem32|WOW64)\\mscoree\.dll$") -and 880 | # WindowsApps executables sometimes just jump to a dll of the same name! 881 | ($ReturnModules[2] -notcontains $StartAddressModule.Replace(".exe", ".dll")) -and 882 | # WoW64 thread 883 | $StackSummary -notmatch "^ntdll;wow64;wow64cpu;") 884 | { 885 | $Detections += "hijacked($($StackSummary))" 886 | } 887 | 888 | # Is the stack base normal? 889 | # Note - MSYS2 will false positive here. 890 | elseif (($ReturnModules[0] -notmatch "^[A-Z]:\\Windows\\Sys(tem32|WOW64)\\ntdll\.dll$") -or 891 | ($ReturnModules[1] -and ($ReturnModules[1] -notmatch "^[A-Z]:\\Windows\\Sys(tem32|WOW64)\\(wow64|kernel32)\.dll$"))) 892 | { 893 | $Detections += "hijacked($($StackSummary))" 894 | } 895 | 896 | # Has a private start address been called indirectly via a wrapper function? 897 | elseif ($Unbacked -and ($ReturnModules -contains $StartAddressModule)) 898 | { 899 | $Detections += "wrapper($($StackSummary))" 900 | } 901 | 902 | # Is there a private start address near the bottom of the stack? 903 | elseif ($Unbacked) 904 | { 905 | $Detections += "early_unbacked($($StackSummary))" 906 | } 907 | 908 | Write-Output $Detections 909 | } 910 | 911 | function Get-LogonSession 912 | { 913 | <# 914 | .NOTES 915 | 916 | Author: Lee Christensen (@tifkin_) 917 | License: BSD 3-Clause 918 | Required Dependencies: None 919 | Optional Dependencies: None 920 | #> 921 | param 922 | ( 923 | [Parameter(Mandatory = $true)] 924 | [UInt32] 925 | $LogonId 926 | ) 927 | 928 | $LogonMap = @{} 929 | Get-WmiObject Win32_LoggedOnUser | %{ 930 | 931 | $Identity = $_.Antecedent | Select-String 'Domain="(.*)",Name="(.*)"' 932 | $LogonSession = $_.Dependent | Select-String 'LogonId="(\d+)"' 933 | 934 | $LogonMap[$LogonSession.Matches[0].Groups[1].Value] = New-Object PSObject -Property @{ 935 | Domain = $Identity.Matches[0].Groups[1].Value 936 | UserName = $Identity.Matches[0].Groups[2].Value 937 | } 938 | } 939 | 940 | Get-WmiObject Win32_LogonSession -Filter "LogonId = `"$($LogonId)`"" | %{ 941 | $LogonType = $Null 942 | switch($_.LogonType) { 943 | $null {$LogonType = 'None'} 944 | 0 { $LogonType = 'System' } 945 | 2 { $LogonType = 'Interactive' } 946 | 3 { $LogonType = 'Network' } 947 | 4 { $LogonType = 'Batch' } 948 | 5 { $LogonType = 'Service' } 949 | 6 { $LogonType = 'Proxy' } 950 | 7 { $LogonType = 'Unlock' } 951 | 8 { $LogonType = 'NetworkCleartext' } 952 | 9 { $LogonType = 'NewCredentials' } 953 | 10 { $LogonType = 'RemoteInteractive' } 954 | 11 { $LogonType = 'CachedInteractive' } 955 | 12 { $LogonType = 'CachedRemoteInteractive' } 956 | 13 { $LogonType = 'CachedUnlock' } 957 | default { $LogonType = $_.LogonType} 958 | } 959 | 960 | New-Object PSObject -Property @{ 961 | UserName = $LogonMap[$_.LogonId].UserName 962 | Domain = $LogonMap[$_.LogonId].Domain 963 | LogonId = $_.LogonId 964 | LogonType = $LogonType 965 | AuthenticationPackage = $_.AuthenticationPackage 966 | Caption = $_.Caption 967 | Description = $_.Description 968 | InstallDate = $_.InstallDate 969 | Name = $_.Name 970 | StartTime = $_.ConvertToDateTime($_.StartTime) 971 | } 972 | } 973 | } 974 | 975 | #region PSReflect 976 | 977 | function New-InMemoryModule 978 | { 979 | <# 980 | .SYNOPSIS 981 | 982 | Creates an in-memory assembly and module 983 | 984 | Author: Matthew Graeber (@mattifestation) 985 | License: BSD 3-Clause 986 | Required Dependencies: None 987 | Optional Dependencies: None 988 | 989 | .DESCRIPTION 990 | 991 | When defining custom enums, structs, and unmanaged functions, it is 992 | necessary to associate to an assembly module. This helper function 993 | creates an in-memory module that can be passed to the 'enum', 994 | 'struct', and Add-Win32Type functions. 995 | 996 | .PARAMETER ModuleName 997 | 998 | Specifies the desired name for the in-memory assembly and module. If 999 | ModuleName is not provided, it will default to a GUID. 1000 | 1001 | .EXAMPLE 1002 | 1003 | $Module = New-InMemoryModule -ModuleName Win32 1004 | #> 1005 | 1006 | Param 1007 | ( 1008 | [Parameter(Position = 0)] 1009 | [ValidateNotNullOrEmpty()] 1010 | [String] 1011 | $ModuleName = [Guid]::NewGuid().ToString() 1012 | ) 1013 | 1014 | $AppDomain = [Reflection.Assembly].Assembly.GetType('System.AppDomain').GetProperty('CurrentDomain').GetValue($null, @()) 1015 | $LoadedAssemblies = $AppDomain.GetAssemblies() 1016 | 1017 | foreach ($Assembly in $LoadedAssemblies) { 1018 | if ($Assembly.FullName -and ($Assembly.FullName.Split(',')[0] -eq $ModuleName)) { 1019 | return $Assembly 1020 | } 1021 | } 1022 | 1023 | $DynAssembly = New-Object Reflection.AssemblyName($ModuleName) 1024 | $Domain = $AppDomain 1025 | $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, 'Run') 1026 | $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule($ModuleName, $False) 1027 | 1028 | return $ModuleBuilder 1029 | } 1030 | 1031 | # A helper function used to reduce typing while defining function 1032 | # prototypes for Add-Win32Type. 1033 | function func 1034 | { 1035 | Param 1036 | ( 1037 | [Parameter(Position = 0, Mandatory = $True)] 1038 | [String] 1039 | $DllName, 1040 | 1041 | [Parameter(Position = 1, Mandatory = $True)] 1042 | [string] 1043 | $FunctionName, 1044 | 1045 | [Parameter(Position = 2, Mandatory = $True)] 1046 | [Type] 1047 | $ReturnType, 1048 | 1049 | [Parameter(Position = 3)] 1050 | [Type[]] 1051 | $ParameterTypes, 1052 | 1053 | [Parameter(Position = 4)] 1054 | [Runtime.InteropServices.CallingConvention] 1055 | $NativeCallingConvention, 1056 | 1057 | [Parameter(Position = 5)] 1058 | [Runtime.InteropServices.CharSet] 1059 | $Charset, 1060 | 1061 | [String] 1062 | $EntryPoint, 1063 | 1064 | [Switch] 1065 | $SetLastError 1066 | ) 1067 | 1068 | $Properties = @{ 1069 | DllName = $DllName 1070 | FunctionName = $FunctionName 1071 | ReturnType = $ReturnType 1072 | } 1073 | 1074 | if ($ParameterTypes) { $Properties['ParameterTypes'] = $ParameterTypes } 1075 | if ($NativeCallingConvention) { $Properties['NativeCallingConvention'] = $NativeCallingConvention } 1076 | if ($Charset) { $Properties['Charset'] = $Charset } 1077 | if ($SetLastError) { $Properties['SetLastError'] = $SetLastError } 1078 | if ($EntryPoint) { $Properties['EntryPoint'] = $EntryPoint } 1079 | 1080 | New-Object PSObject -Property $Properties 1081 | } 1082 | 1083 | function Add-Win32Type 1084 | { 1085 | <# 1086 | .SYNOPSIS 1087 | 1088 | Creates a .NET type for an unmanaged Win32 function. 1089 | 1090 | Author: Matthew Graeber (@mattifestation) 1091 | License: BSD 3-Clause 1092 | Required Dependencies: None 1093 | Optional Dependencies: func 1094 | 1095 | .DESCRIPTION 1096 | 1097 | Add-Win32Type enables you to easily interact with unmanaged (i.e. 1098 | Win32 unmanaged) functions in PowerShell. After providing 1099 | Add-Win32Type with a function signature, a .NET type is created 1100 | using reflection (i.e. csc.exe is never called like with Add-Type). 1101 | 1102 | The 'func' helper function can be used to reduce typing when defining 1103 | multiple function definitions. 1104 | 1105 | .PARAMETER DllName 1106 | 1107 | The name of the DLL. 1108 | 1109 | .PARAMETER FunctionName 1110 | 1111 | The name of the target function. 1112 | 1113 | .PARAMETER EntryPoint 1114 | 1115 | The DLL export function name. This argument should be specified if the 1116 | specified function name is different than the name of the exported 1117 | function. 1118 | 1119 | .PARAMETER ReturnType 1120 | 1121 | The return type of the function. 1122 | 1123 | .PARAMETER ParameterTypes 1124 | 1125 | The function parameters. 1126 | 1127 | .PARAMETER NativeCallingConvention 1128 | 1129 | Specifies the native calling convention of the function. Defaults to 1130 | stdcall. 1131 | 1132 | .PARAMETER Charset 1133 | 1134 | If you need to explicitly call an 'A' or 'W' Win32 function, you can 1135 | specify the character set. 1136 | 1137 | .PARAMETER SetLastError 1138 | 1139 | Indicates whether the callee calls the SetLastError Win32 API 1140 | function before returning from the attributed method. 1141 | 1142 | .PARAMETER Module 1143 | 1144 | The in-memory module that will host the functions. Use 1145 | New-InMemoryModule to define an in-memory module. 1146 | 1147 | .PARAMETER Namespace 1148 | 1149 | An optional namespace to prepend to the type. Add-Win32Type defaults 1150 | to a namespace consisting only of the name of the DLL. 1151 | 1152 | .EXAMPLE 1153 | 1154 | $Mod = New-InMemoryModule -ModuleName Win32 1155 | 1156 | $FunctionDefinitions = @( 1157 | (func kernel32 GetProcAddress ([IntPtr]) @([IntPtr], [String]) -Charset Ansi -SetLastError), 1158 | (func kernel32 GetModuleHandle ([Intptr]) @([String]) -SetLastError), 1159 | (func ntdll RtlGetCurrentPeb ([IntPtr]) @()) 1160 | ) 1161 | 1162 | $Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' 1163 | $Kernel32 = $Types['kernel32'] 1164 | $Ntdll = $Types['ntdll'] 1165 | $Ntdll::RtlGetCurrentPeb() 1166 | $ntdllbase = $Kernel32::GetModuleHandle('ntdll') 1167 | $Kernel32::GetProcAddress($ntdllbase, 'RtlGetCurrentPeb') 1168 | 1169 | .NOTES 1170 | 1171 | Inspired by Lee Holmes' Invoke-WindowsApi http://poshcode.org/2189 1172 | 1173 | When defining multiple function prototypes, it is ideal to provide 1174 | Add-Win32Type with an array of function signatures. That way, they 1175 | are all incorporated into the same in-memory module. 1176 | #> 1177 | 1178 | [OutputType([Hashtable])] 1179 | Param( 1180 | [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] 1181 | [String] 1182 | $DllName, 1183 | 1184 | [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] 1185 | [String] 1186 | $FunctionName, 1187 | 1188 | [Parameter(ValueFromPipelineByPropertyName = $True)] 1189 | [String] 1190 | $EntryPoint, 1191 | 1192 | [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] 1193 | [Type] 1194 | $ReturnType, 1195 | 1196 | [Parameter(ValueFromPipelineByPropertyName = $True)] 1197 | [Type[]] 1198 | $ParameterTypes, 1199 | 1200 | [Parameter(ValueFromPipelineByPropertyName = $True)] 1201 | [Runtime.InteropServices.CallingConvention] 1202 | $NativeCallingConvention = [Runtime.InteropServices.CallingConvention]::StdCall, 1203 | 1204 | [Parameter(ValueFromPipelineByPropertyName = $True)] 1205 | [Runtime.InteropServices.CharSet] 1206 | $Charset = [Runtime.InteropServices.CharSet]::Auto, 1207 | 1208 | [Parameter(ValueFromPipelineByPropertyName = $True)] 1209 | [Switch] 1210 | $SetLastError, 1211 | 1212 | [Parameter(Mandatory = $True)] 1213 | [ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})] 1214 | $Module, 1215 | 1216 | [ValidateNotNull()] 1217 | [String] 1218 | $Namespace = '' 1219 | ) 1220 | 1221 | BEGIN 1222 | { 1223 | $TypeHash = @{} 1224 | } 1225 | 1226 | PROCESS 1227 | { 1228 | if ($Module -is [Reflection.Assembly]) 1229 | { 1230 | if ($Namespace) 1231 | { 1232 | $TypeHash[$DllName] = $Module.GetType("$Namespace.$DllName") 1233 | } 1234 | else 1235 | { 1236 | $TypeHash[$DllName] = $Module.GetType($DllName) 1237 | } 1238 | } 1239 | else 1240 | { 1241 | # Define one type for each DLL 1242 | if (!$TypeHash.ContainsKey($DllName)) 1243 | { 1244 | if ($Namespace) 1245 | { 1246 | $TypeHash[$DllName] = $Module.DefineType("$Namespace.$DllName", 'Public,BeforeFieldInit') 1247 | } 1248 | else 1249 | { 1250 | $TypeHash[$DllName] = $Module.DefineType($DllName, 'Public,BeforeFieldInit') 1251 | } 1252 | } 1253 | 1254 | $Method = $TypeHash[$DllName].DefineMethod( 1255 | $FunctionName, 1256 | 'Public,Static,PinvokeImpl', 1257 | $ReturnType, 1258 | $ParameterTypes) 1259 | 1260 | # Make each ByRef parameter an Out parameter 1261 | $i = 1 1262 | foreach($Parameter in $ParameterTypes) 1263 | { 1264 | if ($Parameter.IsByRef) 1265 | { 1266 | [void] $Method.DefineParameter($i, 'Out', $null) 1267 | } 1268 | 1269 | $i++ 1270 | } 1271 | 1272 | $DllImport = [Runtime.InteropServices.DllImportAttribute] 1273 | $SetLastErrorField = $DllImport.GetField('SetLastError') 1274 | $CallingConventionField = $DllImport.GetField('CallingConvention') 1275 | $CharsetField = $DllImport.GetField('CharSet') 1276 | $EntryPointField = $DllImport.GetField('EntryPoint') 1277 | if ($SetLastError) { $SLEValue = $True } else { $SLEValue = $False } 1278 | 1279 | if ($PSBoundParameters['EntryPoint']) { $ExportedFuncName = $EntryPoint } else { $ExportedFuncName = $FunctionName } 1280 | 1281 | # Equivalent to C# version of [DllImport(DllName)] 1282 | $Constructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor([String]) 1283 | $DllImportAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($Constructor, 1284 | $DllName, [Reflection.PropertyInfo[]] @(), [Object[]] @(), 1285 | [Reflection.FieldInfo[]] @($SetLastErrorField, 1286 | $CallingConventionField, 1287 | $CharsetField, 1288 | $EntryPointField), 1289 | [Object[]] @($SLEValue, 1290 | ([Runtime.InteropServices.CallingConvention] $NativeCallingConvention), 1291 | ([Runtime.InteropServices.CharSet] $Charset), 1292 | $ExportedFuncName)) 1293 | 1294 | $Method.SetCustomAttribute($DllImportAttribute) 1295 | } 1296 | } 1297 | 1298 | END 1299 | { 1300 | if ($Module -is [Reflection.Assembly]) 1301 | { 1302 | return $TypeHash 1303 | } 1304 | 1305 | $ReturnTypes = @{} 1306 | 1307 | foreach ($Key in $TypeHash.Keys) 1308 | { 1309 | $Type = $TypeHash[$Key].CreateType() 1310 | 1311 | $ReturnTypes[$Key] = $Type 1312 | } 1313 | 1314 | return $ReturnTypes 1315 | } 1316 | } 1317 | 1318 | function psenum 1319 | { 1320 | <# 1321 | .SYNOPSIS 1322 | 1323 | Creates an in-memory enumeration for use in your PowerShell session. 1324 | 1325 | Author: Matthew Graeber (@mattifestation) 1326 | License: BSD 3-Clause 1327 | Required Dependencies: None 1328 | Optional Dependencies: None 1329 | 1330 | .DESCRIPTION 1331 | 1332 | The 'psenum' function facilitates the creation of enums entirely in 1333 | memory using as close to a "C style" as PowerShell will allow. 1334 | 1335 | .PARAMETER Module 1336 | 1337 | The in-memory module that will host the enum. Use 1338 | New-InMemoryModule to define an in-memory module. 1339 | 1340 | .PARAMETER FullName 1341 | 1342 | The fully-qualified name of the enum. 1343 | 1344 | .PARAMETER Type 1345 | 1346 | The type of each enum element. 1347 | 1348 | .PARAMETER EnumElements 1349 | 1350 | A hashtable of enum elements. 1351 | 1352 | .PARAMETER Bitfield 1353 | 1354 | Specifies that the enum should be treated as a bitfield. 1355 | 1356 | .EXAMPLE 1357 | 1358 | $Mod = New-InMemoryModule -ModuleName Win32 1359 | 1360 | $ImageSubsystem = psenum $Mod PE.IMAGE_SUBSYSTEM UInt16 @{ 1361 | UNKNOWN = 0 1362 | NATIVE = 1 # Image doesn't require a subsystem. 1363 | WINDOWS_GUI = 2 # Image runs in the Windows GUI subsystem. 1364 | WINDOWS_CUI = 3 # Image runs in the Windows character subsystem. 1365 | OS2_CUI = 5 # Image runs in the OS/2 character subsystem. 1366 | POSIX_CUI = 7 # Image runs in the Posix character subsystem. 1367 | NATIVE_WINDOWS = 8 # Image is a native Win9x driver. 1368 | WINDOWS_CE_GUI = 9 # Image runs in the Windows CE subsystem. 1369 | EFI_APPLICATION = 10 1370 | EFI_BOOT_SERVICE_DRIVER = 11 1371 | EFI_RUNTIME_DRIVER = 12 1372 | EFI_ROM = 13 1373 | XBOX = 14 1374 | WINDOWS_BOOT_APPLICATION = 16 1375 | } 1376 | 1377 | .NOTES 1378 | 1379 | PowerShell purists may disagree with the naming of this function but 1380 | again, this was developed in such a way so as to emulate a "C style" 1381 | definition as closely as possible. Sorry, I'm not going to name it 1382 | New-Enum. :P 1383 | #> 1384 | 1385 | [OutputType([Type])] 1386 | Param 1387 | ( 1388 | [Parameter(Position = 0, Mandatory = $True)] 1389 | [ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})] 1390 | $Module, 1391 | 1392 | [Parameter(Position = 1, Mandatory = $True)] 1393 | [ValidateNotNullOrEmpty()] 1394 | [String] 1395 | $FullName, 1396 | 1397 | [Parameter(Position = 2, Mandatory = $True)] 1398 | [Type] 1399 | $Type, 1400 | 1401 | [Parameter(Position = 3, Mandatory = $True)] 1402 | [ValidateNotNullOrEmpty()] 1403 | [Hashtable] 1404 | $EnumElements, 1405 | 1406 | [Switch] 1407 | $Bitfield 1408 | ) 1409 | 1410 | if ($Module -is [Reflection.Assembly]) 1411 | { 1412 | return ($Module.GetType($FullName)) 1413 | } 1414 | 1415 | $EnumType = $Type -as [Type] 1416 | 1417 | $EnumBuilder = $Module.DefineEnum($FullName, 'Public', $EnumType) 1418 | 1419 | if ($Bitfield) 1420 | { 1421 | $FlagsConstructor = [FlagsAttribute].GetConstructor(@()) 1422 | $FlagsCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($FlagsConstructor, @()) 1423 | $EnumBuilder.SetCustomAttribute($FlagsCustomAttribute) 1424 | } 1425 | 1426 | foreach ($Key in $EnumElements.Keys) 1427 | { 1428 | # Apply the specified enum type to each element 1429 | $null = $EnumBuilder.DefineLiteral($Key, $EnumElements[$Key] -as $EnumType) 1430 | } 1431 | 1432 | $EnumBuilder.CreateType() 1433 | } 1434 | 1435 | # A helper function used to reduce typing while defining struct 1436 | # fields. 1437 | function field 1438 | { 1439 | Param 1440 | ( 1441 | [Parameter(Position = 0, Mandatory = $True)] 1442 | [UInt16] 1443 | $Position, 1444 | 1445 | [Parameter(Position = 1, Mandatory = $True)] 1446 | [Type] 1447 | $Type, 1448 | 1449 | [Parameter(Position = 2)] 1450 | [UInt16] 1451 | $Offset, 1452 | 1453 | [Object[]] 1454 | $MarshalAs 1455 | ) 1456 | 1457 | @{ 1458 | Position = $Position 1459 | Type = $Type -as [Type] 1460 | Offset = $Offset 1461 | MarshalAs = $MarshalAs 1462 | } 1463 | } 1464 | 1465 | function struct 1466 | { 1467 | <# 1468 | .SYNOPSIS 1469 | 1470 | Creates an in-memory struct for use in your PowerShell session. 1471 | 1472 | Author: Matthew Graeber (@mattifestation) 1473 | License: BSD 3-Clause 1474 | Required Dependencies: None 1475 | Optional Dependencies: field 1476 | 1477 | .DESCRIPTION 1478 | 1479 | The 'struct' function facilitates the creation of structs entirely in 1480 | memory using as close to a "C style" as PowerShell will allow. Struct 1481 | fields are specified using a hashtable where each field of the struct 1482 | is comprosed of the order in which it should be defined, its .NET 1483 | type, and optionally, its offset and special marshaling attributes. 1484 | 1485 | One of the features of 'struct' is that after your struct is defined, 1486 | it will come with a built-in GetSize method as well as an explicit 1487 | converter so that you can easily cast an IntPtr to the struct without 1488 | relying upon calling SizeOf and/or PtrToStructure in the Marshal 1489 | class. 1490 | 1491 | .PARAMETER Module 1492 | 1493 | The in-memory module that will host the struct. Use 1494 | New-InMemoryModule to define an in-memory module. 1495 | 1496 | .PARAMETER FullName 1497 | 1498 | The fully-qualified name of the struct. 1499 | 1500 | .PARAMETER StructFields 1501 | 1502 | A hashtable of fields. Use the 'field' helper function to ease 1503 | defining each field. 1504 | 1505 | .PARAMETER PackingSize 1506 | 1507 | Specifies the memory alignment of fields. 1508 | 1509 | .PARAMETER ExplicitLayout 1510 | 1511 | Indicates that an explicit offset for each field will be specified. 1512 | 1513 | .EXAMPLE 1514 | 1515 | $Mod = New-InMemoryModule -ModuleName Win32 1516 | 1517 | $ImageDosSignature = psenum $Mod PE.IMAGE_DOS_SIGNATURE UInt16 @{ 1518 | DOS_SIGNATURE = 0x5A4D 1519 | OS2_SIGNATURE = 0x454E 1520 | OS2_SIGNATURE_LE = 0x454C 1521 | VXD_SIGNATURE = 0x454C 1522 | } 1523 | 1524 | $ImageDosHeader = struct $Mod PE.IMAGE_DOS_HEADER @{ 1525 | e_magic = field 0 $ImageDosSignature 1526 | e_cblp = field 1 UInt16 1527 | e_cp = field 2 UInt16 1528 | e_crlc = field 3 UInt16 1529 | e_cparhdr = field 4 UInt16 1530 | e_minalloc = field 5 UInt16 1531 | e_maxalloc = field 6 UInt16 1532 | e_ss = field 7 UInt16 1533 | e_sp = field 8 UInt16 1534 | e_csum = field 9 UInt16 1535 | e_ip = field 10 UInt16 1536 | e_cs = field 11 UInt16 1537 | e_lfarlc = field 12 UInt16 1538 | e_ovno = field 13 UInt16 1539 | e_res = field 14 UInt16[] -MarshalAs @('ByValArray', 4) 1540 | e_oemid = field 15 UInt16 1541 | e_oeminfo = field 16 UInt16 1542 | e_res2 = field 17 UInt16[] -MarshalAs @('ByValArray', 10) 1543 | e_lfanew = field 18 Int32 1544 | } 1545 | 1546 | # Example of using an explicit layout in order to create a union. 1547 | $TestUnion = struct $Mod TestUnion @{ 1548 | field1 = field 0 UInt32 0 1549 | field2 = field 1 IntPtr 0 1550 | } -ExplicitLayout 1551 | 1552 | .NOTES 1553 | 1554 | PowerShell purists may disagree with the naming of this function but 1555 | again, this was developed in such a way so as to emulate a "C style" 1556 | definition as closely as possible. Sorry, I'm not going to name it 1557 | New-Struct. :P 1558 | #> 1559 | 1560 | [OutputType([Type])] 1561 | Param 1562 | ( 1563 | [Parameter(Position = 1, Mandatory = $True)] 1564 | [ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})] 1565 | $Module, 1566 | 1567 | [Parameter(Position = 2, Mandatory = $True)] 1568 | [ValidateNotNullOrEmpty()] 1569 | [String] 1570 | $FullName, 1571 | 1572 | [Parameter(Position = 3, Mandatory = $True)] 1573 | [ValidateNotNullOrEmpty()] 1574 | [Hashtable] 1575 | $StructFields, 1576 | 1577 | [Reflection.Emit.PackingSize] 1578 | $PackingSize = [Reflection.Emit.PackingSize]::Unspecified, 1579 | 1580 | [Switch] 1581 | $ExplicitLayout 1582 | ) 1583 | 1584 | if ($Module -is [Reflection.Assembly]) 1585 | { 1586 | return ($Module.GetType($FullName)) 1587 | } 1588 | 1589 | [Reflection.TypeAttributes] $StructAttributes = 'AnsiClass, 1590 | Class, 1591 | Public, 1592 | Sealed, 1593 | BeforeFieldInit' 1594 | 1595 | if ($ExplicitLayout) 1596 | { 1597 | $StructAttributes = $StructAttributes -bor [Reflection.TypeAttributes]::ExplicitLayout 1598 | } 1599 | else 1600 | { 1601 | $StructAttributes = $StructAttributes -bor [Reflection.TypeAttributes]::SequentialLayout 1602 | } 1603 | 1604 | $StructBuilder = $Module.DefineType($FullName, $StructAttributes, [ValueType], $PackingSize) 1605 | $ConstructorInfo = [Runtime.InteropServices.MarshalAsAttribute].GetConstructors()[0] 1606 | $SizeConst = @([Runtime.InteropServices.MarshalAsAttribute].GetField('SizeConst')) 1607 | 1608 | $Fields = New-Object Hashtable[]($StructFields.Count) 1609 | 1610 | # Sort each field according to the orders specified 1611 | # Unfortunately, PSv2 doesn't have the luxury of the 1612 | # hashtable [Ordered] accelerator. 1613 | foreach ($Field in $StructFields.Keys) 1614 | { 1615 | $Index = $StructFields[$Field]['Position'] 1616 | $Fields[$Index] = @{FieldName = $Field; Properties = $StructFields[$Field]} 1617 | } 1618 | 1619 | foreach ($Field in $Fields) 1620 | { 1621 | $FieldName = $Field['FieldName'] 1622 | $FieldProp = $Field['Properties'] 1623 | 1624 | $Offset = $FieldProp['Offset'] 1625 | $Type = $FieldProp['Type'] 1626 | $MarshalAs = $FieldProp['MarshalAs'] 1627 | 1628 | $NewField = $StructBuilder.DefineField($FieldName, $Type, 'Public') 1629 | 1630 | if ($MarshalAs) 1631 | { 1632 | $UnmanagedType = $MarshalAs[0] -as ([Runtime.InteropServices.UnmanagedType]) 1633 | if ($MarshalAs[1]) 1634 | { 1635 | $Size = $MarshalAs[1] 1636 | $AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, 1637 | $UnmanagedType, $SizeConst, @($Size)) 1638 | } 1639 | else 1640 | { 1641 | $AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, [Object[]] @($UnmanagedType)) 1642 | } 1643 | 1644 | $NewField.SetCustomAttribute($AttribBuilder) 1645 | } 1646 | 1647 | if ($ExplicitLayout) { $NewField.SetOffset($Offset) } 1648 | } 1649 | 1650 | # Make the struct aware of its own size. 1651 | # No more having to call [Runtime.InteropServices.Marshal]::SizeOf! 1652 | $SizeMethod = $StructBuilder.DefineMethod('GetSize', 1653 | 'Public, Static', 1654 | [Int], 1655 | [Type[]] @()) 1656 | $ILGenerator = $SizeMethod.GetILGenerator() 1657 | # Thanks for the help, Jason Shirk! 1658 | $ILGenerator.Emit([Reflection.Emit.OpCodes]::Ldtoken, $StructBuilder) 1659 | $ILGenerator.Emit([Reflection.Emit.OpCodes]::Call, 1660 | [Type].GetMethod('GetTypeFromHandle')) 1661 | $ILGenerator.Emit([Reflection.Emit.OpCodes]::Call, 1662 | [Runtime.InteropServices.Marshal].GetMethod('SizeOf', [Type[]] @([Type]))) 1663 | $ILGenerator.Emit([Reflection.Emit.OpCodes]::Ret) 1664 | 1665 | # Allow for explicit casting from an IntPtr 1666 | # No more having to call [Runtime.InteropServices.Marshal]::PtrToStructure! 1667 | $ImplicitConverter = $StructBuilder.DefineMethod('op_Implicit', 1668 | 'PrivateScope, Public, Static, HideBySig, SpecialName', 1669 | $StructBuilder, 1670 | [Type[]] @([IntPtr])) 1671 | $ILGenerator2 = $ImplicitConverter.GetILGenerator() 1672 | $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Nop) 1673 | $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ldarg_0) 1674 | $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ldtoken, $StructBuilder) 1675 | $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Call, 1676 | [Type].GetMethod('GetTypeFromHandle')) 1677 | $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Call, 1678 | [Runtime.InteropServices.Marshal].GetMethod('PtrToStructure', [Type[]] @([IntPtr], [Type]))) 1679 | $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Unbox_Any, $StructBuilder) 1680 | $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ret) 1681 | 1682 | $StructBuilder.CreateType() 1683 | } 1684 | 1685 | #endregion PSReflect 1686 | 1687 | #region PSReflect Definitions (Thread) 1688 | 1689 | $Module = New-InMemoryModule -ModuleName GetInjectedThread 1690 | 1691 | #region Constants 1692 | $UNTRUSTED_MANDATORY_LEVEL = "S-1-16-0" 1693 | $LOW_MANDATORY_LEVEL = "S-1-16-4096" 1694 | $MEDIUM_MANDATORY_LEVEL = "S-1-16-8192" 1695 | $MEDIUM_PLUS_MANDATORY_LEVEL = "S-1-16-8448" 1696 | $HIGH_MANDATORY_LEVEL = "S-1-16-12288" 1697 | $SYSTEM_MANDATORY_LEVEL = "S-1-16-16384" 1698 | $PROTECTED_PROCESS_MANDATORY_LEVEL = "S-1-16-20480" 1699 | $SECURE_PROCESS_MANDATORY_LEVEL = "S-1-16-28672" 1700 | #endregion Constants 1701 | 1702 | #region Enums 1703 | $LuidAttributes = psenum $Module LuidAttributes UInt32 @{ 1704 | DISABLED = '0x00000000' 1705 | SE_PRIVILEGE_ENABLED_BY_DEFAULT = '0x00000001' 1706 | SE_PRIVILEGE_ENABLED = '0x00000002' 1707 | SE_PRIVILEGE_REMOVED = '0x00000004' 1708 | SE_PRIVILEGE_USED_FOR_ACCESS = '0x80000000' 1709 | } -Bitfield 1710 | 1711 | $MemProtection = psenum $Module MemProtection UInt32 @{ 1712 | PAGE_EXECUTE = 0x10 1713 | PAGE_EXECUTE_READ = 0x20 1714 | PAGE_EXECUTE_READWRITE = 0x40 1715 | PAGE_EXECUTE_WRITECOPY = 0x80 1716 | PAGE_NOACCESS = 0x01 1717 | PAGE_READONLY = 0x02 1718 | PAGE_READWRITE = 0x04 1719 | PAGE_WRITECOPY = 0x08 1720 | PAGE_TARGETS_INVALID = 0x40000000 1721 | PAGE_TARGETS_NO_UPDATE = 0x40000000 1722 | PAGE_GUARD = 0x100 1723 | PAGE_NOCACHE = 0x200 1724 | PAGE_WRITECOMBINE = 0x400 1725 | } -Bitfield 1726 | 1727 | $MemState = psenum $Module MemState UInt32 @{ 1728 | MEM_COMMIT = 0x1000 1729 | MEM_RESERVE = 0x2000 1730 | MEM_FREE = 0x10000 1731 | } 1732 | 1733 | $MemType = psenum $Module MemType UInt32 @{ 1734 | MEM_PRIVATE = 0x20000 1735 | MEM_MAPPED = 0x40000 1736 | MEM_IMAGE = 0x1000000 1737 | } 1738 | 1739 | $PROCESS_ACCESS = psenum $Module PROCESS_ACCESS UInt32 @{ 1740 | PROCESS_TERMINATE = 0x00000001 1741 | PROCESS_CREATE_THREAD = 0x00000002 1742 | PROCESS_VM_OPERATION = 0x00000008 1743 | PROCESS_VM_READ = 0x00000010 1744 | PROCESS_VM_WRITE = 0x00000020 1745 | PROCESS_DUP_HANDLE = 0x00000040 1746 | PROCESS_CREATE_PROCESS = 0x00000080 1747 | PROCESS_SET_QUOTA = 0x00000100 1748 | PROCESS_SET_INFORMATION = 0x00000200 1749 | PROCESS_QUERY_INFORMATION = 0x00000400 1750 | PROCESS_SUSPEND_RESUME = 0x00000800 1751 | PROCESS_QUERY_LIMITED_INFORMATION = 0x00001000 1752 | DELETE = 0x00010000 1753 | READ_CONTROL = 0x00020000 1754 | WRITE_DAC = 0x00040000 1755 | WRITE_OWNER = 0x00080000 1756 | SYNCHRONIZE = 0x00100000 1757 | PROCESS_ALL_ACCESS = 0x001f1ffb 1758 | } -Bitfield 1759 | 1760 | $SecurityEntity = psenum $Module SecurityEntity UInt32 @{ 1761 | SeCreateTokenPrivilege = 1 1762 | SeAssignPrimaryTokenPrivilege = 2 1763 | SeLockMemoryPrivilege = 3 1764 | SeIncreaseQuotaPrivilege = 4 1765 | SeUnsolicitedInputPrivilege = 5 1766 | SeMachineAccountPrivilege = 6 1767 | SeTcbPrivilege = 7 1768 | SeSecurityPrivilege = 8 1769 | SeTakeOwnershipPrivilege = 9 1770 | SeLoadDriverPrivilege = 10 1771 | SeSystemProfilePrivilege = 11 1772 | SeSystemtimePrivilege = 12 1773 | SeProfileSingleProcessPrivilege = 13 1774 | SeIncreaseBasePriorityPrivilege = 14 1775 | SeCreatePagefilePrivilege = 15 1776 | SeCreatePermanentPrivilege = 16 1777 | SeBackupPrivilege = 17 1778 | SeRestorePrivilege = 18 1779 | SeShutdownPrivilege = 19 1780 | SeDebugPrivilege = 20 1781 | SeAuditPrivilege = 21 1782 | SeSystemEnvironmentPrivilege = 22 1783 | SeChangeNotifyPrivilege = 23 1784 | SeRemoteShutdownPrivilege = 24 1785 | SeUndockPrivilege = 25 1786 | SeSyncAgentPrivilege = 26 1787 | SeEnableDelegationPrivilege = 27 1788 | SeManageVolumePrivilege = 28 1789 | SeImpersonatePrivilege = 29 1790 | SeCreateGlobalPrivilege = 30 1791 | SeTrustedCredManAccessPrivilege = 31 1792 | SeRelabelPrivilege = 32 1793 | SeIncreaseWorkingSetPrivilege = 33 1794 | SeTimeZonePrivilege = 34 1795 | SeCreateSymbolicLinkPrivilege = 35 1796 | } 1797 | 1798 | $THREAD_ACCESS = psenum $Module THREAD_ACCESS UInt32 @{ 1799 | THREAD_TERMINATE = 0x00000001 1800 | THREAD_SUSPEND_RESUME = 0x00000002 1801 | THREAD_GET_CONTEXT = 0x00000008 1802 | THREAD_SET_CONTEXT = 0x00000010 1803 | THREAD_SET_INFORMATION = 0x00000020 1804 | THREAD_QUERY_INFORMATION = 0x00000040 1805 | THREAD_SET_THREAD_TOKEN = 0x00000080 1806 | THREAD_IMPERSONATE = 0x00000100 1807 | THREAD_DIRECT_IMPERSONATION = 0x00000200 1808 | THREAD_SET_LIMITED_INFORMATION = 0x00000400 1809 | THREAD_QUERY_LIMITED_INFORMATION = 0x00000800 1810 | DELETE = 0x00010000 1811 | READ_CONTROL = 0x00020000 1812 | WRITE_DAC = 0x00040000 1813 | WRITE_OWNER = 0x00080000 1814 | SYNCHRONIZE = 0x00100000 1815 | THREAD_ALL_ACCESS = 0x001f0ffb 1816 | } -Bitfield 1817 | 1818 | $TOKEN_ACCESS = psenum $Module TOKEN_ACCESS UInt32 @{ 1819 | TOKEN_DUPLICATE = 0x00000002 1820 | TOKEN_IMPERSONATE = 0x00000004 1821 | TOKEN_QUERY = 0x00000008 1822 | TOKEN_QUERY_SOURCE = 0x00000010 1823 | TOKEN_ADJUST_PRIVILEGES = 0x00000020 1824 | TOKEN_ADJUST_GROUPS = 0x00000040 1825 | TOKEN_ADJUST_DEFAULT = 0x00000080 1826 | TOKEN_ADJUST_SESSIONID = 0x00000100 1827 | DELETE = 0x00010000 1828 | READ_CONTROL = 0x00020000 1829 | WRITE_DAC = 0x00040000 1830 | WRITE_OWNER = 0x00080000 1831 | SYNCHRONIZE = 0x00100000 1832 | STANDARD_RIGHTS_REQUIRED = 0x000F0000 1833 | TOKEN_ALL_ACCESS = 0x001f01ff 1834 | } -Bitfield 1835 | 1836 | $TokenInformationClass = psenum $Module TOKEN_INFORMATION_CLASS UInt16 @{ 1837 | TokenUser = 1 1838 | TokenGroups = 2 1839 | TokenPrivileges = 3 1840 | TokenOwner = 4 1841 | TokenPrimaryGroup = 5 1842 | TokenDefaultDacl = 6 1843 | TokenSource = 7 1844 | TokenType = 8 1845 | TokenImpersonationLevel = 9 1846 | TokenStatistics = 10 1847 | TokenRestrictedSids = 11 1848 | TokenSessionId = 12 1849 | TokenGroupsAndPrivileges = 13 1850 | TokenSessionReference = 14 1851 | TokenSandBoxInert = 15 1852 | TokenAuditPolicy = 16 1853 | TokenOrigin = 17 1854 | TokenElevationType = 18 1855 | TokenLinkedToken = 19 1856 | TokenElevation = 20 1857 | TokenHasRestrictions = 21 1858 | TokenAccessInformation = 22 1859 | TokenVirtualizationAllowed = 23 1860 | TokenVirtualizationEnabled = 24 1861 | TokenIntegrityLevel = 25 1862 | TokenUIAccess = 26 1863 | TokenMandatoryPolicy = 27 1864 | TokenLogonSid = 28 1865 | TokenIsAppContainer = 29 1866 | TokenCapabilities = 30 1867 | TokenAppContainerSid = 31 1868 | TokenAppContainerNumber = 32 1869 | TokenUserClaimAttributes = 33 1870 | TokenDeviceClaimAttributes = 34 1871 | TokenRestrictedUserClaimAttributes = 35 1872 | TokenRestrictedDeviceClaimAttributes = 36 1873 | TokenDeviceGroups = 37 1874 | TokenRestrictedDeviceGroups = 38 1875 | TokenSecurityAttributes = 39 1876 | TokenIsRestricted = 40 1877 | MaxTokenInfoClass = 41 1878 | } 1879 | 1880 | $WORKING_SET_EX_BLOCK = psenum $Module WORKING_SET_EX_BLOCK UInt32 @{ 1881 | Valid = 0x00000001 1882 | Shared = 0x00008000 1883 | } -Bitfield 1884 | #endregion Enums 1885 | 1886 | #region Structs 1887 | $LUID = struct $Module Luid @{ 1888 | LowPart = field 0 $SecurityEntity 1889 | HighPart = field 1 Int32 1890 | } 1891 | 1892 | $LUID_AND_ATTRIBUTES = struct $Module LuidAndAttributes @{ 1893 | Luid = field 0 $LUID 1894 | Attributes = field 1 UInt32 1895 | } 1896 | 1897 | $MEMORYBASICINFORMATION = struct $Module MEMORY_BASIC_INFORMATION @{ 1898 | BaseAddress = field 0 UIntPtr 1899 | AllocationBase = field 1 UIntPtr 1900 | AllocationProtect = field 2 UInt32 1901 | RegionSize = field 3 UIntPtr 1902 | State = field 4 UInt32 1903 | Protect = field 5 UInt32 1904 | Type = field 6 UInt32 1905 | } 1906 | 1907 | $SID_AND_ATTRIBUTES = struct $Module SidAndAttributes @{ 1908 | Sid = field 0 IntPtr 1909 | Attributes = field 1 UInt32 1910 | } 1911 | 1912 | $TOKEN_MANDATORY_LABEL = struct $Module TokenMandatoryLabel @{ 1913 | Label = field 0 $SID_AND_ATTRIBUTES; 1914 | } 1915 | 1916 | $TOKEN_PRIVILEGES = struct $Module TokenPrivileges @{ 1917 | PrivilegeCount = field 0 UInt32 1918 | Privileges = field 1 $LUID_AND_ATTRIBUTES.MakeArrayType() -MarshalAs @('ByValArray', 50) 1919 | } 1920 | 1921 | $TOKEN_USER = struct $Module TOKEN_USER @{ 1922 | User = field 0 $SID_AND_ATTRIBUTES 1923 | } 1924 | 1925 | $WORKING_SET_EX_INFORMATION = struct $Module WorkingSetExInformation @{ 1926 | VirtualAddress = field 0 IntPtr 1927 | VirtualAttributes = field 1 IntPtr 1928 | } 1929 | 1930 | $THREAD_BASIC_INFORMATION = struct $Module THREAD_BASIC_INFORMATION @{ 1931 | ExitStatus = field 0 Int32 1932 | TebBaseAddress = field 1 IntPtr 1933 | UniqueProcess = field 2 IntPtr 1934 | UniqueThread = field 3 IntPtr 1935 | AffinityMask = field 4 IntPtr 1936 | Priority = field 5 Int32 1937 | BasePriority = field 6 Int32 1938 | } 1939 | 1940 | $TIB64 = struct $Module NT_TIB64 @{ 1941 | ExceptionList = field 0 IntPtr 1942 | StackBase = field 1 IntPtr 1943 | StackLimit = field 2 IntPtr 1944 | SubSystemTib = field 3 IntPtr 1945 | FiberData = field 4 IntPtr 1946 | ArbitraryUserPointer = field 5 IntPtr 1947 | Self = field 6 IntPtr 1948 | } 1949 | #endregion Structs 1950 | 1951 | #region Function Definitions 1952 | $FunctionDefinitions = @( 1953 | (func kernel32 CloseHandle ([bool]) @( 1954 | [IntPtr] #_In_ HANDLE hObject 1955 | ) -SetLastError), 1956 | 1957 | (func advapi32 ConvertSidToStringSid ([bool]) @( 1958 | [IntPtr] #_In_ PSID Sid, 1959 | [IntPtr].MakeByRefType() #_Out_ LPTSTR *StringSid 1960 | ) -SetLastError), 1961 | 1962 | (func advapi32 GetTokenInformation ([bool]) @( 1963 | [IntPtr], #_In_ HANDLE TokenHandle 1964 | [Int32], #_In_ TOKEN_INFORMATION_CLASS TokenInformationClass 1965 | [IntPtr], #_Out_opt_ LPVOID TokenInformation 1966 | [UInt32], #_In_ DWORD TokenInformationLength 1967 | [UInt32].MakeByRefType() #_Out_ PDWORD ReturnLength 1968 | ) -SetLastError), 1969 | 1970 | (func ntdll NtQueryInformationThread ([UInt32]) @( 1971 | [IntPtr], #_In_ HANDLE ThreadHandle, 1972 | [Int32], #_In_ THREADINFOCLASS ThreadInformationClass, 1973 | [IntPtr], #_Inout_ PVOID ThreadInformation, 1974 | [Int32], #_In_ ULONG ThreadInformationLength, 1975 | [IntPtr] #_Out_opt_ PULONG ReturnLength 1976 | )), 1977 | 1978 | (func ntdll NtQueryInformationThread ([UInt32]) @( 1979 | [IntPtr], #_In_ HANDLE ThreadHandle, 1980 | [Int32], #_In_ THREADINFOCLASS ThreadInformationClass, 1981 | $THREAD_BASIC_INFORMATION.MakeByRefType(), #_Inout_ PVOID ThreadInformation, 1982 | [Int32], #_In_ ULONG ThreadInformationLength, 1983 | [IntPtr] #_Out_opt_ PULONG ReturnLength 1984 | )), 1985 | 1986 | (func kernel32 OpenProcess ([IntPtr]) @( 1987 | [UInt32], #_In_ DWORD dwDesiredAccess, 1988 | [bool], #_In_ BOOL bInheritHandle, 1989 | [UInt32] #_In_ DWORD dwProcessId 1990 | ) -SetLastError), 1991 | 1992 | (func advapi32 OpenProcessToken ([bool]) @( 1993 | [IntPtr], #_In_ HANDLE ProcessHandle 1994 | [UInt32], #_In_ DWORD DesiredAccess 1995 | [IntPtr].MakeByRefType() #_Out_ PHANDLE TokenHandle 1996 | ) -SetLastError), 1997 | 1998 | (func kernel32 OpenThread ([IntPtr]) @( 1999 | [UInt32], #_In_ DWORD dwDesiredAccess, 2000 | [bool], #_In_ BOOL bInheritHandle, 2001 | [UInt32] #_In_ DWORD dwThreadId 2002 | ) -SetLastError), 2003 | 2004 | (func advapi32 OpenThreadToken ([bool]) @( 2005 | [IntPtr], #_In_ HANDLE ThreadHandle 2006 | [UInt32], #_In_ DWORD DesiredAccess 2007 | [bool], #_In_ BOOL OpenAsSelf 2008 | [IntPtr].MakeByRefType() #_Out_ PHANDLE TokenHandle 2009 | ) -SetLastError), 2010 | 2011 | (func kernel32 QueryFullProcessImageName ([bool]) @( 2012 | [IntPtr] #_In_ HANDLE hProcess 2013 | [UInt32] #_In_ DWORD dwFlags, 2014 | [System.Text.StringBuilder] #_Out_ LPTSTR lpExeName, 2015 | [UInt32].MakeByRefType() #_Inout_ PDWORD lpdwSize 2016 | ) -SetLastError), 2017 | 2018 | (func kernel32 ReadProcessMemory ([Bool]) @( 2019 | [IntPtr], # _In_ HANDLE hProcess 2020 | [IntPtr], # _In_ LPCVOID lpBaseAddress 2021 | [Byte[]], # _Out_ LPVOID lpBuffer 2022 | [Int], # _In_ SIZE_T nSize 2023 | [Int].MakeByRefType() # _Out_ SIZE_T *lpNumberOfBytesRead 2024 | ) -SetLastError), 2025 | 2026 | (func kernel32 VirtualQueryEx ([Int32]) @( 2027 | [IntPtr], #_In_ HANDLE hProcess, 2028 | [IntPtr], #_In_opt_ LPCVOID lpAddress, 2029 | $MEMORYBASICINFORMATION.MakeByRefType(), #_Out_ PMEMORY_BASIC_INFORMATION lpBuffer, 2030 | [UInt32] #_In_ SIZE_T dwLength 2031 | ) -SetLastError), 2032 | 2033 | (func kernel32 IsWow64Process ([Bool]) @( 2034 | [IntPtr], #_In_ HANDLE hProcess, 2035 | [Bool].MakeByRefType() #_Out_ PBOOL Wow64Process 2036 | ) -SetLastError), 2037 | 2038 | (func kernel32 K32GetMappedFileName ([Int32]) @( 2039 | [IntPtr] #_In_ HANDLE hProcess, 2040 | [IntPtr] #_In_ LPVOID lpv, 2041 | [System.Text.StringBuilder] #_Out_ LPTSTR lpFilename, 2042 | [Int32] #_In_ DWORD nSize 2043 | ) -SetLastError), 2044 | 2045 | (func kernel32 QueryDosDevice ([Int32]) @( 2046 | [String] #_In_ LPCWSTR lpDeviceName, 2047 | [System.Text.StringBuilder] #_Out_ LPWSTR lpTargetPath, 2048 | [Int32] #_In_ DWORD ucchMax 2049 | ) -SetLastError), 2050 | 2051 | (func kernel32 K32QueryWorkingSetEx ([Bool]) @( 2052 | [IntPtr] #_In_ HANDLE hProcess, 2053 | $WORKING_SET_EX_INFORMATION.MakeByRefType(), #_In_ PVOID pv, 2054 | [Int32] #_In_ DWORD cb 2055 | ) -SetLastError), 2056 | 2057 | (func kernel32 GetModuleHandle ([IntPtr]) @( 2058 | [String] #_In_ LPCSTR lpModuleName 2059 | ) -SetLastError), 2060 | 2061 | (func kernel32 GetProcAddress ([IntPtr]) @( 2062 | [IntPtr] #_In_ HANDLE hModule, 2063 | [String] #_In_ LPCSTR lpProcName 2064 | ) -Charset Ansi -SetLastError) 2065 | ) 2066 | 2067 | $Types = $FunctionDefinitions | Add-Win32Type -Module $Module -Namespace 'Win32SysInfo' 2068 | $Kernel32 = $Types['kernel32'] 2069 | $Ntdll = $Types['ntdll'] 2070 | $Advapi32 = $Types['advapi32'] 2071 | #endregion Function Definitions 2072 | 2073 | #endregion PSReflect Definitions (Thread) 2074 | 2075 | #region Win32 API Abstractions 2076 | 2077 | function CloseHandle 2078 | { 2079 | <# 2080 | .SYNOPSIS 2081 | 2082 | Closes an open object handle. 2083 | 2084 | .DESCRIPTION 2085 | 2086 | The CloseHandle function closes handles to the following objects: 2087 | - Access token 2088 | - Communications device 2089 | - Console input 2090 | - Console screen buffer 2091 | - Event 2092 | - File 2093 | - File mapping 2094 | - I/O completion port 2095 | - Job 2096 | - Mailslot 2097 | - Memory resource notification 2098 | - Mutex 2099 | - Named pipe 2100 | - Pipe 2101 | - Process 2102 | - Semaphore 2103 | - Thread 2104 | - Transaction 2105 | - Waitable timer 2106 | 2107 | The documentation for the functions that create these objects indicates that CloseHandle should be used when you are finished with the object, and what happens to pending operations on the object after the handle is closed. In general, CloseHandle invalidates the specified object handle, decrements the object's handle count, and performs object retention checks. After the last handle to an object is closed, the object is removed from the system. 2108 | 2109 | .PARAMETER Handle 2110 | 2111 | A valid handle to an open object. 2112 | 2113 | .NOTES 2114 | 2115 | Author - Jared Atkinson (@jaredcatkinson) 2116 | 2117 | .LINK 2118 | 2119 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx 2120 | 2121 | .EXAMPLE 2122 | #> 2123 | 2124 | param 2125 | ( 2126 | [Parameter(Mandatory = $true)] 2127 | [IntPtr] 2128 | $Handle 2129 | ) 2130 | 2131 | <# 2132 | (func kernel32 CloseHandle ([bool]) @( 2133 | [IntPtr] #_In_ HANDLE hObject 2134 | ) -SetLastError) 2135 | #> 2136 | 2137 | $Success = $Kernel32::CloseHandle($Handle); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2138 | 2139 | if(-not $Success) 2140 | { 2141 | Write-Debug "Close Handle Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2142 | } 2143 | } 2144 | 2145 | function ConvertSidToStringSid 2146 | { 2147 | <# 2148 | .SYNOPSIS 2149 | 2150 | The ConvertSidToStringSid function converts a security identifier (SID) to a string format suitable for display, storage, or transmission. 2151 | 2152 | .DESCRIPTION 2153 | 2154 | The ConvertSidToStringSid function uses the standard S-R-I-S-S… format for SID strings. 2155 | 2156 | .PARAMETER SidPointer 2157 | 2158 | A pointer to the SID structure to be converted. 2159 | 2160 | .NOTES 2161 | 2162 | Author - Jared Atkinson (@jaredcatkinson) 2163 | 2164 | .LINK 2165 | 2166 | https://msdn.microsoft.com/en-us/library/windows/desktop/aa376399(v=vs.85).aspx 2167 | 2168 | .EXAMPLE 2169 | #> 2170 | 2171 | param 2172 | ( 2173 | [Parameter(Mandatory = $true)] 2174 | [IntPtr] 2175 | $SidPointer 2176 | ) 2177 | 2178 | <# 2179 | (func advapi32 ConvertSidToStringSid ([bool]) @( 2180 | [IntPtr] #_In_ PSID Sid, 2181 | [IntPtr].MakeByRefType() #_Out_ LPTSTR *StringSid 2182 | ) -SetLastError) 2183 | #> 2184 | 2185 | $StringPtr = [IntPtr]::Zero 2186 | $Success = $Advapi32::ConvertSidToStringSid($SidPointer, [ref]$StringPtr); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2187 | 2188 | if(-not $Success) 2189 | { 2190 | Write-Debug "ConvertSidToStringSid Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2191 | } 2192 | 2193 | Write-Output ([System.Runtime.InteropServices.Marshal]::PtrToStringAuto($StringPtr)) 2194 | } 2195 | 2196 | function GetTokenInformation 2197 | { 2198 | <# 2199 | .SYNOPSIS 2200 | 2201 | .DESCRIPTION 2202 | 2203 | .PARAMETER TokenHandle 2204 | 2205 | .PARAMETER TokenInformationClass 2206 | 2207 | .NOTES 2208 | 2209 | Author - Jared Atkinson (@jaredcatkinson) 2210 | 2211 | .LINK 2212 | 2213 | .EXAMPLE 2214 | #> 2215 | 2216 | param 2217 | ( 2218 | [Parameter(Mandatory = $true)] 2219 | [IntPtr] 2220 | $TokenHandle, 2221 | 2222 | [Parameter(Mandatory = $true)] 2223 | $TokenInformationClass 2224 | ) 2225 | 2226 | <# 2227 | (func advapi32 GetTokenInformation ([bool]) @( 2228 | [IntPtr], #_In_ HANDLE TokenHandle 2229 | [Int32], #_In_ TOKEN_INFORMATION_CLASS TokenInformationClass 2230 | [IntPtr], #_Out_opt_ LPVOID TokenInformation 2231 | [UInt32], #_In_ DWORD TokenInformationLength 2232 | [UInt32].MakeByRefType() #_Out_ PDWORD ReturnLength 2233 | ) -SetLastError) 2234 | #> 2235 | 2236 | # initial query to determine the necessary buffer size 2237 | $TokenPtrSize = 0 2238 | $Success = $Advapi32::GetTokenInformation($TokenHandle, $TokenInformationClass, 0, $TokenPtrSize, [ref]$TokenPtrSize) 2239 | $TokenPtr = [IntPtr]::Zero 2240 | 2241 | if($TokenPtrSize -ne 0) 2242 | { 2243 | [IntPtr]$TokenPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($TokenPtrSize) 2244 | # retrieve the proper buffer value 2245 | $Success = $Advapi32::GetTokenInformation($TokenHandle, $TokenInformationClass, $TokenPtr, $TokenPtrSize, [ref]$TokenPtrSize); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2246 | } 2247 | 2248 | if($Success) 2249 | { 2250 | switch($TokenInformationClass) 2251 | { 2252 | 1 # TokenUser 2253 | { 2254 | $TokenUser = $TokenPtr -as $TOKEN_USER 2255 | ConvertSidToStringSid -SidPointer $TokenUser.User.Sid 2256 | } 2257 | 3 # TokenPrivilege 2258 | { 2259 | # query the process token with the TOKEN_INFORMATION_CLASS = 3 enum to retrieve a TOKEN_PRIVILEGES structure 2260 | try 2261 | { 2262 | $TokenPrivileges = $TokenPtr -as $TOKEN_PRIVILEGES 2263 | 2264 | $sb = New-Object System.Text.StringBuilder 2265 | 2266 | for ($i = 0; $i -lt $TokenPrivileges.PrivilegeCount; $i++) 2267 | { 2268 | if ((($TokenPrivileges.Privileges[$i].Attributes -as $LuidAttributes) -band $LuidAttributes::SE_PRIVILEGE_ENABLED) -eq $LuidAttributes::SE_PRIVILEGE_ENABLED) 2269 | { 2270 | $sb.Append(", $($TokenPrivileges.Privileges[$i].Luid.LowPart.ToString())") | Out-Null 2271 | } 2272 | } 2273 | Write-Output $sb.ToString().TrimStart(', ') 2274 | } 2275 | catch {} 2276 | } 2277 | 17 # TokenOrigin 2278 | { 2279 | $TokenOrigin = $TokenPtr -as $LUID 2280 | Write-Output (Get-LogonSession -LogonId $TokenOrigin.LowPart) 2281 | } 2282 | 22 # TokenAccessInformation 2283 | { 2284 | 2285 | } 2286 | 25 # TokenIntegrityLevel 2287 | { 2288 | $TokenIntegrity = $TokenPtr -as $TOKEN_MANDATORY_LABEL 2289 | switch(ConvertSidToStringSid -SidPointer $TokenIntegrity.Label.Sid) 2290 | { 2291 | $UNTRUSTED_MANDATORY_LEVEL 2292 | { 2293 | Write-Output "UNTRUSTED_MANDATORY_LEVEL" 2294 | } 2295 | $LOW_MANDATORY_LEVEL 2296 | { 2297 | Write-Output "LOW_MANDATORY_LEVEL" 2298 | } 2299 | $MEDIUM_MANDATORY_LEVEL 2300 | { 2301 | Write-Output "MEDIUM_MANDATORY_LEVEL" 2302 | } 2303 | $MEDIUM_PLUS_MANDATORY_LEVEL 2304 | { 2305 | Write-Output "MEDIUM_PLUS_MANDATORY_LEVEL" 2306 | } 2307 | $HIGH_MANDATORY_LEVEL 2308 | { 2309 | Write-Output "HIGH_MANDATORY_LEVEL" 2310 | } 2311 | $SYSTEM_MANDATORY_LEVEL 2312 | { 2313 | Write-Output "SYSTEM_MANDATORY_LEVEL" 2314 | } 2315 | $PROTECTED_PROCESS_MANDATORY_LEVEL 2316 | { 2317 | Write-Output "PROTECTED_PROCESS_MANDATORY_LEVEL" 2318 | } 2319 | $SECURE_PROCESS_MANDATORY_LEVEL 2320 | { 2321 | Write-Output "SECURE_PROCESS_MANDATORY_LEVEL" 2322 | } 2323 | } 2324 | } 2325 | } 2326 | } 2327 | else 2328 | { 2329 | Write-Debug "GetTokenInformation Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2330 | } 2331 | try 2332 | { 2333 | [System.Runtime.InteropServices.Marshal]::FreeHGlobal($TokenPtr) 2334 | } 2335 | catch 2336 | { 2337 | 2338 | } 2339 | } 2340 | 2341 | function NtQueryInformationThread_Win32StartAddress 2342 | { 2343 | <# 2344 | .SYNOPSIS 2345 | 2346 | Retrieves information about the specified thread. 2347 | 2348 | .DESCRIPTION 2349 | 2350 | .PARAMETER ThreadHandle 2351 | 2352 | .NOTES 2353 | 2354 | Author - Jared Atkinson (@jaredcatkinson) 2355 | 2356 | .LINK 2357 | 2358 | .EXAMPLE 2359 | #> 2360 | 2361 | param 2362 | ( 2363 | [Parameter(Mandatory = $true)] 2364 | [IntPtr] 2365 | $ThreadHandle 2366 | ) 2367 | 2368 | <# 2369 | (func ntdll NtQueryInformationThread ([Int32]) @( 2370 | [IntPtr], #_In_ HANDLE ThreadHandle, 2371 | [Int32], #_In_ THREADINFOCLASS ThreadInformationClass, 2372 | [IntPtr], #_Inout_ PVOID ThreadInformation, 2373 | [Int32], #_In_ ULONG ThreadInformationLength, 2374 | [IntPtr] #_Out_opt_ PULONG ReturnLength 2375 | )) 2376 | #> 2377 | 2378 | $Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal([IntPtr]::Size) 2379 | 2380 | $NtStatus = $Ntdll::NtQueryInformationThread($ThreadHandle, 9, $Buffer, [IntPtr]::Size, [IntPtr]::Zero); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2381 | 2382 | if($NtStatus -ne 0) 2383 | { 2384 | Write-Debug "NtQueryInformationThread Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2385 | } 2386 | 2387 | 2388 | $Win32StartAddress = [System.Runtime.InteropServices.Marshal]::ReadIntPtr($Buffer) 2389 | [System.Runtime.InteropServices.Marshal]::FreeHGlobal($Buffer) 2390 | Write-Output $Win32StartAddress 2391 | } 2392 | 2393 | function OpenProcess 2394 | { 2395 | <# 2396 | .SYNOPSIS 2397 | 2398 | Opens an existing local process object. 2399 | 2400 | .DESCRIPTION 2401 | 2402 | To open a handle to another local process and obtain full access rights, you must enable the SeDebugPrivilege privilege. 2403 | The handle returned by the OpenProcess function can be used in any function that requires a handle to a process, such as the wait functions, provided the appropriate access rights were requested. 2404 | When you are finished with the handle, be sure to close it using the CloseHandle function. 2405 | 2406 | .PARAMETER ProcessId 2407 | 2408 | The identifier of the local process to be opened. 2409 | If the specified process is the System Process (0x00000000), the function fails and the last error code is ERROR_INVALID_PARAMETER. If the specified process is the Idle process or one of the CSRSS processes, this function fails and the last error code is ERROR_ACCESS_DENIED because their access restrictions prevent user-level code from opening them. 2410 | 2411 | .PARAMETER DesiredAccess 2412 | 2413 | The access to the process object. This access right is checked against the security descriptor for the process. This parameter can be one or more of the process access rights. 2414 | If the caller has enabled the SeDebugPrivilege privilege, the requested access is granted regardless of the contents of the security descriptor. 2415 | 2416 | .PARAMETER InheritHandle 2417 | 2418 | If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle. 2419 | 2420 | .NOTES 2421 | 2422 | Author: Jared Atkinson (@jaredcatkinson) 2423 | License: BSD 3-Clause 2424 | Required Dependencies: PSReflect 2425 | Optional Dependencies: PROCESS_ACCESS 2426 | 2427 | (func kernel32 OpenProcess ([IntPtr]) @( 2428 | [UInt32], #_In_ DWORD dwDesiredAccess 2429 | [bool], #_In_ BOOL bInheritHandle 2430 | [UInt32] #_In_ DWORD dwProcessId 2431 | ) -EntryPoint OpenProcess -SetLastError) 2432 | 2433 | .LINK 2434 | 2435 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms684320(v=vs.85).aspx 2436 | 2437 | .LINK 2438 | 2439 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms684880(v=vs.85).aspx 2440 | 2441 | .EXAMPLE 2442 | #> 2443 | 2444 | [CmdletBinding()] 2445 | param 2446 | ( 2447 | [Parameter(Mandatory = $true)] 2448 | [UInt32] 2449 | $ProcessId, 2450 | 2451 | [Parameter(Mandatory = $true)] 2452 | [ValidateSet('PROCESS_TERMINATE','PROCESS_CREATE_THREAD','PROCESS_VM_OPERATION','PROCESS_VM_READ','PROCESS_VM_WRITE','PROCESS_DUP_HANDLE','PROCESS_CREATE_PROCESS','PROCESS_SET_QUOTA','PROCESS_SET_INFORMATION','PROCESS_QUERY_INFORMATION','PROCESS_SUSPEND_RESUME','PROCESS_QUERY_LIMITED_INFORMATION','DELETE','READ_CONTROL','WRITE_DAC','WRITE_OWNER','SYNCHRONIZE','PROCESS_ALL_ACCESS')] 2453 | [string[]] 2454 | $DesiredAccess, 2455 | 2456 | [Parameter()] 2457 | [bool] 2458 | $InheritHandle = $false 2459 | ) 2460 | 2461 | # Calculate Desired Access Value 2462 | $dwDesiredAccess = 0 2463 | 2464 | foreach($val in $DesiredAccess) 2465 | { 2466 | $dwDesiredAccess = $dwDesiredAccess -bor $PROCESS_ACCESS::$val 2467 | } 2468 | 2469 | $hProcess = $Kernel32::OpenProcess($dwDesiredAccess, $InheritHandle, $ProcessId); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2470 | 2471 | if($hProcess -eq 0) 2472 | { 2473 | #throw "OpenProcess Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2474 | } 2475 | 2476 | Write-Output $hProcess 2477 | } 2478 | 2479 | function OpenProcessToken 2480 | { 2481 | <# 2482 | .SYNOPSIS 2483 | 2484 | The OpenProcessToken function opens the access token associated with a process. 2485 | 2486 | .PARAMETER ProcessHandle 2487 | 2488 | A handle to the process whose access token is opened. The process must have the PROCESS_QUERY_INFORMATION access permission. 2489 | 2490 | .PARAMETER DesiredAccess 2491 | 2492 | Specifies an access mask that specifies the requested types of access to the access token. These requested access types are compared with the discretionary access control list (DACL) of the token to determine which accesses are granted or denied. 2493 | For a list of access rights for access tokens, see Access Rights for Access-Token Objects. 2494 | 2495 | .NOTES 2496 | 2497 | Author: Jared Atkinson (@jaredcatkinson) 2498 | License: BSD 3-Clause 2499 | Required Dependencies: PSReflect 2500 | Optional Dependencies: TOKEN_ACCESS (Enumeration) 2501 | 2502 | (func advapi32 OpenProcessToken ([bool]) @( 2503 | [IntPtr], #_In_ HANDLE ProcessHandle 2504 | [UInt32], #_In_ DWORD DesiredAccess 2505 | [IntPtr].MakeByRefType() #_Out_ PHANDLE TokenHandle 2506 | ) -EntryPoint OpenProcessToken -SetLastError) 2507 | 2508 | .LINK 2509 | 2510 | https://msdn.microsoft.com/en-us/library/windows/desktop/aa379295(v=vs.85).aspx 2511 | 2512 | .LINK 2513 | 2514 | https://msdn.microsoft.com/en-us/library/windows/desktop/aa374905(v=vs.85).aspx 2515 | 2516 | .EXAMPLE 2517 | #> 2518 | 2519 | [OutputType([IntPtr])] 2520 | [CmdletBinding()] 2521 | param 2522 | ( 2523 | [Parameter(Mandatory = $true)] 2524 | [IntPtr] 2525 | $ProcessHandle, 2526 | 2527 | [Parameter(Mandatory = $true)] 2528 | [ValidateSet('TOKEN_ASSIGN_PRIMARY','TOKEN_DUPLICATE','TOKEN_IMPERSONATE','TOKEN_QUERY','TOKEN_QUERY_SOURCE','TOKEN_ADJUST_PRIVILEGES','TOKEN_ADJUST_GROUPS','TOKEN_ADJUST_DEFAULT','TOKEN_ADJUST_SESSIONID','DELETE','READ_CONTROL','WRITE_DAC','WRITE_OWNER','SYNCHRONIZE','STANDARD_RIGHTS_REQUIRED','TOKEN_ALL_ACCESS')] 2529 | [string[]] 2530 | $DesiredAccess 2531 | ) 2532 | 2533 | # Calculate Desired Access Value 2534 | $dwDesiredAccess = 0 2535 | 2536 | foreach($val in $DesiredAccess) 2537 | { 2538 | $dwDesiredAccess = $dwDesiredAccess -bor $TOKEN_ACCESS::$val 2539 | } 2540 | 2541 | $hToken = [IntPtr]::Zero 2542 | $Success = $Advapi32::OpenProcessToken($ProcessHandle, $dwDesiredAccess, [ref]$hToken); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2543 | 2544 | if(-not $Success) 2545 | { 2546 | throw "OpenProcessToken Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2547 | } 2548 | 2549 | Write-Output $hToken 2550 | } 2551 | 2552 | function OpenThread 2553 | { 2554 | <# 2555 | .SYNOPSIS 2556 | 2557 | Opens an existing thread object. 2558 | 2559 | .DESCRIPTION 2560 | 2561 | The handle returned by OpenThread can be used in any function that requires a handle to a thread, such as the wait functions, provided you requested the appropriate access rights. The handle is granted access to the thread object only to the extent it was specified in the dwDesiredAccess parameter. 2562 | When you are finished with the handle, be sure to close it by using the CloseHandle function. 2563 | 2564 | .PARAMETER ThreadId 2565 | 2566 | The identifier of the thread to be opened. 2567 | 2568 | .PARAMETER DesiredAccess 2569 | 2570 | The access to the thread object. This access right is checked against the security descriptor for the thread. This parameter can be one or more of the thread access rights. 2571 | If the caller has enabled the SeDebugPrivilege privilege, the requested access is granted regardless of the contents of the security descriptor. 2572 | 2573 | .PARAMETER InheritHandle 2574 | 2575 | If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle. 2576 | 2577 | .NOTES 2578 | 2579 | Author: Jared Atkinson (@jaredcatkinson) 2580 | License: BSD 3-Clause 2581 | Required Dependencies: PSReflect 2582 | Optional Dependencies: THREAD_ACCESS (Enumeration) 2583 | 2584 | (func kernel32 OpenThread ([IntPtr]) @( 2585 | [UInt32], #_In_ DWORD dwDesiredAccess 2586 | [bool], #_In_ BOOL bInheritHandle 2587 | [UInt32] #_In_ DWORD dwThreadId 2588 | ) -EntryPoint OpenThread -SetLastError) 2589 | 2590 | .LINK 2591 | 2592 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms684335(v=vs.85).aspx 2593 | 2594 | .LINK 2595 | 2596 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms686769(v=vs.85).aspx 2597 | 2598 | .EXAMPLE 2599 | #> 2600 | 2601 | [CmdletBinding()] 2602 | param 2603 | ( 2604 | [Parameter(Mandatory = $true)] 2605 | [UInt32] 2606 | $ThreadId, 2607 | 2608 | [Parameter(Mandatory = $true)] 2609 | [ValidateSet('THREAD_TERMINATE','THREAD_SUSPEND_RESUME','THREAD_GET_CONTEXT','THREAD_SET_CONTEXT','THREAD_SET_INFORMATION','THREAD_QUERY_INFORMATION','THREAD_SET_THREAD_TOKEN','THREAD_IMPERSONATE','THREAD_DIRECT_IMPERSONATION','THREAD_SET_LIMITED_INFORMATION','THREAD_QUERY_LIMITED_INFORMATION','DELETE','READ_CONTROL','WRITE_DAC','WRITE_OWNER','SYNCHRONIZE','THREAD_ALL_ACCESS')] 2610 | [string[]] 2611 | $DesiredAccess, 2612 | 2613 | [Parameter()] 2614 | [bool] 2615 | $InheritHandle = $false 2616 | ) 2617 | 2618 | # Calculate Desired Access Value 2619 | $dwDesiredAccess = 0 2620 | 2621 | foreach($val in $DesiredAccess) 2622 | { 2623 | $dwDesiredAccess = $dwDesiredAccess -bor $THREAD_ACCESS::$val 2624 | } 2625 | 2626 | $hThread = $Kernel32::OpenThread($dwDesiredAccess, $InheritHandle, $ThreadId); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2627 | 2628 | if($hThread -eq 0) 2629 | { 2630 | #throw "OpenThread Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2631 | } 2632 | 2633 | Write-Output $hThread 2634 | } 2635 | 2636 | function OpenThreadToken 2637 | { 2638 | <# 2639 | .SYNOPSIS 2640 | 2641 | The OpenThreadToken function opens the access token associated with a thread 2642 | 2643 | .DESCRIPTION 2644 | 2645 | Tokens with the anonymous impersonation level cannot be opened. 2646 | Close the access token handle returned through the Handle parameter by calling CloseHandle. 2647 | 2648 | .PARAMETER ThreadHandle 2649 | 2650 | A handle to the thread whose access token is opened. 2651 | 2652 | .PARAMETER DesiredAccess 2653 | 2654 | Specifies an access mask that specifies the requested types of access to the access token. These requested access types are reconciled against the token's discretionary access control list (DACL) to determine which accesses are granted or denied. 2655 | 2656 | .PARAMETER OpenAsSelf 2657 | 2658 | TRUE if the access check is to be made against the process-level security context. 2659 | FALSE if the access check is to be made against the current security context of the thread calling the OpenThreadToken function. 2660 | The OpenAsSelf parameter allows the caller of this function to open the access token of a specified thread when the caller is impersonating a token at SecurityIdentification level. Without this parameter, the calling thread cannot open the access token on the specified thread because it is impossible to open executive-level objects by using the SecurityIdentification impersonation level. 2661 | 2662 | .NOTES 2663 | 2664 | Author: Jared Atkinson (@jaredcatkinson) 2665 | License: BSD 3-Clause 2666 | Required Dependencies: PSReflect 2667 | Optional Dependencies: $TOKEN_ACCESS (Enumeration) 2668 | 2669 | (func advapi32 OpenThreadToken ([bool]) @( 2670 | [IntPtr], #_In_ HANDLE ThreadHandle 2671 | [UInt32], #_In_ DWORD DesiredAccess 2672 | [bool], #_In_ BOOL OpenAsSelf 2673 | [IntPtr].MakeByRefType() #_Out_ PHANDLE TokenHandle 2674 | ) -EntryPoint OpenThreadToken -SetLastError) 2675 | 2676 | .LINK 2677 | 2678 | https://msdn.microsoft.com/en-us/library/windows/desktop/aa379296(v=vs.85).aspx 2679 | 2680 | .LINK 2681 | 2682 | https://msdn.microsoft.com/en-us/library/windows/desktop/aa374905(v=vs.85).aspx 2683 | 2684 | .EXAMPLE 2685 | #> 2686 | 2687 | [CmdletBinding()] 2688 | param 2689 | ( 2690 | [Parameter(Mandatory = $true)] 2691 | [IntPtr] 2692 | $ThreadHandle, 2693 | 2694 | [Parameter(Mandatory = $true)] 2695 | [ValidateSet('TOKEN_ASSIGN_PRIMARY','TOKEN_DUPLICATE','TOKEN_IMPERSONATE','TOKEN_QUERY','TOKEN_QUERY_SOURCE','TOKEN_ADJUST_PRIVILEGES','TOKEN_ADJUST_GROUPS','TOKEN_ADJUST_DEFAULT','TOKEN_ADJUST_SESSIONID','DELETE','READ_CONTROL','WRITE_DAC','WRITE_OWNER','SYNCHRONIZE','STANDARD_RIGHTS_REQUIRED','TOKEN_ALL_ACCESS')] 2696 | [string[]] 2697 | $DesiredAccess, 2698 | 2699 | [Parameter()] 2700 | [bool] 2701 | $OpenAsSelf = $false 2702 | ) 2703 | 2704 | # Calculate Desired Access Value 2705 | $dwDesiredAccess = 0 2706 | 2707 | foreach($val in $DesiredAccess) 2708 | { 2709 | $dwDesiredAccess = $dwDesiredAccess -bor $TOKEN_ACCESS::$val 2710 | } 2711 | 2712 | $hToken = [IntPtr]::Zero 2713 | $Success = $Advapi32::OpenThreadToken($ThreadHandle, $dwDesiredAccess, $OpenAsSelf, [ref]$hToken); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2714 | 2715 | if(-not $Success) 2716 | { 2717 | throw "OpenThreadToken Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2718 | } 2719 | 2720 | Write-Output $hToken 2721 | } 2722 | 2723 | function QueryFullProcessImageName 2724 | { 2725 | <# 2726 | .SYNOPSIS 2727 | 2728 | Retrieves the full name of the executable image for the specified process. 2729 | 2730 | .PARAMETER ProcessHandle 2731 | 2732 | A handle to the process. This handle must be created with the PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION access right. 2733 | 2734 | .PARAMETER Flags 2735 | 2736 | This parameter can be one of the following values. 2737 | 0x00 - The name should use the Win32 path format. 2738 | 0x01 - The name should use the native system path format. 2739 | 2740 | .NOTES 2741 | 2742 | Author - Jared Atkinson (@jaredcatkinson) 2743 | 2744 | .LINK 2745 | 2746 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms684919(v=vs.85).aspx 2747 | 2748 | .EXAMPLE 2749 | #> 2750 | 2751 | param 2752 | ( 2753 | [Parameter(Mandatory = $true)] 2754 | [IntPtr] 2755 | $ProcessHandle, 2756 | 2757 | [Parameter()] 2758 | [UInt32] 2759 | $Flags = 0 2760 | ) 2761 | 2762 | $capacity = 2048 2763 | $sb = New-Object -TypeName System.Text.StringBuilder($capacity) 2764 | 2765 | $Success = $Kernel32::QueryFullProcessImageName($ProcessHandle, $Flags, $sb, [ref]$capacity); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2766 | 2767 | if(-not $Success) 2768 | { 2769 | Write-Debug "QueryFullProcessImageName Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2770 | } 2771 | 2772 | Write-Output $sb.ToString() 2773 | } 2774 | 2775 | function ReadProcessMemory 2776 | { 2777 | <# 2778 | .SYNOPSIS 2779 | 2780 | Reads data from an area of memory in a specified process. The entire area to be read must be accessible or the operation fails. 2781 | 2782 | .DESCRIPTION 2783 | 2784 | ReadProcessMemory copies the data in the specified address range from the address space of the specified process into the specified buffer of the current process. Any process that has a handle with PROCESS_VM_READ access can call the function. 2785 | 2786 | The entire area to be read must be accessible, and if it is not accessible, the function fails. 2787 | 2788 | .PARAMETER ProcessHandle 2789 | 2790 | A handle to the process with memory that is being read. The handle must have PROCESS_VM_READ access to the process. 2791 | 2792 | .PARAMETER BaseAddress 2793 | 2794 | The base address in the specified process from which to read. Before any data transfer occurs, the system verifies that all data in the base address and memory of the specified size is accessible for read access, and if it is not accessible the function fails. 2795 | 2796 | .PARAMETER Size 2797 | 2798 | The number of bytes to be read from the specified process. 2799 | 2800 | .NOTES 2801 | 2802 | Author - Jared Atkinson (@jaredcatkinson) 2803 | 2804 | .LINK 2805 | 2806 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms680553(v=vs.85).aspx 2807 | 2808 | .EXAMPLE 2809 | #> 2810 | 2811 | param 2812 | ( 2813 | [Parameter(Mandatory = $true)] 2814 | [IntPtr] 2815 | $ProcessHandle, 2816 | 2817 | [Parameter(Mandatory = $true)] 2818 | [IntPtr] 2819 | $BaseAddress, 2820 | 2821 | [Parameter(Mandatory = $true)] 2822 | [Int] 2823 | $Size 2824 | ) 2825 | 2826 | <# 2827 | (func kernel32 ReadProcessMemory ([Bool]) @( 2828 | [IntPtr], # _In_ HANDLE hProcess 2829 | [IntPtr], # _In_ LPCVOID lpBaseAddress 2830 | [Byte[]], # _Out_ LPVOID lpBuffer 2831 | [Int], # _In_ SIZE_T nSize 2832 | [Int].MakeByRefType() # _Out_ SIZE_T *lpNumberOfBytesRead 2833 | ) -SetLastError) # MSDN states to call GetLastError if the return value is false. 2834 | #> 2835 | 2836 | $Buffer = New-Object byte[]($Size) 2837 | [Int32]$NumberOfBytesRead = 0 2838 | 2839 | $Success = $Kernel32::ReadProcessMemory($ProcessHandle, $BaseAddress, $Buffer, $Buffer.Length, [ref]$NumberOfBytesRead); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2840 | 2841 | if(-not $Success) 2842 | { 2843 | Write-Debug "ReadProcessMemory Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2844 | } 2845 | 2846 | Write-Output $Buffer 2847 | } 2848 | 2849 | function VirtualQueryEx 2850 | { 2851 | <# 2852 | .SYNOPSIS 2853 | 2854 | Retrieves information about a range of pages within the virtual address space of a specified process. 2855 | 2856 | .PARAMETER ProcessHandle 2857 | 2858 | A handle to the process whose memory information is queried. The handle must have been opened with the PROCESS_QUERY_INFORMATION access right, which enables using the handle to read information from the process object. 2859 | 2860 | .PARAMETER BaseAddress 2861 | 2862 | The base address of the region of pages to be queried. This value is rounded down to the next page boundary. 2863 | 2864 | .NOTES 2865 | 2866 | Author - Jared Atkinson (@jaredcatkinson) 2867 | 2868 | .LINK 2869 | 2870 | https://msdn.microsoft.com/en-us/library/windows/desktop/aa366907(v=vs.85).aspx 2871 | 2872 | .EXAMPLE 2873 | #> 2874 | 2875 | param 2876 | ( 2877 | [Parameter(Mandatory = $true)] 2878 | [IntPtr] 2879 | $ProcessHandle, 2880 | 2881 | [Parameter(Mandatory = $true)] 2882 | [IntPtr] 2883 | $BaseAddress 2884 | ) 2885 | 2886 | <# 2887 | (func kernel32 VirtualQueryEx ([Int32]) @( 2888 | [IntPtr], #_In_ HANDLE hProcess, 2889 | [IntPtr], #_In_opt_ LPCVOID lpAddress, 2890 | $MEMORYBASICINFORMATION.MakeByRefType(), #_Out_ PMEMORY_BASIC_INFORMATION lpBuffer, 2891 | [UInt32] #_In_ SIZE_T dwLength 2892 | ) -SetLastError) 2893 | #> 2894 | 2895 | $MemoryBasicInfo = [Activator]::CreateInstance($MEMORYBASICINFORMATION) 2896 | $BytesWritten = $Kernel32::VirtualQueryEx($ProcessHandle, $BaseAddress, [Ref]$MemoryBasicInfo, $MEMORYBASICINFORMATION::GetSize()); $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2897 | 2898 | if($BytesWritten -eq 0) 2899 | { 2900 | Write-Debug "VirtualQueryEx Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2901 | } 2902 | 2903 | Write-Output $MemoryBasicInfo 2904 | } 2905 | 2906 | function IsWow64Process 2907 | { 2908 | <# 2909 | .SYNOPSIS 2910 | 2911 | Determines whether the specified process is running under WOW64 on an x64 processor. 2912 | 2913 | .PARAMETER ProcessHandle 2914 | 2915 | A handle to the process. The handle must have the PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION access right. 2916 | 2917 | .NOTES 2918 | 2919 | Author - John Uhlmann (@jdu2600) 2920 | 2921 | .LINK 2922 | 2923 | https://docs.microsoft.com/en-us/windows/win32/api/wow64apiset/nf-wow64apiset-iswow64process 2924 | 2925 | .EXAMPLE 2926 | #> 2927 | 2928 | param 2929 | ( 2930 | [Parameter(Mandatory = $true)] 2931 | [IntPtr] 2932 | $ProcessHandle 2933 | ) 2934 | 2935 | <# 2936 | (func kernel32 IsWow64Process ([Bool]) @( 2937 | [IntPtr], #_In_ HANDLE hProcess, 2938 | [Bool].MakeByRefType() #_Out_ PBOOL Wow64Process 2939 | ) -SetLastError) 2940 | #> 2941 | 2942 | $Wow64Process = $false 2943 | $Success = $Kernel32::IsWow64Process($ProcessHandle, [ref]$Wow64Process); 2944 | 2945 | if (-not $Success) 2946 | { 2947 | $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 2948 | Write-Debug "IsWow64Process Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 2949 | } 2950 | 2951 | Write-Output $Wow64Process 2952 | } 2953 | 2954 | function GetMappedFileName 2955 | { 2956 | <# 2957 | .SYNOPSIS 2958 | 2959 | Checks whether the specified address is within a memory-mapped file in the address space of the specified process. If so, the function returns the name of the memory-mapped file. 2960 | 2961 | .PARAMETER ProcessHandle 2962 | 2963 | A handle to the process. This handle must be created with the PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION access right. 2964 | 2965 | .PARAMETER Address 2966 | 2967 | The address to be verified. 2968 | 2969 | .NOTES 2970 | 2971 | Author - John Uhlmann (@jdu2600) 2972 | 2973 | .LINK 2974 | 2975 | https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmappedfilenamea 2976 | 2977 | .EXAMPLE 2978 | #> 2979 | 2980 | param 2981 | ( 2982 | [Parameter(Mandatory = $true)] 2983 | [IntPtr] 2984 | $ProcessHandle, 2985 | 2986 | [Parameter(Mandatory = $true)] 2987 | [IntPtr] 2988 | $Address 2989 | ) 2990 | <# 2991 | (func kernel32 K32GetMappedFileName ([Int32]) @( 2992 | [IntPtr] #_In_ HANDLE hProcess 2993 | [IntPtr] #_In_ LPVOID lpv, 2994 | [System.Text.StringBuilder] #_Out_ LPTSTR lpFilename, 2995 | [UInt32] #_In_ DWORD nSize 2996 | ) -SetLastError) 2997 | 2998 | (func kernel32 QueryDosDevice ([Int32]) @( 2999 | [String] #_In_ LPCWSTR lpDeviceName, 3000 | [System.Text.StringBuilder] #_Out_ LPWSTR lpTargetPath, 3001 | [Int32] #_In_ DWORD ucchMax 3002 | ) -SetLastError) 3003 | #> 3004 | 3005 | $Capacity = 2048 3006 | $StringBuffer = New-Object -TypeName System.Text.StringBuilder($Capacity) 3007 | 3008 | # K32GetMappedFileName returns a device name such as \Device\Harddisk0\Windows\System32\ntdll.dll 3009 | $BytesCopied = $Kernel32::K32GetMappedFileName($ProcessHandle, $Address, $StringBuffer, $Capacity); 3010 | if ($BytesCopied -eq 0) 3011 | { 3012 | $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 3013 | Write-Debug "GetMappedFileName Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 3014 | } 3015 | $Path = $StringBuffer.ToString() 3016 | 3017 | # Replace all device volume names with their drive letters 3018 | Get-WmiObject Win32_Volume | 3019 | Where-Object { $_.DriveLetter } | 3020 | ForEach-Object { 3021 | $BytesCopied = $Kernel32::QueryDosDevice($_.DriveLetter, $StringBuffer, $Capacity) 3022 | if ($BytesCopied -ne 0) 3023 | { 3024 | $Path = $Path -replace "^$([regex]::Escape($StringBuffer.ToString()))", $_.DriveLetter 3025 | } 3026 | } 3027 | 3028 | Write-Output $Path 3029 | } 3030 | 3031 | 3032 | function IsWorkingSetPage 3033 | { 3034 | <# 3035 | .SYNOPSIS 3036 | 3037 | Checks whether the specified address is within the working set of the specified process. 3038 | For MEM_IMAGE pages, this indicates that it has been locally modified. 3039 | 3040 | .PARAMETER ProcessHandle 3041 | 3042 | A handle to the process. This handle must be created with the PROCESS_QUERY_INFORMATION access right. 3043 | 3044 | .PARAMETER Address 3045 | 3046 | The address to be checked. 3047 | 3048 | .NOTES 3049 | 3050 | Author - John Uhlmann (@jdu2600) 3051 | 3052 | .LINK 3053 | 3054 | https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-queryworkingsetex 3055 | 3056 | .EXAMPLE 3057 | #> 3058 | 3059 | param 3060 | ( 3061 | [Parameter(Mandatory = $true)] 3062 | [IntPtr] 3063 | $ProcessHandle, 3064 | 3065 | [Parameter(Mandatory = $true)] 3066 | [IntPtr] 3067 | $Address 3068 | ) 3069 | <# 3070 | (func kernel32 K32QueryWorkingSetEx ([Bool]) @( 3071 | [IntPtr] #_In_ HANDLE hProcess, 3072 | [IntPtr] #_In_ PVOID pv, 3073 | [Int32] #_In_ DWORD cb 3074 | ) -SetLastError) 3075 | #> 3076 | 3077 | $WorkingSetInfo = [Activator]::CreateInstance($WORKING_SET_EX_INFORMATION) 3078 | $WorkingSetInfo.VirtualAddress = $Address 3079 | $Success = $Kernel32::K32QueryWorkingSetEx($ProcessHandle, [Ref]$WorkingSetInfo, $WORKING_SET_EX_INFORMATION::GetSize()); 3080 | 3081 | if (-not $Success) 3082 | { 3083 | $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 3084 | Write-Debug "QueryWorkingSetEx Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 3085 | } 3086 | 3087 | Write-Output (($WorkingSetInfo.VirtualAttributes.ToInt64() -band $WORKING_SET_EX_BLOCK::Shared) -ne $WORKING_SET_EX_BLOCK::Shared) 3088 | } 3089 | 3090 | function GetProcAddress 3091 | { 3092 | <# 3093 | .SYNOPSIS 3094 | 3095 | Retrieves the address of an exported function or variable from the specified module. 3096 | 3097 | .PARAMETER ModuleName 3098 | 3099 | The module name. It must already be loaded in the current process. 3100 | 3101 | .PARAMETER ProcName 3102 | 3103 | The function or variable name. 3104 | 3105 | .NOTES 3106 | 3107 | Author - John Uhlmann (@jdu2600) 3108 | 3109 | .LINK 3110 | 3111 | https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress 3112 | 3113 | .EXAMPLE 3114 | #> 3115 | 3116 | param 3117 | ( 3118 | [Parameter(Mandatory = $true)] 3119 | [string] 3120 | $ModuleName, 3121 | 3122 | [Parameter(Mandatory = $true)] 3123 | [string] 3124 | $ProcName 3125 | ) 3126 | <# 3127 | (func kernel32 GetModuleHandle ([IntPtr]) @( 3128 | [String] #_In_ LPCSTR lpModuleName 3129 | ) -SetLastError), 3130 | 3131 | (func kernel32 GetProcAddress ([IntPtr]) @( 3132 | [IntPtr] #_In_ HANDLE hModule, 3133 | [String] #_In_ LPCSTR lpProcName 3134 | ) -Charset Ansi -SetLastError) 3135 | #> 3136 | 3137 | $hModule = $Kernel32::GetModuleHandle($ModuleName) 3138 | if ($hModule -eq 0) 3139 | { 3140 | $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 3141 | Write-Debug "GetModuleHandle Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 3142 | } 3143 | 3144 | $ProcAddress = $Kernel32::GetProcAddress($hModule, $ProcName) 3145 | if ($ProcAddress -eq 0) 3146 | { 3147 | $LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() 3148 | Write-Debug "GetProcAddress Error: $(([ComponentModel.Win32Exception] $LastError).Message)" 3149 | } 3150 | 3151 | Write-Output $ProcAddress 3152 | } 3153 | #endregion Win32 API Abstractions -------------------------------------------------------------------------------- /Get-InjectedThreadEx/CfgBitMap.cpp: -------------------------------------------------------------------------------- 1 | #include "Get-InjectedThreadEx.h" 2 | 3 | BOOL InSystemImageRange(PVOID Address) { 4 | return Address >= (PVOID)0x7FF800000000 && Address < (PVOID)0x7FFFFFFF0000; 5 | } 6 | 7 | constexpr UINT64 XFG_MASK_UNSET = ~0xFFFDBFFF7EDFFB71; // mask of unset bits 8 | constexpr UINT64 XFG_MASK_SET = 0x8000060010500070; // mask of set bits 9 | constexpr UINT64 XFG_MASK_ALL = XFG_MASK_UNSET | XFG_MASK_SET; 10 | BOOL IsValidXfgHash(UINT64 xfgHash) { 11 | return XFG_MASK_SET == (xfgHash & XFG_MASK_ALL); 12 | } 13 | 14 | static PULONG_PTR FindCfgBitMap() { 15 | // Find non-exported ntdll!LdrSystemDllInitBlock.CfgBitMap by looking at the first instruction of LdrControlFlowGuardEnforced 16 | // 48833d [80be1400], 00 CMP qword ptr[LdrSystemDllInitBlock.CfgBitMap], 0x0 17 | #pragma warning(suppress: 6387) // ntdll is always loaded 18 | auto pLdrControlFlowGuardEnforced = GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrControlFlowGuardEnforced"); 19 | if (NULL == pLdrControlFlowGuardEnforced) 20 | return NULL; 21 | 22 | auto ppCfgBitMap = (PULONG_PTR*)((ULONG_PTR)pLdrControlFlowGuardEnforced + 8 + *(DWORD*)((ULONG_PTR)pLdrControlFlowGuardEnforced + 3)); 23 | 24 | MEMORY_BASIC_INFORMATION mbi; 25 | if (!VirtualQuery(*ppCfgBitMap, &mbi, sizeof(mbi)) || MEM_MAPPED != mbi.Type || *ppCfgBitMap != mbi.AllocationBase) { 26 | LogError("FATAL: ntdll!LdrSystemDllInitBlock.CfgBitMap not found\n"); 27 | ExitProcess(1); 28 | } 29 | 30 | return *ppCfgBitMap; 31 | } 32 | 33 | constexpr auto CFG_INVALID = 0b00; // no address in this range is a valid target 34 | constexpr auto CFG_ALL_VALID = 0b11; // all addresses in this range are valid. 35 | constexpr auto CFG_ALIGNED_VALID = 0b01; // the only valid target is 16-byte aligned 36 | constexpr auto CFG_EXPORT_SUPPRESSED = 0b10; // this range contains an export-suppressed target 37 | 38 | BOOL GetCfgBitsForAddress(PVOID address, PULONG pCfgBits) { 39 | static auto pCfgBitMap = FindCfgBitMap(); 40 | const PULONG_PTR pLocalEntry = pCfgBitMap + ((ULONG_PTR)address >> 9); 41 | const ULONG cfgOffset = (((ULONG_PTR)(address)) >> 3) & 0x3E; 42 | ULONG_PTR localEntry; 43 | // We use ReadProcessMemory to safely read the volatile CfgBitMap. 44 | if (ReadProcessMemory(GetCurrentProcess(), pLocalEntry, &localEntry, sizeof(localEntry), NULL)) { 45 | *pCfgBits = (localEntry >> cfgOffset) & 0b11; 46 | return TRUE; 47 | } 48 | return FALSE; 49 | } 50 | -------------------------------------------------------------------------------- /Get-InjectedThreadEx/Get-InjectedThreadEx.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #pragma comment(lib, "ntdll.lib") 4 | #pragma comment(lib, "DbgHelp.lib") 5 | #pragma comment(lib, "Zydis.lib") 6 | 7 | #define WIN32_LEAN_AND_MEAN 8 | #define NOMINMAX 9 | #include 10 | #include 11 | //#include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | // x86/x86-64 disassembler 26 | // https://github.com/zyantific/zydis - MIT 27 | #define ZYDIS_STATIC_BUILD 28 | #include 29 | 30 | // https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation 31 | constexpr auto MAX_LONG_PATH = 0x7FFF; 32 | 33 | //////////////////////////////////////////////////////////////////////////////////////////////////// 34 | // Logging 35 | #define Log(_fmt_, ...) printf(_fmt_ "\n", ##__VA_ARGS__) 36 | #define LogError(_fmt_, ...) fprintf(stderr, "[!] " _fmt_ "\n", ##__VA_ARGS__) 37 | #if _DEBUG 38 | #define LogDebug(_fmt_, ...) printf("[#] " _fmt_ "\n", ##__VA_ARGS__) 39 | #else 40 | #define LogDebug(...) 41 | #endif 42 | 43 | //////////////////////////////////////////////////////////////////////////////////////////////////// 44 | // Native API 45 | #define SE_DEBUG_PRIVILEGE (20L) 46 | typedef LONG NTSTATUS; 47 | #define NT_SUCCESS(Status) ((Status) >= 0) 48 | extern "C" { 49 | NTSTATUS RtlAdjustPrivilege(ULONG Privilege, BOOLEAN Enable, BOOLEAN CurrentThread, PBOOLEAN Enabled); 50 | } 51 | 52 | 53 | //////////////////////////////////////////////////////////////////////////////////////////////////// 54 | // CfgBitMap.cpp 55 | BOOL GetCfgBitsForAddress(PVOID address, PULONG pCfgBits); 56 | BOOL IsValidXfgHash(UINT64 xfgHash); 57 | 58 | //////////////////////////////////////////////////////////////////////////////////////////////////// 59 | // Memory.cpp 60 | std::string ToHex(std::string bytes); 61 | BOOL IsExecutable(const MEMORY_BASIC_INFORMATION& mbi); 62 | BOOL InSystemImageRange(PVOID Address); 63 | BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, PDWORD64 buffer); 64 | BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, std::string& buffer); 65 | BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, std::string& buffer, const MEMORY_BASIC_INFORMATION& mbi); 66 | BOOL GetMappedFileNameAsDosPath(HANDLE hProcess, PVOID address, std::wstring& buffer); 67 | 68 | //////////////////////////////////////////////////////////////////////////////////////////////////// 69 | // Process.cpp 70 | BOOL IsElevated(); 71 | BOOL IsDotNet(HANDLE hProcess); 72 | 73 | //////////////////////////////////////////////////////////////////////////////////////////////////// 74 | // StackClimb.cpp 75 | constexpr auto MIN_FRAMES = 5; 76 | BOOL StackClimb64(const HANDLE hProcess, const PVOID stackBuffer[], const size_t stackBufferCount, std::vector& callStackFrames, bool* pbDetection, int offset = 0); 77 | 78 | //////////////////////////////////////////////////////////////////////////////////////////////////// 79 | // Symbol.cpp 80 | BOOL GetNearestSymbol(HANDLE hProcess, PVOID address, std::string& symbol, bool bIncludeDisplacement = false); 81 | BOOL GetNearestSymbolWithPdb(HANDLE hProcess, PVOID address, std::string& symbol, bool bIncludeDisplacement = false); 82 | PVOID GetSymbolAddress(const char symbol[]); 83 | 84 | //////////////////////////////////////////////////////////////////////////////////////////////////// 85 | // Unwind.cpp 86 | HRESULT CalculateFrameSize(PVOID returnAddress, PDWORD pFrameSize, const std::wstring& filepath, PVOID remoteBase); 87 | BOOL IsValidCallSite(HANDLE hProcess, bool bIsWow64, const MEMORY_BASIC_INFORMATION& mbi, PVOID callsite, bool* bCallFound); 88 | 89 | int main(int, char* []); 90 | -------------------------------------------------------------------------------- /Get-InjectedThreadEx/Get-InjectedThreadEx.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33627.172 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Get-InjectedThreadEx", "Get-InjectedThreadEx.vcxproj", "{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}" 7 | ProjectSection(ProjectDependencies) = postProject 8 | {88A23124-5640-35A0-B890-311D7A67A7D2} = {88A23124-5640-35A0-B890-311D7A67A7D2} 9 | EndProjectSection 10 | EndProject 11 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Zydis", "zydis\msvc\zydis\Zydis.vcxproj", "{88A23124-5640-35A0-B890-311D7A67A7D2}" 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug Kernel|x64 = Debug Kernel|x64 16 | Debug Kernel|x86 = Debug Kernel|x86 17 | Debug MD DLL|x64 = Debug MD DLL|x64 18 | Debug MD DLL|x86 = Debug MD DLL|x86 19 | Debug MD|x64 = Debug MD|x64 20 | Debug MD|x86 = Debug MD|x86 21 | Debug MT DLL|x64 = Debug MT DLL|x64 22 | Debug MT DLL|x86 = Debug MT DLL|x86 23 | Debug MT|x64 = Debug MT|x64 24 | Debug MT|x86 = Debug MT|x86 25 | Debug|x64 = Debug|x64 26 | Debug|x86 = Debug|x86 27 | Release Kernel|x64 = Release Kernel|x64 28 | Release Kernel|x86 = Release Kernel|x86 29 | Release MD DLL|x64 = Release MD DLL|x64 30 | Release MD DLL|x86 = Release MD DLL|x86 31 | Release MD|x64 = Release MD|x64 32 | Release MD|x86 = Release MD|x86 33 | Release MT DLL|x64 = Release MT DLL|x64 34 | Release MT DLL|x86 = Release MT DLL|x86 35 | Release MT|x64 = Release MT|x64 36 | Release MT|x86 = Release MT|x86 37 | Release|x64 = Release|x64 38 | Release|x86 = Release|x86 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug Kernel|x64.ActiveCfg = Debug|x64 42 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug Kernel|x64.Build.0 = Debug|x64 43 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug Kernel|x86.ActiveCfg = Debug|x64 44 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug Kernel|x86.Build.0 = Debug|x64 45 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD DLL|x64.ActiveCfg = Debug|x64 46 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD DLL|x64.Build.0 = Debug|x64 47 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD DLL|x86.ActiveCfg = Debug|x64 48 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD DLL|x86.Build.0 = Debug|x64 49 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD|x64.ActiveCfg = Debug|x64 50 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD|x64.Build.0 = Debug|x64 51 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD|x86.ActiveCfg = Debug|x64 52 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD|x86.Build.0 = Debug|x64 53 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT DLL|x64.ActiveCfg = Debug|x64 54 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT DLL|x64.Build.0 = Debug|x64 55 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT DLL|x86.ActiveCfg = Debug|x64 56 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT DLL|x86.Build.0 = Debug|x64 57 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT|x64.ActiveCfg = Debug|x64 58 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT|x64.Build.0 = Debug|x64 59 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT|x86.ActiveCfg = Debug|x64 60 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT|x86.Build.0 = Debug|x64 61 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug|x64.ActiveCfg = Debug|x64 62 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug|x64.Build.0 = Debug|x64 63 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug|x86.ActiveCfg = Debug|x64 64 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release Kernel|x64.ActiveCfg = Release|x64 65 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release Kernel|x64.Build.0 = Release|x64 66 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release Kernel|x86.ActiveCfg = Release|x64 67 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release Kernel|x86.Build.0 = Release|x64 68 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD DLL|x64.ActiveCfg = Release|x64 69 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD DLL|x64.Build.0 = Release|x64 70 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD DLL|x86.ActiveCfg = Release|x64 71 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD DLL|x86.Build.0 = Release|x64 72 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD|x64.ActiveCfg = Release|x64 73 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD|x64.Build.0 = Release|x64 74 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD|x86.ActiveCfg = Release|x64 75 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD|x86.Build.0 = Release|x64 76 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT DLL|x64.ActiveCfg = Release|x64 77 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT DLL|x64.Build.0 = Release|x64 78 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT DLL|x86.ActiveCfg = Release|x64 79 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT DLL|x86.Build.0 = Release|x64 80 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT|x64.ActiveCfg = Release|x64 81 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT|x64.Build.0 = Release|x64 82 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT|x86.ActiveCfg = Release|x64 83 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT|x86.Build.0 = Release|x64 84 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release|x64.ActiveCfg = Release|x64 85 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release|x64.Build.0 = Release|x64 86 | {256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release|x86.ActiveCfg = Release|x64 87 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug Kernel|x64.ActiveCfg = Debug Kernel|x64 88 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug Kernel|x64.Build.0 = Debug Kernel|x64 89 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug Kernel|x86.ActiveCfg = Debug Kernel|Win32 90 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug Kernel|x86.Build.0 = Debug Kernel|Win32 91 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD DLL|x64.ActiveCfg = Debug MD DLL|x64 92 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD DLL|x64.Build.0 = Debug MD DLL|x64 93 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD DLL|x86.ActiveCfg = Debug MD DLL|Win32 94 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD DLL|x86.Build.0 = Debug MD DLL|Win32 95 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD|x64.ActiveCfg = Debug MD|x64 96 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD|x64.Build.0 = Debug MD|x64 97 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD|x86.ActiveCfg = Debug MD|Win32 98 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD|x86.Build.0 = Debug MD|Win32 99 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT DLL|x64.ActiveCfg = Debug MT DLL|x64 100 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT DLL|x64.Build.0 = Debug MT DLL|x64 101 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT DLL|x86.ActiveCfg = Debug MT DLL|Win32 102 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT DLL|x86.Build.0 = Debug MT DLL|Win32 103 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT|x64.ActiveCfg = Debug MT|x64 104 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT|x64.Build.0 = Debug MT|x64 105 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT|x86.ActiveCfg = Debug MT|Win32 106 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT|x86.Build.0 = Debug MT|Win32 107 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug|x64.ActiveCfg = Debug MD|x64 108 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug|x64.Build.0 = Debug MD|x64 109 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug|x86.ActiveCfg = Debug MD|Win32 110 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Debug|x86.Build.0 = Debug MD|Win32 111 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release Kernel|x64.ActiveCfg = Release Kernel|x64 112 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release Kernel|x64.Build.0 = Release Kernel|x64 113 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release Kernel|x86.ActiveCfg = Release Kernel|Win32 114 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release Kernel|x86.Build.0 = Release Kernel|Win32 115 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD DLL|x64.ActiveCfg = Release MD DLL|x64 116 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD DLL|x64.Build.0 = Release MD DLL|x64 117 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD DLL|x86.ActiveCfg = Release MD DLL|Win32 118 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD DLL|x86.Build.0 = Release MD DLL|Win32 119 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD|x64.ActiveCfg = Release MD|x64 120 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD|x64.Build.0 = Release MD|x64 121 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD|x86.ActiveCfg = Release MD|Win32 122 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD|x86.Build.0 = Release MD|Win32 123 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT DLL|x64.ActiveCfg = Release MT DLL|x64 124 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT DLL|x64.Build.0 = Release MT DLL|x64 125 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT DLL|x86.ActiveCfg = Release MT DLL|Win32 126 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT DLL|x86.Build.0 = Release MT DLL|Win32 127 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT|x64.ActiveCfg = Release MT|x64 128 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT|x64.Build.0 = Release MT|x64 129 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT|x86.ActiveCfg = Release MT|Win32 130 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT|x86.Build.0 = Release MT|Win32 131 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release|x64.ActiveCfg = Release MD DLL|x64 132 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release|x64.Build.0 = Release MD DLL|x64 133 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release|x86.ActiveCfg = Release MT DLL|Win32 134 | {88A23124-5640-35A0-B890-311D7A67A7D2}.Release|x86.Build.0 = Release MT DLL|Win32 135 | EndGlobalSection 136 | GlobalSection(SolutionProperties) = preSolution 137 | HideSolutionNode = FALSE 138 | EndGlobalSection 139 | GlobalSection(ExtensibilityGlobals) = postSolution 140 | SolutionGuid = {2409743E-DDE1-4665-8C29-FA5C5C8DEEFE} 141 | EndGlobalSection 142 | EndGlobal 143 | -------------------------------------------------------------------------------- /Get-InjectedThreadEx/Get-InjectedThreadEx.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | x64 7 | 8 | 9 | Release 10 | x64 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 16.0 27 | Win32Proj 28 | {256d0782-6d09-44b2-ad5c-fc8ef085d1d3} 29 | GetInjectedThreadEx 30 | 10.0 31 | 32 | 33 | 34 | Application 35 | true 36 | v142 37 | Unicode 38 | 39 | 40 | Application 41 | false 42 | v143 43 | true 44 | Unicode 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | $(SolutionDir)\phnt;$(SolutionDir)\zydis\include;$(SolutionDir)\zydis\dependencies\zycore\include;$(IncludePath) 60 | $(SolutionDir)\zydis\msvc\bin\$(Configuration)$(Platform);$(LibraryPath) 61 | 62 | 63 | $(SolutionDir)\phnt;$(SolutionDir)\zydis\include;$(SolutionDir)\zydis\dependencies\zycore\include;$(IncludePath) 64 | $(SolutionDir)\zydis\msvc\bin\$(Configuration)$(Platform);$(LibraryPath) 65 | 66 | 67 | 68 | Level3 69 | true 70 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 71 | true 72 | stdcpp20 73 | stdc17 74 | Guard 75 | ProgramDatabase 76 | pch.h 77 | 78 | 79 | Console 80 | true 81 | 82 | 83 | 84 | 85 | Level3 86 | true 87 | true 88 | true 89 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 90 | true 91 | stdcpp20 92 | stdc17 93 | Guard 94 | pch.h 95 | 96 | 97 | Console 98 | true 99 | true 100 | true 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /Get-InjectedThreadEx/Get-InjectedThreadEx.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | Source Files 23 | 24 | 25 | Source Files 26 | 27 | 28 | Source Files 29 | 30 | 31 | Source Files 32 | 33 | 34 | Source Files 35 | 36 | 37 | Source Files 38 | 39 | 40 | 41 | 42 | Header Files 43 | 44 | 45 | -------------------------------------------------------------------------------- /Get-InjectedThreadEx/Memory.cpp: -------------------------------------------------------------------------------- 1 | #include "Get-InjectedThreadEx.h" 2 | 3 | BOOL IsExecutable(const MEMORY_BASIC_INFORMATION& mbi) { 4 | constexpr auto PAGE_EXECUTE_ANY = PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; 5 | return MEM_COMMIT == mbi.State && 0 != (mbi.Protect & PAGE_EXECUTE_ANY); 6 | } 7 | 8 | BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, std::string& buffer, const MEMORY_BASIC_INFORMATION& mbi) { 9 | if (MEM_COMMIT != mbi.State || 0 != (mbi.Protect & PAGE_GUARD)) 10 | return FALSE; 11 | // TODO(jdu) NEXT handle reads split over multiple regions 12 | return ReadProcessMemory(hProcess, address, buffer.data(), buffer.size(), NULL); 13 | } 14 | 15 | BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, std::string& buffer) { 16 | MEMORY_BASIC_INFORMATION mbi; 17 | if(!VirtualQueryEx(hProcess, address, &mbi, sizeof(mbi))) 18 | return FALSE; 19 | return ReadProcessMemorySafely(hProcess, address, buffer, mbi); 20 | } 21 | 22 | BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, PDWORD64 buffer) { 23 | MEMORY_BASIC_INFORMATION mbi; 24 | if (!VirtualQueryEx(hProcess, address, &mbi, sizeof(mbi))) 25 | return FALSE; 26 | auto b = MEM_COMMIT != mbi.State; 27 | auto b2 = 0 != (mbi.Protect & PAGE_GUARD); 28 | auto b3 = (MEM_COMMIT != mbi.State || 0 != (mbi.Protect & PAGE_GUARD)); 29 | if (MEM_COMMIT != mbi.State || 0 != (mbi.Protect & PAGE_GUARD)) 30 | return FALSE; 31 | return ReadProcessMemory(hProcess, address, buffer, sizeof(DWORD64), NULL); 32 | } 33 | 34 | BOOL GetMappedFileNameAsDosPath(HANDLE hProcess, PVOID address, std::wstring& buffer) { 35 | 36 | // Cache #1 - the mapping of shared image allocations to DOS paths 37 | const auto bIsSharedDll = InSystemImageRange(address); 38 | const auto allocationBase = (ULONG_PTR)address & ~0xFFFFull; 39 | static std::unordered_map s_DosPathCache; 40 | if (bIsSharedDll) { 41 | const auto it = s_DosPathCache.find(allocationBase); 42 | if (it != s_DosPathCache.end()) { 43 | buffer = it->second; 44 | return TRUE; 45 | } 46 | } 47 | 48 | // Cache #2 - the mapping from Device prefixes to Dos prefixes 49 | static std::unordered_map s_Device2DosPrefixMap; 50 | if (s_Device2DosPrefixMap.empty()) { 51 | wchar_t drive[3] = L"A:"; 52 | for (drive[0] = L'A'; drive[0] <= L'Z'; drive[0]++) { 53 | std::wstring deviceName; 54 | deviceName.resize(MAX_PATH); 55 | if (!QueryDosDeviceW(drive, deviceName.data(), (DWORD)deviceName.size())) { 56 | const auto dwError = GetLastError(); 57 | if (ERROR_FILE_NOT_FOUND != dwError) 58 | LogError("QueryDosDeviceW(%S) failed with %d\n", drive, dwError); 59 | continue; 60 | } 61 | deviceName.resize(wcslen(deviceName.c_str())); 62 | s_Device2DosPrefixMap.insert(std::make_pair(deviceName, std::wstring(drive))); 63 | } 64 | } 65 | 66 | buffer.clear(); 67 | buffer.resize(MAX_PATH); 68 | 69 | // Note - K32GetMappedFileName returns the device path such as \Device\Harddisk0\Windows\System32\ntdll.dll 70 | auto nBytesReturned = K32GetMappedFileNameW(hProcess, address, buffer.data(), (DWORD)buffer.size()); 71 | if (MAX_PATH == nBytesReturned) { 72 | buffer.resize(MAX_LONG_PATH); 73 | nBytesReturned = K32GetMappedFileNameW(hProcess, address, buffer.data(), (DWORD)buffer.size()); 74 | if (MAX_LONG_PATH == nBytesReturned) 75 | LogError("K32GetMappedFileNameW(%p) exceeded maximum long path - %S\n", address, buffer.substr(0, MAX_PATH).c_str()); 76 | } 77 | 78 | if (0 == nBytesReturned) 79 | return FALSE; 80 | 81 | // We have a device path - now convert it to a DOS path 82 | constexpr auto DEVICE_PREFIX = L"\\Device\\"; 83 | if (buffer.length() < sizeof(DEVICE_PREFIX) || 0 != _wcsnicmp(buffer.c_str(), DEVICE_PREFIX, sizeof(DEVICE_PREFIX))) { 84 | LogError("K32GetMappedFileNameW(%p) did not return device path - %S\n", address, buffer.substr(0, MAX_PATH).c_str()); 85 | return TRUE; // best effort - return what we have 86 | } 87 | 88 | // Loopkup DOS prefix of "\device\" component in our cached mapping 89 | const auto pos = buffer.find(L'\\', std::char_traits::length(DEVICE_PREFIX)); 90 | const auto devicePrefixLength = (std::wstring::npos == pos) ? buffer.length() : pos; 91 | const auto device2DosPrefixMapping = s_Device2DosPrefixMap.find(buffer.substr(0, devicePrefixLength)); 92 | if (s_Device2DosPrefixMap.end() == device2DosPrefixMapping) { 93 | LogError("Could not resolve device prefix to DOS drive letter - %S\n", buffer.substr(0, MAX_PATH).c_str()); 94 | return TRUE; // best effort - return what we have 95 | } 96 | 97 | buffer = device2DosPrefixMapping->second + buffer.substr(devicePrefixLength); 98 | buffer.resize(wcslen(buffer.c_str())); 99 | 100 | // Update our path cache 101 | if (nBytesReturned && bIsSharedDll) 102 | s_DosPathCache.insert(std::make_pair(allocationBase, buffer)); 103 | 104 | return TRUE; 105 | } 106 | 107 | std::string ToHex(std::string bytes) { 108 | std::stringstream hexBytes; 109 | hexBytes << std::hex << std::setfill('0'); 110 | for (size_t i = 0; i < bytes.length(); i++) { 111 | hexBytes << std::setw(2) << (DWORD)(BYTE)bytes[i]; 112 | } 113 | return hexBytes.str(); 114 | } -------------------------------------------------------------------------------- /Get-InjectedThreadEx/Process.cpp: -------------------------------------------------------------------------------- 1 | #include "Get-InjectedThreadEx.h" 2 | 3 | BOOL IsElevated() 4 | { 5 | BOOL bIsElevated = FALSE; 6 | 7 | DWORD dwSize; 8 | HANDLE hToken = NULL; 9 | TOKEN_ELEVATION elevation; 10 | 11 | if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken) && 12 | GetTokenInformation(hToken, TokenElevation, &elevation, sizeof(elevation), &dwSize) && 13 | sizeof(elevation) == dwSize) 14 | { 15 | bIsElevated = elevation.TokenIsElevated; 16 | } 17 | 18 | if (hToken) { 19 | (void)CloseHandle(hToken); 20 | } 21 | 22 | return bIsElevated; 23 | } 24 | 25 | BOOL GetProcessPath(HANDLE hProcess, std::wstring& buffer) { 26 | buffer.clear(); 27 | 28 | buffer.resize(MAX_PATH); 29 | DWORD size = (DWORD)buffer.size(); 30 | if (!QueryFullProcessImageNameW(hProcess, 0, buffer.data(), &size)) { 31 | buffer.resize(MAX_LONG_PATH); 32 | size = (DWORD)buffer.size(); 33 | if (!QueryFullProcessImageNameW(hProcess, 0, buffer.data(), &size)) { 34 | buffer.clear(); 35 | return FALSE; 36 | } 37 | } 38 | 39 | buffer.resize(size); 40 | return TRUE; 41 | } 42 | 43 | BOOL IsDotNet(HANDLE hProcess) { 44 | std::wstring executable; 45 | if (!GetProcessPath(hProcess, executable)) 46 | return FALSE; 47 | 48 | // Map the executable's PE headers. 49 | constexpr auto FILE_SHARE_ALL = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; 50 | auto hFile = CreateFileW(executable.c_str(), GENERIC_READ, FILE_SHARE_ALL, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); 51 | auto hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY, 0, 0, NULL); 52 | auto pMapping = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0x1000); 53 | 54 | // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only 55 | // The last directory is actually the CLR Runtime Header - not the "COM Descriptor". 56 | // If it exists, then the PE file is a CLR assembly - aka .NET 57 | ULONG size = 0; 58 | BOOL bIsDotNet = NULL != ImageDirectoryEntryToData(pMapping, TRUE, IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR, &size); 59 | 60 | if (pMapping) 61 | (void)UnmapViewOfFile(pMapping); 62 | if (hMapping) 63 | (void)CloseHandle(hMapping); 64 | if (hFile) 65 | (void)CloseHandle(hFile); 66 | 67 | return bIsDotNet; 68 | } 69 | -------------------------------------------------------------------------------- /Get-InjectedThreadEx/StackClimb.cpp: -------------------------------------------------------------------------------- 1 | #include "Get-InjectedThreadEx.h" 2 | 3 | 4 | void SymboliseCallStack(HANDLE hProcess, const std::vector returnAddresses, const std::string last, std::vector &callStackFrames) { 5 | for (const auto address : returnAddresses) { 6 | std::string frameSymbol; 7 | if (!GetNearestSymbolWithPdb(hProcess, address, frameSymbol)) 8 | frameSymbol = ""; 9 | callStackFrames.push_back(frameSymbol); 10 | } 11 | callStackFrames.push_back(last); 12 | } 13 | 14 | BOOL StackClimb64(const HANDLE hProcess, 15 | const PVOID stackBuffer[], 16 | const size_t stackBufferCount, 17 | std::vector& callStackFrames, 18 | bool* pbDetection, 19 | int offset) 20 | { 21 | std::vector returnAddresses; 22 | 23 | // Query SystemInfo for the usermode address bounds 24 | static SYSTEM_INFO s_si{}; 25 | if (!s_si.lpMaximumApplicationAddress) { 26 | GetSystemInfo(&s_si); 27 | if (0 == s_si.lpMaximumApplicationAddress) { 28 | LogError("GetSystemInfo() failed"); 29 | return FALSE; 30 | } 31 | 32 | // Note - My code makes some assumptions about page and allocation sizes. 33 | if (0x1000 != s_si.dwPageSize || 0x10000 != s_si.dwAllocationGranularity) { 34 | LogError("System not supported. PageSize=0x%x AllocationGranularity=0x%x", s_si.dwPageSize, s_si.dwAllocationGranularity); 35 | return FALSE; 36 | } 37 | } 38 | 39 | auto lastRspOffset = 0u; 40 | const auto StackBufferLast = &stackBuffer[stackBufferCount - 1]; 41 | for (auto i = offset; !*pbDetection && (i < stackBufferCount) && (returnAddresses.size() < MIN_FRAMES); i += 2) { 42 | const PVOID candidateRip = StackBufferLast[-i]; 43 | 44 | // Skip any invalid usermode addresses 45 | if (candidateRip < s_si.lpMinimumApplicationAddress || candidateRip >= s_si.lpMaximumApplicationAddress) 46 | continue; 47 | 48 | MEMORY_BASIC_INFORMATION mbi; 49 | if (!VirtualQueryEx(hProcess, candidateRip, &mbi, sizeof(mbi))) 50 | return FALSE; 51 | 52 | // Return address must be executable 53 | if (!IsExecutable(mbi)) 54 | continue; 55 | 56 | // We can't read this executable page due to the Guard - so just alert. 57 | if (0 != (mbi.State & PAGE_GUARD)) { 58 | SymboliseCallStack(hProcess, returnAddresses, "GUARD", callStackFrames); 59 | *pbDetection = true; 60 | return TRUE; 61 | } 62 | 63 | bool bValidCallsite; 64 | if (MEM_IMAGE != mbi.Type) { 65 | if (IsValidCallSite(hProcess, false, mbi, candidateRip, &bValidCallsite) && bValidCallsite) { 66 | // A suspicious frame was found! 67 | SymboliseCallStack(hProcess, returnAddresses, "PRIVATE", callStackFrames); 68 | *pbDetection = true; 69 | return TRUE; 70 | } 71 | continue; // Not a return address - keep searching 72 | } 73 | 74 | std::wstring candidateRipMappedPath; 75 | if (!GetMappedFileNameAsDosPath(hProcess, candidateRip, candidateRipMappedPath)) 76 | return FALSE; 77 | 78 | DWORD frameSize = 0; 79 | if (0 != lastRspOffset) { 80 | auto status = CalculateFrameSize(candidateRip, &frameSize, candidateRipMappedPath, mbi.AllocationBase); 81 | if (S_OK == status && (i - lastRspOffset) != frameSize) 82 | continue; // Invalid frame size - keep searching 83 | 84 | if (IS_ERROR(status)) // but not WARNINGs 85 | continue; 86 | 87 | // We could not calculate exact frame size - but do have a lowerbound. 88 | // TODO(jdu) We could scan ahead one (or more) frames here to see if we recover? 89 | if (S_OK != status && (i - lastRspOffset) < frameSize) 90 | continue; // Frame too small - keep searching 91 | } 92 | 93 | if (!IsValidCallSite(hProcess, false, mbi, candidateRip, &bValidCallsite) && !bValidCallsite) 94 | continue; // Not a return address - keep searching 95 | 96 | auto status = CalculateFrameSize(candidateRip, &frameSize, candidateRipMappedPath, mbi.AllocationBase); 97 | // LogDebug(" %03i: %p %s valid=%d frameSize=%d FP=%d", i, candidateRip, frameSymbol.c_str(), bValidCallsite, frameSize, S_OK != status); 98 | returnAddresses.push_back(candidateRip); 99 | 100 | lastRspOffset = i; 101 | 102 | // Frames must be at least 0x20 bytes due to fastcall parameter shadow space 103 | i += 4; 104 | } 105 | 106 | // Just use exports if nothing suspicious was found 107 | // TODO(jdu) We only need to return this at all so that we know if a 2nd pass is required. 108 | // Perhaps move 2nd pass and RIP logic here. 109 | if (callStackFrames.empty()) { 110 | for (const auto address : returnAddresses) { 111 | std::string frameSymbol; 112 | (void)GetNearestSymbol(hProcess, address, frameSymbol); 113 | callStackFrames.push_back(frameSymbol); 114 | } 115 | } 116 | 117 | return TRUE; 118 | } -------------------------------------------------------------------------------- /Get-InjectedThreadEx/Symbol.cpp: -------------------------------------------------------------------------------- 1 | #include "Get-InjectedThreadEx.h" 2 | 3 | typedef struct _SYMBOL_INFO_FULL : SYMBOL_INFO { 4 | CHAR NameBuffer[MAX_SYM_NAME - 1]; 5 | } SYMBOL_INFO_FULL; 6 | #define INIT_FAILED ((HANDLE)-3) 7 | 8 | constexpr auto SYMOPTS = SYMOPT_UNDNAME | SYMOPT_CASE_INSENSITIVE | SYMOPT_FAIL_CRITICAL_ERRORS | SYMOPT_INCLUDE_32BIT_MODULES; 9 | 10 | constexpr auto MicrosoftPublicSymbols = L"srv**https://msdl.microsoft.com/download/symbols"; 11 | 12 | static HANDLE GetUniqueIdForSymbols() { 13 | // Initialise once - note we're only resolving symbols locally. 14 | static HANDLE s_hUniqueId = NULL; // INVALID_HANDLE_VALUE == GetCurrentProcess()! 15 | if (NULL == s_hUniqueId) { 16 | if (!SymSetOptions(SymGetOptions() | SYMOPTS)) 17 | LogError("SymSetOptions() failed with 0x%x", GetLastError()); 18 | s_hUniqueId = GetCurrentProcess(); 19 | if (!SymInitializeW(s_hUniqueId, MicrosoftPublicSymbols, FALSE)) { 20 | LogError("SymInitialize() failed with 0x%x", GetLastError()); 21 | s_hUniqueId = INIT_FAILED; 22 | } 23 | else 24 | { 25 | // Check that we're using public symbols - not just exports 26 | auto hNtdll = GetModuleHandleW(L"ntdll.dll"); 27 | std::wstring ntdllPath; 28 | if (!GetMappedFileNameAsDosPath(GetCurrentProcess(), hNtdll, ntdllPath)) 29 | LogError("GetMappedFilename(ntdll) failed"); 30 | if(!SymLoadModuleExW(s_hUniqueId, NULL, ntdllPath.c_str(), NULL, (DWORD64)hNtdll, 0, NULL, 0)) 31 | LogError("SymLoadModule(ntdll) failed"); 32 | if (!GetSymbolAddress("ntdll!TppWorkerThread")) 33 | LogError("WARNING Symbols not found - falling back to exports"); 34 | 35 | auto hKernel32 = GetModuleHandleW(L"kernel32.dll"); 36 | std::wstring kernel32Path; 37 | if (!GetMappedFileNameAsDosPath(GetCurrentProcess(), hKernel32, kernel32Path)) 38 | LogError("GetMappedFilename(kernel32) failed"); 39 | if (!SymLoadModuleExW(s_hUniqueId, NULL, kernel32Path.c_str(), NULL, (DWORD64)hKernel32, 0, NULL, 0)) 40 | LogError("SymLoadModule(kernel32) failed"); 41 | } 42 | } 43 | return s_hUniqueId; 44 | } 45 | 46 | PVOID GetSymbolAddress(const char symbol[]) { 47 | const auto hUniqueId = GetUniqueIdForSymbols(); 48 | if (INIT_FAILED == hUniqueId) 49 | return NULL; 50 | 51 | SYMBOL_INFO_FULL symbolInfo{}; 52 | symbolInfo.SizeOfStruct = sizeof(SYMBOL_INFO); 53 | symbolInfo.MaxNameLen = MAX_SYM_NAME; 54 | if (!SymFromName(hUniqueId, symbol, &symbolInfo)) 55 | return NULL; 56 | 57 | return (PVOID)symbolInfo.Address; 58 | } 59 | 60 | 61 | // pretty print ![+] 62 | void GetPrettySymbol(HANDLE hProcess, PVOID address, std::wstring& modulePath, std::string& symbol, bool bIncludeDisplacement) { 63 | symbol.resize(128); 64 | symbol = std::filesystem::path(modulePath).filename().string(); 65 | 66 | SYMBOL_INFO_FULL symbolInfo{}; 67 | symbolInfo.SizeOfStruct = sizeof(SYMBOL_INFO); 68 | symbolInfo.MaxNameLen = MAX_SYM_NAME; 69 | DWORD64 displacement = 0; 70 | if (SymFromAddr(hProcess, (DWORD64)address, &displacement, &symbolInfo)) { 71 | symbol += "!" + std::string(symbolInfo.Name); 72 | } 73 | else if (bIncludeDisplacement) { 74 | MEMORY_BASIC_INFORMATION mbi; 75 | if (VirtualQueryEx(hProcess, address, &mbi, sizeof(mbi))) 76 | displacement = (DWORD64)address - (DWORD64)mbi.AllocationBase; 77 | } 78 | 79 | if (bIncludeDisplacement && displacement) { 80 | char buffer[_MAX_U64TOSTR_BASE16_COUNT]; 81 | _ui64toa_s(displacement, buffer, sizeof(buffer), 16); 82 | symbol += "+0x" + std::string(buffer); 83 | } 84 | 85 | symbol.resize(strlen(symbol.c_str())); 86 | } 87 | 88 | BOOL GetNearestSymbolWithPdb(HANDLE hProcess, PVOID address, std::string& symbol, bool bIncludeDisplacement) { 89 | if (!SymSetOptions(SymGetOptions() | SYMOPTS)) 90 | LogError("SymSetOptions() failed with 0x%x", GetLastError()); 91 | 92 | if (!SymInitializeW(hProcess, MicrosoftPublicSymbols, FALSE)) { 93 | LogError("SymInitialize() failed with 0x%x", GetLastError()); 94 | return FALSE; 95 | } 96 | 97 | std::wstring modulePath; 98 | if (!GetMappedFileNameAsDosPath(hProcess, address, modulePath)) { 99 | if (symbol.empty()) { 100 | char buffer[_MAX_U64TOSTR_BASE16_COUNT]; 101 | _ui64toa_s((DWORD64)address, buffer, sizeof(buffer), 16); 102 | symbol = "0x" + std::string(buffer); 103 | } 104 | (void)SymCleanup(hProcess); 105 | return TRUE; 106 | } 107 | 108 | MEMORY_BASIC_INFORMATION mbi; 109 | if (!VirtualQueryEx(hProcess, address, &mbi, sizeof(mbi)) || 110 | !SymLoadModuleExW(hProcess, NULL, modulePath.c_str(), NULL, (DWORD64)mbi.AllocationBase, 0, NULL, 0)) 111 | { 112 | (void)SymCleanup(hProcess); 113 | // fallback to best-effort 114 | const auto hUniqueId = GetUniqueIdForSymbols(); 115 | GetPrettySymbol(hUniqueId, address, modulePath, symbol, bIncludeDisplacement); 116 | return FALSE; 117 | } 118 | 119 | GetPrettySymbol(hProcess, address, modulePath, symbol, bIncludeDisplacement); 120 | 121 | (void)SymCleanup(hProcess); 122 | return TRUE; 123 | } 124 | 125 | 126 | BOOL GetNearestSymbol(HANDLE hProcess, PVOID address, std::string& symbol, bool bIncludeDisplacement) { 127 | const auto hUniqueId = GetUniqueIdForSymbols(); 128 | if (INIT_FAILED == hUniqueId) 129 | return FALSE; 130 | 131 | // Maintain a cache of symbols in shared image allocations 132 | static std::unordered_map s_SymbolCache; 133 | const auto it = s_SymbolCache.find(address); 134 | if (it != s_SymbolCache.end()) { 135 | symbol = it->second; 136 | return TRUE; 137 | } 138 | 139 | // module name (from remote process) 140 | std::wstring modulePath; 141 | if (!GetMappedFileNameAsDosPath(hProcess, address, modulePath)) { 142 | char buffer[_MAX_U64TOSTR_BASE16_COUNT]; 143 | _ui64toa_s((DWORD64)address, buffer, sizeof(buffer), 16); 144 | symbol = "0x" + std::string(buffer); 145 | return TRUE; 146 | } 147 | 148 | GetPrettySymbol(hUniqueId, address, modulePath, symbol, bIncludeDisplacement); 149 | 150 | if (InSystemImageRange(address)) 151 | s_SymbolCache.insert(std::make_pair(address, symbol)); 152 | 153 | return TRUE; 154 | } 155 | -------------------------------------------------------------------------------- /Get-InjectedThreadEx/Unwind.cpp: -------------------------------------------------------------------------------- 1 | // Reference: 2 | // https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64 3 | // 4 | // Other useful references: 5 | // https://codemachine.com/articles/x64_deep_dive.html 6 | // http://www.uninformed.org/?v=4&a=1 Improving Automated Analysis of Windows x64 Binaries 7 | // http://www.nynaeve.net/?p=113 Programming against the x64 exception handling support 8 | // https://www.sciencedirect.com/science/article/pii/S1742287618300458 Building stack traces from memory dump of Windows x64 9 | // https://github.com/reactos/reactos/blob/master/sdk/lib/rtl/amd64/unwind.c 10 | // https://auscitte.github.io/posts/Exception-Directory-pefile Boots for Walking Backwards: Teaching pefile How to Understand SEH-Related Data in 64-bit PE Files 11 | // https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs 12 | 13 | #include "Get-InjectedThreadEx.h" 14 | 15 | typedef enum _UNWIND_OP_CODES { 16 | UWOP_PUSH_NONVOL = 0, /* info == register number */ 17 | UWOP_ALLOC_LARGE, /* no info, alloc size in next 2 slots */ 18 | UWOP_ALLOC_SMALL, /* info == size of allocation / 8 - 1 */ 19 | UWOP_SET_FPREG, /* no info, FP = RSP + UNWIND_INFO.FPRegOffset*16 */ 20 | UWOP_SAVE_NONVOL, /* info == register number, offset in next slot */ 21 | UWOP_SAVE_NONVOL_FAR, /* info == register number, offset in next 2 slots */ 22 | UWOP_EPILOG, /* added in v2. UNDOCUMENTED. */ 23 | UWOP_SAVE_XMM128 = 8, /* info == XMM reg number, offset in next slot */ 24 | UWOP_SAVE_XMM128_FAR, /* info == XMM reg number, offset in next 2 slots */ 25 | UWOP_PUSH_MACHFRAME /* info == 0: no dwError-code, 1: dwError-code */ 26 | } UNWIND_CODE_OPS; 27 | 28 | // from ehdata.h 29 | #pragma warning (push) 30 | #pragma warning (disable: 4201) 31 | typedef union _UNWIND_CODE { 32 | struct { 33 | unsigned char CodeOffset; 34 | unsigned char UnwindOp : 4; 35 | unsigned char OpInfo : 4; 36 | }; 37 | unsigned short FrameOffset; 38 | } UNWIND_CODE, * PUNWIND_CODE; 39 | typedef struct _UNWIND_INFO { 40 | unsigned char Version : 3; 41 | unsigned char Flags : 5; 42 | unsigned char SizeOfProlog; 43 | unsigned char CountOfCodes; 44 | unsigned char FrameRegister : 4; 45 | unsigned char FrameOffset : 4; 46 | UNWIND_CODE UnwindCode[1]; 47 | /* UNWIND_CODE MoreUnwindCode[((CountOfCodes+1)&~1)-1]; 48 | * union { 49 | * OPTIONAL unsigned long ExceptionHandler; 50 | * OPTIONAL unsigned long FunctionEntry; 51 | * }; 52 | * OPTIONAL unsigned long ExceptionData[]; 53 | */ 54 | } UNWIND_INFO, * PUNWIND_INFO; 55 | #pragma warning (pop) 56 | 57 | constexpr auto W_FPREG = 1; // WARNING - frame pointer in use; 58 | static HRESULT CalculateFrameSize_Internal(const PRUNTIME_FUNCTION pRuntimeFunction, const DWORD64 ImageBase, const DWORD rva, PDWORD pFrameSize) 59 | { 60 | HRESULT status = S_OK; 61 | 62 | const auto pUnwindInfo = (PUNWIND_INFO)(pRuntimeFunction->UnwindInfoAddress + ImageBase); 63 | if (!pUnwindInfo || pUnwindInfo->Version > 2) { 64 | LogError("UNWIND_INFO v%d not supported", pUnwindInfo->Version); 65 | return E_NOTIMPL; 66 | } 67 | 68 | const auto codeOffset = rva - pRuntimeFunction->BeginAddress; 69 | 70 | // Loop over unwind codes and calculate total stack space used by target function. 71 | BYTE i = 0; 72 | while (i < pUnwindInfo->CountOfCodes) 73 | { 74 | // Warning: This implementation is not complete and may not work for some edge cases. 75 | // For example, it does not handle handle RIP pointing into an epilog. 76 | // But that should be rare for our early frames use case. 77 | 78 | const auto unwindOperation = pUnwindInfo->UnwindCode[i].UnwindOp; 79 | const auto operationInfo = pUnwindInfo->UnwindCode[i].OpInfo; 80 | const auto bApplyOperation = codeOffset > pUnwindInfo->UnwindCode[i].CodeOffset; 81 | 82 | i++; 83 | switch (unwindOperation) { 84 | case UWOP_PUSH_NONVOL: 85 | if (bApplyOperation) 86 | (*pFrameSize)++; 87 | break; 88 | case UWOP_ALLOC_LARGE: 89 | switch (operationInfo) { 90 | case 0: 91 | if (bApplyOperation) 92 | *pFrameSize += pUnwindInfo->UnwindCode[i].FrameOffset; 93 | i++; 94 | break; 95 | case 1: 96 | if (bApplyOperation) 97 | *pFrameSize += *(DWORD*)(&pUnwindInfo->UnwindCode[i]) / sizeof(PVOID); 98 | i += 3; 99 | break; 100 | default: 101 | LogError("UWOP_ALLOC_LARGE operationInfo is invalid: (%d)", operationInfo); 102 | return E_INVALIDARG; 103 | } 104 | break; 105 | case UWOP_ALLOC_SMALL: 106 | if (bApplyOperation) 107 | *pFrameSize += operationInfo + 1; 108 | break; 109 | case UWOP_SET_FPREG: 110 | if (bApplyOperation) { 111 | // Frame pointer in use. Our frame size calculation is inaccurate if alloca() was used. 112 | // https://learn.microsoft.com/en-us/cpp/build/stack-usage 113 | // > If space is dynamically allocated (alloca) in a function, then a 114 | // > nonvolatile register must be used as a frame pointer to mark the 115 | // > base of the fixed part of the stack and that register must be saved 116 | // > and initialized in the prolog. 117 | *pFrameSize = pUnwindInfo->FrameOffset * 2; 118 | status = W_FPREG; // This is a WARNING code - not an ERROR 119 | } 120 | break; 121 | case UWOP_SAVE_NONVOL: 122 | case UWOP_SAVE_XMM128: 123 | case UWOP_EPILOG: 124 | i++; 125 | break; 126 | case UWOP_SAVE_NONVOL_FAR: 127 | case UWOP_SAVE_XMM128_FAR: 128 | i += 2; 129 | break; 130 | case UWOP_PUSH_MACHFRAME: 131 | if (bApplyOperation) { 132 | *pFrameSize += 5 + operationInfo; 133 | } 134 | break; 135 | default: 136 | if (bApplyOperation) { 137 | LogError("UNWIND_INFO operation is not implemented: %d", unwindOperation); 138 | return E_INVALIDARG; 139 | } 140 | } 141 | } 142 | 143 | if (0 != (UNW_FLAG_CHAININFO & pUnwindInfo->Flags)) { 144 | const auto pPrimaryUwindInfo = (PRUNTIME_FUNCTION) & (pUnwindInfo->UnwindCode[(pUnwindInfo->CountOfCodes + 1) & ~1]); 145 | return CalculateFrameSize_Internal(pPrimaryUwindInfo, ImageBase, rva, pFrameSize); 146 | } 147 | 148 | // Add the size of the return address. 149 | *pFrameSize += 1; 150 | 151 | return status; 152 | } 153 | 154 | static_assert(!IS_ERROR(W_FPREG)); 155 | static_assert(!FAILED(W_FPREG)); 156 | // Calculates the total stack space (in PVOIDs) used by the stack frame. 157 | HRESULT CalculateFrameSize(PVOID returnAddress, PDWORD pFrameSize, const std::wstring& filepath, PVOID remoteBase) 158 | { 159 | // Check a cache of UNWIND_INFO lookups first. 160 | static std::unordered_map frameSizes; 161 | const auto it = frameSizes.find(returnAddress); 162 | if (it != frameSizes.end()) { 163 | *pFrameSize = it->second; 164 | return S_OK; 165 | } 166 | 167 | const DWORD rva = (DWORD)((ULONG_PTR)returnAddress - (ULONG_PTR)remoteBase); 168 | PRUNTIME_FUNCTION pRuntimeFunction = NULL; 169 | DWORD64 localBase; 170 | 171 | auto hModule = GetModuleHandleW(filepath.c_str()); 172 | if (hModule == remoteBase) { 173 | // Image is already loaded in our process - lock it in memory now. 174 | hModule = LoadLibraryW(filepath.c_str()); 175 | if (NULL == hModule) { 176 | LogError("LoadLibrary(%S) failed", filepath.c_str()); 177 | return E_UNEXPECTED; 178 | 179 | } 180 | 181 | pRuntimeFunction = RtlLookupFunctionEntry((DWORD64)returnAddress, &localBase, NULL); 182 | if (NULL == pRuntimeFunction) 183 | return E_FAIL; 184 | } 185 | else { 186 | // Image is not loaded - so load it now. 187 | // We don't want to execute anything malicious in our process - so map it now as a read-only resource. 188 | hModule = LoadLibraryExW(filepath.c_str(), NULL, LOAD_LIBRARY_AS_IMAGE_RESOURCE | LOAD_LIBRARY_AS_DATAFILE); 189 | if (NULL == hModule) { 190 | LogDebug("LoadLibraryEx(%S, AS_IMAGE_RESOURCE) failed", filepath.c_str()); 191 | return E_UNEXPECTED; 192 | } 193 | localBase = (DWORD64)hModule & ~0xFFFF; // The lower bits of resource-only module handles are used as flags. 194 | 195 | // Being resource-only means that we can't use the usual APIs. 196 | // Instead, manually walk the UNWIND_INFO in the exception directory of the image. 197 | ULONG indexLo = 0; 198 | ULONG indexHi; 199 | auto pTable = (PRUNTIME_FUNCTION)ImageDirectoryEntryToData((PVOID)localBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXCEPTION, &indexHi); 200 | if (!pTable) { 201 | (void)FreeLibrary(hModule); 202 | const auto error = GetLastError(); 203 | if (ERROR_SUCCESS != error) 204 | LogError("ImageDirectoryEntryToData(%S) failed with %d", filepath.c_str(), error); 205 | else 206 | LogDebug("ImageDirectoryEntryToData(%S) - Exception Directory not found"); 207 | return E_FAIL; 208 | } 209 | 210 | indexHi = indexHi / sizeof(RUNTIME_FUNCTION); 211 | while (indexHi > indexLo) { 212 | const ULONG indexMid = (indexLo + indexHi) / 2; 213 | pRuntimeFunction = &pTable[indexMid]; 214 | if (rva < pRuntimeFunction->BeginAddress) { 215 | indexHi = indexMid; // search lower 216 | pRuntimeFunction = NULL; 217 | continue; 218 | } 219 | if (rva >= pRuntimeFunction->EndAddress) { 220 | indexLo = indexMid + 1; // search higher 221 | pRuntimeFunction = NULL; 222 | continue; 223 | } 224 | break; // found 225 | } 226 | } 227 | 228 | if (!pRuntimeFunction) { 229 | (void)FreeLibrary(hModule); 230 | return E_FAIL; // leaf functions can't call further functions 231 | } 232 | 233 | *pFrameSize = 0; 234 | auto status = CalculateFrameSize_Internal(pRuntimeFunction, localBase, rva, pFrameSize); 235 | if (S_OK == status) 236 | frameSizes.insert(std::make_pair(returnAddress, *pFrameSize)); 237 | 238 | if (!FreeLibrary(hModule)) 239 | LogError("FreeLibrary(%S) failed", filepath.c_str()); 240 | 241 | return status; 242 | } 243 | 244 | BOOL IsValidCallSite( 245 | HANDLE hProcess, 246 | bool bIsWow64, 247 | const MEMORY_BASIC_INFORMATION& mbi, 248 | PVOID callsite, 249 | bool* bCallFound) 250 | { 251 | // Check a cache of valid callsites first 252 | static std::unordered_set validCallSites; 253 | if (validCallSites.contains(callsite)) { 254 | *bCallFound = true; 255 | return TRUE; 256 | } 257 | 258 | // Otherwise read the preceding bytes and check for a valid call instruction 259 | std::string precedingBytes{}; 260 | precedingBytes.resize(std::min(11ull, (ULONG_PTR)callsite - (ULONG_PTR)mbi.BaseAddress)); 261 | if(!ReadProcessMemorySafely(hProcess, (PVOID)((ULONG_PTR)callsite - precedingBytes.size()), precedingBytes, mbi)) 262 | return FALSE; 263 | 264 | ZydisDecoder decoder; 265 | if (bIsWow64) 266 | (void)ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_COMPAT_32, ZYDIS_STACK_WIDTH_32); 267 | else 268 | (void)ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64); 269 | 270 | *bCallFound = false; 271 | ZydisDecodedInstruction instruction; 272 | // call instructions are 2-11 bytes, but check some common lengths first 273 | static const std::array callLengthSearchOrder = { 5, 6, 7, 2, 3, 4, 8, 9, 10, 11 }; 274 | for (const auto& lengthToCheck : callLengthSearchOrder) { 275 | if (lengthToCheck <= precedingBytes.size() && 276 | ZYAN_SUCCESS(ZydisDecoderDecodeInstruction(&decoder, nullptr, precedingBytes.data() + precedingBytes.size() - lengthToCheck, lengthToCheck, &instruction)) && 277 | lengthToCheck == instruction.length && 278 | ZYDIS_MNEMONIC_CALL == instruction.mnemonic) 279 | { 280 | *bCallFound = true; 281 | // Note - For direct calls, we could (attempt to) improve this check by validating 282 | // that the call target matches the return address. 283 | // Though we would need to handle an Tail Call Optimised (TCO) functions... 284 | // 285 | // In practice this additional validation isn't required for our use case. 286 | // False positivies are rare in early stack frames. 287 | 288 | // Cache the result 289 | if (MEM_IMAGE == mbi.Type && InSystemImageRange(callsite)) 290 | validCallSites.insert(callsite); 291 | break; 292 | } 293 | } 294 | 295 | return TRUE; 296 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2021 Florian Bernd 4 | Copyright (c) 2014-2021 Joel Höner 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![screenshot](Get-InjectedThreadEx.png) 2 | 3 | # Get-InjectedThreadEx 4 | 5 | [Get-InjectedThreadEx.exe](https://github.com/jdu2600/Get-InjectedThreadEx/releases/latest) scans all running threads looking for suspicious Win32StartAddresses. 6 | 7 | Win32Startaddress anomalies include - 8 | * not MEM_IMAGE 9 | * non-MEM_IMAGE return address within the first 5 stack frames 10 | * MEM_IMAGE and on a private (modified) page 11 | * MEM_IMAGE and x64 dll and not a valid indirect call target 12 | * MEM_IMAGE and unexpected Win32 dll 13 | * MEM_IMAGE and x64 and unexpected prolog 14 | * MEM_IMAGE and preceded by unexpected bytes 15 | 16 | See my [BSides Canberra 2023 talk](https://github.com/jdu2600/conference_talks/blob/main/2023-09-bsidescbr-GetInjectedThreadEx.pdf) and [Elastic Security Labs blog](https://www.elastic.co/security-labs/get-injectedthreadex-detection-thread-creation-trampolines) for more details. 17 | 18 | --------------------------------------------------------------------------------