{
	"id": "c3ddebcb-8930-42c9-ba29-8c6b2f969bae",
	"created_at": "2026-04-06T00:21:40.865271Z",
	"updated_at": "2026-04-10T13:12:16.058509Z",
	"deleted_at": null,
	"sha1_hash": "b7c5d3b5f5f6fdec5363fd5dcc10c235a9b7c639",
	"title": "Fun with the new bpfdoor (2023)",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 673585,
	"plain_text": "Fun with the new bpfdoor (2023)\r\nBy unfinished.bike\r\nPublished: 2023-05-14 · Archived: 2026-04-05 18:33:45 UTC\r\nI was recently provided a sample of the recently announced stealthier variant of bpfdoor, malware targeting Linux\r\nthat is almost certainly a state-funded Chinese threat actor (Red Menshen). The sample analyzed was\r\na8a32ec29a31f152ba20a30eb483520fe50f2dce6c9aa9135d88f7c9c511d7, detectable by 11 of 62 detectors on\r\nVirusTotal.\r\nI was particularly curious what the bpfdoor surface area looked like, and if it was easy it was to detect using\r\nexisting open-source tools and common Linux command-line utilities.\r\nTo experiment, I used my favorite VM manager on macOS or Linux for this analysis: Lima, with the default\r\nUbuntu 22.10 image.\r\nRunning bpfdoor as a regular user\r\nI first ran bpfdoor as an unprivileged user to see what system calls would be executed:\r\nstrace -o /tmp/st.user -f ./x.bin\r\nI've removed the less interesting lines of output, but the program does astonishingly little:\r\nhttps://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nPage 1 of 10\n\n2655 execve(\"./x.bin\", [\"./x.bin\"], 0x7fff9dad6ff8 /* 23 vars */) = 0\r\n2655 openat(AT_FDCWD, \"/lib/x86_64-linux-gnu/libc.so.6\", O_RDONLY|O_CLOEXEC) = 3\r\n2655 openat(AT_FDCWD, \"/var/run/initd.lock\", O_RDWR|O_CREAT, 0666) = -1 EACCES (Permission denied)\r\n2655 flock(-1, LOCK_EX|LOCK_NB) = -1 EBADF (Bad file descriptor)\r\n2655 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7ff8d1b39a10\r\n2655 +++ exited with 0 +++\r\n2656 close(0) = 0\r\n2656 close(1) = 0\r\n2656 close(2) = 0\r\n2656 setsid() = 2656\r\n2656 getrandom(\"\\xa4\\xd5\\x9d\\x71\\xb3\\xe0\\x98\\xe1\", 8, GRND_NONBLOCK) = 8\r\n2656 socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) = -1 EPERM (Operation not permitted)\r\n2656 exit_group(0) = ?\r\n2656 +++ exited with 0 +++\r\nThe only noteworthy things here are:\r\nIt tries to create /var/run/initd.lock but fails because it requires root\r\nIt tries to set up a raw socket to listen to all protocols but fails because it requires root.\r\nIt forks into the background via clone() and setsid() .\r\nIt's not unusual to see a bug with the flock() call to fd=-1 because openat() returned an error rather than a file\r\nhandle.\r\nRunning as root\r\n2669 openat(AT_FDCWD, \"/var/run/initd.lock\", O_RDWR|O_CREAT, 0666) = 3\r\n2669 flock(3, LOCK_EX|LOCK_NB) = 0\r\n2669 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb6d948ba10\r\n2669 exit_group(0 \u003cunfinished ...\u003e\r\n3319 close(0 \u003cunfinished ...\u003e\r\n2669 +++ exited with 0 +++\r\n3319 close(1) = 0\r\n3319 close(2) = 0\r\n3319 setsid() = 3319\r\n3319 getrandom(\"\\x6c\\x07\\x1c\\x75\\x6b\\xae\\xfe\\xdf\", 8, GRND_NONBLOCK) = 8\r\n3319 socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) = 0\r\n3319 setsockopt(0, SOL_SOCKET, SO_ATTACH_FILTER, {len=30, filter=0x7ffd2270fa90}, 16) = 0\r\n3319 recvfrom(0, \"RUU\\341\\314\\22RU\\300\\250\\5\\2\\10\\0E\\0\\0Lp\\220\\0\\0@\\6~\\272\\300\\250\\5\\2\\300\\250\"..., 65536, 0, N\r\n3319 recvfrom(0, \"RUU\\341\\314\\22RU\\300\\250\\5\\2\\10\\0E\\0\\0(p\\221\\0\\0@\\6~\\335\\300\\250\\5\\2\\300\\250\"..., 65536, 0, N\r\n3319 recvfrom(0, \"RUU\\341\\314\\22RU\\300\\250\\5\\2\\10\\0E\\0\\0Lp\\222\\0\\0@\\6~\\270\\300\\250\\5\\2\\300\\250\"..., 65536, 0, N\r\n3319 recvfrom(0, \"RUU\\341\\314\\22RU\\300\\250\\5\\2\\10\\0E\\0\\0(p\\223\\0\\0@\\6~\\333\\300\\250\\5\\2\\300\\250\"..., 65536, 0, N\r\nFirst, it opens a lock, which works this time:\r\nhttps://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nPage 2 of 10\n\n-rw-r--r-- 1 root root 0 May 13 12:45 /run/initd.lock\r\nAs mentioned in the bpfdoor analysis by deep instinct, we can see that it sets a BPF filter via setsockopt() , and\r\nloops waiting for the magic byte sequence: \\x44\\x30\\xCD\\x9F\\x5E\\x14\\x27\\x66 .\r\nOne thing I find fascinating is how simple the initialization is: the previous iteration of bpfdoor did so much more\r\nin the name of “stealth”:\r\ncopies itself to /dev/shm\r\nrenaming itself in the process table via prctl\r\ndeletes itself from disk\r\ntimestomping\r\nRed Menshen must have noticed that every method for achieving stealth is also a reliable detection method. So,\r\nthe new bpfdoor keeps it simple by not trying to be stealthy. In fact, this binary does so little that it's suspicious. In\r\n2023, most advanced evasion methods are not worth it on Linux: it is good enough to hide in plain sight.\r\nDetection\r\nUsing the make detect rule from osquery-detection-kit, I examined which existing rules would alert on the\r\npresence of the latest bpfdoor. 3 of them did:\r\nunexpected raw socket: unexpected packet sniffers, just like this one! Near-zero false-positive rate.\r\nrecently created executables: programs executed within 45 seconds of when it likely landed on disk, based\r\non ctime and btime. This catch-all has found every malware it's encountered, but it requires a\r\ncomprehensive exception list.\r\nunexpected /var/run file: Inspired by reading the bpfdoor technical analysis, it's good to see this fired when\r\nfaced with the real thing.\r\nThat said, I think we can do better. Let's see what the malware looks like from /proc.\r\nExploring bpfdoor using /proc\r\nTo get an idea of what I can use for further detecting bpfdoor, I wanted to see how it was seen via /proc. First,\r\nwhat libraries does it link against? Based on the report, I'm not expecting anything other than libc:\r\n% sudo cat /proc/3319/maps\r\n00400000-00448000 r-xp 00000000 fc:01 3210 /tmp/x.bin\r\n00648000-00649000 r--p 00048000 fc:01 3210 /tmp/x.bin\r\n00649000-0064a000 rw-p 00049000 fc:01 3210 /tmp/x.bin\r\n0064a000-0066a000 rw-p 00000000 00:00 0\r\n00c36000-00c57000 rw-p 00000000 00:00 0 [heap]\r\n7fb6d9200000-7fb6d9222000 r--p 00000000 fc:01 3648 /usr/lib/x86_64-linux-gnu/libc.so.6\r\nhttps://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nPage 3 of 10\n\n7fb6d9222000-7fb6d939b000 r-xp 00022000 fc:01 3648 /usr/lib/x86_64-linux-gnu/libc.so.6\r\n7fb6d939b000-7fb6d93f2000 r--p 0019b000 fc:01 3648 /usr/lib/x86_64-linux-gnu/libc.so.6\r\n7fb6d93f2000-7fb6d93f6000 r--p 001f1000 fc:01 3648 /usr/lib/x86_64-linux-gnu/libc.so.6\r\n7fb6d93f6000-7fb6d93f8000 rw-p 001f5000 fc:01 3648 /usr/lib/x86_64-linux-gnu/libc.so.6\r\n7fb6d93f8000-7fb6d9405000 rw-p 00000000 00:00 0\r\n7fb6d948b000-7fb6d948e000 rw-p 00000000 00:00 0\r\n7fb6d9495000-7fb6d9497000 rw-p 00000000 00:00 0\r\n7fb6d9497000-7fb6d9498000 r--p 00000000 fc:01 3645 /usr/lib/x86_64-linux-gnu/ld-linux-x86-\r\n7fb6d9498000-7fb6d94c1000 r-xp 00001000 fc:01 3645 /usr/lib/x86_64-linux-gnu/ld-linux-x86-\r\n7fb6d94c1000-7fb6d94cb000 r--p 0002a000 fc:01 3645 /usr/lib/x86_64-linux-gnu/ld-linux-x86-\r\n7fb6d94cb000-7fb6d94cd000 r--p 00034000 fc:01 3645 /usr/lib/x86_64-linux-gnu/ld-linux-x86-\r\n7fb6d94cd000-7fb6d94cf000 rw-p 00036000 fc:01 3645 /usr/lib/x86_64-linux-gnu/ld-linux-x86-\r\n7ffd226f0000-7ffd22711000 rw-p 00000000 00:00 0 [stack]\r\n7ffd22720000-7ffd22724000 r--p 00000000 00:00 0 [vvar]\r\n7ffd22724000-7ffd22726000 r-xp 00000000 00:00 0 [vdso]\r\nffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]\r\nWhat about open file handles?\r\n% sudo lsof -p 3319\r\nCOMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\r\nx.bin 3319 root cwd DIR 0,52 200 9 /tmp/lima/osquery-defense-kit/out\r\nx.bin 3319 root rtd DIR 252,1 4096 2 /\r\nx.bin 3319 root txt REG 252,1 302576 3210 /tmp/x.bin\r\nx.bin 3319 root mem REG 252,1 2072888 3648 /usr/lib/x86_64-linux-gnu/libc.so.6\r\nx.bin 3319 root mem REG 252,1 228720 3645 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\r\nx.bin 3319 root 0u pack 33049 0t0 ALL type=SOCK_RAW\r\nx.bin 3319 root 3u REG 0,25 0 1322 /run/initd.lock\r\nlsof is handy, but to see the raw socket from /proc, we need to do a little bit more digging:\r\n# cat /proc/net/packet\r\nsk RefCnt Type Proto Iface R Rmem User Inode\r\nffff92d346ba6800 3 3 88cc 2 1 0 100 19458\r\nffff92d34631d800 3 3 0003 0 1 241920 0 33089\r\nThe Inode field is misleading, but you can use it to find the associated process ID via:\r\n$ sudo find /proc -type l -lname \"socket:\\[33089\\]\" 2\u003e/dev/null\r\n/proc/3319/task/3319/fd/0\r\n/proc/3319/fd/0\r\nhttps://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nPage 4 of 10\n\nAlternatively, you can use this to see all filehandles for the process ID:\r\n$ ls -la /proc/3319/fd\r\ntotal 0\r\ndr-x------ 2 root root 0 May 13 13:03 .\r\ndr-xr-xr-x 9 root root 0 May 13 13:03 ..\r\nlrwx------ 1 root root 64 May 13 13:03 0 -\u003e 'socket:[33089]'\r\nlrwx------ 1 root root 64 May 13 13:03 3 -\u003e /run/initd.lock\r\nOnce you have a process ID, you can resolve the path to the program:\r\nsudo ls -lad /proc/3319/exe\r\nlrwxrwxrwx 1 root root 0 May 14 00:48 /proc/3319/exe -\u003e /tmp/x.bin\r\nExploring bpfdoor using strings\r\nRunning strings \u003cpath\u003e reveals some interesting messages:\r\n[-] Execute command failed\r\n/var/run/initd.lock\r\nlibtom/libtomcrypt has been bundled in, so we see lines such as:\r\nLTC_ARGCHK '%s' failure on line %d of file %s\r\nX.509v%i certificate\r\n Issued by: [%s]%s (%s)\r\n Issued to: [%s]%s (%s, %s)\r\n Subject: %s\r\n Validity: %s - %s\r\n OCSP: %s\r\n Serial number:\r\n...\r\nLibTomCrypt 1.17 (Tom St Denis, tomstdenis@gmail.com)\r\nLibTomCrypt is public domain software.\r\nBuilt on Oct 4 2022 at 16:09:32\r\nThat last string is important: this iteration of bpfdoor could have been wandering around Cyberspace since\r\nOctober 2022 (7 months ago) without detection. It also appears that the bad guys used Red Hat Enterprise Linux\r\n7.0 (nearly 10 years old!) to build the binary:\r\nGCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)\r\nhttps://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nPage 5 of 10\n\nNew detection possibilities\r\nAfter looking at /proc, a couple of new detection ideas came up:\r\nPrograms with /var/run lock files open\r\nRoot processes with a socket and no shared libraries\r\nWorld-readable lock files in /var/run\r\nMinimalist socket users with few open files\r\nProcesses where fd 0 is a non-UNIX socket\r\nThere are certainly more possibilities depending on how this backdoor is launched: for example, based on cwd or\r\ncgroup. I have not yet seen information published on how this backdoor is actually executed.\r\nI implemented each of these detection ideas: once for osquery to use in production, and once in shell just for fun.\r\nThe osquery queries have been tested across Ubuntu, Fedora, Arch Linux, and NixOS, and the shell scripts have\r\nonly been tested on Ubuntu.\r\nPrograms with /run lock files left open\r\nIt's unusual for a program to have an open file in /var/run, but I suspect this may eventually find a false positive.\r\nHere's an osquery and a shell script to find these:\r\nSELECT p.* FROM processes p JOIN process_open_files pof ON p.pid = pof.pid AND pof.path LIKE \"/run/%.lock\";\r\nsudo find /proc -lname \"/run/*.lock\" 2\u003e/dev/null\r\nRoot processes with a socket and no shared libraries\r\nMost programs that use a socket are either fully static, or import a library like OpenSSL. bpfdoor isn't either. Here\r\nis another osquery and shell pair:\r\nSELECT p.*,\r\n COUNT(DISTINCT pmm.path) AS pmm_count\r\nFROM processes p\r\n JOIN process_open_sockets pos ON p.pid = pos.pid\r\n LEFT JOIN process_memory_map pmm ON p.pid = pmm.pid\r\n AND pmm.path LIKE \"%.so.%\"\r\n -- Yes, this is a weird performance optimization\r\nWHERE p.pid IN (\r\n SELECT pid\r\n FROM processes\r\n WHERE p.euid = 0\r\n AND p.path NOT IN (\r\n '/usr/bin/containerd',\r\n '/usr/bin/fusermount3',\r\nhttps://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nPage 6 of 10\n\n'/usr/sbin/acpid',\r\n '/usr/sbin/mcelog',\r\n '/usr/bin/docker-proxy'\r\n )\r\n )\r\nGROUP BY pos.pid -- libc.so, ld-linux\r\nHAVING pmm_count = 2;\r\ncd /proc || exit\r\nfor pid in *; do\r\n [[ ! -f ${pid}/exe || ${pid} =~ \"self\" ]] \u0026\u0026 continue\r\n euid=$(grep Uid /proc/${pid}/status | awk '{ print $2 }')\r\n [[ \"${euid}\" != 0 ]] \u0026\u0026 continue\r\n sockets=$(sudo find /proc/${pid}/fd -lname \"socket:*\" | wc -l)\r\n [[ \"${sockets}\" == 0 ]] \u0026\u0026 continue\r\n libs=$(sudo find /proc/${pid}/map_files/ -type l -lname \"*.so.*\" -exec readlink {} \\; | sort -u | wc -l)\r\n [[ \"${libs}\" != 2 ]] \u0026\u0026 continue\r\n path=$(readlink /proc/$pid/exe)\r\n name=$(cat /proc/$pid/comm)\r\n echo \"euid=0 process with sockets and no libs: ${name} [${pid}] at ${path}\"\r\ndone\r\nWorld readable lock files in /var/run\r\nTypically lock files are readable only by the root user. Malware often uses very relaxed file permissions.\r\nSELECT * FROM file WHERE path LIKE \"/tmp/%.lock\" AND mode = \"0644\";\r\nfind /run/*.lock -perm 644\r\nMinimalist socket users with few open files\r\nThis creative query reveals minimalist programs that behave like a backdoor might:\r\nhave 0-1 open files\r\nhave 1-2 sockets open\r\nIt's an uncommon situation, but it is bound to have false positives in software that is designed in a way that each\r\nprocess has a specific role:\r\nhttps://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nPage 7 of 10\n\nSELECT p.pid,\r\n p.path,\r\n p.name,\r\n p.start_time,\r\n GROUP_CONCAT(DISTINCT pos.protocol) AS protocols,\r\n pof.path AS pof_path,\r\n COUNT(DISTINCT pos.fd) AS scount,\r\n COUNT(DISTINCT pof.path) AS fcount,\r\n GROUP_CONCAT(DISTINCT pof.path) AS open_files,\r\n p.cgroup_path\r\nFROM processes p\r\n JOIN process_open_sockets pos ON p.pid = pos.pid\r\n AND pos.protocol \u003e 0\r\n LEFT JOIN process_open_files pof ON p.pid = pof.pid\r\nWHERE p.start_time \u003c (strftime('%s', 'now') -60)\r\nAND p.path NOT IN (\r\n '/bin/registry',\r\n '/usr/bin/docker-proxy',\r\n '/usr/sbin/chronyd',\r\n '/usr/sbin/cups-browsed',\r\n '/usr/sbin/cupsd',\r\n '/usr/sbin/sshd'\r\n)\r\nAND p.path NOT LIKE '/nix/store/%-openssh-%/bin/sshd'\r\nGROUP BY p.pid\r\nHAVING scount \u003c= 2\r\n AND fcount \u003c= 1;\r\ncd /proc || exit\r\nfor pid in *; do\r\n [[ ! -f ${pid}/exe || ${pid} =~ \"self\" ]] \u0026\u0026 continue\r\n fds=$(find /proc/${pid}/fd -lname \"/*\" | wc -l)\r\n [[ \"${fds}\" == 0 ]] \u0026\u0026 continue\r\n [[ \"${fds}\" -gt 1 ]] \u0026\u0026 continue\r\n # WARNING: ss -xp will print two fds on the same line if connected. Use grep -o instead of -c\r\n #ss -xp | grep -v \"^u_\" | grep -o pid=${pid},\"\r\n all_sockets=$(find /proc/${pid}/fd -lname \"socket:*\" | wc -l)\r\n [[ \"${all_sockets}\" -gt 2 ]] \u0026\u0026 continue\r\n # this isn't exactly what we want - ss doesn't show TYPE=sock of protocol=UNIX :(\r\n unix_sockets=$(ss -ap | grep \"^u_\" | grep -o \"pid=${pid},\" | wc -l)\r\n sockets=$(($all_sockets - $unix_sockets))\r\nhttps://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nPage 8 of 10\n\n[[ \"${sockets}\" == 0 ]] \u0026\u0026 continue\r\n [[ \"${sockets}\" -gt 2 ]] \u0026\u0026 continue\r\n path=$(readlink /proc/$pid/exe)\r\n [[ \"${path}\" == \"/usr/sbin/sshd\" ]] \u0026\u0026 continue\r\n name=$(cat /proc/$pid/comm)\r\n echo \"minimalist socket user (${sockets} sockets and ${fds} files): ${name} [${pid}] at ${path}\"\r\ndone\r\nfd0 is a socket\r\nI've saved my favorite for last. File descriptor 0 is usually stdin, but in bpfdoors case, it is actually the socket it\r\nuses to listen to traffic on. I've never seen this behavior before outside of bpfdoor:\r\nSELECT * FROM process_open_sockets WHERE fd=0 AND family != 1;\r\ncd /proc || exit\r\nfor pid in *; do\r\n [[ ! -f ${pid}/exe || ${pid} =~ \"self\" ]] \u0026\u0026 continue\r\n ino=$(readlink /proc/$pid/fd/0 | grep -o 'socket:.*' | cut -d\"[\" -f2 | cut -d\"]\" -f1)\r\n grep -q \" ${ino}\" /proc/$pid/net/unix \u0026\u0026 continue\r\n path=$(readlink /proc/$pid/exe)\r\n name=$(cat /proc/$pid/comm)\r\n echo \"fd0 is a socket: ${name} [${pid}] at ${path}\"\r\ndone\r\nFinal Thoughts\r\nUltimately, I was happy to see that this variant was detectable using osquery-defense-kit, and even happier that I\r\ncould add additional rules to find future similar malware. Two philosophical viewpoints are critical to success in\r\ndetection:\r\nKnowing what is considered normal in your environment\r\nEvasion is a means of detection\r\nIf you are interested in open-source queries that can find bpfdoor and other unusual programs, check out:\r\nhttps://github.com/chainguard-dev/osquery-defense-kit/\r\nhttps://github.com/tstromberg/sunlight\r\nhttps://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nPage 9 of 10\n\nThanks to Kevin Beaumont for providing the bpfdoor sample for analysis.\r\nSource: https://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nhttps://unfinished.bike/fun-with-the-new-bpfdoor-2023\r\nPage 10 of 10",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://unfinished.bike/fun-with-the-new-bpfdoor-2023"
	],
	"report_names": [
		"fun-with-the-new-bpfdoor-2023"
	],
	"threat_actors": [
		{
			"id": "9c8a7541-1ce3-450a-9e41-494bc7af11a4",
			"created_at": "2023-01-06T13:46:39.358343Z",
			"updated_at": "2026-04-10T02:00:03.300601Z",
			"deleted_at": null,
			"main_name": "Red Menshen",
			"aliases": [
				"Earth Bluecrow",
				"Red Dev 18"
			],
			"source_name": "MISPGALAXY:Red Menshen",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434900,
	"ts_updated_at": 1775826736,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/b7c5d3b5f5f6fdec5363fd5dcc10c235a9b7c639.pdf",
		"text": "https://archive.orkl.eu/b7c5d3b5f5f6fdec5363fd5dcc10c235a9b7c639.txt",
		"img": "https://archive.orkl.eu/b7c5d3b5f5f6fdec5363fd5dcc10c235a9b7c639.jpg"
	}
}