#include "runtime_private.h" #include #include #include namespace BK_RUNTIME_INTERNAL { const char *WinsockOperationName(WinsockOperation op) noexcept { switch (op) { case WinsockOperation::WsaSend: return "WSASend"; case WinsockOperation::WsaRecv: return "WSARecv"; case WinsockOperation::Send: return "send"; case WinsockOperation::Recv: return "recv"; case WinsockOperation::Connect: return "connect"; case WinsockOperation::WsaConnect: return "WSAConnect"; case WinsockOperation::GetAddrInfoW: return "GetAddrInfoW"; default: return "winsock"; } } const char *ModuleOperationName(ModuleHookOperation op) noexcept { switch (op) { case ModuleHookOperation::LoadLibraryA: return "LoadLibraryA"; case ModuleHookOperation::LoadLibraryW: return "LoadLibraryW"; case ModuleHookOperation::LoadLibraryExA: return "LoadLibraryExA"; case ModuleHookOperation::LoadLibraryExW: return "LoadLibraryExW"; case ModuleHookOperation::LdrLoadDll: return "LdrLoadDll"; case ModuleHookOperation::RtlAddFunctionTable: return "RtlAddFunctionTable"; case ModuleHookOperation::RtlInstallFunctionTableCallback: return "RtlInstallFunctionTableCallback"; case ModuleHookOperation::RtlDeleteFunctionTable: return "RtlDeleteFunctionTable"; default: return "LoadLibrary"; } } static inline std::uint32_t BuildCallerFlags(const IC_STACKTRACE::CallerClassification &cls) noexcept { std::uint32_t flags = cls.Flags; flags |= (static_cast(cls.ImmediateCaller) << BLACKBIRD_HOOK_CALLER_IMMED_SHIFT); flags |= (static_cast(cls.DeepestOrigin) << BLACKBIRD_HOOK_CALLER_DEEP_SHIFT); return flags; } bool SendWinsockEvent(const WinsockCapturedEvent &evt) noexcept { using namespace BKIPC; auto cls = IC_STACKTRACE::ClassifyTrace(evt.Stack); if (cls.Flags & IC_STACKTRACE::kCallerFlagAllSystem) return true; BLACKBIRD_IPC_HOOK_EVENT record{}; const char *opName = WinsockOperationName(evt.Operation); std::size_t sampleSize = std::min(evt.Data.size(), RTL_NUMBER_OF(record.DataSample)); record.Kind = BlackbirdIpcHookEventWinsock; record.ProcessId = GetCurrentProcessId(); record.ThreadId = evt.ThreadId; record.Operation = static_cast(evt.Operation); record.Caller = reinterpret_cast(evt.Caller); record.Context0 = static_cast(static_cast(evt.Socket)); record.Context1 = evt.Args[0]; record.Context2 = evt.Args[1]; record.Context3 = evt.Args[2]; record.ArgCount = 4; for (std::size_t i = 0; i < RTL_NUMBER_OF(evt.Args); ++i) { record.Args[i] = evt.Args[i]; } record.DataSize = static_cast(sampleSize); record.CallerFlags = BuildCallerFlags(cls); (void)strncpy_s(record.ApiName, opName, _TRUNCATE); (void)strncpy_s(record.ModuleName, "WS2_32", _TRUNCATE); if (sampleSize != 0) { CopyMemory(record.DataSample, evt.Data.data(), sampleSize); } CopyHookStack(evt.Stack, record); return PublishHookEvent(record); } bool SendNtEvent(const NtCapturedEvent &evt) noexcept { using namespace BKIPC; auto cls = IC_STACKTRACE::ClassifyTrace(evt.Stack); if (cls.Flags & IC_STACKTRACE::kCallerFlagAllSystem) return true; BLACKBIRD_IPC_HOOK_EVENT record{}; const char *functionName = (evt.FunctionName != nullptr && evt.FunctionName[0] != '\0') ? evt.FunctionName : "NtCall"; record.Kind = BlackbirdIpcHookEventNt; record.ProcessId = GetCurrentProcessId(); record.ThreadId = evt.ThreadId; record.Operation = static_cast(evt.Operation); record.Caller = reinterpret_cast(evt.Caller); record.Context0 = evt.Args[0]; record.Context1 = evt.Args[1]; record.Context2 = evt.Args[2]; record.Context3 = evt.Args[3]; record.ArgCount = 8; for (std::size_t i = 0; i < RTL_NUMBER_OF(record.Args); ++i) { record.Args[i] = evt.Args[i]; } record.DataSize = (evt.DataSize > RTL_NUMBER_OF(record.DataSample)) ? RTL_NUMBER_OF(record.DataSample) : evt.DataSize; record.CallerFlags = BuildCallerFlags(cls); CopyHookStack(evt.Stack, record); if (record.DataSize != 0) { CopyMemory(record.DataSample, evt.DataSample, record.DataSize); } (void)strncpy_s(record.ApiName, functionName, _TRUNCATE); (void)strncpy_s(record.ModuleName, "ntdll", _TRUNCATE); return PublishHookEvent(record); } bool SendKiEvent(const KiCapturedEvent &evt) noexcept { using namespace BKIPC; auto cls = IC_STACKTRACE::ClassifyTrace(evt.Stack); if (cls.Flags & IC_STACKTRACE::kCallerFlagAllSystem) return true; const char *stubName = evt.StubName ? evt.StubName : ""; BLACKBIRD_IPC_HOOK_EVENT record{}; record.Kind = BlackbirdIpcHookEventKi; record.ProcessId = GetCurrentProcessId(); record.ThreadId = evt.ThreadId; record.Operation = 0; record.Caller = reinterpret_cast(evt.Caller); record.Context0 = reinterpret_cast(evt.StackPointer); record.ArgCount = 0; record.DataSize = 0; record.CallerFlags = BuildCallerFlags(cls); CopyHookStack(evt.Stack, record); (void)strncpy_s(record.ApiName, (stubName[0] != '\0') ? stubName : "KiUserApcDispatcher", _TRUNCATE); (void)strncpy_s(record.ModuleName, "ntdll", _TRUNCATE); return PublishHookEvent(record); } bool SendModuleEvent(const ModuleCapturedEvent &evt) noexcept { using namespace BKIPC; auto cls = IC_STACKTRACE::ClassifyTrace(evt.Stack); if (cls.Flags & IC_STACKTRACE::kCallerFlagAllSystem) return true; BLACKBIRD_IPC_HOOK_EVENT record{}; const char *functionName = ModuleOperationName(evt.Operation); std::size_t sampleSize = std::min(evt.NameSample.size(), RTL_NUMBER_OF(record.DataSample)); record.Kind = BlackbirdIpcHookEventModule; record.ProcessId = GetCurrentProcessId(); record.ThreadId = evt.ThreadId; record.Operation = static_cast(evt.Operation); record.Caller = reinterpret_cast(evt.Caller); record.Context0 = reinterpret_cast(evt.ModuleHandle); record.Context1 = evt.Args[0]; record.Context2 = evt.Args[1]; record.Context3 = evt.Args[2]; record.ArgCount = 4; record.CallerFlags = BuildCallerFlags(cls); for (std::size_t i = 0; i < RTL_NUMBER_OF(evt.Args); ++i) { record.Args[i] = evt.Args[i]; } if (sampleSize != 0) { record.DataSize = static_cast(sampleSize); CopyMemory(record.DataSample, evt.NameSample.data(), sampleSize); } CopyHookStack(evt.Stack, record); (void)strncpy_s(record.ApiName, functionName, _TRUNCATE); (void)strncpy_s(record.ModuleName, (evt.SourceModule != nullptr) ? evt.SourceModule : "KERNEL32", _TRUNCATE); return PublishHookEvent(record); } void FlushHookEvents() noexcept { { std::vector events = g_WinsockController.ConsumeEvents(); for (const auto &evt : events) { (void)SendWinsockEvent(evt); } } { std::vector events = g_NtHookController.ConsumeEvents(); for (const auto &evt : events) (void)SendNtEvent(evt); } { std::vector events = g_KiHookController.ConsumeEvents(); for (const auto &evt : events) (void)SendKiEvent(evt); } { std::vector events = g_ModuleHookController.ConsumeEvents(); for (const auto &evt : events) (void)SendModuleEvent(evt); } } bool SendHookIntegrityEvent(std::uint32_t integrityMask, std::uint32_t winsockMismatches, std::uint32_t ntMismatches, std::uint32_t kiMismatches, std::uint32_t moduleMismatches) noexcept { using namespace BKIPC; BLACKBIRD_IPC_HOOK_EVENT record{}; record.Kind = BlackbirdIpcHookEventIntegrity; record.ProcessId = GetCurrentProcessId(); record.ThreadId = GetCurrentThreadId(); record.Operation = (integrityMask != 0u) ? 1u : 0u; record.Caller = 0; record.Context0 = integrityMask; record.Context1 = winsockMismatches; record.Context2 = ntMismatches; record.Context3 = kiMismatches; record.ArgCount = 3; record.Args[0] = g_IntegrityCheckCount; record.Args[1] = static_cast(GetTickCount64()); record.Args[2] = moduleMismatches; (void)strncpy_s(record.ApiName, "HookIntegrity", _TRUNCATE); (void)strncpy_s(record.ModuleName, "SR71", _TRUNCATE); return PublishHookEvent(record); } bool IsSuspiciousPatchedPrologue(const std::uint8_t bytes[16]) noexcept { if (bytes == nullptr) { return false; } if (bytes[0] == 0xC3 || bytes[0] == 0xC2 || bytes[0] == 0xE9 || bytes[0] == 0xE8 || bytes[0] == 0xEB || bytes[0] == 0xCC) { return true; } if (bytes[0] == 0x33 && bytes[1] == 0xC0 && bytes[2] == 0xC3) { return true; } if (bytes[0] == 0x48 && bytes[1] == 0x31 && bytes[2] == 0xC0 && bytes[3] == 0xC3) { return true; } if (bytes[0] == 0xB8 && bytes[5] == 0xC3) { return true; } if (bytes[0] == 0x48 && bytes[1] == 0xB8 && bytes[10] == 0xFF && bytes[11] == 0xE0) { return true; } if (bytes[0] == 0xFF && bytes[1] == 0x25) { return true; } return false; } const std::uint8_t *RvaToFilePointer(const std::uint8_t *imageBase, std::size_t imageSize, DWORD rva, std::size_t bytesNeeded) noexcept { if (imageBase == nullptr || imageSize < sizeof(IMAGE_DOS_HEADER) || bytesNeeded == 0) { return nullptr; } const auto *dos = reinterpret_cast(imageBase); if (dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew <= 0 || static_cast(dos->e_lfanew) > (imageSize - sizeof(IMAGE_NT_HEADERS64))) { return nullptr; } const auto *nt = reinterpret_cast(imageBase + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE || nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { return nullptr; } if (rva < nt->OptionalHeader.SizeOfHeaders) { if (static_cast(rva) > imageSize || bytesNeeded > (imageSize - static_cast(rva))) { return nullptr; } return imageBase + rva; } const auto *section = IMAGE_FIRST_SECTION(nt); for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) { DWORD sectionRva = section->VirtualAddress; DWORD rawSize = section->SizeOfRawData; DWORD virtualSize = section->Misc.VirtualSize; DWORD span = (rawSize > virtualSize) ? rawSize : virtualSize; if (span == 0) { continue; } if (rva < sectionRva || rva >= (sectionRva + span)) { continue; } DWORD offset = rva - sectionRva; if (offset > rawSize || bytesNeeded > static_cast(rawSize - offset)) { return nullptr; } std::size_t fileOffset = static_cast(section->PointerToRawData) + offset; if (fileOffset > imageSize || bytesNeeded > (imageSize - fileOffset)) { return nullptr; } return imageBase + fileOffset; } return nullptr; } bool RefreshExpectedExportBytes(HMODULE moduleHandle, const char *exportName, ExportProbeCache &cache) noexcept { wchar_t modulePath[MAX_PATH]{}; HANDLE fileHandle = INVALID_HANDLE_VALUE; HANDLE mappingHandle = nullptr; const std::uint8_t *view = nullptr; bool success = false; if (moduleHandle == nullptr || exportName == nullptr) { return false; } DWORD pathChars = GetModuleFileNameW(moduleHandle, modulePath, RTL_NUMBER_OF(modulePath)); if (pathChars == 0 || pathChars >= RTL_NUMBER_OF(modulePath)) { return false; } if (cache.ExpectedCaptured && _wcsicmp(cache.ModulePath, modulePath) == 0) { return true; } fileHandle = CreateFileW(modulePath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (fileHandle == INVALID_HANDLE_VALUE) { return false; } mappingHandle = CreateFileMappingW(fileHandle, nullptr, PAGE_READONLY, 0, 0, nullptr); if (mappingHandle == nullptr) { CloseHandle(fileHandle); return false; } view = static_cast(MapViewOfFile(mappingHandle, FILE_MAP_READ, 0, 0, 0)); if (view != nullptr) { LARGE_INTEGER size{}; if (GetFileSizeEx(fileHandle, &size) && size.QuadPart > 0 && static_cast(size.QuadPart) <= static_cast(SIZE_MAX)) { std::size_t imageSize = static_cast(size.QuadPart); const auto *dos = reinterpret_cast(view); if (imageSize >= sizeof(IMAGE_DOS_HEADER) && dos->e_magic == IMAGE_DOS_SIGNATURE && dos->e_lfanew > 0 && static_cast(dos->e_lfanew) <= (imageSize - sizeof(IMAGE_NT_HEADERS64))) { const auto *nt = reinterpret_cast(view + dos->e_lfanew); if (nt->Signature == IMAGE_NT_SIGNATURE && nt->OptionalHeader.NumberOfRvaAndSizes > IMAGE_DIRECTORY_ENTRY_EXPORT) { const auto &exportDirEntry = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; const auto *exportDir = reinterpret_cast(RvaToFilePointer( view, imageSize, exportDirEntry.VirtualAddress, sizeof(IMAGE_EXPORT_DIRECTORY))); if (exportDir != nullptr) { const auto *nameRvAs = reinterpret_cast(RvaToFilePointer( view, imageSize, exportDir->AddressOfNames, exportDir->NumberOfNames * sizeof(DWORD))); const auto *nameOrdinals = reinterpret_cast(RvaToFilePointer( view, imageSize, exportDir->AddressOfNameOrdinals, exportDir->NumberOfNames * sizeof(WORD))); const auto *functionRvAs = reinterpret_cast(RvaToFilePointer( view, imageSize, exportDir->AddressOfFunctions, exportDir->NumberOfFunctions * sizeof(DWORD))); if (nameRvAs != nullptr && nameOrdinals != nullptr && functionRvAs != nullptr) { for (DWORD i = 0; i < exportDir->NumberOfNames; ++i) { const char *name = reinterpret_cast( RvaToFilePointer(view, imageSize, nameRvAs[i], 1)); if (name == nullptr || strcmp(name, exportName) != 0) { continue; } WORD ordinal = nameOrdinals[i]; if (ordinal >= exportDir->NumberOfFunctions) { break; } DWORD functionRva = functionRvAs[ordinal]; if (functionRva >= exportDirEntry.VirtualAddress && functionRva < (exportDirEntry.VirtualAddress + exportDirEntry.Size)) { break; } const std::uint8_t *expected = RvaToFilePointer(view, imageSize, functionRva, 16); if (expected != nullptr) { std::memcpy(cache.Expected, expected, 16); (void)wcscpy_s(cache.ModulePath, modulePath); cache.ExpectedCaptured = true; success = true; } break; } } } } } } UnmapViewOfFile(view); } CloseHandle(mappingHandle); CloseHandle(fileHandle); return success; } bool ProbeExportPatchState(const wchar_t *moduleName, const char *exportName, ExportProbeCache &cache, bool &present, bool &tampered, bool &suspicious, bool &expectedMismatch, std::uint8_t sample[16]) noexcept { HMODULE moduleHandle = nullptr; FARPROC exportAddress = nullptr; present = false; tampered = false; suspicious = false; expectedMismatch = false; if (sample != nullptr) { std::memset(sample, 0, 16); } if (moduleName == nullptr || exportName == nullptr || sample == nullptr) { return false; } moduleHandle = GetModuleHandleW(moduleName); if (moduleHandle == nullptr) { return true; } exportAddress = GetProcAddress(moduleHandle, exportName); if (exportAddress == nullptr) { return true; } present = true; std::memcpy(sample, exportAddress, 16); suspicious = IsSuspiciousPatchedPrologue(sample); if (RefreshExpectedExportBytes(moduleHandle, exportName, cache)) { expectedMismatch = std::memcmp(sample, cache.Expected, 16) != 0; } tampered = suspicious || expectedMismatch; return true; } bool SendPatchTamperEvent(std::uint32_t operation, const char *apiName, const char *moduleName, bool tampered, bool suspicious, bool expectedMismatch, const std::uint8_t sample[16]) noexcept { using namespace BKIPC; BLACKBIRD_IPC_HOOK_EVENT record{}; record.Kind = BlackbirdIpcHookEventIntegrity; record.ProcessId = GetCurrentProcessId(); record.ThreadId = GetCurrentThreadId(); record.Operation = operation; record.Caller = 0; record.Context0 = tampered ? 1u : 0u; record.Context1 = suspicious ? 1u : 0u; record.Context2 = expectedMismatch ? 1u : 0u; record.Context3 = g_IntegrityCheckCount; record.ArgCount = 1; record.Args[0] = static_cast(GetTickCount64()); record.DataSize = 16; std::memcpy(record.DataSample, sample, 16); (void)strncpy_s(record.ApiName, apiName != nullptr ? apiName : "UnknownPatchProbe", _TRUNCATE); (void)strncpy_s(record.ModuleName, moduleName != nullptr ? moduleName : "unknown", _TRUNCATE); return PublishHookEvent(record); } void PollAmsiEtwPatchWatchdog(ULONGLONG now) noexcept { bool present = false; bool tampered = false; bool suspicious = false; bool expectedMismatch = false; std::uint8_t sample[16]{}; if (ProbeExportPatchState(L"amsi.dll", "AmsiScanBuffer", g_AmsiProbe, present, tampered, suspicious, expectedMismatch, sample)) { if (present) { bool stateChanged = g_AmsiFirstPoll || (tampered != g_LastAmsiTampered); g_AmsiFirstPoll = false; bool publish = stateChanged || (tampered && (now - g_LastAmsiPublishTick >= kIntegrityRepublishPeriodMs)); if (publish && SendPatchTamperEvent(kIntegrityOperationAmsiPatch, "AmsiScanBuffer", "amsi", tampered, suspicious, expectedMismatch, sample)) { g_LastAmsiPublishTick = now; } g_LastAmsiTampered = tampered; } else { std::memset(&g_AmsiProbe, 0, sizeof(g_AmsiProbe)); g_AmsiFirstPoll = false; g_LastAmsiTampered = false; } } if (ProbeExportPatchState(L"ntdll.dll", "EtwEventWrite", g_EtwProbe, present, tampered, suspicious, expectedMismatch, sample)) { if (present) { bool stateChanged = g_EtwFirstPoll || (tampered != g_LastEtwTampered); g_EtwFirstPoll = false; bool publish = stateChanged || (tampered && (now - g_LastEtwPublishTick >= kIntegrityRepublishPeriodMs)); if (publish && SendPatchTamperEvent(kIntegrityOperationEtwPatch, "EtwEventWrite", "ntdll", tampered, suspicious, expectedMismatch, sample)) { g_LastEtwPublishTick = now; } g_LastEtwTampered = tampered; } else { std::memset(&g_EtwProbe, 0, sizeof(g_EtwProbe)); g_EtwFirstPoll = false; g_LastEtwTampered = false; } } } void PollHookIntegrityWatchdog() noexcept { ULONGLONG now = GetTickCount64(); if (now - g_LastIntegrityCheckTick < kIntegrityCheckPeriodMs) { return; } g_LastIntegrityCheckTick = now; ++g_IntegrityCheckCount; if (!g_WinsockInitialized && !g_NtInitialized && !g_KiInitialized && !g_ModuleInitialized) { PollAmsiEtwPatchWatchdog(now); return; } std::uint32_t winsockMismatches = 0; std::uint32_t ntMismatches = 0; std::uint32_t kiMismatches = 0; std::uint32_t moduleMismatches = 0; std::uint32_t integrityMask = 0; if (g_WinsockInitialized && !KeCheckWinsockHookIntegrity(&winsockMismatches)) { integrityMask |= kIntegrityMaskWinsock; } if (g_NtInitialized && !KeCheckNtHookIntegrity(&ntMismatches)) { integrityMask |= kIntegrityMaskNt; } if (g_KiInitialized && !KeCheckKiHookIntegrity(&kiMismatches)) { integrityMask |= kIntegrityMaskKi; } if (g_ModuleInitialized && !KeCheckModuleHookIntegrity(&moduleMismatches)) { integrityMask |= kIntegrityMaskModule; } bool stateChanged = integrityMask != g_LastIntegrityMask; bool publish = false; if (integrityMask != 0u) { publish = stateChanged || (now - g_LastIntegrityPublishTick >= kIntegrityRepublishPeriodMs); } else if (stateChanged && g_LastIntegrityMask != 0u) { publish = true; } g_LastIntegrityMask = integrityMask; if (publish && SendHookIntegrityEvent(integrityMask, winsockMismatches, ntMismatches, kiMismatches, moduleMismatches)) { g_LastIntegrityPublishTick = now; } PollAmsiEtwPatchWatchdog(now); } } // namespace BK_RUNTIME_INTERNAL