mirror of
https://github.com/jdu2600/Get-InjectedThreadEx
synced 2026-06-08 14:59:49 +00:00
BSides Canberra 2023 (#2)
This commit is contained in:
+398
@@ -0,0 +1,398 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "Get-InjectedThreadEx/zydis"]
|
||||
path = Get-InjectedThreadEx/zydis
|
||||
url = https://github.com/zyantific/zydis
|
||||
@@ -0,0 +1,569 @@
|
||||
// Get-InjectedThreadEx.cpp : A C++ implementation of Get-InjectedThreadEx.ps1
|
||||
//
|
||||
// .SYNOPSIS
|
||||
//
|
||||
// Looks for threads that were created as a result of code injection.
|
||||
//
|
||||
// .DESCRIPTION
|
||||
//
|
||||
// Memory resident malware (fileless malware) often uses a form of memory injection to get code execution.
|
||||
// Get-InjectedThreadEx looks at each running thread to determine if it is the result of memory injection.
|
||||
//
|
||||
// Win32StartAddress
|
||||
//
|
||||
// original
|
||||
// - not MEM_IMAGE
|
||||
// new
|
||||
// - not MEM_COMMIT
|
||||
// - MEM_IMAGE and Win32StartAddress is on a private (modified) page
|
||||
// - MEM_IMAGE and x64 dll and Win32StartAddress is CFG violation or suppressed export
|
||||
// - MEM_IMAGE and Win32StartAddress is in a suspicious module
|
||||
// - MEM_IMAGE and x64 and Win32StartAddress is unexpected prolog
|
||||
// - MEM_IMAGE and Win32StartAddress is preceded by unexpected bytes
|
||||
// - MEM_IMAGE and x64 and Win32StartAddress wraps non-MEM_IMAGE start address
|
||||
//
|
||||
// KNOWN LIMITATIONS:
|
||||
// - Only detects suspicious thread creations - not hijacks of existing threads.
|
||||
// - Some WoW64 support not implemented.
|
||||
|
||||
#include "Get-InjectedThreadEx/Get-InjectedThreadEx.h"
|
||||
|
||||
BOOL ScanThread(HANDLE hProcess, BOOL bIsDotNet, PSS_THREAD_ENTRY& thread, std::string& symbol, std::vector<std::string>& detections) {
|
||||
if (thread.Flags & PSS_THREAD_FLAGS_TERMINATED)
|
||||
return FALSE;
|
||||
|
||||
if (!GetNearestSymbol(hProcess, thread.Win32StartAddress, symbol, true))
|
||||
return FALSE;
|
||||
|
||||
if (thread.ContextRecord && thread.ContextRecord->Dr6)
|
||||
detections.push_back("hw_breakpoint");
|
||||
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (!VirtualQueryEx(hProcess, thread.Win32StartAddress, &mbi, sizeof(mbi)))
|
||||
return FALSE;
|
||||
|
||||
if (MEM_IMAGE != mbi.Type && MEM_COMMIT == mbi.State) {
|
||||
detections.push_back("PRIVATE");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Has our MEM_IMAGE Win32StartAddress been (naively) hooked?
|
||||
// https://blog.redbluepurple.io/offensive-research/bypassing-injection-detection//creating-the-thread
|
||||
// Note - checking against bytes on disk after the fact won't help with false positives
|
||||
// as the hook can easily be removed after thread start.
|
||||
// Detection gap - the hook could easily be deeper, potentially even in a subsequent call. :-(
|
||||
// Microsoft-Windows-Threat-Intelligence ETW events should detect this more robustly.
|
||||
PSAPI_WORKING_SET_EX_INFORMATION pwsei{};
|
||||
pwsei.VirtualAddress = thread.Win32StartAddress;
|
||||
if (K32QueryWorkingSetEx(hProcess, &pwsei, sizeof(pwsei)) && !pwsei.VirtualAttributes.Shared) {
|
||||
// I'm slightly worried about security vendor hooks landing on the same page
|
||||
// as ntdll!TppWorkerThread and causing a false positive flood.
|
||||
// So ignore ntdll modifications if we've been hooked too.
|
||||
static bool s_edrFalsePositive = symbol.starts_with("ntdll.dll!") &&
|
||||
K32QueryWorkingSetEx(GetCurrentProcess(), &pwsei, sizeof(pwsei)) && !pwsei.VirtualAttributes.Shared;
|
||||
if(!s_edrFalsePositive)
|
||||
detections.push_back("private_image");
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Check for suspcious CFG BitMap states in our local pristine copy of the x64 bitmap
|
||||
// Notes - executable CFG bitmaps are not shared - only library (dll) ones.
|
||||
// - only 16-bytes aligned addresses, as this is a SetProcessValidCallTargets() requirement.
|
||||
ULONG cfgBits;
|
||||
if (InSystemImageRange(thread.Win32StartAddress) && 0 == ((ULONG_PTR)thread.Win32StartAddress & 0xF) &&
|
||||
GetCfgBitsForAddress(thread.Win32StartAddress, &cfgBits) && 0 == cfgBits)
|
||||
{
|
||||
detections.push_back("cfg_invalid");
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Suspicious start modules
|
||||
|
||||
std::wstring mappedPath;
|
||||
// The file path assocated with Win32StartAddressModule
|
||||
if (!GetMappedFileNameAsDosPath(hProcess, thread.Win32StartAddress, mappedPath))
|
||||
return FALSE;
|
||||
|
||||
// There are no valid thread entry points (that I know of) in many Win32 modules.
|
||||
const std::array<std::string, 11> modulesWithoutThreadEntrypoints = {
|
||||
"kernel32", "kernelbase", "user32", "advapi32",
|
||||
"psapi", "dbghelp", "imagehlp", "powrprof",
|
||||
"verifier", "setupapi", "rpcrt4" }; // ...and many more
|
||||
const auto startModule = std::filesystem::path(mappedPath).stem().string();
|
||||
for (const auto& module : modulesWithoutThreadEntrypoints)
|
||||
if (startModule == module) {
|
||||
(void)GetNearestSymbolWithPdb(hProcess, thread.Win32StartAddress, symbol);
|
||||
detections.push_back("unexpected(" + startModule + ")");
|
||||
}
|
||||
|
||||
// kernel32!LoadLibrary
|
||||
// And, even if there are, LoadLibrary is always a suspicious start address.
|
||||
static auto hKernel32 = GetModuleHandleW(L"kernel32.dll");
|
||||
static auto pLoadLibraryW = GetProcAddress(hKernel32, "LoadLibraryW");
|
||||
static auto pLoadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA");
|
||||
if (pLoadLibraryA == thread.Win32StartAddress || pLoadLibraryW == thread.Win32StartAddress)
|
||||
detections.push_back("unexpected(" + symbol + ")");
|
||||
|
||||
// ntdll.dll but not a known entrypoint.
|
||||
// These are the only valid thread entry points in ntdll that I know of.
|
||||
static const std::array<PVOID, 4> ntdllThreadEntryPoints = {
|
||||
GetSymbolAddress("ntdll!TppWorkerThread"),
|
||||
GetSymbolAddress("ntdll!EtwpLogger"),
|
||||
GetSymbolAddress("ntdll!DbgUiRemoteBreakin"),
|
||||
GetSymbolAddress("ntdll!RtlpQueryProcessDebugInformationRemote")
|
||||
};
|
||||
|
||||
if (mappedPath.ends_with(L"\\System32\\ntdll.dll")) {
|
||||
auto bInvalidNtdllEntry = true;
|
||||
for (const auto& address : ntdllThreadEntryPoints)
|
||||
bInvalidNtdllEntry &= address == thread.Win32StartAddress;
|
||||
if (bInvalidNtdllEntry) {
|
||||
detections.push_back("unexpected(" + symbol + ")");
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Common setup for the disassembler
|
||||
constexpr auto MAX_INSN_LENGTH = 11ull; // theoretically 15, but empirically lower
|
||||
ZydisDecoder decoder;
|
||||
ZydisDecodedInstruction instruction;
|
||||
// WoW64 can be inferred from the TEB address.
|
||||
const bool bIsWow64 = (ULONG_PTR)thread.TebBaseAddress < 0x80000000;
|
||||
if (bIsWow64)
|
||||
(void)ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_COMPAT_32, ZYDIS_STACK_WIDTH_32);
|
||||
else
|
||||
(void)ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Check the bytes immmediately after Win32StartAddress
|
||||
// They must be a function entrypoint.
|
||||
// ... but almost anything is a valid entrypoint!
|
||||
// x64 prologs have more structure (albiet mostly by convention) - so we'll stick those.
|
||||
// See https://learn.microsoft.com/en-us/cpp/build/prolog-and-epilog
|
||||
//
|
||||
// Note - the loader ignores AddressOfEntry in CLR assemblies so we need to ignore them too.
|
||||
auto bIsDotNetProcessEntrypoint = bIsDotNet && mappedPath.ends_with(L".exe");
|
||||
|
||||
if (!bIsWow64 && !bIsDotNetProcessEntrypoint) {
|
||||
std::string startBytes;
|
||||
constexpr auto MAX_PROLOG_SIZE = 64;
|
||||
startBytes.resize(MAX_PROLOG_SIZE);
|
||||
if (!ReadProcessMemorySafely(hProcess, thread.Win32StartAddress, startBytes, mbi))
|
||||
return FALSE;
|
||||
|
||||
auto i = 0;
|
||||
bool bValidInstruction = true;
|
||||
ZyanU64 instructionPointer = (ZyanU64)thread.Win32StartAddress; // track this to calculate relative targets
|
||||
auto framePointer = ZYDIS_REGISTER_RSP;
|
||||
ZydisDecoderContext ctx{};
|
||||
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT];
|
||||
ZydisRegisterContext registers{};
|
||||
std::string originalBytes; // if we follow a jump keep original bytes
|
||||
|
||||
const auto IsStackOperation = [&]() -> bool {
|
||||
return ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) &&
|
||||
ZYDIS_OPERAND_TYPE_REGISTER == operands[0].type && ZYDIS_REGISTER_RSP == operands[0].reg.value;
|
||||
|
||||
};
|
||||
|
||||
const auto IsSaveRegisterOperation = [&]() -> bool {
|
||||
return ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) &&
|
||||
ZYDIS_OPERAND_TYPE_MEMORY == operands[0].type && (ZYDIS_REGISTER_RSP == operands[0].mem.base || framePointer == operands[0].mem.base) &&
|
||||
ZYDIS_OPERAND_TYPE_REGISTER == operands[1].type;
|
||||
};
|
||||
|
||||
const auto IsFramePointerOperation = [&]() {
|
||||
const auto bIsFP = ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) &&
|
||||
ZYDIS_OPERAND_TYPE_REGISTER == operands[0].type &&
|
||||
((ZYDIS_OPERAND_TYPE_REGISTER == operands[1].type && (ZYDIS_REGISTER_RSP == operands[1].reg.value || framePointer == operands[1].reg.value)) ||
|
||||
(ZYDIS_OPERAND_TYPE_MEMORY == operands[1].type && (ZYDIS_REGISTER_RSP == operands[1].mem.base || framePointer == operands[1].mem.base)));
|
||||
if (bIsFP)
|
||||
framePointer = operands[0].reg.value;
|
||||
return bIsFP;
|
||||
};
|
||||
|
||||
const auto IsRegDestination = [&](ZydisRegister reg) -> bool {
|
||||
return ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) &&
|
||||
ZYDIS_OPERAND_TYPE_REGISTER == operands[0].type && reg == operands[0].reg.value;
|
||||
};
|
||||
|
||||
const auto IsRegSource = [&](ZydisRegister reg) -> bool {
|
||||
return ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) &&
|
||||
((ZYDIS_OPERAND_TYPE_REGISTER == operands[1].type && reg == operands[1].reg.value) ||
|
||||
(ZYDIS_OPERAND_TYPE_MEMORY == operands[1].type && reg == operands[1].mem.base));
|
||||
};
|
||||
|
||||
const auto SaveRegister = [&](ZydisRegister reg) -> void {
|
||||
ZyanU64 regValue = 0;
|
||||
registers.values[reg] = 0;
|
||||
// Note - assumes prior call to IsRegDestination
|
||||
if (ZYDIS_OPERAND_TYPE_REGISTER == operands[0].type && reg == operands[0].reg.value &&
|
||||
ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&instruction, &operands[1], instructionPointer, ®Value)))
|
||||
{
|
||||
registers.values[reg] = regValue;
|
||||
}
|
||||
};
|
||||
|
||||
bool bThreadParameterInRax = false;
|
||||
bool bCallThreadParameter = false;
|
||||
bool bRaxSetLastInstruction = false;
|
||||
int nFollowedJumps = 0;
|
||||
constexpr auto MAX_JUMPS = 3;
|
||||
const auto FollowJump = [&]() -> bool {
|
||||
ZyanU64 jmpTarget = 0;
|
||||
if (nFollowedJumps < MAX_JUMPS &&
|
||||
ZYAN_SUCCESS(ZydisDecoderDecodeOperands(&decoder, &ctx, &instruction, operands, ZYDIS_MAX_OPERAND_COUNT)) &&
|
||||
ZYAN_SUCCESS(ZydisCalcAbsoluteAddressEx(&instruction, &operands[0], instructionPointer, ®isters, &jmpTarget)) &&
|
||||
0 != jmpTarget)
|
||||
{
|
||||
// If the jump target isn't an immediate then we need to read it from the calculated address.
|
||||
if ((ZYDIS_OPERAND_TYPE_MEMORY == operands[0].type && ZYDIS_REGISTER_RIP == operands[0].mem.base))
|
||||
{
|
||||
// If RCX has been mixed into RAX then we can't follow the jump.
|
||||
// Such start addresses are useful proxy call functions for adversaries.
|
||||
if (bRaxSetLastInstruction && bThreadParameterInRax) {
|
||||
bCallThreadParameter = true;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// This looks like a CFG check.
|
||||
// mov RAX, indirect-call
|
||||
// jmp __guard_dispatch_icall_fptr
|
||||
// The CFG thunk will transfer execution to RAX on success - so just jump to RAX now.
|
||||
if (bRaxSetLastInstruction && InSystemImageRange((PVOID)jmpTarget))
|
||||
jmpTarget = registers.values[ZYDIS_REGISTER_RAX];
|
||||
|
||||
// Read the jump target
|
||||
if (!ReadProcessMemorySafely(hProcess, (PVOID)jmpTarget, &jmpTarget))
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
std::string jmpBytes = std::move(startBytes);
|
||||
startBytes.resize(MAX_PROLOG_SIZE);
|
||||
if (ReadProcessMemorySafely(hProcess, (PVOID)jmpTarget, startBytes)) {
|
||||
|
||||
jmpBytes.resize(i + instruction.length);
|
||||
originalBytes += ToHex(jmpBytes) + "|";
|
||||
|
||||
// reset loop for new bytes
|
||||
instructionPointer = jmpTarget;
|
||||
instruction.length = 0;
|
||||
i = 0;
|
||||
return TRUE;
|
||||
}
|
||||
startBytes = std::move(originalBytes);
|
||||
}
|
||||
return FALSE;
|
||||
};
|
||||
|
||||
bool bPrologStarted = false;
|
||||
bool bPrologFinished = false;
|
||||
bool bRaxSet = false;
|
||||
bool bTestRcx = false;
|
||||
for (i = 0; bValidInstruction && !bPrologFinished && i <= startBytes.size() - MAX_INSN_LENGTH; i += instruction.length) {
|
||||
bValidInstruction = ZYAN_SUCCESS(ZydisDecoderDecodeInstruction(&decoder, &ctx, startBytes.data() + i, startBytes.length() - i, &instruction));
|
||||
bRaxSetLastInstruction = bRaxSet;
|
||||
bRaxSet = false;
|
||||
switch (instruction.mnemonic) {
|
||||
case ZYDIS_MNEMONIC_PUSH:
|
||||
// push nonvolatile
|
||||
bPrologStarted = true;
|
||||
break;
|
||||
case ZYDIS_MNEMONIC_MOV:
|
||||
// mov [RSP+n], nonvolatile
|
||||
if (IsSaveRegisterOperation())
|
||||
bPrologStarted = true;
|
||||
// mov frame-pointer, RSP
|
||||
else if (IsFramePointerOperation())
|
||||
bPrologStarted = true;
|
||||
// mov RAX, fixed-allocation-size
|
||||
// mov RAX, indirect-call-target
|
||||
else if (IsRegDestination(ZYDIS_REGISTER_RAX)) {
|
||||
SaveRegister(ZYDIS_REGISTER_RAX);
|
||||
bRaxSet = true;
|
||||
bThreadParameterInRax |= IsRegSource(ZYDIS_REGISTER_RCX);
|
||||
}
|
||||
else if (IsRegDestination(ZYDIS_REGISTER_EAX))
|
||||
bRaxSet = true;
|
||||
// sometimes stub functions reorder parameters or set static values
|
||||
// mov FastcallParamReg, *
|
||||
else
|
||||
bValidInstruction = IsRegDestination(ZYDIS_REGISTER_RCX) || IsRegDestination(ZYDIS_REGISTER_RDX) || IsRegDestination(ZYDIS_REGISTER_R8) || IsRegDestination(ZYDIS_REGISTER_R9);
|
||||
break;
|
||||
case ZYDIS_MNEMONIC_CALL:
|
||||
// call __chkstk() is the only call allowed in a prolog
|
||||
// It uses a special calling convention.
|
||||
bValidInstruction = bRaxSetLastInstruction;
|
||||
break;
|
||||
case ZYDIS_MNEMONIC_LEA:
|
||||
// lea frame-pointer, [RSP-n]
|
||||
if (IsFramePointerOperation())
|
||||
bValidInstruction = true;
|
||||
// lea RAX,[RIP+n]
|
||||
else if (IsRegDestination(ZYDIS_REGISTER_RAX)) {
|
||||
SaveRegister(ZYDIS_REGISTER_RAX);
|
||||
bRaxSet = true;
|
||||
}
|
||||
// Some "functions" are just stubs around other functions with
|
||||
// one (or more) fixed parameters.
|
||||
// lea RCX, [n] - set first parameter.
|
||||
else
|
||||
bValidInstruction = IsRegDestination(ZYDIS_REGISTER_RCX);
|
||||
break;
|
||||
case ZYDIS_MNEMONIC_SUB: // prolog delimiter
|
||||
bPrologFinished = IsStackOperation();
|
||||
break;
|
||||
case ZYDIS_MNEMONIC_TEST:
|
||||
// test RCX, RCX - is the first parameter NULL?
|
||||
// Checking for a non-NULL parameter and bailing early is
|
||||
// a common optimisation.
|
||||
bTestRcx = IsRegSource(ZYDIS_REGISTER_RCX) && IsRegDestination(ZYDIS_REGISTER_RCX);
|
||||
break;
|
||||
case ZYDIS_MNEMONIC_JZ:
|
||||
// test RCX, RCX
|
||||
// jz early-exit - don't follow
|
||||
bValidInstruction = bTestRcx;
|
||||
break;
|
||||
case ZYDIS_MNEMONIC_JNZ:
|
||||
// test RCX, RCX
|
||||
// jnz true-entry-point - follow
|
||||
bValidInstruction = bTestRcx;
|
||||
if (!FollowJump())
|
||||
bPrologFinished = bTestRcx;
|
||||
break;
|
||||
case ZYDIS_MNEMONIC_JMP:
|
||||
// Some functions start with a short jmp to provide hotpatch space.
|
||||
// jmp n - follow
|
||||
bPrologFinished = FollowJump();
|
||||
break;
|
||||
default:
|
||||
bValidInstruction = false;
|
||||
}
|
||||
instructionPointer += instruction.length;
|
||||
}
|
||||
startBytes.resize(i);
|
||||
if (bCallThreadParameter)
|
||||
detections.push_back(std::string("proxy_call(" + originalBytes + ToHex(startBytes) + ")"));
|
||||
else if (!bPrologFinished)
|
||||
detections.push_back(std::string("prolog(" + originalBytes + ToHex(startBytes) + ")"));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Check the bytes immmediately before Win32StartAddress
|
||||
// The byte preceding a function prolog is typically a return, or filler byte.
|
||||
// False positives can occur if data was included in a code section. This was
|
||||
// common in older compilers...
|
||||
std::string tailBytes;
|
||||
tailBytes.resize(std::min(MAX_INSN_LENGTH, (ULONG_PTR)thread.Win32StartAddress - (ULONG_PTR)mbi.AllocationBase));
|
||||
if (!ReadProcessMemorySafely(hProcess, (PVOID)((ULONG_PTR)thread.Win32StartAddress - tailBytes.size()), tailBytes))
|
||||
return FALSE;
|
||||
|
||||
|
||||
// False positives can occur if data was included in a code section. This was common in older compilers...
|
||||
// ...and also in new compilers that support XFG. In this case, the 8-byte XFG hash is immediately before.
|
||||
// https://blog.quarkslab.com/how-the-msvc-compiler-generates-xfg-function-prototype-hashes.html
|
||||
const auto& tailbytesEnd = tailBytes.data() + tailBytes.size();
|
||||
auto bIsValidTail = tailBytes.size() >= sizeof(UINT64) &&
|
||||
IsValidXfgHash(*(UINT64*)(tailbytesEnd - sizeof(UINT64)));
|
||||
|
||||
// The byte preceding a function prolog is typically a return, or filler byte.
|
||||
bIsValidTail |= tailBytes.empty() || '\x00' == tailBytes.back(); // NUL filled.
|
||||
for (auto i = 1; !bIsValidTail && i <= tailBytes.size(); i++) {
|
||||
if (!ZYAN_SUCCESS(ZydisDecoderDecodeInstruction(&decoder, NULL, tailbytesEnd - i, i, &instruction)) || instruction.length != i)
|
||||
continue;
|
||||
switch (instruction.mnemonic) {
|
||||
// valid basic block end instructions
|
||||
case ZYDIS_MNEMONIC_CALL:
|
||||
case ZYDIS_MNEMONIC_JMP:
|
||||
case ZYDIS_MNEMONIC_RET:
|
||||
// valid alignment filler instructions
|
||||
case ZYDIS_MNEMONIC_NOP:
|
||||
case ZYDIS_MNEMONIC_INT3:
|
||||
bIsValidTail = true;;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bIsValidTail)
|
||||
detections.push_back(std::string("tail(" + ToHex(tailBytes) + ")"));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Check for suspicious call stacks
|
||||
// [expected] ntdll!RtlUserThreadStart -> kernel32!BaseThreadInitThunk -> Win32StartAddress
|
||||
// https://www.trustedsec.com/blog/avoiding-get-injectedthread-for-internal-thread-creation/
|
||||
//
|
||||
|
||||
if (bIsWow64)
|
||||
return TRUE; // TODO(jdu) Implement x86 stack climbing?
|
||||
|
||||
// The TIB is the first element of the TEB. Read the TIB to determine the stack limits.
|
||||
NT_TIB64 tib;
|
||||
if (!ReadProcessMemory(hProcess, thread.TebBaseAddress, &tib, sizeof(tib), NULL))
|
||||
return FALSE;
|
||||
|
||||
// Determine the consumed stack size (and check for stack pivoting such as ROP).
|
||||
const auto stackPointer = thread.ContextRecord ? thread.ContextRecord->Rsp : tib.StackLimit;
|
||||
if (stackPointer > tib.StackBase || stackPointer < tib.StackLimit) {
|
||||
detections.push_back("stack_pivot");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Read the (partial) base of stack contents - 1.5 pages seems sufficient given current stack randomisation
|
||||
PVOID stackBuffer[0x1800 / sizeof(PVOID)];
|
||||
const auto stackReadLength = std::min(sizeof(stackBuffer), (tib.StackBase - stackPointer) & ~0xF);
|
||||
if (!ReadProcessMemory(hProcess, (PVOID)(tib.StackBase - stackReadLength), stackBuffer, stackReadLength, NULL))
|
||||
return FALSE;
|
||||
|
||||
|
||||
// Search the stack bottom up for the (probable) initial return addresses of the first 3+2 frames.
|
||||
// Note - x64 stack frames are 16-byte aligned.
|
||||
std::vector<std::string> callStackFrames;
|
||||
bool bCallStackDetection = false;
|
||||
const auto stackBufferCount = stackReadLength / sizeof(PVOID);
|
||||
if (!StackClimb64(hProcess, stackBuffer, stackBufferCount, callStackFrames, &bCallStackDetection))
|
||||
return FALSE;
|
||||
|
||||
// If the thread has been hijacked, then the return address alignment might be off.
|
||||
// Search the skipped offsets this time.
|
||||
if(0 == callStackFrames.size() && !StackClimb64(hProcess, stackBuffer, stackBufferCount, callStackFrames, &bCallStackDetection, 1))
|
||||
return FALSE;
|
||||
|
||||
// Not enough stack frames discovered yet - append RIP
|
||||
if (!bCallStackDetection && thread.ContextRecord && callStackFrames.size() < MIN_FRAMES) {
|
||||
|
||||
if (!VirtualQueryEx(hProcess, (PVOID)thread.ContextRecord->Rip, &mbi, sizeof(mbi)))
|
||||
return FALSE;
|
||||
|
||||
if (!IsExecutable(mbi))
|
||||
LogError("pid:%d, tid:%d RIP:%llx is not executable", thread.ProcessId, thread.ThreadId, thread.ContextRecord->Rip);
|
||||
|
||||
if (MEM_IMAGE != mbi.Type) {
|
||||
callStackFrames.push_back("PRIVATE");
|
||||
bCallStackDetection = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string callStackSummary;
|
||||
callStackSummary.reserve(callStackFrames.size() * 16);
|
||||
for (const auto& entry : callStackFrames) {
|
||||
if (entry == "ntdll.dll!RtlUserThreadStart" || entry == "kernel32.dll!BaseThreadInitThunk")
|
||||
continue; // skip common frames
|
||||
if (!callStackSummary.empty())
|
||||
callStackSummary += "|";
|
||||
callStackSummary += entry.substr(0, entry.find_first_of('<')); // trim type information
|
||||
}
|
||||
|
||||
if (bCallStackDetection)
|
||||
if(callStackFrames.size() < 4)
|
||||
detections.push_back("spoof(" + callStackSummary + ")");
|
||||
else
|
||||
detections.push_back("wrapper(" + callStackSummary + ")");
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
struct FalsePositive {
|
||||
const std::wstring ProcessName;
|
||||
const std::string Symbol;
|
||||
const size_t Count;
|
||||
};
|
||||
|
||||
BOOL IsKnownFalsePositive(const HANDLE hProcess, const PROCESSENTRY32 &processEntry, const PSS_THREAD_ENTRY thread, std::string &symbol, const std::vector<std::string> &detections) {
|
||||
|
||||
static const std::array<FalsePositive, 2> falsePositives = { {
|
||||
{ L"dwm.exe", "dwmcore.dll!CMit::RunInputThreadStatic", 1 },
|
||||
{ L"vctip.exe", "vctip.exe!CorExeMain", 1 }
|
||||
} };
|
||||
|
||||
(void)GetNearestSymbolWithPdb(hProcess, thread.Win32StartAddress, symbol);
|
||||
|
||||
BOOL bIsFalsePositive = FALSE;
|
||||
for (const auto &fp : falsePositives)
|
||||
bIsFalsePositive |= processEntry.szExeFile == fp.ProcessName && symbol == fp.Symbol && detections.size() == fp.Count;
|
||||
|
||||
return bIsFalsePositive;
|
||||
}
|
||||
|
||||
int main(int, char* []) {
|
||||
if (!IsElevated())
|
||||
LogError("WARNING Not running as Administrator");
|
||||
|
||||
BOOLEAN _;
|
||||
if(!NT_SUCCESS(RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &_)))
|
||||
LogError("WARNING RtlAdjustPrivilege(DEBUG) failed");
|
||||
|
||||
const auto tsScanStarted = GetTickCount64();
|
||||
UINT32 nProcessesTotal = 0;
|
||||
UINT32 nProcessesScanned = 0;
|
||||
UINT32 nThreadsScanned = 0;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Scan each process
|
||||
HANDLE hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (hProcessSnapshot == INVALID_HANDLE_VALUE) {
|
||||
LogError("CreateToolhelp32Snapshot(PROCESS) failed. LastError:%d", GetLastError());
|
||||
return 0;
|
||||
}
|
||||
|
||||
PROCESSENTRY32 processEntry{};
|
||||
processEntry.dwSize = sizeof(PROCESSENTRY32);
|
||||
if (!Process32First(hProcessSnapshot, &processEntry)) {
|
||||
LogError("Process32First failed. LastError:%d", GetLastError());
|
||||
return 0;
|
||||
}
|
||||
|
||||
do {
|
||||
if (processEntry.th32ProcessID <= 4)
|
||||
continue; // skip Idle and System
|
||||
|
||||
nProcessesTotal++;
|
||||
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processEntry.th32ProcessID);
|
||||
if (NULL == hProcess)
|
||||
continue; // skip process - Access is Denied, or process stopped
|
||||
|
||||
auto bIsDotNet = IsDotNet(hProcess);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Scan all threads in the process
|
||||
HPSS hThreadSnapshot = NULL;
|
||||
HPSSWALK hWalk = NULL;
|
||||
const auto captureFlags = PSS_CAPTURE_THREADS | PSS_CAPTURE_THREAD_CONTEXT;
|
||||
const auto contextFlags = CONTEXT_CONTROL | CONTEXT_DEBUG_REGISTERS;
|
||||
if (S_OK == PssCaptureSnapshot(hProcess, captureFlags, contextFlags, &hThreadSnapshot) && S_OK == PssWalkMarkerCreate(NULL, &hWalk)) {
|
||||
nProcessesScanned++;
|
||||
PSS_THREAD_ENTRY thread;
|
||||
while (S_OK == PssWalkSnapshot(hThreadSnapshot, PSS_WALK_THREADS, hWalk, &thread, sizeof(thread))) {
|
||||
std::string symbol;
|
||||
std::vector<std::string> detections;
|
||||
(void)ScanThread(hProcess, bIsDotNet, thread, symbol, detections);
|
||||
nThreadsScanned++;
|
||||
|
||||
if (detections.size() > 0 && !IsKnownFalsePositive(hProcess, processEntry, thread, symbol, detections)) {
|
||||
(void)GetNearestSymbolWithPdb(hProcess, thread.Win32StartAddress, symbol, true);
|
||||
Log("ProcessName : %S", processEntry.szExeFile);
|
||||
Log("pid:tid : %d:%d", thread.ProcessId, thread.ThreadId);
|
||||
Log("Win32StartAddress : %s", symbol.c_str());
|
||||
Log("Detections :");
|
||||
for (const auto& detection : detections)
|
||||
Log(" - %s", detection.c_str());
|
||||
Log("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (NULL != hProcess)
|
||||
(void)CloseHandle(hProcess);
|
||||
if (NULL != hThreadSnapshot)
|
||||
(void)PssFreeSnapshot(GetCurrentProcess(), hThreadSnapshot);
|
||||
if (NULL != hWalk)
|
||||
(void)PssWalkMarkerFree(hWalk);
|
||||
} while (Process32Next(hProcessSnapshot, &processEntry));
|
||||
|
||||
if (INVALID_HANDLE_VALUE != hProcessSnapshot)
|
||||
(void)CloseHandle(hProcessSnapshot);
|
||||
|
||||
Log("Scanned %d threads in %d (of %d) processes in %.2f seconds", nThreadsScanned, nProcessesScanned, nProcessesTotal, (GetTickCount64() - tsScanStarted) / 1000.0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "Get-InjectedThreadEx.h"
|
||||
|
||||
BOOL InSystemImageRange(PVOID Address) {
|
||||
return Address >= (PVOID)0x7FF800000000 && Address < (PVOID)0x7FFFFFFF0000;
|
||||
}
|
||||
|
||||
constexpr UINT64 XFG_MASK_UNSET = ~0xFFFDBFFF7EDFFB71; // mask of unset bits
|
||||
constexpr UINT64 XFG_MASK_SET = 0x8000060010500070; // mask of set bits
|
||||
constexpr UINT64 XFG_MASK_ALL = XFG_MASK_UNSET | XFG_MASK_SET;
|
||||
BOOL IsValidXfgHash(UINT64 xfgHash) {
|
||||
return XFG_MASK_SET == (xfgHash & XFG_MASK_ALL);
|
||||
}
|
||||
|
||||
static PULONG_PTR FindCfgBitMap() {
|
||||
// Find non-exported ntdll!LdrSystemDllInitBlock.CfgBitMap by looking at the first instruction of LdrControlFlowGuardEnforced
|
||||
// 48833d [80be1400], 00 CMP qword ptr[LdrSystemDllInitBlock.CfgBitMap], 0x0
|
||||
#pragma warning(suppress: 6387) // ntdll is always loaded
|
||||
auto pLdrControlFlowGuardEnforced = GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrControlFlowGuardEnforced");
|
||||
if (NULL == pLdrControlFlowGuardEnforced)
|
||||
return NULL;
|
||||
|
||||
auto ppCfgBitMap = (PULONG_PTR*)((ULONG_PTR)pLdrControlFlowGuardEnforced + 8 + *(DWORD*)((ULONG_PTR)pLdrControlFlowGuardEnforced + 3));
|
||||
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (!VirtualQuery(*ppCfgBitMap, &mbi, sizeof(mbi)) || MEM_MAPPED != mbi.Type || *ppCfgBitMap != mbi.AllocationBase) {
|
||||
LogError("FATAL: ntdll!LdrSystemDllInitBlock.CfgBitMap not found\n");
|
||||
ExitProcess(1);
|
||||
}
|
||||
|
||||
return *ppCfgBitMap;
|
||||
}
|
||||
|
||||
constexpr auto CFG_INVALID = 0b00; // no address in this range is a valid target
|
||||
constexpr auto CFG_ALL_VALID = 0b11; // all addresses in this range are valid.
|
||||
constexpr auto CFG_ALIGNED_VALID = 0b01; // the only valid target is 16-byte aligned
|
||||
constexpr auto CFG_EXPORT_SUPPRESSED = 0b10; // this range contains an export-suppressed target
|
||||
|
||||
BOOL GetCfgBitsForAddress(PVOID address, PULONG pCfgBits) {
|
||||
static auto pCfgBitMap = FindCfgBitMap();
|
||||
const PULONG_PTR pLocalEntry = pCfgBitMap + ((ULONG_PTR)address >> 9);
|
||||
const ULONG cfgOffset = (((ULONG_PTR)(address)) >> 3) & 0x3E;
|
||||
ULONG_PTR localEntry;
|
||||
// We use ReadProcessMemory to safely read the volatile CfgBitMap.
|
||||
if (ReadProcessMemory(GetCurrentProcess(), pLocalEntry, &localEntry, sizeof(localEntry), NULL)) {
|
||||
*pCfgBits = (localEntry >> cfgOffset) & 0b11;
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#pragma comment(lib, "ntdll.lib")
|
||||
#pragma comment(lib, "DbgHelp.lib")
|
||||
#pragma comment(lib, "Zydis.lib")
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <Windows.h>
|
||||
#include <dbghelp.h>
|
||||
//#include <ehdata.h>
|
||||
#include <processsnapshot.h>
|
||||
#include <psapi.h>
|
||||
#include <tlhelp32.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
// x86/x86-64 disassembler
|
||||
// https://github.com/zyantific/zydis - MIT
|
||||
#define ZYDIS_STATIC_BUILD
|
||||
#include <Zydis/Zydis.h>
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
|
||||
constexpr auto MAX_LONG_PATH = 0x7FFF;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Logging
|
||||
#define Log(_fmt_, ...) printf(_fmt_ "\n", ##__VA_ARGS__)
|
||||
#define LogError(_fmt_, ...) fprintf(stderr, "[!] " _fmt_ "\n", ##__VA_ARGS__)
|
||||
#if _DEBUG
|
||||
#define LogDebug(_fmt_, ...) printf("[#] " _fmt_ "\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define LogDebug(...)
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Native API
|
||||
#define SE_DEBUG_PRIVILEGE (20L)
|
||||
typedef LONG NTSTATUS;
|
||||
#define NT_SUCCESS(Status) ((Status) >= 0)
|
||||
extern "C" {
|
||||
NTSTATUS RtlAdjustPrivilege(ULONG Privilege, BOOLEAN Enable, BOOLEAN CurrentThread, PBOOLEAN Enabled);
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// CfgBitMap.cpp
|
||||
BOOL GetCfgBitsForAddress(PVOID address, PULONG pCfgBits);
|
||||
BOOL IsValidXfgHash(UINT64 xfgHash);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Memory.cpp
|
||||
std::string ToHex(std::string bytes);
|
||||
BOOL IsExecutable(const MEMORY_BASIC_INFORMATION& mbi);
|
||||
BOOL InSystemImageRange(PVOID Address);
|
||||
BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, PDWORD64 buffer);
|
||||
BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, std::string& buffer);
|
||||
BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, std::string& buffer, const MEMORY_BASIC_INFORMATION& mbi);
|
||||
BOOL GetMappedFileNameAsDosPath(HANDLE hProcess, PVOID address, std::wstring& buffer);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Process.cpp
|
||||
BOOL IsElevated();
|
||||
BOOL IsDotNet(HANDLE hProcess);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// StackClimb.cpp
|
||||
constexpr auto MIN_FRAMES = 5;
|
||||
BOOL StackClimb64(const HANDLE hProcess, const PVOID stackBuffer[], const size_t stackBufferCount, std::vector<std::string>& callStackFrames, bool* pbDetection, int offset = 0);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Symbol.cpp
|
||||
BOOL GetNearestSymbol(HANDLE hProcess, PVOID address, std::string& symbol, bool bIncludeDisplacement = false);
|
||||
BOOL GetNearestSymbolWithPdb(HANDLE hProcess, PVOID address, std::string& symbol, bool bIncludeDisplacement = false);
|
||||
PVOID GetSymbolAddress(const char symbol[]);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Unwind.cpp
|
||||
HRESULT CalculateFrameSize(PVOID returnAddress, PDWORD pFrameSize, const std::wstring& filepath, PVOID remoteBase);
|
||||
BOOL IsValidCallSite(HANDLE hProcess, bool bIsWow64, const MEMORY_BASIC_INFORMATION& mbi, PVOID callsite, bool* bCallFound);
|
||||
|
||||
int main(int, char* []);
|
||||
@@ -0,0 +1,142 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.33627.172
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Get-InjectedThreadEx", "Get-InjectedThreadEx.vcxproj", "{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2} = {88A23124-5640-35A0-B890-311D7A67A7D2}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Zydis", "zydis\msvc\zydis\Zydis.vcxproj", "{88A23124-5640-35A0-B890-311D7A67A7D2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug Kernel|x64 = Debug Kernel|x64
|
||||
Debug Kernel|x86 = Debug Kernel|x86
|
||||
Debug MD DLL|x64 = Debug MD DLL|x64
|
||||
Debug MD DLL|x86 = Debug MD DLL|x86
|
||||
Debug MD|x64 = Debug MD|x64
|
||||
Debug MD|x86 = Debug MD|x86
|
||||
Debug MT DLL|x64 = Debug MT DLL|x64
|
||||
Debug MT DLL|x86 = Debug MT DLL|x86
|
||||
Debug MT|x64 = Debug MT|x64
|
||||
Debug MT|x86 = Debug MT|x86
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release Kernel|x64 = Release Kernel|x64
|
||||
Release Kernel|x86 = Release Kernel|x86
|
||||
Release MD DLL|x64 = Release MD DLL|x64
|
||||
Release MD DLL|x86 = Release MD DLL|x86
|
||||
Release MD|x64 = Release MD|x64
|
||||
Release MD|x86 = Release MD|x86
|
||||
Release MT DLL|x64 = Release MT DLL|x64
|
||||
Release MT DLL|x86 = Release MT DLL|x86
|
||||
Release MT|x64 = Release MT|x64
|
||||
Release MT|x86 = Release MT|x86
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug Kernel|x64.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug Kernel|x64.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug Kernel|x86.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug Kernel|x86.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD DLL|x64.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD DLL|x64.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD DLL|x86.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD DLL|x86.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD|x64.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD|x64.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD|x86.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MD|x86.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT DLL|x64.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT DLL|x64.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT DLL|x86.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT DLL|x86.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT|x64.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT|x64.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT|x86.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug MT|x86.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug|x64.Build.0 = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release Kernel|x64.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release Kernel|x64.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release Kernel|x86.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release Kernel|x86.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD DLL|x64.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD DLL|x64.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD DLL|x86.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD DLL|x86.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD|x64.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD|x64.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD|x86.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MD|x86.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT DLL|x64.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT DLL|x64.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT DLL|x86.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT DLL|x86.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT|x64.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT|x64.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT|x86.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release MT|x86.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release|x64.ActiveCfg = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release|x64.Build.0 = Release|x64
|
||||
{256D0782-6D09-44B2-AD5C-FC8EF085D1D3}.Release|x86.ActiveCfg = Release|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug Kernel|x64.ActiveCfg = Debug Kernel|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug Kernel|x64.Build.0 = Debug Kernel|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug Kernel|x86.ActiveCfg = Debug Kernel|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug Kernel|x86.Build.0 = Debug Kernel|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD DLL|x64.ActiveCfg = Debug MD DLL|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD DLL|x64.Build.0 = Debug MD DLL|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD DLL|x86.ActiveCfg = Debug MD DLL|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD DLL|x86.Build.0 = Debug MD DLL|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD|x64.ActiveCfg = Debug MD|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD|x64.Build.0 = Debug MD|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD|x86.ActiveCfg = Debug MD|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MD|x86.Build.0 = Debug MD|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT DLL|x64.ActiveCfg = Debug MT DLL|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT DLL|x64.Build.0 = Debug MT DLL|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT DLL|x86.ActiveCfg = Debug MT DLL|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT DLL|x86.Build.0 = Debug MT DLL|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT|x64.ActiveCfg = Debug MT|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT|x64.Build.0 = Debug MT|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT|x86.ActiveCfg = Debug MT|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug MT|x86.Build.0 = Debug MT|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug|x64.ActiveCfg = Debug MD|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug|x64.Build.0 = Debug MD|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug|x86.ActiveCfg = Debug MD|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Debug|x86.Build.0 = Debug MD|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release Kernel|x64.ActiveCfg = Release Kernel|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release Kernel|x64.Build.0 = Release Kernel|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release Kernel|x86.ActiveCfg = Release Kernel|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release Kernel|x86.Build.0 = Release Kernel|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD DLL|x64.ActiveCfg = Release MD DLL|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD DLL|x64.Build.0 = Release MD DLL|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD DLL|x86.ActiveCfg = Release MD DLL|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD DLL|x86.Build.0 = Release MD DLL|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD|x64.ActiveCfg = Release MD|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD|x64.Build.0 = Release MD|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD|x86.ActiveCfg = Release MD|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MD|x86.Build.0 = Release MD|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT DLL|x64.ActiveCfg = Release MT DLL|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT DLL|x64.Build.0 = Release MT DLL|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT DLL|x86.ActiveCfg = Release MT DLL|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT DLL|x86.Build.0 = Release MT DLL|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT|x64.ActiveCfg = Release MT|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT|x64.Build.0 = Release MT|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT|x86.ActiveCfg = Release MT|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release MT|x86.Build.0 = Release MT|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release|x64.ActiveCfg = Release MD DLL|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release|x64.Build.0 = Release MD DLL|x64
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release|x86.ActiveCfg = Release MT DLL|Win32
|
||||
{88A23124-5640-35A0-B890-311D7A67A7D2}.Release|x86.Build.0 = Release MT DLL|Win32
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {2409743E-DDE1-4665-8C29-FA5C5C8DEEFE}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\Get-InjectedThreadEx.cpp" />
|
||||
<ClCompile Include="CfgBitMap.cpp" />
|
||||
<ClCompile Include="Memory.cpp" />
|
||||
<ClCompile Include="Process.cpp" />
|
||||
<ClCompile Include="StackClimb.cpp" />
|
||||
<ClCompile Include="Symbol.cpp" />
|
||||
<ClCompile Include="Unwind.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Get-InjectedThreadEx.h" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{256d0782-6d09-44b2-ad5c-fc8ef085d1d3}</ProjectGuid>
|
||||
<RootNamespace>GetInjectedThreadEx</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<IncludePath>$(SolutionDir)\phnt;$(SolutionDir)\zydis\include;$(SolutionDir)\zydis\dependencies\zycore\include;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>$(SolutionDir)\zydis\msvc\bin\$(Configuration)$(Platform);$(LibraryPath)</LibraryPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<IncludePath>$(SolutionDir)\phnt;$(SolutionDir)\zydis\include;$(SolutionDir)\zydis\dependencies\zycore\include;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>$(SolutionDir)\zydis\msvc\bin\$(Configuration)$(Platform);$(LibraryPath)</LibraryPath>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<LanguageStandard_C>stdc17</LanguageStandard_C>
|
||||
<ControlFlowGuard>Guard</ControlFlowGuard>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<LanguageStandard_C>stdc17</LanguageStandard_C>
|
||||
<ControlFlowGuard>Guard</ControlFlowGuard>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\Get-InjectedThreadEx.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="CfgBitMap.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Symbol.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Unwind.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Memory.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Process.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="StackClimb.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Get-InjectedThreadEx.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "Get-InjectedThreadEx.h"
|
||||
|
||||
BOOL IsExecutable(const MEMORY_BASIC_INFORMATION& mbi) {
|
||||
constexpr auto PAGE_EXECUTE_ANY = PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
return MEM_COMMIT == mbi.State && 0 != (mbi.Protect & PAGE_EXECUTE_ANY);
|
||||
}
|
||||
|
||||
BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, std::string& buffer, const MEMORY_BASIC_INFORMATION& mbi) {
|
||||
if (MEM_COMMIT != mbi.State || 0 != (mbi.Protect & PAGE_GUARD))
|
||||
return FALSE;
|
||||
// TODO(jdu) NEXT handle reads split over multiple regions
|
||||
return ReadProcessMemory(hProcess, address, buffer.data(), buffer.size(), NULL);
|
||||
}
|
||||
|
||||
BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, std::string& buffer) {
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if(!VirtualQueryEx(hProcess, address, &mbi, sizeof(mbi)))
|
||||
return FALSE;
|
||||
return ReadProcessMemorySafely(hProcess, address, buffer, mbi);
|
||||
}
|
||||
|
||||
BOOL ReadProcessMemorySafely(HANDLE hProcess, PVOID address, PDWORD64 buffer) {
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (!VirtualQueryEx(hProcess, address, &mbi, sizeof(mbi)))
|
||||
return FALSE;
|
||||
auto b = MEM_COMMIT != mbi.State;
|
||||
auto b2 = 0 != (mbi.Protect & PAGE_GUARD);
|
||||
auto b3 = (MEM_COMMIT != mbi.State || 0 != (mbi.Protect & PAGE_GUARD));
|
||||
if (MEM_COMMIT != mbi.State || 0 != (mbi.Protect & PAGE_GUARD))
|
||||
return FALSE;
|
||||
return ReadProcessMemory(hProcess, address, buffer, sizeof(DWORD64), NULL);
|
||||
}
|
||||
|
||||
BOOL GetMappedFileNameAsDosPath(HANDLE hProcess, PVOID address, std::wstring& buffer) {
|
||||
|
||||
// Cache #1 - the mapping of shared image allocations to DOS paths
|
||||
const auto bIsSharedDll = InSystemImageRange(address);
|
||||
const auto allocationBase = (ULONG_PTR)address & ~0xFFFFull;
|
||||
static std::unordered_map<ULONG_PTR, std::wstring> s_DosPathCache;
|
||||
if (bIsSharedDll) {
|
||||
const auto it = s_DosPathCache.find(allocationBase);
|
||||
if (it != s_DosPathCache.end()) {
|
||||
buffer = it->second;
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache #2 - the mapping from Device prefixes to Dos prefixes
|
||||
static std::unordered_map<std::wstring, std::wstring> s_Device2DosPrefixMap;
|
||||
if (s_Device2DosPrefixMap.empty()) {
|
||||
wchar_t drive[3] = L"A:";
|
||||
for (drive[0] = L'A'; drive[0] <= L'Z'; drive[0]++) {
|
||||
std::wstring deviceName;
|
||||
deviceName.resize(MAX_PATH);
|
||||
if (!QueryDosDeviceW(drive, deviceName.data(), (DWORD)deviceName.size())) {
|
||||
const auto dwError = GetLastError();
|
||||
if (ERROR_FILE_NOT_FOUND != dwError)
|
||||
LogError("QueryDosDeviceW(%S) failed with %d\n", drive, dwError);
|
||||
continue;
|
||||
}
|
||||
deviceName.resize(wcslen(deviceName.c_str()));
|
||||
s_Device2DosPrefixMap.insert(std::make_pair(deviceName, std::wstring(drive)));
|
||||
}
|
||||
}
|
||||
|
||||
buffer.clear();
|
||||
buffer.resize(MAX_PATH);
|
||||
|
||||
// Note - K32GetMappedFileName returns the device path such as \Device\Harddisk0\Windows\System32\ntdll.dll
|
||||
auto nBytesReturned = K32GetMappedFileNameW(hProcess, address, buffer.data(), (DWORD)buffer.size());
|
||||
if (MAX_PATH == nBytesReturned) {
|
||||
buffer.resize(MAX_LONG_PATH);
|
||||
nBytesReturned = K32GetMappedFileNameW(hProcess, address, buffer.data(), (DWORD)buffer.size());
|
||||
if (MAX_LONG_PATH == nBytesReturned)
|
||||
LogError("K32GetMappedFileNameW(%p) exceeded maximum long path - %S\n", address, buffer.substr(0, MAX_PATH).c_str());
|
||||
}
|
||||
|
||||
if (0 == nBytesReturned)
|
||||
return FALSE;
|
||||
|
||||
// We have a device path - now convert it to a DOS path
|
||||
constexpr auto DEVICE_PREFIX = L"\\Device\\";
|
||||
if (buffer.length() < sizeof(DEVICE_PREFIX) || 0 != _wcsnicmp(buffer.c_str(), DEVICE_PREFIX, sizeof(DEVICE_PREFIX))) {
|
||||
LogError("K32GetMappedFileNameW(%p) did not return device path - %S\n", address, buffer.substr(0, MAX_PATH).c_str());
|
||||
return TRUE; // best effort - return what we have
|
||||
}
|
||||
|
||||
// Loopkup DOS prefix of "\device\<name>" component in our cached mapping
|
||||
const auto pos = buffer.find(L'\\', std::char_traits<WCHAR>::length(DEVICE_PREFIX));
|
||||
const auto devicePrefixLength = (std::wstring::npos == pos) ? buffer.length() : pos;
|
||||
const auto device2DosPrefixMapping = s_Device2DosPrefixMap.find(buffer.substr(0, devicePrefixLength));
|
||||
if (s_Device2DosPrefixMap.end() == device2DosPrefixMapping) {
|
||||
LogError("Could not resolve device prefix to DOS drive letter - %S\n", buffer.substr(0, MAX_PATH).c_str());
|
||||
return TRUE; // best effort - return what we have
|
||||
}
|
||||
|
||||
buffer = device2DosPrefixMapping->second + buffer.substr(devicePrefixLength);
|
||||
buffer.resize(wcslen(buffer.c_str()));
|
||||
|
||||
// Update our path cache
|
||||
if (nBytesReturned && bIsSharedDll)
|
||||
s_DosPathCache.insert(std::make_pair(allocationBase, buffer));
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::string ToHex(std::string bytes) {
|
||||
std::stringstream hexBytes;
|
||||
hexBytes << std::hex << std::setfill('0');
|
||||
for (size_t i = 0; i < bytes.length(); i++) {
|
||||
hexBytes << std::setw(2) << (DWORD)(BYTE)bytes[i];
|
||||
}
|
||||
return hexBytes.str();
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "Get-InjectedThreadEx.h"
|
||||
|
||||
BOOL IsElevated()
|
||||
{
|
||||
BOOL bIsElevated = FALSE;
|
||||
|
||||
DWORD dwSize;
|
||||
HANDLE hToken = NULL;
|
||||
TOKEN_ELEVATION elevation;
|
||||
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken) &&
|
||||
GetTokenInformation(hToken, TokenElevation, &elevation, sizeof(elevation), &dwSize) &&
|
||||
sizeof(elevation) == dwSize)
|
||||
{
|
||||
bIsElevated = elevation.TokenIsElevated;
|
||||
}
|
||||
|
||||
if (hToken) {
|
||||
(void)CloseHandle(hToken);
|
||||
}
|
||||
|
||||
return bIsElevated;
|
||||
}
|
||||
|
||||
BOOL GetProcessPath(HANDLE hProcess, std::wstring& buffer) {
|
||||
buffer.clear();
|
||||
|
||||
buffer.resize(MAX_PATH);
|
||||
DWORD size = (DWORD)buffer.size();
|
||||
if (!QueryFullProcessImageNameW(hProcess, 0, buffer.data(), &size)) {
|
||||
buffer.resize(MAX_LONG_PATH);
|
||||
size = (DWORD)buffer.size();
|
||||
if (!QueryFullProcessImageNameW(hProcess, 0, buffer.data(), &size)) {
|
||||
buffer.clear();
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.resize(size);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL IsDotNet(HANDLE hProcess) {
|
||||
std::wstring executable;
|
||||
if (!GetProcessPath(hProcess, executable))
|
||||
return FALSE;
|
||||
|
||||
// Map the executable's PE headers.
|
||||
constexpr auto FILE_SHARE_ALL = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
|
||||
auto hFile = CreateFileW(executable.c_str(), GENERIC_READ, FILE_SHARE_ALL, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
auto hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
|
||||
auto pMapping = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0x1000);
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only
|
||||
// The last directory is actually the CLR Runtime Header - not the "COM Descriptor".
|
||||
// If it exists, then the PE file is a CLR assembly - aka .NET
|
||||
ULONG size = 0;
|
||||
BOOL bIsDotNet = NULL != ImageDirectoryEntryToData(pMapping, TRUE, IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR, &size);
|
||||
|
||||
if (pMapping)
|
||||
(void)UnmapViewOfFile(pMapping);
|
||||
if (hMapping)
|
||||
(void)CloseHandle(hMapping);
|
||||
if (hFile)
|
||||
(void)CloseHandle(hFile);
|
||||
|
||||
return bIsDotNet;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "Get-InjectedThreadEx.h"
|
||||
|
||||
|
||||
void SymboliseCallStack(HANDLE hProcess, const std::vector<PVOID> returnAddresses, const std::string last, std::vector<std::string> &callStackFrames) {
|
||||
for (const auto address : returnAddresses) {
|
||||
std::string frameSymbol;
|
||||
if (!GetNearestSymbolWithPdb(hProcess, address, frameSymbol))
|
||||
frameSymbol = "<ERROR>";
|
||||
callStackFrames.push_back(frameSymbol);
|
||||
}
|
||||
callStackFrames.push_back(last);
|
||||
}
|
||||
|
||||
BOOL StackClimb64(const HANDLE hProcess,
|
||||
const PVOID stackBuffer[],
|
||||
const size_t stackBufferCount,
|
||||
std::vector<std::string>& callStackFrames,
|
||||
bool* pbDetection,
|
||||
int offset)
|
||||
{
|
||||
std::vector<PVOID> returnAddresses;
|
||||
|
||||
// Query SystemInfo for the usermode address bounds
|
||||
static SYSTEM_INFO s_si{};
|
||||
if (!s_si.lpMaximumApplicationAddress) {
|
||||
GetSystemInfo(&s_si);
|
||||
if (0 == s_si.lpMaximumApplicationAddress) {
|
||||
LogError("GetSystemInfo() failed");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Note - My code makes some assumptions about page and allocation sizes.
|
||||
if (0x1000 != s_si.dwPageSize || 0x10000 != s_si.dwAllocationGranularity) {
|
||||
LogError("System not supported. PageSize=0x%x AllocationGranularity=0x%x", s_si.dwPageSize, s_si.dwAllocationGranularity);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
auto lastRspOffset = 0u;
|
||||
const auto StackBufferLast = &stackBuffer[stackBufferCount - 1];
|
||||
for (auto i = offset; !*pbDetection && (i < stackBufferCount) && (returnAddresses.size() < MIN_FRAMES); i += 2) {
|
||||
const PVOID candidateRip = StackBufferLast[-i];
|
||||
|
||||
// Skip any invalid usermode addresses
|
||||
if (candidateRip < s_si.lpMinimumApplicationAddress || candidateRip >= s_si.lpMaximumApplicationAddress)
|
||||
continue;
|
||||
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (!VirtualQueryEx(hProcess, candidateRip, &mbi, sizeof(mbi)))
|
||||
return FALSE;
|
||||
|
||||
// Return address must be executable
|
||||
if (!IsExecutable(mbi))
|
||||
continue;
|
||||
|
||||
// We can't read this executable page due to the Guard - so just alert.
|
||||
if (0 != (mbi.State & PAGE_GUARD)) {
|
||||
SymboliseCallStack(hProcess, returnAddresses, "GUARD", callStackFrames);
|
||||
*pbDetection = true;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
bool bValidCallsite;
|
||||
if (MEM_IMAGE != mbi.Type) {
|
||||
if (IsValidCallSite(hProcess, false, mbi, candidateRip, &bValidCallsite) && bValidCallsite) {
|
||||
// A suspicious frame was found!
|
||||
SymboliseCallStack(hProcess, returnAddresses, "PRIVATE", callStackFrames);
|
||||
*pbDetection = true;
|
||||
return TRUE;
|
||||
}
|
||||
continue; // Not a return address - keep searching
|
||||
}
|
||||
|
||||
std::wstring candidateRipMappedPath;
|
||||
if (!GetMappedFileNameAsDosPath(hProcess, candidateRip, candidateRipMappedPath))
|
||||
return FALSE;
|
||||
|
||||
DWORD frameSize = 0;
|
||||
if (0 != lastRspOffset) {
|
||||
auto status = CalculateFrameSize(candidateRip, &frameSize, candidateRipMappedPath, mbi.AllocationBase);
|
||||
if (S_OK == status && (i - lastRspOffset) != frameSize)
|
||||
continue; // Invalid frame size - keep searching
|
||||
|
||||
if (IS_ERROR(status)) // but not WARNINGs
|
||||
continue;
|
||||
|
||||
// We could not calculate exact frame size - but do have a lowerbound.
|
||||
// TODO(jdu) We could scan ahead one (or more) frames here to see if we recover?
|
||||
if (S_OK != status && (i - lastRspOffset) < frameSize)
|
||||
continue; // Frame too small - keep searching
|
||||
}
|
||||
|
||||
if (!IsValidCallSite(hProcess, false, mbi, candidateRip, &bValidCallsite) && !bValidCallsite)
|
||||
continue; // Not a return address - keep searching
|
||||
|
||||
auto status = CalculateFrameSize(candidateRip, &frameSize, candidateRipMappedPath, mbi.AllocationBase);
|
||||
// LogDebug(" %03i: %p %s valid=%d frameSize=%d FP=%d", i, candidateRip, frameSymbol.c_str(), bValidCallsite, frameSize, S_OK != status);
|
||||
returnAddresses.push_back(candidateRip);
|
||||
|
||||
lastRspOffset = i;
|
||||
|
||||
// Frames must be at least 0x20 bytes due to fastcall parameter shadow space
|
||||
i += 4;
|
||||
}
|
||||
|
||||
// Just use exports if nothing suspicious was found
|
||||
// TODO(jdu) We only need to return this at all so that we know if a 2nd pass is required.
|
||||
// Perhaps move 2nd pass and RIP logic here.
|
||||
if (callStackFrames.empty()) {
|
||||
for (const auto address : returnAddresses) {
|
||||
std::string frameSymbol;
|
||||
(void)GetNearestSymbol(hProcess, address, frameSymbol);
|
||||
callStackFrames.push_back(frameSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
#include "Get-InjectedThreadEx.h"
|
||||
|
||||
typedef struct _SYMBOL_INFO_FULL : SYMBOL_INFO {
|
||||
CHAR NameBuffer[MAX_SYM_NAME - 1];
|
||||
} SYMBOL_INFO_FULL;
|
||||
#define INIT_FAILED ((HANDLE)-3)
|
||||
|
||||
constexpr auto SYMOPTS = SYMOPT_UNDNAME | SYMOPT_CASE_INSENSITIVE | SYMOPT_FAIL_CRITICAL_ERRORS | SYMOPT_INCLUDE_32BIT_MODULES;
|
||||
|
||||
constexpr auto MicrosoftPublicSymbols = L"srv**https://msdl.microsoft.com/download/symbols";
|
||||
|
||||
static HANDLE GetUniqueIdForSymbols() {
|
||||
// Initialise once - note we're only resolving symbols locally.
|
||||
static HANDLE s_hUniqueId = NULL; // INVALID_HANDLE_VALUE == GetCurrentProcess()!
|
||||
if (NULL == s_hUniqueId) {
|
||||
if (!SymSetOptions(SymGetOptions() | SYMOPTS))
|
||||
LogError("SymSetOptions() failed with 0x%x", GetLastError());
|
||||
s_hUniqueId = GetCurrentProcess();
|
||||
if (!SymInitializeW(s_hUniqueId, MicrosoftPublicSymbols, FALSE)) {
|
||||
LogError("SymInitialize() failed with 0x%x", GetLastError());
|
||||
s_hUniqueId = INIT_FAILED;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check that we're using public symbols - not just exports
|
||||
auto hNtdll = GetModuleHandleW(L"ntdll.dll");
|
||||
std::wstring ntdllPath;
|
||||
if (!GetMappedFileNameAsDosPath(GetCurrentProcess(), hNtdll, ntdllPath))
|
||||
LogError("GetMappedFilename(ntdll) failed");
|
||||
if(!SymLoadModuleExW(s_hUniqueId, NULL, ntdllPath.c_str(), NULL, (DWORD64)hNtdll, 0, NULL, 0))
|
||||
LogError("SymLoadModule(ntdll) failed");
|
||||
if (!GetSymbolAddress("ntdll!TppWorkerThread"))
|
||||
LogError("WARNING Symbols not found - falling back to exports");
|
||||
|
||||
auto hKernel32 = GetModuleHandleW(L"kernel32.dll");
|
||||
std::wstring kernel32Path;
|
||||
if (!GetMappedFileNameAsDosPath(GetCurrentProcess(), hKernel32, kernel32Path))
|
||||
LogError("GetMappedFilename(kernel32) failed");
|
||||
if (!SymLoadModuleExW(s_hUniqueId, NULL, kernel32Path.c_str(), NULL, (DWORD64)hKernel32, 0, NULL, 0))
|
||||
LogError("SymLoadModule(kernel32) failed");
|
||||
}
|
||||
}
|
||||
return s_hUniqueId;
|
||||
}
|
||||
|
||||
PVOID GetSymbolAddress(const char symbol[]) {
|
||||
const auto hUniqueId = GetUniqueIdForSymbols();
|
||||
if (INIT_FAILED == hUniqueId)
|
||||
return NULL;
|
||||
|
||||
SYMBOL_INFO_FULL symbolInfo{};
|
||||
symbolInfo.SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||
symbolInfo.MaxNameLen = MAX_SYM_NAME;
|
||||
if (!SymFromName(hUniqueId, symbol, &symbolInfo))
|
||||
return NULL;
|
||||
|
||||
return (PVOID)symbolInfo.Address;
|
||||
}
|
||||
|
||||
|
||||
// pretty print <module>!<symbol>[+<displacement>]
|
||||
void GetPrettySymbol(HANDLE hProcess, PVOID address, std::wstring& modulePath, std::string& symbol, bool bIncludeDisplacement) {
|
||||
symbol.resize(128);
|
||||
symbol = std::filesystem::path(modulePath).filename().string();
|
||||
|
||||
SYMBOL_INFO_FULL symbolInfo{};
|
||||
symbolInfo.SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||
symbolInfo.MaxNameLen = MAX_SYM_NAME;
|
||||
DWORD64 displacement = 0;
|
||||
if (SymFromAddr(hProcess, (DWORD64)address, &displacement, &symbolInfo)) {
|
||||
symbol += "!" + std::string(symbolInfo.Name);
|
||||
}
|
||||
else if (bIncludeDisplacement) {
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (VirtualQueryEx(hProcess, address, &mbi, sizeof(mbi)))
|
||||
displacement = (DWORD64)address - (DWORD64)mbi.AllocationBase;
|
||||
}
|
||||
|
||||
if (bIncludeDisplacement && displacement) {
|
||||
char buffer[_MAX_U64TOSTR_BASE16_COUNT];
|
||||
_ui64toa_s(displacement, buffer, sizeof(buffer), 16);
|
||||
symbol += "+0x" + std::string(buffer);
|
||||
}
|
||||
|
||||
symbol.resize(strlen(symbol.c_str()));
|
||||
}
|
||||
|
||||
BOOL GetNearestSymbolWithPdb(HANDLE hProcess, PVOID address, std::string& symbol, bool bIncludeDisplacement) {
|
||||
if (!SymSetOptions(SymGetOptions() | SYMOPTS))
|
||||
LogError("SymSetOptions() failed with 0x%x", GetLastError());
|
||||
|
||||
if (!SymInitializeW(hProcess, MicrosoftPublicSymbols, FALSE)) {
|
||||
LogError("SymInitialize() failed with 0x%x", GetLastError());
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
std::wstring modulePath;
|
||||
if (!GetMappedFileNameAsDosPath(hProcess, address, modulePath)) {
|
||||
if (symbol.empty()) {
|
||||
char buffer[_MAX_U64TOSTR_BASE16_COUNT];
|
||||
_ui64toa_s((DWORD64)address, buffer, sizeof(buffer), 16);
|
||||
symbol = "0x" + std::string(buffer);
|
||||
}
|
||||
(void)SymCleanup(hProcess);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (!VirtualQueryEx(hProcess, address, &mbi, sizeof(mbi)) ||
|
||||
!SymLoadModuleExW(hProcess, NULL, modulePath.c_str(), NULL, (DWORD64)mbi.AllocationBase, 0, NULL, 0))
|
||||
{
|
||||
(void)SymCleanup(hProcess);
|
||||
// fallback to best-effort
|
||||
const auto hUniqueId = GetUniqueIdForSymbols();
|
||||
GetPrettySymbol(hUniqueId, address, modulePath, symbol, bIncludeDisplacement);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
GetPrettySymbol(hProcess, address, modulePath, symbol, bIncludeDisplacement);
|
||||
|
||||
(void)SymCleanup(hProcess);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
BOOL GetNearestSymbol(HANDLE hProcess, PVOID address, std::string& symbol, bool bIncludeDisplacement) {
|
||||
const auto hUniqueId = GetUniqueIdForSymbols();
|
||||
if (INIT_FAILED == hUniqueId)
|
||||
return FALSE;
|
||||
|
||||
// Maintain a cache of symbols in shared image allocations
|
||||
static std::unordered_map<PVOID, std::string> s_SymbolCache;
|
||||
const auto it = s_SymbolCache.find(address);
|
||||
if (it != s_SymbolCache.end()) {
|
||||
symbol = it->second;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// module name (from remote process)
|
||||
std::wstring modulePath;
|
||||
if (!GetMappedFileNameAsDosPath(hProcess, address, modulePath)) {
|
||||
char buffer[_MAX_U64TOSTR_BASE16_COUNT];
|
||||
_ui64toa_s((DWORD64)address, buffer, sizeof(buffer), 16);
|
||||
symbol = "0x" + std::string(buffer);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
GetPrettySymbol(hUniqueId, address, modulePath, symbol, bIncludeDisplacement);
|
||||
|
||||
if (InSystemImageRange(address))
|
||||
s_SymbolCache.insert(std::make_pair(address, symbol));
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// Reference:
|
||||
// https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64
|
||||
//
|
||||
// Other useful references:
|
||||
// https://codemachine.com/articles/x64_deep_dive.html
|
||||
// http://www.uninformed.org/?v=4&a=1 Improving Automated Analysis of Windows x64 Binaries
|
||||
// http://www.nynaeve.net/?p=113 Programming against the x64 exception handling support
|
||||
// https://www.sciencedirect.com/science/article/pii/S1742287618300458 Building stack traces from memory dump of Windows x64
|
||||
// https://github.com/reactos/reactos/blob/master/sdk/lib/rtl/amd64/unwind.c
|
||||
// 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
|
||||
// https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs
|
||||
|
||||
#include "Get-InjectedThreadEx.h"
|
||||
|
||||
typedef enum _UNWIND_OP_CODES {
|
||||
UWOP_PUSH_NONVOL = 0, /* info == register number */
|
||||
UWOP_ALLOC_LARGE, /* no info, alloc size in next 2 slots */
|
||||
UWOP_ALLOC_SMALL, /* info == size of allocation / 8 - 1 */
|
||||
UWOP_SET_FPREG, /* no info, FP = RSP + UNWIND_INFO.FPRegOffset*16 */
|
||||
UWOP_SAVE_NONVOL, /* info == register number, offset in next slot */
|
||||
UWOP_SAVE_NONVOL_FAR, /* info == register number, offset in next 2 slots */
|
||||
UWOP_EPILOG, /* added in v2. UNDOCUMENTED. */
|
||||
UWOP_SAVE_XMM128 = 8, /* info == XMM reg number, offset in next slot */
|
||||
UWOP_SAVE_XMM128_FAR, /* info == XMM reg number, offset in next 2 slots */
|
||||
UWOP_PUSH_MACHFRAME /* info == 0: no dwError-code, 1: dwError-code */
|
||||
} UNWIND_CODE_OPS;
|
||||
|
||||
// from ehdata.h
|
||||
#pragma warning (push)
|
||||
#pragma warning (disable: 4201)
|
||||
typedef union _UNWIND_CODE {
|
||||
struct {
|
||||
unsigned char CodeOffset;
|
||||
unsigned char UnwindOp : 4;
|
||||
unsigned char OpInfo : 4;
|
||||
};
|
||||
unsigned short FrameOffset;
|
||||
} UNWIND_CODE, * PUNWIND_CODE;
|
||||
typedef struct _UNWIND_INFO {
|
||||
unsigned char Version : 3;
|
||||
unsigned char Flags : 5;
|
||||
unsigned char SizeOfProlog;
|
||||
unsigned char CountOfCodes;
|
||||
unsigned char FrameRegister : 4;
|
||||
unsigned char FrameOffset : 4;
|
||||
UNWIND_CODE UnwindCode[1];
|
||||
/* UNWIND_CODE MoreUnwindCode[((CountOfCodes+1)&~1)-1];
|
||||
* union {
|
||||
* OPTIONAL unsigned long ExceptionHandler;
|
||||
* OPTIONAL unsigned long FunctionEntry;
|
||||
* };
|
||||
* OPTIONAL unsigned long ExceptionData[];
|
||||
*/
|
||||
} UNWIND_INFO, * PUNWIND_INFO;
|
||||
#pragma warning (pop)
|
||||
|
||||
constexpr auto W_FPREG = 1; // WARNING - frame pointer in use;
|
||||
static HRESULT CalculateFrameSize_Internal(const PRUNTIME_FUNCTION pRuntimeFunction, const DWORD64 ImageBase, const DWORD rva, PDWORD pFrameSize)
|
||||
{
|
||||
HRESULT status = S_OK;
|
||||
|
||||
const auto pUnwindInfo = (PUNWIND_INFO)(pRuntimeFunction->UnwindInfoAddress + ImageBase);
|
||||
if (!pUnwindInfo || pUnwindInfo->Version > 2) {
|
||||
LogError("UNWIND_INFO v%d not supported", pUnwindInfo->Version);
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
const auto codeOffset = rva - pRuntimeFunction->BeginAddress;
|
||||
|
||||
// Loop over unwind codes and calculate total stack space used by target function.
|
||||
BYTE i = 0;
|
||||
while (i < pUnwindInfo->CountOfCodes)
|
||||
{
|
||||
// Warning: This implementation is not complete and may not work for some edge cases.
|
||||
// For example, it does not handle handle RIP pointing into an epilog.
|
||||
// But that should be rare for our early frames use case.
|
||||
|
||||
const auto unwindOperation = pUnwindInfo->UnwindCode[i].UnwindOp;
|
||||
const auto operationInfo = pUnwindInfo->UnwindCode[i].OpInfo;
|
||||
const auto bApplyOperation = codeOffset > pUnwindInfo->UnwindCode[i].CodeOffset;
|
||||
|
||||
i++;
|
||||
switch (unwindOperation) {
|
||||
case UWOP_PUSH_NONVOL:
|
||||
if (bApplyOperation)
|
||||
(*pFrameSize)++;
|
||||
break;
|
||||
case UWOP_ALLOC_LARGE:
|
||||
switch (operationInfo) {
|
||||
case 0:
|
||||
if (bApplyOperation)
|
||||
*pFrameSize += pUnwindInfo->UnwindCode[i].FrameOffset;
|
||||
i++;
|
||||
break;
|
||||
case 1:
|
||||
if (bApplyOperation)
|
||||
*pFrameSize += *(DWORD*)(&pUnwindInfo->UnwindCode[i]) / sizeof(PVOID);
|
||||
i += 3;
|
||||
break;
|
||||
default:
|
||||
LogError("UWOP_ALLOC_LARGE operationInfo is invalid: (%d)", operationInfo);
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
break;
|
||||
case UWOP_ALLOC_SMALL:
|
||||
if (bApplyOperation)
|
||||
*pFrameSize += operationInfo + 1;
|
||||
break;
|
||||
case UWOP_SET_FPREG:
|
||||
if (bApplyOperation) {
|
||||
// Frame pointer in use. Our frame size calculation is inaccurate if alloca() was used.
|
||||
// https://learn.microsoft.com/en-us/cpp/build/stack-usage
|
||||
// > If space is dynamically allocated (alloca) in a function, then a
|
||||
// > nonvolatile register must be used as a frame pointer to mark the
|
||||
// > base of the fixed part of the stack and that register must be saved
|
||||
// > and initialized in the prolog.
|
||||
*pFrameSize = pUnwindInfo->FrameOffset * 2;
|
||||
status = W_FPREG; // This is a WARNING code - not an ERROR
|
||||
}
|
||||
break;
|
||||
case UWOP_SAVE_NONVOL:
|
||||
case UWOP_SAVE_XMM128:
|
||||
case UWOP_EPILOG:
|
||||
i++;
|
||||
break;
|
||||
case UWOP_SAVE_NONVOL_FAR:
|
||||
case UWOP_SAVE_XMM128_FAR:
|
||||
i += 2;
|
||||
break;
|
||||
case UWOP_PUSH_MACHFRAME:
|
||||
if (bApplyOperation) {
|
||||
*pFrameSize += 5 + operationInfo;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (bApplyOperation) {
|
||||
LogError("UNWIND_INFO operation is not implemented: %d", unwindOperation);
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (0 != (UNW_FLAG_CHAININFO & pUnwindInfo->Flags)) {
|
||||
const auto pPrimaryUwindInfo = (PRUNTIME_FUNCTION) & (pUnwindInfo->UnwindCode[(pUnwindInfo->CountOfCodes + 1) & ~1]);
|
||||
return CalculateFrameSize_Internal(pPrimaryUwindInfo, ImageBase, rva, pFrameSize);
|
||||
}
|
||||
|
||||
// Add the size of the return address.
|
||||
*pFrameSize += 1;
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
static_assert(!IS_ERROR(W_FPREG));
|
||||
static_assert(!FAILED(W_FPREG));
|
||||
// Calculates the total stack space (in PVOIDs) used by the stack frame.
|
||||
HRESULT CalculateFrameSize(PVOID returnAddress, PDWORD pFrameSize, const std::wstring& filepath, PVOID remoteBase)
|
||||
{
|
||||
// Check a cache of UNWIND_INFO lookups first.
|
||||
static std::unordered_map<PVOID, DWORD> frameSizes;
|
||||
const auto it = frameSizes.find(returnAddress);
|
||||
if (it != frameSizes.end()) {
|
||||
*pFrameSize = it->second;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const DWORD rva = (DWORD)((ULONG_PTR)returnAddress - (ULONG_PTR)remoteBase);
|
||||
PRUNTIME_FUNCTION pRuntimeFunction = NULL;
|
||||
DWORD64 localBase;
|
||||
|
||||
auto hModule = GetModuleHandleW(filepath.c_str());
|
||||
if (hModule == remoteBase) {
|
||||
// Image is already loaded in our process - lock it in memory now.
|
||||
hModule = LoadLibraryW(filepath.c_str());
|
||||
if (NULL == hModule) {
|
||||
LogError("LoadLibrary(%S) failed", filepath.c_str());
|
||||
return E_UNEXPECTED;
|
||||
|
||||
}
|
||||
|
||||
pRuntimeFunction = RtlLookupFunctionEntry((DWORD64)returnAddress, &localBase, NULL);
|
||||
if (NULL == pRuntimeFunction)
|
||||
return E_FAIL;
|
||||
}
|
||||
else {
|
||||
// Image is not loaded - so load it now.
|
||||
// We don't want to execute anything malicious in our process - so map it now as a read-only resource.
|
||||
hModule = LoadLibraryExW(filepath.c_str(), NULL, LOAD_LIBRARY_AS_IMAGE_RESOURCE | LOAD_LIBRARY_AS_DATAFILE);
|
||||
if (NULL == hModule) {
|
||||
LogDebug("LoadLibraryEx(%S, AS_IMAGE_RESOURCE) failed", filepath.c_str());
|
||||
return E_UNEXPECTED;
|
||||
}
|
||||
localBase = (DWORD64)hModule & ~0xFFFF; // The lower bits of resource-only module handles are used as flags.
|
||||
|
||||
// Being resource-only means that we can't use the usual APIs.
|
||||
// Instead, manually walk the UNWIND_INFO in the exception directory of the image.
|
||||
ULONG indexLo = 0;
|
||||
ULONG indexHi;
|
||||
auto pTable = (PRUNTIME_FUNCTION)ImageDirectoryEntryToData((PVOID)localBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXCEPTION, &indexHi);
|
||||
if (!pTable) {
|
||||
(void)FreeLibrary(hModule);
|
||||
const auto error = GetLastError();
|
||||
if (ERROR_SUCCESS != error)
|
||||
LogError("ImageDirectoryEntryToData(%S) failed with %d", filepath.c_str(), error);
|
||||
else
|
||||
LogDebug("ImageDirectoryEntryToData(%S) - Exception Directory not found");
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
indexHi = indexHi / sizeof(RUNTIME_FUNCTION);
|
||||
while (indexHi > indexLo) {
|
||||
const ULONG indexMid = (indexLo + indexHi) / 2;
|
||||
pRuntimeFunction = &pTable[indexMid];
|
||||
if (rva < pRuntimeFunction->BeginAddress) {
|
||||
indexHi = indexMid; // search lower
|
||||
pRuntimeFunction = NULL;
|
||||
continue;
|
||||
}
|
||||
if (rva >= pRuntimeFunction->EndAddress) {
|
||||
indexLo = indexMid + 1; // search higher
|
||||
pRuntimeFunction = NULL;
|
||||
continue;
|
||||
}
|
||||
break; // found
|
||||
}
|
||||
}
|
||||
|
||||
if (!pRuntimeFunction) {
|
||||
(void)FreeLibrary(hModule);
|
||||
return E_FAIL; // leaf functions can't call further functions
|
||||
}
|
||||
|
||||
*pFrameSize = 0;
|
||||
auto status = CalculateFrameSize_Internal(pRuntimeFunction, localBase, rva, pFrameSize);
|
||||
if (S_OK == status)
|
||||
frameSizes.insert(std::make_pair(returnAddress, *pFrameSize));
|
||||
|
||||
if (!FreeLibrary(hModule))
|
||||
LogError("FreeLibrary(%S) failed", filepath.c_str());
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
BOOL IsValidCallSite(
|
||||
HANDLE hProcess,
|
||||
bool bIsWow64,
|
||||
const MEMORY_BASIC_INFORMATION& mbi,
|
||||
PVOID callsite,
|
||||
bool* bCallFound)
|
||||
{
|
||||
// Check a cache of valid callsites first
|
||||
static std::unordered_set<PVOID> validCallSites;
|
||||
if (validCallSites.contains(callsite)) {
|
||||
*bCallFound = true;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Otherwise read the preceding bytes and check for a valid call instruction
|
||||
std::string precedingBytes{};
|
||||
precedingBytes.resize(std::min(11ull, (ULONG_PTR)callsite - (ULONG_PTR)mbi.BaseAddress));
|
||||
if(!ReadProcessMemorySafely(hProcess, (PVOID)((ULONG_PTR)callsite - precedingBytes.size()), precedingBytes, mbi))
|
||||
return FALSE;
|
||||
|
||||
ZydisDecoder decoder;
|
||||
if (bIsWow64)
|
||||
(void)ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_COMPAT_32, ZYDIS_STACK_WIDTH_32);
|
||||
else
|
||||
(void)ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64);
|
||||
|
||||
*bCallFound = false;
|
||||
ZydisDecodedInstruction instruction;
|
||||
// call instructions are 2-11 bytes, but check some common lengths first
|
||||
static const std::array<int, 10> callLengthSearchOrder = { 5, 6, 7, 2, 3, 4, 8, 9, 10, 11 };
|
||||
for (const auto& lengthToCheck : callLengthSearchOrder) {
|
||||
if (lengthToCheck <= precedingBytes.size() &&
|
||||
ZYAN_SUCCESS(ZydisDecoderDecodeInstruction(&decoder, nullptr, precedingBytes.data() + precedingBytes.size() - lengthToCheck, lengthToCheck, &instruction)) &&
|
||||
lengthToCheck == instruction.length &&
|
||||
ZYDIS_MNEMONIC_CALL == instruction.mnemonic)
|
||||
{
|
||||
*bCallFound = true;
|
||||
// Note - For direct calls, we could (attempt to) improve this check by validating
|
||||
// that the call target matches the return address.
|
||||
// Though we would need to handle an Tail Call Optimised (TCO) functions...
|
||||
//
|
||||
// In practice this additional validation isn't required for our use case.
|
||||
// False positivies are rare in early stack frames.
|
||||
|
||||
// Cache the result
|
||||
if (MEM_IMAGE == mbi.Type && InSystemImageRange(callsite))
|
||||
validCallSites.insert(callsite);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
Submodule
+1
Submodule Get-InjectedThreadEx/zydis added at a6d0c713b7
@@ -0,0 +1,23 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2021 Florian Bernd
|
||||
Copyright (c) 2014-2021 Joel Höner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Reference in New Issue
Block a user