├── .gitignore ├── README.md ├── TickTock.sln └── TickTock ├── TickTock.cpp ├── TickTock.vcxproj └── TickTock.vcxproj.filters /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TickTock 2 | 3 | This repository demonstrates a PoC memory scanner for enumerating timer-queue timers as used in Ekko Sleep Obfuscation: https://github.com/Cracked5pider/Ekko. For a full technical walkthrough please see the accompanying blog post here: https://labs.withsecure.com/publications/hunting-for-timer-queue-timers.html. 4 | 5 | The screenshot below demonstrates the results of scanning for timer-queue timers while Ekko is running: 6 | 7 | HuntingForTimers_TickTock 8 | 9 | NB As a word of caution this PoC was tested on Windows 10 1607 and Windows 10 21h2. However, as it relies on undocumented functionality it may break due to future Windows releases. 10 | 11 | Additionally, this tool requires symbols to be correctly configured and hence you will need to install the Debugging Tools for Windows (WinDbg) as a pre-requisite. 12 | 13 | # Related Work 14 | https://github.com/joe-desimone/patriot 15 | -------------------------------------------------------------------------------- /TickTock.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32414.318 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TickTock", "TickTock\TickTock.vcxproj", "{6C9669AC-EF17-4B5E-8E46-6D6179C2D2E6}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {6C9669AC-EF17-4B5E-8E46-6D6179C2D2E6}.Debug|x64.ActiveCfg = Debug|x64 17 | {6C9669AC-EF17-4B5E-8E46-6D6179C2D2E6}.Debug|x64.Build.0 = Debug|x64 18 | {6C9669AC-EF17-4B5E-8E46-6D6179C2D2E6}.Debug|x86.ActiveCfg = Release|Win32 19 | {6C9669AC-EF17-4B5E-8E46-6D6179C2D2E6}.Release|x64.ActiveCfg = Release|x64 20 | {6C9669AC-EF17-4B5E-8E46-6D6179C2D2E6}.Release|x64.Build.0 = Release|x64 21 | {6C9669AC-EF17-4B5E-8E46-6D6179C2D2E6}.Release|x86.ActiveCfg = Release|Win32 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ExtensibilityGlobals) = postSolution 27 | SolutionGuid = {06A51008-FFCA-46E9-9EF3-D6720F11487A} 28 | EndGlobalSection 29 | EndGlobal 30 | -------------------------------------------------------------------------------- /TickTock/TickTock.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | // 18 | // From Ntdef.h. 19 | // 20 | // Treat anything not STATUS_SUCCESS as an error. 21 | #define NT_SUCCESS(Status) (((NTSTATUS)(Status)) == 0) 22 | #define STATUS_SUCCESS ((NTSTATUS)0x00000000L) 23 | 24 | // 25 | // Scanner config options. 26 | // 27 | bool bShowDebugOutput = false; 28 | std::string defaultSymbolPath = "C:\\Symbols\\;"; 29 | 30 | // 31 | // Run time linking for NtQueryInformationProcess. 32 | // 33 | typedef NTSTATUS(NTAPI* pNtQueryInformationProcess)( 34 | IN HANDLE ProcessHandle, 35 | IN PROCESSINFOCLASS ProcessInformationClass, 36 | OUT PVOID ProcessInformation, 37 | IN ULONG ProcessInformationLength, 38 | OUT PULONG ReturnLength OPTIONAL 39 | ); 40 | pNtQueryInformationProcess myNtQueryInformationProcess; 41 | 42 | // 43 | // Required threadpool symbols. 44 | // 45 | using myPtr = intptr_t; 46 | myPtr pTppTimerpCleanupGroupMemberVFuncs = NULL; 47 | myPtr pRtlpTpTimerFinalizationCallback = NULL; 48 | myPtr pRtlpTpTimerCallback = NULL; 49 | myPtr pTppTimerpTaskVFuncs = NULL; 50 | 51 | // 52 | // Threadpool heap allocation constants. 53 | // 54 | // RtlCreateTimer: Heap = RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, 0i64, 0x60i64); 55 | // NB In both cases we dont need to retrieve full alloc to find what we need. 56 | int rtlCreateTimerHeapAllocMinSizeToRead = 15; 57 | // TpAllocTimer: Heap = RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, (TppHeapTag + 0x100000) | 8u, 0x168i64); 58 | int tpAllocTimerHeapAllocMinSizeToRead = 25; 59 | 60 | // 61 | // Templated wrappers around ReadProcessMemory. 62 | // 63 | template 64 | T readProcessMemory(HANDLE hProcess, LPVOID targetAddress) 65 | { 66 | T returnValue; 67 | (void)ReadProcessMemory(hProcess, targetAddress, &returnValue, sizeof(T), NULL); 68 | return returnValue; 69 | } 70 | 71 | template 72 | void readProcessMemory(HANDLE hProcess, PVOID targetAddress, SIZE_T elementsToRead, std::vector &buffer) 73 | { 74 | (void)ReadProcessMemory(hProcess, targetAddress, buffer.data(), elementsToRead * sizeof(T), NULL); 75 | return; 76 | } 77 | 78 | // 79 | // Takes a symbol name and finds its address. 80 | // 81 | // https://learn.microsoft.com/en-us/windows/win32/debug/retrieving-symbol-information-by-name 82 | NTSTATUS ResolveSymbolFromName(HANDLE hProcess, PCSTR symbolName, myPtr &address) 83 | { 84 | NTSTATUS status = STATUS_SUCCESS; 85 | 86 | // [0] Prepare buffer for SYMBOL_INFO. 87 | ULONG64 buffer[(sizeof(SYMBOL_INFO) + 88 | MAX_SYM_NAME * sizeof(TCHAR) + 89 | sizeof(ULONG64) - 1) / 90 | sizeof(ULONG64)]; 91 | PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer; 92 | 93 | pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO); 94 | pSymbol->MaxNameLen = MAX_SYM_NAME; 95 | 96 | // [1] Resolve symbol name. 97 | if (!SymFromName(hProcess, symbolName, pSymbol)) 98 | { 99 | std::cout << "[-] SymFromName returned an error: " << GetLastError() << " for symbol name: " << symbolName << "\n"; 100 | status = STATUS_ASSERTION_FAILURE; 101 | goto Cleanup; 102 | } 103 | address = pSymbol->Address; 104 | 105 | Cleanup: 106 | return status; 107 | } 108 | 109 | // 110 | // Takes an address and resolves its corresponding symbol. 111 | // 112 | // https://learn.microsoft.com/en-us/windows/win32/debug/retrieving-symbol-information-by-address 113 | NTSTATUS ResolveSymbolFromAddress(HANDLE hProcess, myPtr targetAddress, std::string &symbol) 114 | { 115 | NTSTATUS status = STATUS_SUCCESS; 116 | 117 | // [0] Prepare buffer for SYMBOL_INFO. 118 | char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)]; 119 | PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer; 120 | 121 | pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO); 122 | pSymbol->MaxNameLen = MAX_SYM_NAME; 123 | 124 | // [1] Resolve address. 125 | if (!SymFromAddr(hProcess, targetAddress, NULL, pSymbol)) 126 | { 127 | printf("[-] Failed to resolve callback function - SymFromAddr returned error : %d\n", GetLastError()); 128 | status = STATUS_ASSERTION_FAILURE; 129 | goto Cleanup; 130 | } 131 | symbol = pSymbol->Name; 132 | 133 | Cleanup: 134 | return status; 135 | } 136 | 137 | // 138 | // Performs scanner initilisation via resolving necessary thread pool pointers and required functions. 139 | // 140 | NTSTATUS InitialiseScanner() 141 | { 142 | NTSTATUS status = STATUS_SUCCESS; 143 | 144 | // [1] Initialise symbols. 145 | if (!SymInitialize(GetCurrentProcess(), defaultSymbolPath.c_str(), TRUE)) 146 | { 147 | std::cout << "[!] SymInitialze returned an error: " << GetLastError() << "\n"; 148 | status = STATUS_ASSERTION_FAILURE; 149 | goto Cleanup; 150 | } 151 | 152 | // [2] Resolve required thread pool symbols. 153 | if (!NT_SUCCESS(ResolveSymbolFromName(GetCurrentProcess(), "ntdll!TppTimerpCleanupGroupMemberVFuncs", pTppTimerpCleanupGroupMemberVFuncs))) 154 | { 155 | std::cout << "[!] Failed to resolve required threadpool symbols; ensure symbols are correctly configured or try reloading symbols in windbg via .reload\n"; 156 | status = STATUS_ASSERTION_FAILURE; 157 | goto Cleanup; 158 | } 159 | 160 | if (!NT_SUCCESS(ResolveSymbolFromName(GetCurrentProcess(), "ntdll!RtlpTpTimerFinalizationCallback", pRtlpTpTimerFinalizationCallback))) 161 | { 162 | std::cout << "[!] Failed to resolve required threadpool symbols; ensure symbols are correctly configured or try reloading symbols in windbg via .reload\n"; 163 | status = STATUS_ASSERTION_FAILURE; 164 | goto Cleanup; 165 | } 166 | 167 | if (!NT_SUCCESS(ResolveSymbolFromName(GetCurrentProcess(), "ntdll!RtlpTpTimerCallback", pRtlpTpTimerCallback))) 168 | { 169 | std::cout << "[!] Failed to resolve required threadpool symbols; ensure symbols are correctly configured or try reloading symbols in windbg via .reload\n"; 170 | status = STATUS_ASSERTION_FAILURE; 171 | goto Cleanup; 172 | } 173 | 174 | if (!NT_SUCCESS(ResolveSymbolFromName(GetCurrentProcess(), "ntdll!TppTimerpTaskVFuncs", pTppTimerpTaskVFuncs))) 175 | { 176 | std::cout << "[!] Failed to resolve required threadpool symbols; ensure symbols are correctly configured or try reloading symbols in windbg via .reload\n"; 177 | status = STATUS_ASSERTION_FAILURE; 178 | goto Cleanup; 179 | } 180 | 181 | // [3] Lastly, resolve NtQueryInformationProcess. 182 | myNtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQueryInformationProcess"); 183 | if (NULL == myNtQueryInformationProcess) 184 | { 185 | std::cout << "[!] Failed to resolve NtQueryInformationProcess\n"; 186 | status = STATUS_ASSERTION_FAILURE; 187 | goto Cleanup; 188 | } 189 | 190 | Cleanup: 191 | SymCleanup(GetCurrentProcess()); 192 | return status; 193 | } 194 | 195 | // 196 | // Parses the remote process PEB to find all the target process heap allocations. 197 | // 198 | NTSTATUS FindProcessHeaps(HANDLE hProcess, std::vector &processHeapVector) 199 | { 200 | NTSTATUS status = STATUS_SUCCESS; 201 | PROCESS_BASIC_INFORMATION ProcessInformation = {}; 202 | PEB peb = {}; 203 | PVOID processHeapsArrayPtr = NULL; 204 | std::vector heapPtrs; 205 | PVOID pebHeapEntry = 0; 206 | uint32_t numberOfHeaps = 0; 207 | 208 | // [0] Sanity check handle. 209 | if (hProcess == INVALID_HANDLE_VALUE) 210 | { 211 | status = STATUS_ASSERTION_FAILURE; 212 | goto Cleanup; 213 | } 214 | 215 | // [1] Locate remote PEB and read it into memory. 216 | status = myNtQueryInformationProcess(hProcess, ProcessBasicInformation, &ProcessInformation, sizeof(ProcessInformation), NULL); 217 | if (status != 0) 218 | { 219 | status = STATUS_ASSERTION_FAILURE; 220 | goto Cleanup; 221 | } 222 | peb = readProcessMemory(hProcess, ProcessInformation.PebBaseAddress); 223 | 224 | // [2] From the peb, retrieve the number of heaps and pointer to the process heaps. 225 | // + 0x0e8 NumberOfHeaps : 3 226 | // + 0x0ec MaximumNumberOfHeaps : 0x10 227 | // + 0x0f0 ProcessHeaps : 0x00007ffe`445b9d40 -> 0x00000196`26510000 Void 228 | // Ref: https://github.com/odzhan/injection/blob/master/ntlib/ntddk.h#L3164-L3166 229 | pebHeapEntry = peb.Reserved9[16]; 230 | numberOfHeaps = (uint64_t)pebHeapEntry & 0xFFFFFFFF; 231 | processHeapsArrayPtr = (PVOID)peb.Reserved9[17]; 232 | heapPtrs.resize(numberOfHeaps); 233 | // Read the process heaps array (which contains ptrs to heap locations). 234 | readProcessMemory(hProcess, processHeapsArrayPtr, numberOfHeaps, heapPtrs); 235 | 236 | // [3] Loop round and record the region size. 237 | for (const auto& heapPtr : heapPtrs) 238 | { 239 | MEMORY_BASIC_INFORMATION mbi = {}; 240 | if (!VirtualQueryEx(hProcess, (PVOID)heapPtr, &mbi, sizeof(mbi))) 241 | { 242 | status = STATUS_ASSERTION_FAILURE; 243 | goto Cleanup; 244 | } 245 | processHeapVector.push_back(mbi); 246 | } 247 | 248 | Cleanup: 249 | return status; 250 | } 251 | 252 | // 253 | // Converts an intptr to vector of bytes. 254 | // 255 | void IntToVectorOfBytes(myPtr originalValue, std::vector &byteVector) 256 | { 257 | byteVector.push_back(originalValue & 0xFF); 258 | byteVector.push_back((originalValue >> 8) & 0xFF); 259 | byteVector.push_back((originalValue >> 16) & 0xFF); 260 | byteVector.push_back((originalValue >> 24) & 0xFF); 261 | byteVector.push_back((originalValue >> 32) & 0xFF); 262 | byteVector.push_back((originalValue >> 40) & 0xFF); 263 | byteVector.push_back((originalValue >> 48) & 0xFF); 264 | byteVector.push_back((originalValue >> 56) & 0xFF); 265 | return; 266 | } 267 | 268 | // 269 | // Takes an address and retrieves the base name of the specified module. 270 | // 271 | NTSTATUS GetModuleBaseNameWrapper(HANDLE hProcess, PVOID targetAddress, std::string& moduleName) 272 | { 273 | NTSTATUS status = STATUS_SUCCESS; 274 | char szModuleBaseName[MAX_PATH]; 275 | 276 | if (GetModuleBaseNameA(hProcess, (HMODULE)targetAddress, szModuleBaseName, sizeof(szModuleBaseName))) 277 | { 278 | moduleName = szModuleBaseName; 279 | } 280 | else 281 | { 282 | printf("[-] GetModuleBaseName returned error : %d\n", GetLastError()); 283 | status = STATUS_ASSERTION_FAILURE; 284 | } 285 | 286 | return status; 287 | } 288 | 289 | // 290 | // Takes an address and performs best effort symbol resolution (i.e., to offset level winlogon+0x63590). 291 | // 292 | NTSTATUS GetBasicSymbolFromAddress(HANDLE hProcess, myPtr targetAddress, std::string &symbol) 293 | { 294 | NTSTATUS status = STATUS_SUCCESS; 295 | MEMORY_BASIC_INFORMATION mbi = {}; 296 | std::string moduleName; 297 | 298 | // [0] Sanity check incoming targetAddress. 299 | if (NULL == targetAddress) 300 | { 301 | status = STATUS_INVALID_PARAMETER; 302 | goto Cleanup; 303 | } 304 | 305 | // [1] Query pages at target addr. 306 | if (!VirtualQueryEx(hProcess, (PVOID)targetAddress, &mbi, sizeof(mbi))) 307 | { 308 | status = STATUS_ASSERTION_FAILURE; 309 | goto Cleanup; 310 | } 311 | 312 | // [2] Try and resolve module at addr. 313 | if (!NT_SUCCESS(GetModuleBaseNameWrapper(hProcess, mbi.AllocationBase, moduleName))) 314 | { 315 | status = STATUS_ASSERTION_FAILURE; 316 | goto Cleanup; 317 | } 318 | 319 | // [3] Calculate offset from allocation base and return as basemodule + offset (e.g. winlogon+0x63590). 320 | symbol = moduleName + "+0x" + std::format("{:x}", (targetAddress - (myPtr)mbi.AllocationBase)); 321 | 322 | Cleanup: 323 | return status; 324 | } 325 | 326 | // 327 | // Rough heuristic to sanity check whether a timer callback ptr looks valid. 328 | // 329 | // https://www.unknowncheats.me/forum/c-and-c-/304873-checking-valid-pointer.html 330 | bool IsInvalidPtr(myPtr targetAddress) 331 | { 332 | static SYSTEM_INFO si = {}; 333 | if (nullptr == si.lpMinimumApplicationAddress) 334 | { 335 | GetSystemInfo(&si); 336 | } 337 | 338 | return ((targetAddress < (myPtr)si.lpMinimumApplicationAddress || targetAddress > (myPtr)si.lpMaximumApplicationAddress)); 339 | } 340 | 341 | // 342 | // Scans a remote process's heap memory in order to find timer-queue timers. 343 | // 344 | // RtlCreateTimer creates two heap allocations: 345 | // 1) One itself via directly calling RtlAllocateHeap. 346 | // 2) One indirectly via calling TpAllocTimer. 347 | // This function looks for a common pattern of pointers that are set on the 348 | // *second* block of memory (via TpAllocTimer). This block of memory also contains a ptr to 349 | // the first allocation, which contains the actual callback and parameter passed 350 | // in the call to CreateTimerQueueTimer. By following this pointer we can 351 | // identify and enumerate timer-queue timers. 352 | void ScanHeapMemory(HANDLE hProcess, std::vector &processHeapVector) 353 | { 354 | bool printDelimiter = true; 355 | 356 | // [0] Convert target ptr to vector of bytes first so we can use STL search algorithm. 357 | std::vector pTppTimerpCleanupGroupMemberVFuncsBytes; 358 | IntToVectorOfBytes(pTppTimerpCleanupGroupMemberVFuncs, pTppTimerpCleanupGroupMemberVFuncsBytes); 359 | 360 | // [1] Loop over each heap. 361 | for (auto heap : processHeapVector) 362 | { 363 | // [2] Read in heap memory. 364 | std::vector heapMemory(heap.RegionSize); 365 | readProcessMemory(hProcess, heap.AllocationBase, heap.RegionSize, heapMemory); 366 | 367 | // [2] Scan heap memory for a pointer to ntdll!TppTimerpCleanupGroupMemberVFuncs. 368 | auto heapMemoryIt = begin(heapMemory); 369 | while (true) 370 | { 371 | heapMemoryIt = search(heapMemoryIt, end(heapMemory), begin(pTppTimerpCleanupGroupMemberVFuncsBytes), end(pTppTimerpCleanupGroupMemberVFuncsBytes)); 372 | 373 | // If we didn't find anything skip to next heap. 374 | if (heapMemoryIt == heapMemory.end()) 375 | { 376 | break; 377 | } 378 | 379 | // Record virtual address for debugging purposes in case we have a match. 380 | PVOID virtualAddrOfTimer = (PCHAR)heap.AllocationBase + (heapMemoryIt - heapMemory.begin()); 381 | 382 | // [3] If we have found our target pointer, we *know* the expected memory layout 383 | // from this point on. Hence, for simplicity, copy over data to new vector of 384 | // intptrs so we can quickly check other offsets for expected ptr values. 385 | 386 | // [3.1] First work out current index and convert to ptr. 387 | myPtr* vectorStart = (myPtr*)heapMemory.data() + ((heapMemoryIt - heapMemory.begin()) / sizeof(myPtr)); 388 | 389 | // [3.2] Advance iterator so we keep scanning remaining 390 | // heap memory if we fail to find a timer. 391 | std::advance(heapMemoryIt, 1); 392 | 393 | // [3.3] Create new vector of intptrs from current index. 394 | // Sanity check our end ptr doesn't overrun vector data. 395 | myPtr* vectorEnd = vectorStart + tpAllocTimerHeapAllocMinSizeToRead; 396 | myPtr* upperBound = (myPtr*)&(*(heapMemory.end() - sizeof(myPtr))); 397 | if (vectorEnd > upperBound) 398 | { 399 | continue; 400 | } 401 | std::vector tpAllocTimerHeapAllocData(vectorStart, vectorEnd); 402 | 403 | // If we have a timer, heap memory allocated by TpAllocTimer 404 | // will look like this: 405 | // 0:000 > dps 0000021c4e003610 L170 406 | // 0000021c`4e003610 00000000`00000001 (Bool indicicating if it has yet to fire?) 407 | // 0000021c`4e003618 00007ffe`4456c1e8 ntdll!TppTimerpCleanupGroupMemberVFuncs <-- Current index 408 | // 0000021c`4e003620 00000000`00000000 409 | // 0000021c`4e003628 00000000`00000000 410 | // 0000021c`4e003630 00007ffe`44458820 ntdll!RtlpTpTimerFinalizationCallback [4] 411 | // 0000021c`4e003638 0000021c`4e003638 412 | // 0000021c`4e003640 0000021c`4e003638 413 | // 0000021c`4e003648 00000000`00000000 414 | // 0000021c`4e003650 00000000`00000000 415 | // 0000021c`4e003658 00000000`00000000 416 | // 0000021c`4e003660 00007ffe`444c60a0 ntdll!RtlpTpTimerCallback [5] 417 | // 0000021c`4e003668 0000021c`4dfff290 ptr to first allocation by RtlCreateTimer which contains the target callback and parameter. [6] 418 | // 0000021c`4e003670 00000000`00000000 419 | // 0000021c`4e003678 00000000`00000000 420 | // 0000021c`4e003680 00000000`00000000 421 | // 0000021c`4e003688 00000000`00000000 422 | // 0000021c`4e003690 00000000`00000000 423 | // 0000021c`4e003698 00000000`00000000 424 | // 0000021c`4e0036a0 0000021c`4e005120 425 | // 0000021c`4e0036a8 0000021c`4e005170 426 | // 0000021c`4e0036b0 0000021c`4e005170 427 | // 0000021c`4e0036b8 00000000`00000002 428 | // 0000021c`4e0036c0 00007ffe`44459ee0 ntdll!RtlCreateTimer + 0x190 429 | // 0000021c`4e0036c8 00000000`00000000 430 | // 0000021c`4e0036d0 00000000`00000001 431 | // 0000021c`4e0036d8 00007ffe`4456c138 ntdll!TppTimerpTaskVFuncs [7] 432 | // 0000021c`4e0036e0 00000001`00000000 433 | // 0000021c`4e0036e8 00000000`00000000 434 | // 0000021c`4e0036f0 00000000`00000000 435 | 436 | // [4] Look ahead for pointer to ntdll!RtlpTpTimerFinalizationCallback. 437 | auto tpAllocTimerHeapAllocDataIt = tpAllocTimerHeapAllocData.begin(); 438 | std::advance(tpAllocTimerHeapAllocDataIt, 3); 439 | if (pRtlpTpTimerFinalizationCallback != *tpAllocTimerHeapAllocDataIt) 440 | { 441 | continue; 442 | } 443 | 444 | // [5] Look ahead for pointer to ntdll!RtlpTpTimerCallback. 445 | std::advance(tpAllocTimerHeapAllocDataIt, 6); 446 | if (pRtlpTpTimerCallback != *tpAllocTimerHeapAllocDataIt) 447 | { 448 | continue; 449 | } 450 | 451 | // [6] Record ptr to initial RtlCreateTimer heap allocation. 452 | // The ptr after ntdll!RtlpTpTimerCallback points to the initial 453 | // heap allocation performed by RtlCreateTimer. This contains the 454 | // target callback and parameter. 455 | std::advance(tpAllocTimerHeapAllocDataIt, 1); 456 | auto rtlCreateTimerHeapDataPtr = *tpAllocTimerHeapAllocDataIt; 457 | 458 | // [7] Look ahead for pointer to ntdll!TppTimerpTaskVFuncs. 459 | std::advance(tpAllocTimerHeapAllocDataIt, 14); 460 | if (pTppTimerpTaskVFuncs != *tpAllocTimerHeapAllocDataIt) 461 | { 462 | continue; 463 | } 464 | 465 | // For debugging purposes. If you want very verbose output move this up to L378 466 | // to show heap output every time we find a ptr to ntdll!TppTimerpCleanupGroupMemberVFuncs. 467 | if (bShowDebugOutput) 468 | { 469 | std::cout << "[+] TpAllocTimer initial heap allocation memory layout:\n"; 470 | std::cout << "REFERENCE: ntdll!TppTimerpCleanupGroupMemberVFuncs --> 0x" << std::hex << (PVOID)pTppTimerpCleanupGroupMemberVFuncs << "\n"; 471 | std::cout << "REFERENCE: ntdll!RtlpTpTimerFinalizationCallback --> 0x" << std::hex << (PVOID)pRtlpTpTimerFinalizationCallback << "\n"; 472 | std::cout << "REFERENCE: ntdll!RtlpTpTimerCallback --> 0x" << std::hex << (PVOID)pRtlpTpTimerCallback << "\n"; 473 | std::cout << "REFERENCE: ntdll!TppTimerpTaskVFuncs --> 0x" << std::hex << (PVOID)pTppTimerpTaskVFuncs << "\n"; 474 | 475 | for (auto entry : tpAllocTimerHeapAllocData) 476 | { 477 | std::cout << " --> 0x" << std::hex << (PVOID)entry << "\n"; 478 | } 479 | } 480 | 481 | // [8] At this point it is *highly* likely we have identified a timer-queue timer. 482 | // The ptr captured in [6] points to the initial heap allocation performed 483 | // by RtlCreateTimer, which will store the timer callback and argument. 484 | std::vector rtlCreateTimerHeapAllocData(rtlCreateTimerHeapAllocMinSizeToRead); 485 | readProcessMemory(hProcess, (PVOID)rtlCreateTimerHeapDataPtr, rtlCreateTimerHeapAllocMinSizeToRead, rtlCreateTimerHeapAllocData); 486 | 487 | // If we have a timer, heap memory will look like this: 488 | // 0:000 > dps @rdi 489 | // 0000021c`88ef3810 0000021c`88ee4c80 490 | // 0000021c`88ef3818 0000021c`88eef800 491 | // 0000021c`88ef3820 00000000`00000000 492 | // 0000021c`88ef3828 dddddddd`00000020 493 | // 0000021c`88ef3830 00007ffa`fbfcd590 ntdll!NtContinue (Callback) 494 | // 0000021c`88ef3838 0000008f`268fd490 ptr to CONTEXT structure (Param) 495 | // 0000021c`88ef3840 dddddddd`00000000 Lower bits/DWORD here are set to 0 on init but this can sometimes be set to other values (e.g. 0x2) 496 | // 0000021c`88ef3848 0000021c`88ee3600 497 | // 0000021c`88ef3850 00000000`00000000 498 | // 0000021c`88ef3858 00000000`00000000 This QWORD will be set to 0 [10.2] 499 | // 0000021c`88ef3860 00000000`00000000 This QWORD will be set to 0 [10.3] 500 | // 0000021c`88ef3868 00000000`dddddd00 501 | // 0000021c`88ef3870 dddddddd`dddddddd 502 | // 0000021c`88ef3878 dddddddd`dddddddd 503 | // 0000021c`88ef3880 dddddddd`dddddddd 504 | // 0000021c`88ef3888 1000520a`1e93ad46 505 | 506 | // For debugging purposes. 507 | if (bShowDebugOutput) 508 | { 509 | std::cout << "[+] RtlCreateTimer initial allocation memory layout:\n"; 510 | int i = 0; 511 | for (auto entry : rtlCreateTimerHeapAllocData) 512 | { 513 | if (i == 4) 514 | { 515 | std::cout << " --> 0x" << std::hex << (PVOID)entry << " (CALLBACK)\n"; 516 | } 517 | else if (i == 5) 518 | { 519 | std::cout << " --> 0x" << std::hex << (PVOID)entry << " (PARAM)\n"; 520 | } 521 | else 522 | { 523 | std::cout << " --> 0x" << std::hex << (PVOID)entry << "\n"; 524 | } 525 | i++; 526 | } 527 | } 528 | 529 | // [9] Retrieve the target callback and parameter. 530 | auto rtlCreateTimerHeapAllocIt = rtlCreateTimerHeapAllocData.begin(); 531 | std::advance(rtlCreateTimerHeapAllocIt, 4); 532 | auto timerCallback = *rtlCreateTimerHeapAllocIt; 533 | std::advance(rtlCreateTimerHeapAllocIt, 1); 534 | auto timerParameter = *rtlCreateTimerHeapAllocIt; 535 | 536 | // [10] Perform basic sanity checking of this heap structure + callback. 537 | // NB These are rough heuristics and are bound to fail in some use cases, 538 | // especially as heap memory changes and timers are deallocated. 539 | { 540 | // [10.1] Sanity check timer callback actually points to something in memory. 541 | if (IsInvalidPtr(timerCallback)) 542 | { 543 | if (bShowDebugOutput) 544 | { 545 | std::cout << "[-] Invalid timer callback ptr: 0x" << std::hex << timerCallback << "\n"; 546 | } 547 | continue; 548 | } 549 | 550 | // [10.2] *(_QWORD *)(startOfHeapAlloc + 0x48) = 0i64; 551 | std::advance(rtlCreateTimerHeapAllocIt, 4); 552 | if (0 != *rtlCreateTimerHeapAllocIt) 553 | { 554 | continue; 555 | } 556 | 557 | // [10.3] *(_QWORD *)(startOfHeapAlloc + 0x50) = 0i64; 558 | std::advance(rtlCreateTimerHeapAllocIt, 1); 559 | if (0 != *rtlCreateTimerHeapAllocIt) 560 | { 561 | continue; 562 | } 563 | } 564 | 565 | // [11] If we got this far we have a timer-queue timer so print out results. 566 | if (printDelimiter) 567 | { 568 | std::cout << "========================================================================================================\n"; 569 | printDelimiter = false; 570 | } 571 | std::cout << "[+] Found timer-queue timer:\n"; 572 | std::cout << "[+] Virtual address of ntdll!TppTimerpCleanupGroupMemberVFuncs ptr found on the heap: 0x" << std::hex << virtualAddrOfTimer << "\n"; 573 | std::cout << "[+] Timer callback: 0x" << std::hex << timerCallback << "\n"; 574 | std::cout << "[+] Timer parameter: 0x" << std::hex << timerParameter << "\n"; 575 | 576 | // [12] Attempt to resolve symbol from timer callback address. 577 | std::string timerCallbackSymbol; 578 | if (!NT_SUCCESS(ResolveSymbolFromAddress(hProcess, timerCallback, timerCallbackSymbol))) 579 | { 580 | // If we couldn't resolve symbol then attempt basic module offset level resolution (e.g. winlogon+0x63590). 581 | if (!NT_SUCCESS(GetBasicSymbolFromAddress(hProcess, timerCallback, timerCallbackSymbol))) 582 | { 583 | std::cout << "[-] Manual symbol resoluton failed\n"; 584 | } 585 | else 586 | { 587 | std::cout << "[+] Timer callback ptr (manually) resolved symbol: " << std::hex << timerCallbackSymbol << "\n"; 588 | } 589 | std::cout << "========================================================================================================\n"; 590 | continue; 591 | } 592 | std::cout << "[+] Timer callback ptr resolved symbol: " << std::hex << timerCallbackSymbol << "\n"; 593 | 594 | // [13] If the target callback is NtContinue then the parameter will be 595 | // a CONTEXT structure, so print out it's contents here. 596 | if ("NtContinue" != timerCallbackSymbol) 597 | { 598 | std::cout << "========================================================================================================\n"; 599 | continue; 600 | } 601 | CONTEXT ctx = {}; 602 | ctx = readProcessMemory(hProcess, (PVOID)timerParameter); 603 | 604 | // [13.1] Perform basic sanity checking on CONTEXT structure. However, 605 | // as we have already resolved NtContinue it should be valid. 606 | if (ctx.ContextFlags & CONTEXT_CONTROL) 607 | { 608 | std::cout << " [+] Timer CONTEXT structure details:\n"; 609 | std::cout << " [+] ctx.Rip: 0x" << std::hex << ctx.Rip << "\n"; 610 | 611 | // [13.2] Resolve symbol for whatever ctx.rip is pointing at. 612 | std::string ctxRipSymbol; 613 | if (NT_SUCCESS(ResolveSymbolFromAddress(hProcess, ctx.Rip, ctxRipSymbol))) 614 | { 615 | std::cout << " [+] ctx.Rip resolved symbol: " << ctxRipSymbol.c_str() << "\n"; 616 | 617 | // [13.3] If Rip is pointing at VirtualProtect then print 618 | // out information about target memory region. 619 | if ("VirtualProtect" == ctxRipSymbol || "VirtualProtectStub" == ctxRipSymbol) 620 | { 621 | MEMORY_BASIC_INFORMATION mbi = {}; 622 | std::string moduleName; 623 | if (VirtualQueryEx(hProcess, (PVOID)ctx.Rcx, &mbi, sizeof(mbi))) 624 | { 625 | std::cout << " [+] Memory info for target region of VirtualProtect call:\n"; 626 | std::cout << " [+] BaseAddress: 0x" << std::hex << mbi.BaseAddress << "\n"; 627 | std::cout << " [+] AllocationBase: 0x" << std::hex << mbi.AllocationBase << "\n"; 628 | std::cout << " [+] State: 0x" << std::hex << mbi.State << "\n"; 629 | std::cout << " [+] Protect: 0x" << std::hex << mbi.Protect << "\n"; 630 | std::cout << " [+] Type: 0x" << std::hex << mbi.Type << "\n"; 631 | 632 | // [13.4] Attempt to resolve module name. 633 | if (NT_SUCCESS(GetModuleBaseNameWrapper(hProcess, mbi.AllocationBase, moduleName))) 634 | { 635 | std::cout << " [+] Module base name: " << moduleName.c_str() << "\n"; 636 | } 637 | } 638 | } 639 | } 640 | std::cout << " [+] ctx.Rcx: 0x" << std::hex << ctx.Rcx << "\n"; 641 | std::cout << " [+] ctx.Rdx: 0x" << std::hex << ctx.Rdx << "\n"; 642 | std::cout << " [+] ctx.R8: 0x" << std::hex << ctx.R8 << "\n"; 643 | std::cout << " [+] ctx.R9: 0x" << std::hex << ctx.R9 << "\n"; 644 | } 645 | std::cout << "========================================================================================================\n"; 646 | } 647 | } 648 | return; 649 | } 650 | 651 | // 652 | // Scans target process for timer-queue timers. 653 | // 654 | NTSTATUS ScanForTimerQueueTimers(DWORD pid) 655 | { 656 | NTSTATUS status = STATUS_SUCCESS; 657 | HANDLE hProcess = INVALID_HANDLE_VALUE; 658 | std::vector processHeapVector; 659 | BOOL bIsWow64 = false; 660 | 661 | // [1] Obtain handle to target process. 662 | hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ , FALSE, pid); 663 | if (hProcess == NULL) 664 | { 665 | std::cout << "[-] Failed to open a handle to pid: " << std::dec << pid << "\n"; 666 | status = STATUS_ASSERTION_FAILURE; 667 | goto Cleanup; 668 | } 669 | 670 | // [2] WOW64 is unsupported/untested, so skip 32 bit processes. 671 | if (IsWow64Process(hProcess, &bIsWow64)) 672 | { 673 | if (bIsWow64) 674 | { 675 | std::cout << "[-] WOW64 is unsupported; skipping scanning pid: " << std::dec << pid << "\n"; 676 | status = STATUS_ASSERTION_FAILURE; 677 | goto Cleanup; 678 | } 679 | } 680 | 681 | // [3] Initialise symbols for target process. 682 | if (!SymInitialize(hProcess, defaultSymbolPath.c_str(), TRUE)) 683 | { 684 | std::cout << "[-] SymInitialize returned error: " << GetLastError() << "\n"; 685 | status = STATUS_ASSERTION_FAILURE; 686 | goto Cleanup; 687 | } 688 | 689 | // [4] Locate process heaps for target process. 690 | if (!NT_SUCCESS(FindProcessHeaps(hProcess, processHeapVector))) 691 | { 692 | std::cout << "[-] Failed to locate process heaps for pid: " << pid << "\n"; 693 | status = STATUS_ASSERTION_FAILURE; 694 | goto Cleanup; 695 | } 696 | 697 | // [5] Scan process heaps for timer-queue timers. 698 | ScanHeapMemory(hProcess, processHeapVector); 699 | 700 | Cleanup: 701 | SymCleanup(hProcess); 702 | return status; 703 | } 704 | 705 | // 706 | // Loop round processes and run memory scan. 707 | // Based on: 708 | // https://learn.microsoft.com/en-us/windows/win32/toolhelp/taking-a-snapshot-and-viewing-processes 709 | // 710 | NTSTATUS ScanProcesses() 711 | { 712 | NTSTATUS status = STATUS_SUCCESS; 713 | HANDLE hProcessSnapshot = INVALID_HANDLE_VALUE; 714 | PROCESSENTRY32 processEntry32 = {}; 715 | 716 | // [0] Take a snapshot of all running processes. 717 | hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 718 | if (hProcessSnapshot == INVALID_HANDLE_VALUE) 719 | { 720 | status = STATUS_ASSERTION_FAILURE; 721 | goto Cleanup; 722 | } 723 | 724 | // [1] Set the size of the proc32 struct and check 725 | // we can retrieve information about the 726 | // first process and bail if not. 727 | processEntry32.dwSize = sizeof(PROCESSENTRY32); 728 | if (!Process32First(hProcessSnapshot, &processEntry32)) 729 | { 730 | status = STATUS_ASSERTION_FAILURE; 731 | goto Cleanup; 732 | } 733 | 734 | // [2] Start looping over all processes. 735 | do 736 | { 737 | if (GetCurrentProcessId() == processEntry32.th32ProcessID) 738 | { 739 | continue; 740 | } 741 | 742 | // [3] Scan for timer-queue timers. 743 | std::wcout << "[+] Scanning process: " << processEntry32.szExeFile << ", pid: " << processEntry32.th32ProcessID << "\n"; 744 | (void)ScanForTimerQueueTimers(processEntry32.th32ProcessID); 745 | } while (Process32Next(hProcessSnapshot, &processEntry32)); 746 | 747 | Cleanup: 748 | 749 | CloseHandle(hProcessSnapshot); 750 | return status; 751 | } 752 | 753 | // 754 | // Sets the specified privilege in the current process access token. 755 | // Based on: 756 | // https://docs.microsoft.com/en-us/windows/win32/secauthz/enabling-and-disabling-privileges-in-c-- 757 | // 758 | BOOL SetPrivilege( 759 | const LPCTSTR lpszPrivilege, 760 | const BOOL bEnablePrivilege 761 | ) 762 | { 763 | TOKEN_PRIVILEGES tp = {}; 764 | LUID luid = {}; 765 | HANDLE hToken = NULL; 766 | 767 | // [1] Obtain handle to process token. 768 | if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) 769 | { 770 | std::cout << "[-] Failed to OpenProcessToken \n"; 771 | return FALSE; 772 | } 773 | 774 | // [2] Look up supplied privilege value and set if required. 775 | if (!LookupPrivilegeValue(NULL, lpszPrivilege, &luid)) 776 | { 777 | std::cout << "[-] SetPrivilege failed: LookupPrivilegeValue error" << GetLastError() << std::endl; 778 | return FALSE; 779 | } 780 | tp.PrivilegeCount = 1; 781 | tp.Privileges[0].Luid = luid; 782 | if (bEnablePrivilege) 783 | { 784 | tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 785 | } 786 | else 787 | { 788 | tp.Privileges[0].Attributes = 0; 789 | } 790 | 791 | // [3] Enable the privilege or disable all privileges. 792 | if (!AdjustTokenPrivileges( 793 | hToken, 794 | FALSE, 795 | &tp, 796 | sizeof(TOKEN_PRIVILEGES), 797 | (PTOKEN_PRIVILEGES)NULL, 798 | (PDWORD)NULL)) 799 | { 800 | std::cout << "[-] AdjustTokenPrivileges failed: LookupPrivilegeValue error" << GetLastError() << std::endl; 801 | return FALSE; 802 | } 803 | 804 | if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) 805 | { 806 | std::cout << "[-] SetPrivilege failed: LookupPrivilegeValue error\n"; 807 | return FALSE; 808 | } 809 | 810 | CloseHandle(hToken); 811 | return TRUE; 812 | } 813 | 814 | // 815 | // Parses command line arguments. 816 | // 817 | NTSTATUS HandleArgs(int argc, char* argv[]) 818 | { 819 | NTSTATUS status = STATUS_SUCCESS; 820 | 821 | if (argc < 2) 822 | { 823 | goto Cleanup; 824 | } 825 | else 826 | { 827 | std::string callstackArg(argv[1]); 828 | if (callstackArg == "--debug") 829 | { 830 | std::cout << "[+] Debug output enabled.\n"; 831 | bShowDebugOutput = true; 832 | } 833 | else 834 | { 835 | std::cout << "[!] Error: Incorrect argument provided. The options are: --debug.\n"; 836 | status = ERROR_INVALID_PARAMETER; 837 | } 838 | } 839 | 840 | Cleanup: 841 | return status; 842 | } 843 | 844 | int main(int argc, char* argv[]) 845 | { 846 | std::cout << R"( 847 | ,----, ,----, 848 | ,/ .`| ,/ .`| 849 | ,` .' : ,-. ,` .' : ,-. 850 | ; ; / ,--, ,--/ /| ; ; / ,--/ /| 851 | .'___,/ ,',--.'| ,--. :/ | .'___,/ ,' ,---. ,--. :/ | 852 | | : | | |, : : ' / | : | ' ,'\ : : ' / 853 | ; |.'; ; `--'_ ,---. | ' / ; |.'; ; / / | ,---. | ' / 854 | `----' | | ,' ,'| / \ ' | : `----' | |. ; ,. : / \ ' | : 855 | ' : ; ' | | / / ' | | \ ' : ;' | |: : / / ' | | \ 856 | | | ' | | : . ' / ' : |. \ | | '' | .; :. ' / ' : |. \ 857 | ' : | ' : |__ ' ; :__ | | ' \ \ ' : || : |' ; :__ | | ' \ \ 858 | ; |.' | | '.'|' | '.'|' : |--' ; |.' \ \ / ' | '.'|' : |--' 859 | '---' ; : ;| : :; |,' '---' `----' | : :; |,' 860 | | , / \ \ / '--' \ \ / '--' 861 | ---`-' `----' `----' 862 | Timer-Queue Timer Enumerator William Burgess @joehowwolf 863 | )" << '\n'; 864 | 865 | std::cout << "*** WARNING: This tool requires symbols to be correctly configured.***\n"; 866 | std::cout << "*** To do this you will need to install the Debugging Tools for Windows (WinDbg).***\n"; 867 | std::cout << "*** Once you have done this:\n"; 868 | std::cout << "*** 1) Create a folder called C:\\Symbols\n"; 869 | std::cout << "*** 2) In windbg, attach to an arbitrary process and run: .symfix+ c:\\symbols\n"; 870 | std::cout << "*** .reload\n"; 871 | std::cout << "*** See https://stackoverflow.com/questions/30019889/how-to-set-up-symbols-in-windbg for further assistance. ***\n\n"; 872 | 873 | NTSTATUS status = STATUS_SUCCESS; 874 | 875 | // [0] Handle command line args. 876 | status = HandleArgs(argc, argv); 877 | if (!NT_SUCCESS(status)) 878 | { 879 | return -1; 880 | } 881 | 882 | // [1] Configure symbol options. 883 | (void)SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS); 884 | 885 | // [2] Initialise required symbols. We need to hunt for pointers 886 | // in heap memory so bail if we can't resolve them. 887 | if (!NT_SUCCESS(InitialiseScanner())) 888 | { 889 | std::cout << "[!] Failed to initialise scanner.\n"; 890 | return -1; 891 | } 892 | 893 | // [3] Acquire SeDebugPriv. 894 | if (!SetPrivilege(SE_DEBUG_NAME, true)) 895 | { 896 | std::cout << "[!] Failed to enable SeDebugPrivilege; only a limited set of processes will be scanned. Try re-running as admin.\n"; 897 | } 898 | 899 | // [4] Start hunting for timer-queue timers. 900 | if (!NT_SUCCESS(ScanProcesses())) 901 | { 902 | std::cout << "[!] Failed to create a process snapshot.\n"; 903 | return -1; 904 | } 905 | 906 | return 0; 907 | } -------------------------------------------------------------------------------- /TickTock/TickTock.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 16.0 23 | Win32Proj 24 | {6c9669ac-ef17-4b5e-8e46-6d6179c2d2e6} 25 | TickTock 26 | 10.0 27 | TickTock 28 | 29 | 30 | 31 | Application 32 | true 33 | v143 34 | Unicode 35 | 36 | 37 | Application 38 | false 39 | v143 40 | true 41 | Unicode 42 | 43 | 44 | Application 45 | true 46 | v143 47 | Unicode 48 | 49 | 50 | Application 51 | false 52 | v143 53 | true 54 | Unicode 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | true 76 | 77 | 78 | false 79 | 80 | 81 | true 82 | 83 | 84 | false 85 | 86 | 87 | 88 | Level3 89 | true 90 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 91 | true 92 | 93 | 94 | Console 95 | true 96 | 97 | 98 | 99 | 100 | Level3 101 | true 102 | true 103 | true 104 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 105 | true 106 | 107 | 108 | Console 109 | true 110 | true 111 | true 112 | 113 | 114 | 115 | 116 | Level3 117 | true 118 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 119 | true 120 | stdcpplatest 121 | 122 | 123 | Console 124 | true 125 | kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;dbghelp.lib;%(AdditionalDependencies) 126 | 127 | 128 | 129 | 130 | Level3 131 | true 132 | true 133 | true 134 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 135 | true 136 | stdcpplatest 137 | 138 | 139 | Console 140 | true 141 | true 142 | true 143 | kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;dbghelp.lib;%(AdditionalDependencies) 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /TickTock/TickTock.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | --------------------------------------------------------------------------------