{
	"id": "7f9812e9-e948-49ce-aead-b3fa5ad53b8f",
	"created_at": "2026-04-06T01:30:45.575483Z",
	"updated_at": "2026-04-10T13:11:53.606494Z",
	"deleted_at": null,
	"sha1_hash": "214fad92e285b270f7c330844b4629e1e403c924",
	"title": "Looking Closer at BPF Bytecode in BPFDoor",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 3977744,
	"plain_text": "Looking Closer at BPF Bytecode in BPFDoor\r\nArchived: 2026-04-06 00:59:06 UTC\r\nSHA256: afa8a32ec29a31f152ba20a30eb483520fe50f2dce6c9aa9135d88f7c9c511d7\r\nMalware Bazaar link\r\nTable of Contents\r\nFamily Introduction\r\nBPF Introduction\r\nThe Need for BPF\r\nStability in BPF\r\neBPF vs cBPF\r\nStudying the BPF Bytecode in BPFDoor\r\nBuilding Capstone\r\nDisassembling BPF Bytecode\r\nInterpreting BPFDoor’s BPF Bytecode\r\nSummary\r\nReferences\r\nFamily Introduction\r\nBPFDoor is a backdoor targeting Linux-based systems. It leverages Berkeley Packet Filter (BPF) technology that\r\nexists natively in Linux kernels since v2.1.75. By using low-level BPF-based packet filtering, it is able to bypass\r\nlocal firewalls and stealthily receive network traffic from its C2.\r\nBPF Introduction\r\nThe Need for BPF\r\nAn operating system (OS) abstracts away the hardware. For example, user-space programs running on the OS do\r\nnot directly interact with networking-related hardware. They do so via APIs exposed by the OS. On Linux, these\r\nare called system calls or syscalls, in short. This kind of a design results in a clear demarcation between the user-space and kernel-space.\r\nConsider a single network packet that reaches the kernel. A user-space packet filtering program wants to look at it.\r\nIn this case, the contents of the entire packet needs to be copied into user-space memory for it to be accessible by\r\nthe user-space program. This incurs a cost in performance and can be expected to be significant on high-traffic\r\nsystems.\r\nWith the introduction of BPF in Linux kernel v2.1.75, packet filtering can occur in kernel-space. A user-space\r\napplication such as tcpdump could provide a filtering program (aka BPF program) which would be compiled and\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 1 of 18\n\nrun completely in kernel-space in a register-based VM. This avoids the performance cost of copying the network\r\npacket into user-space.\r\nStability in BPF\r\nTo avoid instability in kernel-space, an arbitrary BPF program cannot be provided. A number of checks are\r\nperformed by the BPF in-kernel verifier. This includes tests such as verifying that the BPF program terminates,\r\nregisters are initialized and the program does not contain any loops that could cause the kernel to lock up. A BPF\r\nprogram can successfully be loaded and executed only after it is verified.\r\neBPF vs cBPF\r\nThe original BPF, also called classic BPF (cBPF), was designed for capturing and filtering network packets that\r\nmatched specific rules.\r\nLinux kernel v3.15 then introduced extended BPF (eBPF) which was more versatile and powerful. It had a larger\r\ninstruction set, leveraged 64-bit registers and more number of them. It could also be leveraged for carrying out\r\nsystem performance analysis.\r\ntcpdump , a user-space network packet analyzer, generates cBPF bytecode but it is then translated to eBPF\r\nbytecode in recent kernels. The following is an example of cBPF instructions generated by tcpdump when\r\ncapturing TCP traffic on port 80 . I’ve also added the C-style bytecode equivalent ( -dd option in tcpdump ) for\r\neach instruction.\r\n$ sudo tcpdump -i wlp4s0 -d \"tcp port 80\"\r\n(000) ldh [12] # { 0x28, 0, 0, 0x0000000c }\r\n(001) jeq #0x86dd jt 2 jf 8 # { 0x15, 0, 6, 0x000086dd }\r\n(002) ldb [20] # { 0x30, 0, 0, 0x00000014 }\r\n(003) jeq #0x6 jt 4 jf 19 # { 0x15, 0, 15, 0x00000006 }\r\n(004) ldh [54] # { 0x28, 0, 0, 0x00000036 }\r\n(005) jeq #0x50 jt 18 jf 6 # { 0x15, 12, 0, 0x00000050 }\r\n(006) ldh [56] # { 0x28, 0, 0, 0x00000038 }\r\n(007) jeq #0x50 jt 18 jf 19 # { 0x15, 10, 11, 0x00000050 }\r\n(008) jeq #0x800 jt 9 jf 19 # { 0x15, 0, 10, 0x00000800 }\r\n(009) ldb [23] # { 0x30, 0, 0, 0x00000017 }\r\n(010) jeq #0x6 jt 11 jf 19 # { 0x15, 0, 8, 0x00000006 }\r\n(011) ldh [20] # { 0x28, 0, 0, 0x00000014 }\r\n(012) jset #0x1fff jt 19 jf 13 # { 0x45, 6, 0, 0x00001fff }\r\n(013) ldxb 4*([14]\u00260xf) # { 0xb1, 0, 0, 0x0000000e }\r\n(014) ldh [x + 14] # { 0x48, 0, 0, 0x0000000e }\r\n(015) jeq #0x50 jt 18 jf 16 # { 0x15, 2, 0, 0x00000050 }\r\n(016) ldh [x + 16] # { 0x48, 0, 0, 0x00000010 }\r\n(017) jeq #0x50 jt 18 jf 19 # { 0x15, 0, 1, 0x00000050 }\r\n(018) ret #262144 # { 0x6, 0, 0, 0x00040000 }\r\n(019) ret #0 # { 0x6, 0, 0, 0x00000000 }\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 2 of 18\n\nStudying the BPF Bytecode in BPFDoor\r\nBuilding Capstone\r\nGiven BPF bytecode, we can use capstone to disassemble it. It supports the disassembly of both cBPF and eBPF\r\nbytecode. Building capstone from source is simple.\r\n$ git clone --recursive https://github.com/capstone-engine/capstone\r\nCloning into 'capstone'...\r\nremote: Enumerating objects: 32768, done.\r\nremote: Counting objects: 100% (1765/1765), done.\r\nremote: Compressing objects: 100% (544/544), done.\r\nremote: Total 32768 (delta 1267), reused 1649 (delta 1206), pack-reused 31003\r\nReceiving objects: 100% (32768/32768), 50.82 MiB | 18.05 MiB/s, done.\r\nResolving deltas: 100% (23271/23271), done.\r\n$ cd capstone\r\n$ ./make.sh\r\n$ cd bindings/python/\r\n$ sudo make install\r\n$ pip freeze | grep capstone\r\ncapstone==5.0.0rc2\r\nDisassembling BPF Bytecode\r\nThe following snap shows the existence of cBPF bytecode of length 240 bytes in the BPFDoor sample. The cBPF\r\nprogram is applied on the socket using a call to setsockopt with SO_ATTACH_FILTER option and a pointer to the\r\ncBPF bytecode.\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 3 of 18\n\n$ xxd -c 8 -g 1 bpf.o\r\n00000000: 28 00 00 00 0c 00 00 00 (.......\r\n00000008: 15 00 00 09 dd 86 00 00 ........\r\n00000010: 30 00 00 00 14 00 00 00 0.......\r\n00000018: 15 00 00 02 06 00 00 00 ........\r\n00000020: 28 00 00 00 38 00 00 00 (...8...\r\n00000028: 15 00 16 0d 50 00 00 00 ....P...\r\n00000030: 15 00 16 00 2c 00 00 00 ....,...\r\n00000038: 15 00 01 00 84 00 00 00 ........\r\n00000040: 15 00 00 14 11 00 00 00 ........\r\n00000048: 28 00 00 00 38 00 00 00 (...8...\r\n00000050: 15 00 11 10 bb 01 00 00 ........\r\n00000058: 15 00 00 11 00 08 00 00 ........\r\n00000060: 30 00 00 00 17 00 00 00 0.......\r\n00000068: 15 00 00 06 06 00 00 00 ........\r\n00000070: 28 00 00 00 14 00 00 00 (.......\r\n00000078: 45 00 0d 00 ff 1f 00 00 E.......\r\n00000080: b1 00 00 00 0e 00 00 00 ........\r\n00000088: 48 00 00 00 10 00 00 00 H.......\r\n00000090: 15 00 09 00 50 00 00 00 ....P...\r\n00000098: 15 00 08 07 bb 01 00 00 ........\r\n000000a0: 15 00 01 00 84 00 00 00 ........\r\n000000a8: 15 00 00 07 11 00 00 00 ........\r\n000000b0: 28 00 00 00 14 00 00 00 (.......\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 4 of 18\n\n000000b8: 45 00 05 00 ff 1f 00 00 E.......\r\n000000c0: b1 00 00 00 0e 00 00 00 ........\r\n000000c8: 48 00 00 00 10 00 00 00 H.......\r\n000000d0: 15 00 01 00 bb 01 00 00 ........\r\n000000d8: 15 00 00 01 16 00 00 00 ........\r\n000000e0: 06 00 00 00 00 00 04 00 ........\r\n000000e8: 06 00 00 00 00 00 00 00 ........\r\nA BPF instruction is 8 bytes in length. I’ve formatted the above hex dump so that each line represents a cBPF\r\ninstruction. capstone can be used to disassemble this bytecode.\r\nIn [1]: from capstone import *\r\nIn [2]: md = Cs(CS_ARCH_BPF, CS_MODE_BPF_CLASSIC)\r\nIn [3]: with open(\"bpf.o\", \"rb\") as ff:\r\n ...: data = ff.read()\r\n ...: linenum = 0\r\n ...: for i in md.disasm(data, 0):\r\n ...: print(f\"{j}: {i.mnemonic} {i.op_str}\")\r\n ...: linenum += 1\r\n0: ldh [0xc]\r\n1: jeq 0x86dd, +0x0, +0x9\r\n2: ldb [0x14]\r\n3: jeq 0x6, +0x0, +0x2\r\n4: ldh [0x38]\r\n5: jeq 0x50, +0x16, +0xd\r\n6: jeq 0x2c, +0x16, +0x0\r\n7: jeq 0x84, +0x1, +0x0\r\n8: jeq 0x11, +0x0, +0x14\r\n9: ldh [0x38]\r\n10: jeq 0x1bb, +0x11, +0x10\r\n11: jeq 0x800, +0x0, +0x11\r\n12: ldb [0x17]\r\n13: jeq 0x6, +0x0, +0x6\r\n14: ldh [0x14]\r\n15: jset 0x1fff, +0xd, +0x0\r\ncapstone failed to disassemble the 17th\r\n instruction. This corresponds to the cBPF bytecode:\r\nb1 00 00 00 0e 00 00 00\r\nLooking at the cBPF bytecode generated by tcpdump earlier (see eBPF vs cBPF section), the above bytecode\r\ncorresponds to the following instruction. Perhaps, capstone is not yet aware of this bytecode-instruction\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 5 of 18\n\nmapping.\r\nldxb 4*([14]\u00260xf)\r\nI removed the above ldxb instruction-specific bytecode from the hex dump, disassembled the remaining\r\nbytecode using capstone and then added the ldxb instruction at the appropriate position in the instruction\r\nsequence.\r\n0: ldh [0xc]\r\n1: jeq 0x86dd, +0x0, +0x9\r\n2: ldb [0x14]\r\n3: jeq 0x6 , +0x0, +0x2\r\n4: ldh [0x38]\r\n5: jeq 0x50, +0x16, +0xd\r\n6: jeq 0x2c, +0x16, +0x0\r\n7: jeq 0x84, +0x1, +0x0\r\n8: jeq 0x11, +0x0, +0x14\r\n9: ldh [0x38]\r\n10: jeq 0x1bb, +0x11, +0x10\r\n11: jeq 0x800, +0x0, +0x11\r\n12: ldb [0x17]\r\n13: jeq 0x6, +0x0, +0x6\r\n14: ldh [0x14]\r\n15: jset 0x1fff, +0xd, +0x0\r\n16: ldxb 4*([14]\u00260xf)\r\n17: ldh [x+0x10]\r\n18: jeq 0x50, +0x9, +0x0\r\n19: jeq 0x1bb, +0x8, +0x7\r\n20: jeq 0x84, +0x1, +0x0\r\n21: jeq 0x11, +0x0, +0x7\r\n22: ldh [0x14]\r\n23: jset 0x1fff, +0x5, +0x0\r\n24: ldxb 4*([14]\u00260xf)\r\n25: ldh [x+0x10]\r\n26: jeq 0x1bb, +0x1, +0x0\r\n27: jeq 0x16, +0x0, +0x1\r\n28: ret 0x40000\r\n29: ret 0x0\r\nInterpreting BPFDoor’s BPF Bytecode\r\nBPFDoor attaches the cBPF program to a AF_PACKET socket. So, packet filtering occurs at layer 2 of the network\r\nstack. Let’s look at each instruction line-by-line.\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 6 of 18\n\n0: ldh [0xc]\r\n1: jeq 0x86dd, +0x0, +0x9\r\n2: ldb [0x14]\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 7 of 18\n\n3: jeq 0x6 , +0x0, +0x2\r\n4: ldh [0x38]\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 8 of 18\n\n5: jeq 0x50, +0x16, +0xd\r\nIf the previously loaded value at line 4 matches 0x50 , control jumps to line 28 (relative offset 0x16 ) else it\r\njumps to line 19 (relative offset 0xd ). This instruction checks if the destination port number is 80 .\r\n6: jeq 0x2c, +0x16, +0x0\r\n7: jeq 0x84, +0x1, +0x0\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 9 of 18\n\n8: jeq 0x11, +0x0, +0x14\r\n9: ldh [0x38]\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 10 of 18\n\n10: jeq 0x1bb, +0x11, +0x10\r\nIf the previously loaded value at line 9 matches 0x1bb , control jumps to line 28 (relative offset 0x11 ) else it\r\njumps to line 27 (relative offset 0x10 ). This instruction checks if the destination port number is 443\r\n11: jeq 0x800, +0x0, +0x11\r\n12: ldb [0x17]\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 11 of 18\n\n13: jeq 0x6, +0x0, +0x6\r\n14: ldh [0x14]\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 12 of 18\n\n15: jset 0x1fff, +0xd, +0x0\r\nThis instruction performs a bitwise AND operation between the previously loaded value at line 14 and 0x1fff . If\r\nthe result is non-zero, control jumps to line 29 (relative offset 0xd ) else line 16 (relative offset 0). This\r\ninstruction basically looks at the value of the Fragment Offset field. If it is non-zero, control jumps to line 29\r\nelse line 16.\r\n16: ldxb 4*([14]\u00260xf)\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 13 of 18\n\n17: ldh [x+0x10]\r\n18: jeq 0x50, +0x9, +0x0\r\nIf the previously loaded value at line 17 matches 0x50 , control jumps to line 28 (relative offset 0x9 ) else it\r\njumps to line 19 (relative offset 0 ). This instruction checks if the destination port number is 80 .\r\n19: jeq 0x1bb, +0x8, +0x7\r\nIf the previously loaded value at line 17 matches 0x1bb , control jumps to line 28 (relative offset 0x8 ) else it\r\njumps to line 27 (relative offset 0x7 ). This instruction checks if the destination port number is 443 .\r\n20: jeq 0x84, +0x1, +0x0\r\n21: jeq 0x11, +0x0, +0x7\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 14 of 18\n\n22: ldh [0x14]\r\n23: jset 0x1fff, +0x5, +0x0\r\nThis instruction performs a bitwise AND operation between the previously loaded value at line 14 and 0x1fff . If\r\nthe result is non-zero, control jumps to line 29 (relative offset 0x5 ) else line 24 (relative offset 0). This\r\ninstruction basically looks at the value of the Fragment Offset field. If it is non-zero, control jumps to line 29\r\nelse line 24.\r\n24: ldxb 4*([14]\u00260xf)\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 15 of 18\n\n25: ldh [x+0x10]\r\n26: jeq 0x1bb, +0x1, +0x0\r\nIf the previously loaded value at line 25 matches 0x1bb , control jumps to line 28 (relative offset 0x1 ) else it\r\njumps to line 27 (relative offset 0). This instruction checks if the destination port number is 443 .\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 16 of 18\n\n27: jeq 0x16, +0x0, +0x1\r\nIf the previously loaded value matches 0x16 , control jumps to line 28 (relative offset 0) else it jumps to line 29\r\n(relative offset 0x1 ). This instruction checks if the destination port number is 22.\r\n28: ret 0x40000\r\nA non-zero return indicates a packet match.\r\n29: ret 0x0\r\nA zero return indicates a packet no-match.\r\nSummary\r\nBPFDoor’s cBPF bytecode filters according to the following rules:\r\nMatch only on IPv4 or IPv6 packets.\r\nMatch only on TCP traffic on ports 80, 443 and 22. In case of IPv4, don’t match on fragmented packets.\r\nThere is no TCP fragmentation over IPv6.\r\nMatch only on UDP/SCTP traffic on ports 443 and 22. In both IPv4 and IPv6 don’t match on fragmented\r\npackets.\r\nI think DeepInstinct’s blog about BPFDoor missed to point out that UDP traffic on only ports 443 and 22 are\r\ncaptured and not port 80.\r\nBPFdoor guides the kernel to set up its socket to only read UDP, TCP, and SCTP traffic coming through ports 22\r\nThe flowchart below shows the overall control flow of the BPF program:\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 17 of 18\n\nReferences\r\nBPFDoor Malware Evolves – Stealthy Sniffing Backdoor Ups Its Game\r\nCapstone - GitHub\r\nAn intro to using eBPF to filter packets in the Linux kernel\r\neBPF - An Overview\r\nA thorough introduction to eBPF\r\nWhat is the difference between BPF and eBPF?\r\nLinux Socket Filtering aka Berkeley Packet Filter (BPF)\r\nEthernet frame\r\nIPv6 packet\r\nTransmission Control Protocol\r\nInternet Protocol version 6 (IPv6) Header\r\nList of IP protocol numbers\r\nThe Transmission Control Protocol\r\nUDP header.png\r\nStream control transmission protocol (SCTP)\r\nIPv4 - Packet Structure\r\nSource: https://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nhttps://nikhilh-20.github.io/blog/cbpf_bpfdoor/\r\nPage 18 of 18",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://nikhilh-20.github.io/blog/cbpf_bpfdoor/"
	],
	"report_names": [
		"cbpf_bpfdoor"
	],
	"threat_actors": [],
	"ts_created_at": 1775439045,
	"ts_updated_at": 1775826713,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/214fad92e285b270f7c330844b4629e1e403c924.pdf",
		"text": "https://archive.orkl.eu/214fad92e285b270f7c330844b4629e1e403c924.txt",
		"img": "https://archive.orkl.eu/214fad92e285b270f7c330844b4629e1e403c924.jpg"
	}
}