├── README.md ├── nt.h ├── patriot.h ├── patriot.cpp └── pefile.cpp /README.md: -------------------------------------------------------------------------------- 1 | # Patriot 2 | ![Patriot_missile_launch_b](https://user-images.githubusercontent.com/56411054/178175726-bc2c843c-103e-4366-8221-45d64a033e00.jpg) 3 | 4 | Small research project for detecting various kinds of in-memory stealth techniques. 5 | 6 | Download the latest release [here](https://github.com/joe-desimone/patriot/releases/tag/v0.3). 7 | 8 | The current version supports the following detections: 9 | - Suspicious CONTEXT structures pointing to VirtualProtect functions. (Targets [research](https://suspicious.actor/2022/05/05/mdsec-nighthawk-study.html) by Austin Hudson [Foliage](https://github.com/y11en/FOLIAGE/tree/master/source) and [Ekko](https://github.com/Cracked5pider/Ekko) by Cracked5pider). 10 | - Validation of MZ/PE headers in memory to detect process hollowing variants. 11 | - Unbacked executable regions running at high integrity. 12 | - Modified code used in module stomping/overwriting. 13 | - Various other anomalies. 14 | 15 | ![image](https://user-images.githubusercontent.com/56411054/178175830-4289dd66-39d8-46c4-bd1d-f31f25baf8fa.png) 16 | 17 | -------------------------------------------------------------------------------- /nt.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | typedef struct _UNICODE_STRING { 5 | USHORT Length; 6 | USHORT MaximumLength; 7 | PWSTR Buffer; 8 | } UNICODE_STRING, * PUNICODE_STRING; 9 | 10 | typedef struct _IO_STATUS_BLOCK 11 | { 12 | union 13 | { 14 | LONG Status; 15 | PVOID Pointer; 16 | }; 17 | ULONG Information; 18 | } IO_STATUS_BLOCK, * PIO_STATUS_BLOCK; 19 | 20 | 21 | typedef struct _OBJECT_ATTRIBUTES { 22 | ULONG Length; 23 | HANDLE RootDirectory; 24 | PUNICODE_STRING ObjectName; 25 | ULONG Attributes; 26 | PVOID SecurityDescriptor; 27 | PVOID SecurityQualityOfService; 28 | } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES; 29 | 30 | typedef ULONG(__stdcall* _NtCreateFile)( 31 | PHANDLE FileHandle, 32 | ACCESS_MASK DesiredAccess, 33 | POBJECT_ATTRIBUTES ObjectAttributes, 34 | PIO_STATUS_BLOCK IoStatusBlock, 35 | PLARGE_INTEGER AllocationSize, 36 | ULONG FileAttributes, 37 | ULONG ShareAccess, 38 | ULONG CreateDisposition, 39 | ULONG CreateOptions, 40 | PVOID EaBuffer, 41 | ULONG EaLength 42 | ); 43 | 44 | typedef void(__stdcall* _RtlInitUnicodeString)( 45 | PUNICODE_STRING DestinationString, 46 | PCWSTR SourceString 47 | ); 48 | 49 | #define InitializeObjectAttributes( p, n, a, r, s ) { \ 50 | (p)->Length = sizeof( OBJECT_ATTRIBUTES ); \ 51 | (p)->RootDirectory = r; \ 52 | (p)->Attributes = a; \ 53 | (p)->ObjectName = n; \ 54 | (p)->SecurityDescriptor = s; \ 55 | (p)->SecurityQualityOfService = NULL; \ 56 | } 57 | 58 | #define OBJ_CASE_INSENSITIVE 0x00000040L 59 | #define FILE_NON_DIRECTORY_FILE 0x00000040 60 | #define FILE_SYNCHRONOUS_IO_NONALERT 0x00000020 61 | #define FILE_OPEN 0x00000001L -------------------------------------------------------------------------------- /patriot.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Patriot memory scanner 3 | * Copyright 2022 Joe Desimone. All rights reserved. 4 | * Contact: @dez_ 5 | */ 6 | 7 | #pragma once 8 | // clang-format off 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | // clang-format on 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include "patriot.h" 22 | 23 | enum LOG_LEVEL 24 | { 25 | debug, 26 | info, 27 | warning, 28 | error 29 | }; 30 | 31 | static inline bool _InvalidPtr(const char* pBuf, SIZE_T bufSz, const char* pData, SIZE_T dataSz) 32 | { 33 | if (pData < pBuf) 34 | { 35 | return true; 36 | } 37 | 38 | if ((pData + dataSz) > (pBuf + bufSz)) 39 | { 40 | return true; 41 | } 42 | 43 | if ((pBuf + bufSz) < pBuf) 44 | { 45 | return true; 46 | } 47 | 48 | if ((pData + dataSz) < pData) 49 | { 50 | return true; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | #define InvalidPtr(pBuf, bufSz, pData, dataSz) \ 57 | _InvalidPtr((const char*)(pBuf), (SIZE_T)(bufSz), (const char*)(pData), (SIZE_T)(dataSz)) 58 | 59 | static bool inline VirtualProtectFunction(void** functions, int count, DWORD64 function) 60 | { 61 | for (int i = 0; i < count; i++) 62 | { 63 | if (functions[i] == (void*)function) 64 | { 65 | return true; 66 | } 67 | } 68 | 69 | return false; 70 | } 71 | 72 | static bool inline IsExecuteSet(DWORD protect) 73 | { 74 | if ((protect == PAGE_EXECUTE) || (protect == PAGE_EXECUTE_READ) || 75 | (protect == PAGE_EXECUTE_READWRITE) || (protect == PAGE_EXECUTE_WRITECOPY)) 76 | { 77 | return true; 78 | } 79 | 80 | return false; 81 | } 82 | 83 | void Log(LOG_LEVEL level, const char* fmt, ...); 84 | bool UnsharedSize(HANDLE hProcess, void* regionBase, SIZE_T regionSize, SIZE_T& unsharedSized); 85 | 86 | typedef struct Module 87 | { 88 | DWORD_PTR moduleBase; 89 | SIZE_T moduleSize; 90 | std::wstring modulePathNt; 91 | std::wstring modulePathDos; 92 | DWORD getPathError; 93 | Module() : moduleBase(0), moduleSize(0), getPathError(0) { modulePathNt.resize(MAX_PATH); } 94 | } Module; 95 | typedef std::shared_ptr SPModule; 96 | typedef std::vector ModuleList; 97 | 98 | typedef std::vector> MemoryMap; 99 | 100 | typedef struct Process 101 | { 102 | DWORD pid; 103 | std::wstring processName; 104 | BOOL bElevated; 105 | HANDLE hProcess; 106 | MemoryMap memoryMap; 107 | ModuleList moduleList; 108 | Process(DWORD p, std::wstring n) 109 | { 110 | pid = p; 111 | processName = n; 112 | bElevated = FALSE; 113 | hProcess = 0; 114 | } 115 | } Process; 116 | typedef std::vector ProcessList; 117 | 118 | typedef struct Finding 119 | { 120 | DWORD pid; 121 | std::wstring processName; 122 | SPModule moduleInfo; 123 | MEMORY_BASIC_INFORMATION mbi; 124 | std::string type; 125 | std::string details; 126 | std::string level; 127 | Finding() : pid(0) {} 128 | } Finding; 129 | typedef std::unique_ptr UPFinding; 130 | extern std::vector Findings; 131 | #define DeleteHandle(x) \ 132 | if (x && x != INVALID_HANDLE_VALUE) \ 133 | { \ 134 | CloseHandle(x); \ 135 | x = 0; \ 136 | } 137 | 138 | #define CleanupError() \ 139 | status = false; \ 140 | goto Cleanup; 141 | 142 | #define CleanupSuccess() \ 143 | status = true; \ 144 | goto Cleanup; 145 | 146 | class PEFile 147 | { 148 | private: 149 | public: 150 | IMAGE_DOS_HEADER dosHeader; 151 | IMAGE_NT_HEADERS64 ntHeader; 152 | IMAGE_NT_HEADERS32* pNtHeader32; 153 | std::string headerBuf; 154 | bool bHeaderLoaded; 155 | bool bDotNet; 156 | std::wstring processName; 157 | DWORD pid; 158 | SPModule moduleInfo; 159 | PEFile(); 160 | 161 | bool LoadHeaderFromDisk(const std::wstring filePathNt); 162 | bool LoadHeaderFromMemory(HANDLE hProcess, DWORD_PTR moduleBase); 163 | bool ParseHeader(); 164 | static bool ValidateIntegrity(PEFile& peDisk, PEFile& peMem, Process& process); 165 | void NewFinding(const std::string level, const std::string subtype, const std::string details); 166 | }; 167 | -------------------------------------------------------------------------------- /patriot.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Patriot memory scanner 3 | * Copyright 2022 Joe Desimone. All rights reserved. 4 | * Contact: @dez_ 5 | */ 6 | 7 | #include "patriot.h" 8 | 9 | #include "nt.h" 10 | 11 | char PATRIOT_VERSION[] = "v0.3"; 12 | std::vector Findings; 13 | LOG_LEVEL logLevel = info; 14 | 15 | void Log(LOG_LEVEL level, const char* fmt, ...) 16 | { 17 | if (level < logLevel) 18 | { 19 | return; 20 | } 21 | int result; 22 | va_list va; 23 | va_start(va, fmt); 24 | result = vprintf(fmt, va); 25 | va_end(va); 26 | } 27 | 28 | // bool FindTimerCallback(void* pBuf, SIZE_T szBuf, const wchar_t* dllName, const char* 29 | // functionName) 30 | //{ 31 | // if (szBuf < 24) 32 | // { 33 | // return false; 34 | // } 35 | // 36 | // void* pFunction = GetProcAddress(GetModuleHandle(dllName), functionName); 37 | // char search[3 * 8] = {0}; 38 | // DWORD i = 0; 39 | // 40 | // memcpy(&search[i], "\x00\x00\x00\x00\x00\x00\x00\x00", 8); 41 | // i += 8; 42 | // memcpy(&search[i], "\x20\x00\x00\x00\x00\x00\x00\x00", 8); 43 | // i += 8; 44 | // memcpy(&search[i], &pFunction, 8); 45 | // i += 8; 46 | // if (memmem(pBuf, szBuf, search, i)) 47 | // { 48 | // return true; 49 | // } 50 | // 51 | // return false; 52 | //} 53 | 54 | bool FindSuspiciousContext(Process& process, void* pBuf, SIZE_T szBuf) 55 | { 56 | if (szBuf < sizeof(CONTEXT)) 57 | { 58 | return false; 59 | } 60 | 61 | CONTEXT* pCtx; 62 | 63 | void* functions[10]; 64 | functions[0] = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtProtectVirtualMemory"); 65 | functions[1] = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "VirtualProtect"); 66 | functions[2] = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "VirtualProtectEx"); 67 | functions[3] = GetProcAddress(GetModuleHandle(L"kernelbase.dll"), "VirtualProtect"); 68 | functions[4] = GetProcAddress(GetModuleHandle(L"kernelbase.dll"), "VirtualProtectEx"); 69 | int count = 5; 70 | 71 | for (int i = 0; i < szBuf - sizeof(CONTEXT); i += 8) 72 | { 73 | char* pcBuf = (char*)pBuf; 74 | pCtx = (CONTEXT*)&pcBuf[i]; 75 | if ((pCtx->ContextFlags & CONTEXT_CONTROL) && 76 | VirtualProtectFunction(functions, count, pCtx->Rip) && 77 | (IsExecuteSet((DWORD)pCtx->R8) || IsExecuteSet((DWORD)pCtx->R9))) 78 | { 79 | DWORD64 target = 0; 80 | if (pCtx->Rcx == (DWORD64)-1) 81 | target = pCtx->Rdx; 82 | else 83 | target = pCtx->Rcx; 84 | 85 | auto finding = std::make_unique(); 86 | 87 | finding->pid = process.pid; 88 | finding->processName = process.processName; 89 | finding->level = "suspect"; 90 | finding->type = "CONTEXT"; 91 | finding->details = std::format( 92 | "Suspicious CONTEXT structure pointing to VirtualProtect class function. Target: " 93 | "{:016x}", 94 | target); 95 | 96 | Findings.push_back(std::move(finding)); 97 | 98 | // printf("Parameters:\n"); 99 | // printf("RIP: %llx\n", pCtx->Rip); 100 | // printf("RCX: %llx\n", pCtx->Rcx); 101 | // printf("RDX: %llx\n", pCtx->Rdx); 102 | // printf("R8: %llx\n", pCtx->R8); 103 | // printf("R9: %llx\n", pCtx->R9); 104 | } 105 | } 106 | return false; 107 | } 108 | 109 | bool EnumerateMemory(Process& process, ModuleList& moduleList) 110 | { 111 | bool status = true; 112 | 113 | auto spModuleInfo = std::make_shared(); 114 | Module* pModuleInfo = spModuleInfo.get(); 115 | 116 | DWORD_PTR pMem = 0; 117 | bool inModule = false; 118 | DWORD modulePathSz = 0; 119 | bool bModuleHasExec = false; 120 | 121 | while (true) 122 | { 123 | MEMORY_BASIC_INFORMATION mbi = {0}; 124 | 125 | if (0 == VirtualQueryEx(process.hProcess, (void*)pMem, &mbi, sizeof(mbi))) 126 | { 127 | if (ERROR_INVALID_PARAMETER == GetLastError()) 128 | { 129 | // End of search 130 | break; 131 | } 132 | else 133 | { 134 | // Process terminated? 135 | return false; 136 | } 137 | 138 | break; 139 | } 140 | 141 | pMem += mbi.RegionSize; 142 | 143 | if (inModule && (pModuleInfo->moduleBase != (DWORD_PTR)mbi.AllocationBase)) 144 | { 145 | // Found the end of the module 146 | if (bModuleHasExec) 147 | { 148 | moduleList.push_back(spModuleInfo); 149 | spModuleInfo = std::make_shared(); 150 | pModuleInfo = spModuleInfo.get(); 151 | } 152 | 153 | inModule = false; 154 | } 155 | 156 | // Testing this combo 157 | if (process.bElevated && mbi.Type != MEM_IMAGE && IsExecuteSet(mbi.Protect) && 158 | mbi.State == MEM_COMMIT) 159 | { 160 | auto finding = std::make_unique(); 161 | finding->pid = process.pid; 162 | finding->processName = process.processName; 163 | finding->level = "suspect"; 164 | finding->type = "elevatedUnbackedExecute"; 165 | finding->details = std::format( 166 | "Elevated unbacked execute at Base: {:016x}, Protection: {:08x}, Size: {:016x}", 167 | (DWORD64)mbi.BaseAddress, mbi.Protect, mbi.RegionSize); 168 | Findings.push_back(std::move(finding)); 169 | } 170 | 171 | if (mbi.State == MEM_COMMIT) 172 | { 173 | auto pMbi = std::make_unique(); 174 | memcpy(pMbi.get(), &mbi, sizeof(mbi)); 175 | process.memoryMap.push_back(std::move(pMbi)); 176 | } 177 | 178 | if (mbi.Type != MEM_IMAGE) 179 | { 180 | continue; 181 | } 182 | 183 | if (IsExecuteSet(mbi.Protect)) 184 | { 185 | bModuleHasExec = true; 186 | } 187 | 188 | if (mbi.Protect & PAGE_GUARD) 189 | { 190 | printf("[!] Guard page found on module\n"); 191 | } 192 | pModuleInfo->moduleSize += mbi.RegionSize; 193 | 194 | if (inModule) 195 | { 196 | continue; 197 | } 198 | 199 | pModuleInfo->moduleBase = (DWORD_PTR)mbi.BaseAddress; 200 | 201 | while (true) 202 | { 203 | // GetMappedFileName can return partial string, so clear last error and check 204 | SetLastError(0); 205 | modulePathSz = GetMappedFileName(process.hProcess, (void*)mbi.BaseAddress, 206 | &pModuleInfo->modulePathNt[0], 207 | (DWORD)pModuleInfo->modulePathNt.size()); 208 | if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) 209 | { 210 | pModuleInfo->modulePathNt.resize(pModuleInfo->modulePathNt.size() * 2); 211 | continue; 212 | } 213 | break; 214 | } 215 | 216 | if (modulePathSz == 0) 217 | { 218 | pModuleInfo->getPathError = GetLastError(); 219 | printf("[!] GetMappedFileName error, %p, %d\n", mbi.BaseAddress, GetLastError()); 220 | continue; 221 | } 222 | 223 | // Trim to fit 224 | pModuleInfo->modulePathNt.resize(modulePathSz); 225 | 226 | // ToDo Nt to Dos path 227 | // https://docs.microsoft.com/en-us/windows/win32/memory/obtaining-a-file-name-from-a-file-handle?redirectedfrom=MSDN 228 | 229 | inModule = true; 230 | } 231 | 232 | Cleanup: 233 | return status; 234 | } 235 | 236 | bool UnsharedSize(HANDLE hProcess, void* regionBase, SIZE_T regionSize, SIZE_T& unsharedSized) 237 | { 238 | bool status = true; 239 | 240 | unsharedSized = 0; 241 | SIZE_T pages = regionSize / 0x1000; 242 | PSAPI_WORKING_SET_EX_INFORMATION* pInfo = 243 | (PSAPI_WORKING_SET_EX_INFORMATION*)malloc(pages * sizeof(PSAPI_WORKING_SET_EX_INFORMATION)); 244 | 245 | for (SIZE_T i = 0; i < pages; i++) 246 | { 247 | pInfo[i].VirtualAddress = (void*)((DWORD_PTR)regionBase + (i * 0x1000)); 248 | } 249 | 250 | if (!QueryWorkingSetEx(hProcess, pInfo, 251 | (DWORD)(pages * sizeof(PSAPI_WORKING_SET_EX_INFORMATION)))) 252 | { 253 | printf("[!] QueryWorkingSet failed: %d\n", GetLastError()); 254 | CleanupError(); 255 | } 256 | 257 | for (SIZE_T i = 0; i < pages; i++) 258 | { 259 | if (0 == pInfo[i].VirtualAttributes.Shared) 260 | { 261 | unsharedSized += 0x1000; 262 | } 263 | } 264 | 265 | Cleanup: 266 | if (pInfo) 267 | { 268 | free(pInfo); 269 | } 270 | return status; 271 | } 272 | 273 | bool IsProcessElevated(HANDLE hProcess, BOOL& bElevated) 274 | { 275 | bool status = true; 276 | 277 | HANDLE hToken = 0; 278 | bElevated = FALSE; 279 | DWORD dwRet = 0; 280 | if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) 281 | { 282 | printf("Error opening process token, err %d\n", GetLastError()); 283 | CleanupError(); 284 | } 285 | 286 | if (!GetTokenInformation(hToken, TokenElevation, &bElevated, sizeof(bElevated), &dwRet)) 287 | { 288 | printf("Error getting token elevation status err %d\n", GetLastError()); 289 | CleanupError(); 290 | } 291 | 292 | Cleanup: 293 | DeleteHandle(hToken); 294 | return status; 295 | } 296 | 297 | void* ScanProc(Process& process) 298 | { 299 | process.hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, process.pid); 300 | if (0 == process.hProcess) 301 | { 302 | return 0; 303 | } 304 | 305 | Log(debug, "[+] Scanning Pid: %d, Process: %ws\n", process.pid, process.processName.c_str()); 306 | 307 | (void)IsProcessElevated(process.hProcess, process.bElevated); 308 | 309 | ModuleList moduleList; 310 | EnumerateMemory(process, moduleList); 311 | for (auto it = moduleList.begin(); it != moduleList.end(); ++it) 312 | { 313 | auto moduleInfo = it->get(); 314 | // printf("Module: [%llx-%llx] %ws\n", moduleInfo->moduleBase, 315 | // moduleInfo->moduleBase + moduleInfo->moduleSize, 316 | // moduleInfo->modulePathNt.c_str()); 317 | 318 | PEFile peDisk; 319 | PEFile peMem; 320 | peMem.pid = process.pid; 321 | peMem.processName = process.processName; 322 | peMem.moduleInfo = *it; 323 | if (peDisk.LoadHeaderFromDisk(moduleInfo->modulePathNt) && 324 | peMem.LoadHeaderFromMemory(process.hProcess, moduleInfo->moduleBase)) 325 | { 326 | PEFile::ValidateIntegrity(peDisk, peMem, process); 327 | } 328 | } 329 | 330 | DWORD_PTR pMem = 0; 331 | for (auto it = process.memoryMap.begin(); it != process.memoryMap.end(); ++it) 332 | { 333 | auto pMbi = it->get(); 334 | void* pBuf = 0; 335 | SIZE_T stRead = 0; 336 | 337 | if (pMbi->State != MEM_COMMIT || pMbi->Protect != PAGE_READWRITE || 338 | pMbi->RegionSize > 1024 * 1024 * 50 || pMbi->Type != MEM_PRIVATE) 339 | { 340 | continue; 341 | } 342 | 343 | pBuf = malloc(pMbi->RegionSize); 344 | 345 | if (!ReadProcessMemory(process.hProcess, pMbi->BaseAddress, pBuf, pMbi->RegionSize, 346 | &stRead)) 347 | { 348 | free(pBuf); 349 | continue; 350 | } 351 | 352 | FindSuspiciousContext(process, pBuf, pMbi->RegionSize); 353 | 354 | /* 355 | Disabling this one for now. Decent enough coverage with the CONTEXT check. 356 | if (FindTimerCallback(pBuf, mbi.RegionSize, L"ntdll.dll", "NtContinue") || 357 | FindTimerCallback(pBuf, mbi.RegionSize, L"ntdll.dll", "RtlRestoreContext")) 358 | { 359 | free(pBuf); 360 | return mbi.BaseAddress; 361 | } 362 | */ 363 | 364 | free(pBuf); 365 | } 366 | 367 | // ToDo check for guard pages and hardware breakpoints on image sections. 368 | // ToDo Enumerate vectored handlers 369 | // https://dimitrifourny.github.io/2020/06/11/dumping-veh-win10.html. 370 | 371 | DeleteHandle(process.hProcess); 372 | 373 | return 0; 374 | } 375 | 376 | void EnumProcess(ProcessList& processList) 377 | { 378 | HANDLE hSnap = INVALID_HANDLE_VALUE; 379 | PROCESSENTRY32 proc32; 380 | 381 | hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 382 | if (hSnap == INVALID_HANDLE_VALUE) 383 | { 384 | goto Cleanup; 385 | } 386 | 387 | proc32.dwSize = sizeof(PROCESSENTRY32); 388 | 389 | if (!Process32First(hSnap, &proc32)) 390 | { 391 | goto Cleanup; 392 | } 393 | 394 | do 395 | { 396 | if (GetCurrentProcessId() == proc32.th32ProcessID) 397 | { 398 | continue; 399 | } 400 | processList.push_back(Process(proc32.th32ProcessID, proc32.szExeFile)); 401 | 402 | } while ((Process32Next(hSnap, &proc32)) == TRUE); 403 | 404 | Cleanup: 405 | 406 | DeleteHandle(hSnap); 407 | 408 | return; 409 | } 410 | 411 | BOOL GetPriv(const wchar_t* privName) 412 | { 413 | HANDLE hToken; 414 | TOKEN_PRIVILEGES tkpPrev; 415 | LUID luid; 416 | TOKEN_PRIVILEGES tkp; 417 | BOOL bRet; 418 | ULONG ulRet; 419 | 420 | if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) 421 | return FALSE; 422 | 423 | bRet = LookupPrivilegeValue(NULL, privName, &luid); 424 | if (!bRet) 425 | { 426 | CloseHandle(hToken); 427 | return bRet; 428 | } 429 | 430 | tkp.PrivilegeCount = 1; 431 | tkp.Privileges[0].Luid = luid; 432 | tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 433 | 434 | bRet = AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), &tkpPrev, &ulRet); 435 | 436 | CloseHandle(hToken); 437 | 438 | return bRet; 439 | } 440 | 441 | int main(int argc, char** argv) 442 | { 443 | printf("== Patriot Memory Scanner ==\n"); 444 | printf("Copyright 2022 Joe Desimone. All rights reserved.\n"); 445 | printf("Contact: @dez_\n"); 446 | printf("Version: %s\n\n", PATRIOT_VERSION); 447 | if (!GetPriv(SE_DEBUG_NAME)) 448 | { 449 | printf("Error getting debug privilege, err: %d\n", GetLastError()); 450 | } 451 | 452 | for (int i = 1; i < argc; ++i) 453 | { 454 | const char* pArg = argv[i]; 455 | 456 | if (0 == _stricmp(pArg, "-v")) 457 | { 458 | logLevel = debug; 459 | } 460 | } 461 | 462 | printf("[+] Scanning..\n"); 463 | 464 | ProcessList processList; 465 | EnumProcess(processList); 466 | for (auto it = processList.begin(); it != processList.end(); ++it) 467 | { 468 | ScanProc(*it); 469 | } 470 | 471 | printf("[+] Scan complete.\n"); 472 | 473 | if (Findings.size() == 0) 474 | { 475 | printf("[+] System clean\n"); 476 | } 477 | else 478 | { 479 | printf("[-] Findings detected!\n\n"); 480 | } 481 | 482 | for (auto it = Findings.begin(); it != Findings.end(); ++it) 483 | { 484 | auto finding = it->get(); 485 | printf("-----\n"); 486 | printf("Level: %s\n", finding->level.c_str()); 487 | printf("Type: %s\n", finding->type.c_str()); 488 | printf("Detail: %s\n", finding->details.c_str()); 489 | printf("PID: %d\n", finding->pid); 490 | if (finding->processName != L"") 491 | { 492 | printf("Process: %ws\n", finding->processName.c_str()); 493 | } 494 | if (finding->moduleInfo.get()) 495 | { 496 | printf("Module path: %ws\n", finding->moduleInfo->modulePathNt.c_str()); 497 | printf("Module base: %llx\n", finding->moduleInfo->moduleBase); 498 | } 499 | printf("-----\n\n"); 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /pefile.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Patriot memory scanner 3 | * Copyright 2022 Joe Desimone. All rights reserved. 4 | * Contact: @dez_ 5 | */ 6 | 7 | #include "nt.h" 8 | #include "patriot.h" 9 | 10 | // Constructor 11 | PEFile::PEFile() 12 | { 13 | ZeroMemory(&dosHeader, sizeof(dosHeader)); 14 | ZeroMemory(&ntHeader, sizeof(ntHeader)); 15 | bHeaderLoaded = false; 16 | bDotNet = false; 17 | pNtHeader32 = 0; 18 | pid = 0; 19 | } 20 | 21 | void PEFile::NewFinding(const std::string level, const std::string subtype, 22 | const std::string details) 23 | { 24 | auto finding = std::make_unique(); 25 | 26 | finding->pid = pid; 27 | finding->processName = processName; 28 | finding->level = level; 29 | finding->type = "peIntegrity"; 30 | finding->details = details; 31 | finding->moduleInfo = moduleInfo; 32 | 33 | Findings.push_back(std::move(finding)); 34 | } 35 | 36 | bool PEFile::ParseHeader() 37 | { 38 | // ToDo errors here should be findings 39 | bool status = true; 40 | char* pBuf = &headerBuf[0]; 41 | SIZE_T bufSz = headerBuf.size(); 42 | IMAGE_NT_HEADERS64* pNtHeader = 0; 43 | IMAGE_DATA_DIRECTORY* pDirectoryCOM = 0; 44 | IMAGE_SECTION_HEADER* pSection = 0; 45 | 46 | if (headerBuf.size() < sizeof(dosHeader)) 47 | { 48 | Log(warning, "[!] Invalid headerBuf size\n"); 49 | CleanupError(); 50 | } 51 | 52 | memcpy(&dosHeader, &headerBuf[0], sizeof(dosHeader)); 53 | 54 | pNtHeader = (IMAGE_NT_HEADERS64*)(pBuf + dosHeader.e_lfanew); 55 | if (InvalidPtr(pBuf, bufSz, pNtHeader, sizeof(IMAGE_NT_HEADERS64))) 56 | { 57 | NewFinding("suspect", "ntHeaderPtr", 58 | std::format("Invalid NtHeader Ptr {:016x}", (DWORD_PTR)pNtHeader)); 59 | CleanupError(); 60 | } 61 | 62 | memcpy(&ntHeader, pNtHeader, sizeof(IMAGE_NT_HEADERS64)); 63 | 64 | if (IMAGE_FILE_MACHINE_I386 == ntHeader.FileHeader.Machine) 65 | { 66 | pNtHeader32 = (IMAGE_NT_HEADERS32*)&ntHeader; 67 | pDirectoryCOM = 68 | &pNtHeader32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR]; 69 | if (pDirectoryCOM->VirtualAddress) 70 | { 71 | bDotNet = true; 72 | } 73 | } 74 | else 75 | { 76 | pDirectoryCOM = 77 | &ntHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR]; 78 | if (pDirectoryCOM->VirtualAddress) 79 | { 80 | bDotNet = true; 81 | } 82 | } 83 | 84 | pSection = IMAGE_FIRST_SECTION(pNtHeader); 85 | 86 | if (InvalidPtr(pBuf, bufSz, pSection, sizeof(*pSection) * ntHeader.FileHeader.NumberOfSections)) 87 | { 88 | NewFinding("suspect", "ntSectionPtr", 89 | std::format("Invalid section header pointer {:016x} * {}", (DWORD_PTR)pSection, 90 | ntHeader.FileHeader.NumberOfSections)); 91 | CleanupError(); 92 | } 93 | 94 | for (int i = 0; i < ntHeader.FileHeader.NumberOfSections; i++) 95 | { 96 | if (pSection->Characteristics & IMAGE_SCN_MEM_EXECUTE & IMAGE_SCN_MEM_WRITE) 97 | { 98 | NewFinding( 99 | "suspect", "rwxCodeSection", 100 | std::format("RWX Code section: {}{}{}{}{}", pSection->Name[0], pSection->Name[1], 101 | pSection->Name[2], pSection->Name[3], pSection->Name[4])); 102 | } 103 | pSection++; 104 | } 105 | 106 | Cleanup: 107 | return status; 108 | } 109 | 110 | bool PEFile::LoadHeaderFromDisk(const std::wstring filePathNt) 111 | { 112 | bool status = true; 113 | 114 | _NtCreateFile NtCreateFile = 115 | (_NtCreateFile)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCreateFile"); 116 | _RtlInitUnicodeString RtlInitUnicodeString = (_RtlInitUnicodeString)GetProcAddress( 117 | GetModuleHandle(L"ntdll.dll"), "RtlInitUnicodeString"); 118 | HANDLE hFile = 0; 119 | DWORD dwRead = 0; 120 | UNICODE_STRING filePath; 121 | OBJECT_ATTRIBUTES oa = {0}; 122 | IO_STATUS_BLOCK iosb = {0}; 123 | RtlInitUnicodeString(&filePath, filePathNt.c_str()); 124 | InitializeObjectAttributes(&oa, &filePath, OBJ_CASE_INSENSITIVE, 0, 0); 125 | 126 | LONG retVal = 0; 127 | 128 | if (bHeaderLoaded) 129 | { 130 | CleanupSuccess(); 131 | } 132 | 133 | retVal = 134 | NtCreateFile(&hFile, FILE_GENERIC_READ | SYNCHRONIZE, &oa, &iosb, 0, 0, FILE_SHARE_READ, 135 | FILE_OPEN, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, 0, 0); 136 | if (retVal < 0) 137 | { 138 | Log(warning, "[!] Error opening: %ws. ntstatus: %x\n", filePathNt.c_str(), retVal); 139 | CleanupError(); 140 | } 141 | 142 | headerBuf.resize(0x1000); 143 | 144 | if (!ReadFile(hFile, &headerBuf[0], (DWORD)headerBuf.size(), &dwRead, 0) || 145 | dwRead != headerBuf.size()) 146 | { 147 | Log(warning, "[!] Error reading: %ws. last error: %d\n", filePathNt.c_str(), 148 | GetLastError()); 149 | CleanupError(); 150 | } 151 | 152 | bHeaderLoaded = true; 153 | 154 | status = ParseHeader(); 155 | 156 | Cleanup: 157 | DeleteHandle(hFile); 158 | return status; 159 | } 160 | 161 | bool PEFile::LoadHeaderFromMemory(HANDLE hProcess, DWORD_PTR moduleBase) 162 | { 163 | bool status = true; 164 | SIZE_T bytesRead = 0; 165 | 166 | if (bHeaderLoaded) 167 | { 168 | CleanupSuccess(); 169 | } 170 | 171 | headerBuf.resize(0x1000); 172 | 173 | if (!ReadProcessMemory(hProcess, (void*)moduleBase, &headerBuf[0], headerBuf.size(), 174 | &bytesRead) || 175 | bytesRead != headerBuf.size()) 176 | { 177 | Log(warning, "[!] Error reading module header: %llx, last error: %d", moduleBase, 178 | GetLastError()); 179 | CleanupError(); 180 | } 181 | 182 | bHeaderLoaded = true; 183 | 184 | status = ParseHeader(); 185 | 186 | Cleanup: 187 | return status; 188 | } 189 | 190 | void UpConvertNtHeader(IMAGE_NT_HEADERS64& ntHeader) 191 | { 192 | // Make a backup of original values; 193 | IMAGE_NT_HEADERS32 ntHeaderOriginal; 194 | memcpy(&ntHeaderOriginal, &ntHeader, sizeof(ntHeaderOriginal)); 195 | 196 | // Update optional header size 197 | ntHeader.FileHeader.SizeOfOptionalHeader = sizeof(IMAGE_OPTIONAL_HEADER64); 198 | // Change magic to PE32+ 199 | ntHeader.OptionalHeader.Magic = 0x20b; 200 | 201 | // Up convert the on-disk values to match the expected in-memory values 202 | ntHeader.OptionalHeader.SectionAlignment = ntHeaderOriginal.OptionalHeader.SectionAlignment; 203 | ntHeader.OptionalHeader.FileAlignment = ntHeaderOriginal.OptionalHeader.FileAlignment; 204 | ntHeader.OptionalHeader.MajorOperatingSystemVersion = 205 | ntHeaderOriginal.OptionalHeader.MajorOperatingSystemVersion; 206 | ntHeader.OptionalHeader.MinorOperatingSystemVersion = 207 | ntHeaderOriginal.OptionalHeader.MinorOperatingSystemVersion; 208 | ntHeader.OptionalHeader.MajorImageVersion = ntHeaderOriginal.OptionalHeader.MajorImageVersion; 209 | ntHeader.OptionalHeader.MinorImageVersion = ntHeaderOriginal.OptionalHeader.MinorImageVersion; 210 | ntHeader.OptionalHeader.MajorSubsystemVersion = 211 | ntHeaderOriginal.OptionalHeader.MajorSubsystemVersion; 212 | ntHeader.OptionalHeader.MinorSubsystemVersion = 213 | ntHeaderOriginal.OptionalHeader.MinorSubsystemVersion; 214 | ntHeader.OptionalHeader.Win32VersionValue = ntHeaderOriginal.OptionalHeader.Win32VersionValue; 215 | ntHeader.OptionalHeader.SizeOfImage = ntHeaderOriginal.OptionalHeader.SizeOfImage; 216 | ntHeader.OptionalHeader.SizeOfHeaders = ntHeaderOriginal.OptionalHeader.SizeOfHeaders; 217 | ntHeader.OptionalHeader.CheckSum = ntHeaderOriginal.OptionalHeader.CheckSum; 218 | ntHeader.OptionalHeader.Subsystem = ntHeaderOriginal.OptionalHeader.Subsystem; 219 | ntHeader.OptionalHeader.DllCharacteristics = ntHeaderOriginal.OptionalHeader.DllCharacteristics; 220 | ntHeader.OptionalHeader.SizeOfStackReserve = ntHeaderOriginal.OptionalHeader.SizeOfStackReserve; 221 | ntHeader.OptionalHeader.SizeOfStackCommit = ntHeaderOriginal.OptionalHeader.SizeOfStackCommit; 222 | ntHeader.OptionalHeader.SizeOfHeapReserve = ntHeaderOriginal.OptionalHeader.SizeOfHeapReserve; 223 | ntHeader.OptionalHeader.SizeOfHeapCommit = ntHeaderOriginal.OptionalHeader.SizeOfHeapCommit; 224 | ntHeader.OptionalHeader.LoaderFlags = ntHeaderOriginal.OptionalHeader.LoaderFlags; 225 | ntHeader.OptionalHeader.NumberOfRvaAndSizes = 226 | ntHeaderOriginal.OptionalHeader.NumberOfRvaAndSizes; 227 | memcpy(&ntHeader.OptionalHeader.DataDirectory, &ntHeaderOriginal.OptionalHeader.DataDirectory, 228 | sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_NUMBEROF_DIRECTORY_ENTRIES); 229 | } 230 | 231 | bool PEFile::ValidateIntegrity(PEFile& peDisk, PEFile& peMem, Process& process) 232 | { 233 | bool status = true; 234 | DWORD optionalHeaderSz = 0; 235 | bool bUpConverted = false; 236 | 237 | if ((peDisk.dosHeader.e_magic != peMem.dosHeader.e_magic) || 238 | (peDisk.dosHeader.e_cblp != peMem.dosHeader.e_cblp) || 239 | (peDisk.dosHeader.e_cp != peMem.dosHeader.e_cp) || 240 | (peDisk.dosHeader.e_cparhdr != peMem.dosHeader.e_cparhdr) || 241 | (peDisk.dosHeader.e_sp != peMem.dosHeader.e_sp) || 242 | (peDisk.dosHeader.e_lfarlc != peMem.dosHeader.e_lfarlc) || 243 | (peDisk.dosHeader.e_lfanew != peMem.dosHeader.e_lfanew)) 244 | { 245 | peMem.NewFinding("suspect", "mzHeader", "MZ Header tampered in memory"); 246 | CleanupSuccess(); 247 | } 248 | 249 | if (peDisk.bDotNet && 250 | peDisk.ntHeader.FileHeader.SizeOfOptionalHeader == sizeof(IMAGE_OPTIONAL_HEADER32) && 251 | peMem.ntHeader.FileHeader.SizeOfOptionalHeader == sizeof(IMAGE_OPTIONAL_HEADER64)) 252 | { 253 | // .NET MSIL binaries up-convert from 32->64 in memory 254 | UpConvertNtHeader(peDisk.ntHeader); 255 | 256 | // Unclobber entry point 257 | peMem.ntHeader.OptionalHeader.AddressOfEntryPoint = 258 | peDisk.ntHeader.OptionalHeader.AddressOfEntryPoint; 259 | 260 | bUpConverted = true; 261 | } 262 | if ((peDisk.ntHeader.Signature != peMem.ntHeader.Signature) || 263 | (memcmp(&peDisk.ntHeader.FileHeader, &peMem.ntHeader.FileHeader, 264 | sizeof(peDisk.ntHeader.FileHeader)) != 0)) 265 | { 266 | peMem.NewFinding("suspect", "ntHeader", "PE Header tampered in memory"); 267 | CleanupSuccess(); 268 | } 269 | 270 | if ((IMAGE_FILE_MACHINE_I386 == peDisk.ntHeader.FileHeader.Machine) && (!bUpConverted)) 271 | { 272 | // x86 image 273 | optionalHeaderSz = sizeof(IMAGE_OPTIONAL_HEADER32); 274 | IMAGE_OPTIONAL_HEADER32* pOptionalDisk = 275 | (IMAGE_OPTIONAL_HEADER32*)&peDisk.ntHeader.OptionalHeader; 276 | IMAGE_OPTIONAL_HEADER32* pOptionalMem = 277 | (IMAGE_OPTIONAL_HEADER32*)&peMem.ntHeader.OptionalHeader; 278 | pOptionalDisk->ImageBase = 0; 279 | pOptionalMem->ImageBase = 0; 280 | } 281 | else 282 | { 283 | optionalHeaderSz = sizeof(IMAGE_OPTIONAL_HEADER64); 284 | peDisk.ntHeader.OptionalHeader.ImageBase = 0; 285 | peMem.ntHeader.OptionalHeader.ImageBase = 0; 286 | } 287 | 288 | if (memcmp(&peDisk.ntHeader.OptionalHeader, &peMem.ntHeader.OptionalHeader, optionalHeaderSz) != 289 | 0) 290 | { 291 | peMem.NewFinding("suspect", "ntOptionalHeader", "NT Optional Header tampered in memory"); 292 | CleanupSuccess(); 293 | } 294 | 295 | // First, ensure everything mapped +X in memory is correlated with a +X PE section on disk 296 | for (auto it = process.memoryMap.begin(); it != process.memoryMap.end(); it++) 297 | { 298 | auto pMbi = it->get(); 299 | if (pMbi->AllocationBase != (void*)peMem.moduleInfo->moduleBase || 300 | !IsExecuteSet(pMbi->Protect)) 301 | { 302 | continue; 303 | } 304 | 305 | Log(debug, "Module: %ws, +X Region: %p\n", peMem.moduleInfo->modulePathNt.c_str(), 306 | pMbi->BaseAddress); 307 | 308 | char* pBuf = &peMem.headerBuf[0]; 309 | SIZE_T bufSz = peMem.headerBuf.size(); 310 | IMAGE_NT_HEADERS64* pNtHeader = (IMAGE_NT_HEADERS64*)(pBuf + peMem.dosHeader.e_lfanew); 311 | PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeader); 312 | bool bFoundSection = false; 313 | DWORD totalSectionSize = 0; 314 | SIZE_T attributedSize = 0; 315 | SIZE_T unsharedSize = 0; 316 | if (InvalidPtr(pBuf, bufSz, pSection, 317 | sizeof(*pSection) * peMem.ntHeader.FileHeader.NumberOfSections)) 318 | { 319 | peMem.NewFinding( 320 | "suspect", "ntSectionHeader", 321 | std::format("Invalid section header pointer", (DWORD_PTR)pMbi->BaseAddress)); 322 | CleanupError(); 323 | } 324 | 325 | for (int i = 0; i < peMem.ntHeader.FileHeader.NumberOfSections; i++, pSection++) 326 | { 327 | DWORD_PTR pSectionAddr = pSection->VirtualAddress + (DWORD_PTR)pMbi->AllocationBase; 328 | /*std::string characteristics; 329 | if (pSection->Characteristics & IMAGE_SCN_MEM_READ) 330 | characteristics += "R"; 331 | if (pSection->Characteristics & IMAGE_SCN_MEM_WRITE) 332 | characteristics += "W"; 333 | if (pSection->Characteristics & IMAGE_SCN_MEM_EXECUTE) 334 | characteristics += "X"; 335 | if (pSection->Characteristics & IMAGE_SCN_MEM_SHARED) 336 | characteristics += "S"; 337 | if (pSection->Characteristics & IMAGE_SCN_MEM_DISCARDABLE) 338 | characteristics += "D"; 339 | 340 | printf("Section: %c%c%c%c%c%c%c%c, Permissions: %s , Size: %x, Addr: %p, flags: %x\n", 341 | pSection->Name[0], pSection->Name[1], pSection->Name[2], pSection->Name[3], 342 | pSection->Name[4], pSection->Name[5], pSection->Name[6], pSection->Name[7], 343 | characteristics.c_str(), pSection->Misc.VirtualSize, pSectionAddr, 344 | pSection->Characteristics);*/ 345 | 346 | if ((pSection->Characteristics & IMAGE_SCN_MEM_EXECUTE) == 0 && 347 | strncmp("W64SVC", (const char*)&pSection->Name, 6) != 0 && 348 | strncmp(".crthunk", (const char*)&pSection->Name, 8) != 0 && 349 | strncmp(".oldntma", (const char*)&pSection->Name, 8) != 0) 350 | 351 | { 352 | // W64SVC section in wow64cpu.dll is read only on disk but RX in memory 353 | // Same for .crthunk and .oldntma in msedge_elf.dll 354 | // They should only be 0x200 in raw size 355 | continue; 356 | } 357 | 358 | DWORD alignedSize = pSection->Misc.VirtualSize; 359 | if (alignedSize % 0x1000) 360 | { 361 | alignedSize += 0x1000 - (alignedSize % 0x1000); 362 | } 363 | 364 | if ((((DWORD_PTR)pMbi->BaseAddress + attributedSize) < pSectionAddr) || 365 | ((DWORD_PTR)pMbi->BaseAddress + attributedSize) > (pSectionAddr + alignedSize)) 366 | { 367 | continue; 368 | } 369 | 370 | // How much fits into this section? 371 | attributedSize += min(alignedSize, pMbi->RegionSize); 372 | 373 | if (attributedSize == pMbi->RegionSize) 374 | { 375 | bFoundSection = true; 376 | break; 377 | } 378 | } 379 | 380 | if (!bFoundSection) 381 | { 382 | peMem.NewFinding( 383 | "suspect", "executableSections", 384 | std::format("Executable region {:016x} does not aligned with section header", 385 | (DWORD_PTR)pMbi->BaseAddress)); 386 | } 387 | 388 | if (UnsharedSize(process.hProcess, pMbi->BaseAddress, pMbi->RegionSize, unsharedSize) && 389 | unsharedSize > 0x1000) 390 | { 391 | peMem.NewFinding("suspect", "modifiedCode", 392 | std::format("Executable region {:016x} likely modified", 393 | (DWORD_PTR)pMbi->BaseAddress)); 394 | } 395 | } 396 | 397 | Cleanup: 398 | return status; 399 | } 400 | --------------------------------------------------------------------------------