{
	"id": "45e4ff7b-d681-407e-ba32-44a1e1bc5db4",
	"created_at": "2026-04-06T00:09:14.166274Z",
	"updated_at": "2026-04-10T03:21:32.574089Z",
	"deleted_at": null,
	"sha1_hash": "368503e3852dce49e0c581546b1131945407c7a1",
	"title": "BPFDoor - Part 2 - The Present",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 666992,
	"plain_text": "BPFDoor - Part 2 - The Present\r\nBy haxrob\r\nPublished: 2025-06-02 · Archived: 2026-04-05 19:07:03 UTC\r\nDespite the venerable BPFDoor malware has once again found itself in the media spotlight. Recent variants avoid\r\nexisting detections, so we will take a look at samples found in significant telecommunications provider breach in\r\nApril 2025.\r\nDetection evasion improvements\r\nWe will be using two following samples as references\r\ncf2d3d9e0246a3220d7c3cc94257447085911b32e1de0aee9d4af7dd6427597d\r\n3f6f108db37d18519f47c5e4182e5e33cc795564f286ae770aa03372133d15c4\r\nWhat's changed\r\nFile descriptors for the more recent BPFDoor compared to the prior reported\r\nNo more SOCK_RAW appearing. You will only find socket of type SOCK_UDP in BPFDoor open file\r\ndescriptors. Despite this, the implant still accepts ICMP , UDP and TCP wakeup packets\r\nThe random selection of custom process names used for masquerading has been replaced with a fixed\r\nprocess name.\r\nMutex locks file paths are adjusted accordingly\r\nRemoval of \"fileless\" feature, no longer writing to /dev/shm and deleting itself from disk\r\nGlobal variables / function names stripped - no more looking for binaries with godpid\r\nSSL for transport encryption with embedded certificate\r\nUpdated BPF filter, now coming at a whopping 229 bytes long\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 1 of 11\n\nGoodbye SOCK_RAW\r\nA common and useful detection opportunity is to look for unexpected processes with open raw sockets. Doing a\r\nlsof | grep SOCK_RAW won't surface BPFDoor anymore. This is how it looks now:\r\n# lsof -p 4073\r\n..\r\n/usr/sbin 4073 root 0u CHR 136,3 0t0 6 /dev/pts/3\r\n/usr/sbin 4073 root 1u CHR 136,3 0t0 6 /dev/pts/3\r\n/usr/sbin 4073 root 2u CHR 136,3 0t0 6 /dev/pts/3\r\n/usr/sbin 4073 root 3u pack 37048 0t0 IP type=SOCK_DGRAM\r\nPrior, variants would open a raw socket (followed by setsockopt to install its BPF filter):\r\nsocket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))\r\n..\r\nsetsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, \u0026filter, sizeof(filter))\r\nNow we see that SOCK_RAW has been replaced with SOCK_DGRAM|SOCK_CLOEXEC\r\nIn other words we now have:\r\nsocket(AF_PACKET, SOCK_DGRAM|SOCK_CLOEXEC, htons(ETH_P_IP))\r\nOne would assume on a surface glance that AF_PACKET with SOCK_DGRAM would only receive UDP packets\r\n( SOCK_DGRAM ), but this is not actually the case.\r\npacket(7) states:\r\n\"When protocol is set to htons(ETH_P_IP), then all IPv4 packets are received. All incoming packets of\r\nthat protocol type will be passed to the packet socket before they are passed to the protocols\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 2 of 11\n\nimplemented in the kernel.\"\r\nHence, as per the description, ETH_P_IP will take precedence, making the socket 'applicable' for IP packets of\r\nany protocol ( TCP , UDP , ICMP ). What is not apparent is that kernel will reports the socket as SOCK_DGRAM\r\nwhich is misleading unless the nuance is understood.\r\nThe data received is slightly different as well: With SOCK_RAW the kernel will pass the whole packet, including\r\nthe link layer, while AF_PACKET strips this out and will everything from the IP header onwards. Hence we see\r\nin the original source with SOCK_RAW discards the 14 byte ethernet header:\r\nWhile the newer version using SOCK_DGRAM assumes the first byte to be the start of the IP header:\r\nThis very minor modification is a particularly improvement to it's stealth\r\nSocket detection opportunities\r\nss -0pb will still surface the BPF filter.\r\n# ss -0pb\r\n..\r\n users:((\"/usr/sbin/smart\",pid=4535,fd=3))\r\n bpf filter (229): 0x30 0 0 0, 0x54 0 0 240 ..\r\nRemoving the b flag (and trimming out systemd related false positives), the socket is listed as p_dgr :\r\n# ss -0p\r\np_dgr .... users:((\"/usr/sbin/smart\",pid=4812,fd=3))\r\nIf monitoring system calls, there are some opportunities. For example, auditd rules for socket with\r\nAF_PACKET . Expect false positives.\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 3 of 11\n\nauditctl -a exit,always -F arch=b64 -S socket -F a0=17\r\nand setsockopt with SO_ATTACH_FILTER :\r\nauditctl -a exit,always -F arch=b64 -S setsockopt -F a2=26\r\nNo more deleted in /dev/shm\r\nAs an evasion technique, BPFDoor would copy itself into /dev/shm , execute itself then delete itself from disk as\r\nan anti-forensic technique.\r\nThe kernel marking a processes' executable inode as (deleted) within a temporary file system such as\r\n/dev/shm proclaims loudly \"this deserves a closer look\".\r\nNow BPFDoor no longer touches /dev/shm nor does it deletes itself. It behaves more like a normal process with\r\nthe exception of its process name masquerading with the prctl system call and overwriting the envp on the\r\nstack.\r\nProcess Names and Mutex Lock\r\nPrior, the malware would randomly select a process name from a fixed list of common process names and a\r\nsomewhat constant file path for its mutex lock. Both of these have changed - The masqueraded process name and\r\nmutex lock path may vary to better match the target environment. For reference, the original would randomly\r\nselect from the following list of process names/arguments on initialization:\r\nA single process name is now hard coded. For example, the following example disguises itself as smartd , a disk\r\nmonitoring daemon:\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 4 of 11\n\nOther new \"single process\" masqueraded process names include:\r\nlldpad -d , /var/run/lldpad.lock\r\ndbus-daemon --system , /var/run/system.pid\r\n/usr/libexec/hald-addon-volume , /var/run/hald-addon.pid\r\n/usr/sbin/console-kit , /var/run/console-kit.pid\r\nNotably, when a magic packet is received, the forked process name remains /usr/libexec/postfix/master and\r\nthe familiar qmgr .\r\n💡\r\nAs with all BPFDoor samples, running 'strings' or similar will miss many strings. For example, in the disassembly,\r\nthe md5 password hashes above are littered with mov instructions. This is due to the use of the strings being\r\ndefined as an array, or as a \"stack string\".\r\nbpfdoor-dump.py can be used to extract obfuscated hashes and other strings from samples. Alternatively, check\r\nout this from elastic.co.\r\nSSL Encryption\r\nVanilla RC4 has been deprecated in the getshell function, replaced SSL using RC4-MD5 for both bind\r\nmode (listening server) or the reverse connection mode (client).\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 5 of 11\n\nfunction names, vars have been renamed during decomp.\r\nThe hardcoded certificate and private key (used in bind / server mode, with SSL_accept is extractable with\r\nstrings . In all samples analyzed, 3 unique self-signed certificates were identified.\r\n-----BEGIN CERTIFICATE-----\r\nMIIB+zCCAWQCCQCtA0agZ+qO5jANBgkqhkiG9w0BAQsFADBCMQswCQYDVQQGEwJY\r\nWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBh\r\nbnkgTHRkMB4XDTIxMTEyMzAyMTc0NloXDTMxMTEyMTAyMTc0NlowQjELMAkGA1UE\r\nBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBD\r\nb21wYW55IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx1JvO+nqr24g\r\nwc8at6x1NtZt7DoDi1/Ge/F70zz4gbxX/OxhOxXKexYrphsHXBzYVEWOyof9Vnok\r\nST7GKdRiRg6OS90WfdWFoVN2EdxwBN+BdozmwRBG1DAdqAhbeUcUFeZO0Fbuo7fr\r\nFvTfsC31khj6ioKJl0d4kfo2zLk6WhcCAwEAATANBgkqhkiG9w0BAQsFAAOBgQA1\r\niC/5g+eN3Hq/627tMbLhipNUtC0OEdtpq20mbUIMXTRYh4kZPAah1LZqx2h72BV1\r\ni8pYJo34kZ/3HyV6UJtBf/jJv1fprEWvo2Lj8YrCpagXh82i7353GUeiKFVr0gx+\r\n4ruTus1m0bX1NZN6XRAbgzar7bfki0HHjWxJB8NRLQ==\r\n-----END CERTIFICATE-----\r\nopenssl x509 --in cert.pem -noout -dates -fingerprint\r\nnotBefore=Nov 23 02:17:46 2021 GMT\r\nnotAfter=Nov 21 02:17:46 2031 GMT\r\nSHA1 Fingerprint=85:CA:7E:BB:F1:1F:53:45:4E:DA:BB:27:DD:DC:59:DB:52:C2:0E:08\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 6 of 11\n\nThis change may explain one reason the newer samples are statically compiled, resulting in a much larger file\r\nexecutable file size - with the benefit of avoiding any linking issues with the SSL library.\r\nPassword hashes\r\nEmbedded passwords in older samples have been replaced with salted hashes. Where hashing was used, the same\r\nconstant salt is present: I5*AYbs@LdaWbsO\r\nThe first character of the password is used as an instruction on which mode to invoke. Passwords must start with\r\neither a m , s or j . This means the the search space for cracking the password hashes is reduced by one\r\ncharacter which can make a huge difference in the time to brute force the key space, reducing the maximum length\r\nfrom 14 to 13 characters.\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 7 of 11\n\n6227cb77cb4ab1d066eebf14e825dbc0a0a7f1e9\r\nWe can crack these hashes with hashcat, using mode 20 - md5($salt.$pass)\r\nFor example:\r\nhashcat -a 3 -m 20 hash:I5*AYbs@LdaWbsO j?1?1?1?1?1?1?1?1?1 -1 ?l?u -i\r\nHere are some hashes taken from samples with their corresponding plain-text password. Cracked hashes that\r\ninclude acronyms which can identify the victim organisation have been omitted.\r\naa73d4574fd91b9648d73b01ea1920f3:I5*AYbs@LdaWbsO:joinfare\r\n5609e5e3d3e7efd85e219901ab06bb61:I5*AYbs@LdaWbsO:jberemote\r\n215c5b9279d3e462eceb9af3b5028c05:I5*AYbs@LdaWbsO:justgetso\r\n629849fe5277500a777087d78ddc5dde:I5*AYbs@LdaWbsO:jusrbackso\r\n5fb2ce4f90c53071b12e65d52445d33d:I5*AYbs@LdaWbsO:javatelnet\r\n73b9989bb8dd522b8e172f2e985810eb:I5*AYbs@LdaWbsO:justgetdata\r\n05b37b412e1d1bfdc6b8643d3c869b01:I5*AYbs@LdaWbsO:justgetcheck\r\n8528eba01dca94e6b0d7c4c8cc39889f:I5*AYbs@LdaWbsO:justgotowork\r\n4cf71dacf1750e2a9f122fba74b86a5d:I5*AYbs@LdaWbsO:senttome\r\n3de78247e0e1c9ca3c291bc060d9b622:I5*AYbs@LdaWbsO:setdefault\r\nd46bf5d43cffd7793665d40fc767ed86:I5*AYbs@LdaWbsO:sentandconn\r\n3d45acc78e9d6de380b3cbdccf38af0a:I5*AYbs@LdaWbsO:setopenview\r\nbpfdoor-dump.py can be used to extract the hashes and generate the respective hashcat commands.\r\nYARA Rules\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 8 of 11\n\nTesting against both Elastic.co's set of BPFDoor yara rules from 2022 does identify the newer statically\r\ncompiled, stripped samples due to the rule Linux_Trojan_BPFDoor_5 including:\r\n{ D0 48 89 45 F8 48 8B 45 F8 0F B6 40 0C C0 E8 04 0F B6 C0 C1 }\r\nThis corresponds to the instructions in the packet_loop function which calculates the offset of the TCP header\r\nfor extracting the magic bytes in the TCP wakeup packets. But the ruleset does miss quite a few samples. For\r\nmaximum coverage, use with Florian Roth's ruleset.\r\nsignature-base/yara/mal_lnx_implant_may22.yar at\r\n391a990859091dbc4c21d15db335b371090f606e · Neo23x0/signature-base\r\nYARA signature and IOC database for my scanners and tools - Neo23x0/signature-base\r\nGitHubNeo23x0\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 9 of 11\n\nFor additional coverage on the latest variants, including the early NotBPDDoor , the following YARA rules can be\r\nused:\r\nrule bpfdoor_cert_variant\r\n{\r\n meta:\r\n description = \"Detects BPFDoor SSL versions\"\r\n reference = \"\"\r\n date = \"2025-04-31\"\r\n hash1 = \"3f6f108db37d18519f47c5e4182e5e33cc795564f286ae770aa03372133d15c4\"\r\n hash2 = \"724bd9163641666e035cef81701856fc9ff2dada2509d55dec14588fd1b5e801\"\r\n hash3 = \"7804f1dfb5d80a80830829c06ae65b410073748038f965f688dbd84d02eb0008\"\r\n hash4 = \"28bfb3f2067c77b83898ef4e41c9fc573e6aaa8581da9b59bddb782205a0b091\"\r\n hash5 = \"29564c19a15b06dd5be2a73d7543288f5b4e9e6668bbd5e48d3093fb6ddf1fdb\"\r\n author = \"@haxrob\"\r\n strings:\r\n $s1 = \"ttcompat\" fullword ascii\r\n $s2 = \"Private key does not match the public certificate\" fullword ascii\r\n $s3 = \"HISTFILE=/dev/null\" fullword ascii\r\n condition:\r\n uint16(0) == 0x457f and (all of them)\r\n}\r\nrule notbpfdoor\r\n{\r\n meta:\r\n description = \"Detects early (2015/2016) variant\"\r\n reference = \"\"\r\n date = \"2025-04-31\"\r\n hash1 = \"ebffd115918f6d181da6d8f5592dffb3e4f08cd4e93dcf7b7f1a2397af0580d9\"\r\n hash2 = \"b2d3c212e71ddbaf015d8793d30317e764131c9beda7971901620d90e6887b30\"\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 10 of 11\n\nauthor = \"@haxrob\"\r\n strings:\r\n $s1 = \"ttcompat\" fullword ascii\r\n $s2 = \"auto install failed, plz manual install it!\" fullword ascii\r\n $s3 = \"unset LC_TIME\" fullword ascii\r\n condition:\r\n uint16(0) == 0x457f and (all of them)\r\n}\r\nSource: https://haxrob.net/bpfdoor-past-and-present-part-2/\r\nhttps://haxrob.net/bpfdoor-past-and-present-part-2/\r\nPage 11 of 11",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://haxrob.net/bpfdoor-past-and-present-part-2/"
	],
	"report_names": [
		"bpfdoor-past-and-present-part-2"
	],
	"threat_actors": [],
	"ts_created_at": 1775434154,
	"ts_updated_at": 1775791292,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/368503e3852dce49e0c581546b1131945407c7a1.pdf",
		"text": "https://archive.orkl.eu/368503e3852dce49e0c581546b1131945407c7a1.txt",
		"img": "https://archive.orkl.eu/368503e3852dce49e0c581546b1131945407c7a1.jpg"
	}
}