{
	"id": "e44e5aac-8805-4cd7-956b-8ae40e7f67ee",
	"created_at": "2026-04-06T00:22:27.098671Z",
	"updated_at": "2026-04-10T13:12:59.869619Z",
	"deleted_at": null,
	"sha1_hash": "5b29f671666c9e98a0fb600a486b2ae9dfaf9188",
	"title": "Declawing PUMAKIT",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 7096754,
	"plain_text": "Declawing PUMAKIT\r\nBy Remco Sprooten, Ruben Groenewoud\r\nPublished: 2024-12-12 · Archived: 2026-04-05 14:09:58 UTC\r\nPUMAKIT at a glance\r\nPUMAKIT is a sophisticated piece of malware, initially uncovered during routine threat hunting on VirusTotal and\r\nnamed after developer-embedded strings found within its binary. Its multi-stage architecture consists of a dropper\r\n( cron ), two memory-resident executables ( /memfd:tgt and /memfd:wpn ), an LKM rootkit module, and a shared\r\nobject (SO) userland rootkit.\r\nThe rootkit component, referenced by the malware authors as “PUMA\", employs an internal Linux function tracer\r\n(ftrace) to hook 18 different syscalls and several kernel functions, enabling it to manipulate core system behaviors.\r\nUnique methods are used to interact with PUMA, including using the rmdir() syscall for privilege escalation and\r\nspecialized commands for extracting configuration and runtime information. Through its staged deployment, the LKM\r\nrootkit ensures it only activates when specific conditions, such as secure boot checks or kernel symbol availability, are\r\nmet. These conditions are verified by scanning the Linux kernel, and all necessary files are embedded as ELF binaries\r\nwithin the dropper.\r\nKey functionalities of the kernel module include privilege escalation, hiding files and directories, concealing itself\r\nfrom system tools, anti-debugging measures, and establishing communication with command-and-control (C2) servers.\r\nKey takeaways\r\nMulti-Stage Architecture: The malware combines a dropper, two memory-resident executables, an LKM\r\nrootkit, and an SO userland rootkit, activating only under specific conditions.\r\nAdvanced Stealth Mechanisms: Hooks 18 syscalls and several kernel functions using ftrace() to hide files,\r\ndirectories, and the rootkit itself, while evading debugging attempts.\r\nUnique Privilege Escalation: Utilizes unconventional hooking methods like the rmdir() syscall for\r\nescalating privileges and interacting with the rootkit.\r\nCritical Functionalities: Includes privilege escalation, C2 communication, anti-debugging, and system\r\nmanipulation to maintain persistence and control.\r\nPUMAKIT Discovery\r\nDuring routine threat hunting on VirusTotal, we came across an intriguing binary named cron. The binary was first\r\nuploaded on September 4, 2024, with 0 detections, raising suspicions about its potential stealthiness. Upon further\r\nexamination, we discovered another related artifact, /memfd:wpn\r\n(deleted) 71cc6a6547b5afda1844792ace7d5437d7e8d6db1ba995e1b2fb760699693f24, uploaded on the same day,\r\nalso with 0 detections.\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 1 of 24\n\nVirusTotal Hunting\r\nWhat caught our attention were the distinct strings embedded in these binaries, hinting at potential manipulation of the\r\nvmlinuz kernel package in /boot/ . This prompted a deeper analysis of the samples, leading to interesting findings\r\nabout their behavior and purpose.\r\nPUMAKIT code analysis\r\nPUMAKIT, named after its embedded LKM rootkit module (named \"PUMA\" by the malware authors) and Kitsune,\r\nthe SO userland rootkit, employs a multi-stage architecture, starting with a dropper that initiates an execution chain.\r\nThe process begins with the cron binary, which creates two memory-resident executables: /memfd:tgt (deleted)\r\nand /memfd:wpn (deleted) . While /memfd:tgt serves as a benign Cron binary, /memfd:wpn acts as a rootkit\r\nloader. The loader is responsible for evaluating system conditions, executing a temporary script ( /tmp/script.sh ),\r\nand ultimately deploying the LKM rootkit. The LKM rootkit contains an embedded SO file - Kitsune - to interact with\r\nthe rootkit from userspace. This execution chain is displayed below.\r\nPUMAKIT infection chain\r\nThis structured design enables PUMAKIT to execute its payload only when specific criteria are met, ensuring stealth\r\nand reducing the likelihood of detection. Each stage of the process is meticulously crafted to hide its presence,\r\nleveraging memory-resident files and precise checks on the target environment.\r\nIn this section, we will dive deeper into the code analysis for the different stages, exploring its components and their\r\nrole in enabling this sophisticated multi-stage malware.\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 2 of 24\n\nStage 1: Cron overview\r\nThe cron binary acts as a dropper. The function below serves as the main logic handler in a PUMAKIT malware\r\nsample. Its primary goals are:\r\n1. Check command-line arguments for a specific keyword ( \"Huinder\" ).\r\n2. If not found, embed and run hidden payloads entirely from memory without dropping them into the filesystem.\r\n3. If found, handle specific “extraction” arguments to dump its embedded components to disk and then gracefully\r\nexit.\r\nIn short, the malware tries to remain stealthy. If run usually (without a particular argument), it executes hidden ELF\r\nbinaries without leaving traces on disk, possibly masquerading as a legitimate process (like cron ).\r\nThe main function of the initial dropper\r\nIf the string Huinder isn’t found among the arguments, the code inside if (!argv_) executes:\r\nwriteToMemfd(...) : This is a hallmark of fileless execution. memfd_create allows the binary to exist entirely in\r\nmemory. The malware writes its embedded payloads ( tgtElfp and wpnElfp ) into anonymous file descriptors rather\r\nthan dropping them onto disk.\r\nfork() and execveat() : The malware forks into a child and parent process. The child redirects its standard output\r\nand error to /dev/null to avoid leaving logs and then executes the “weapon” payload ( wpnElfp ) using\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 3 of 24\n\nexecveat() . The parent waits for the child and then executes the “target” payload ( tgtElfp ). Both payloads are\r\nexecuted from memory, not from a file on disk, making detection and forensic analysis more difficult.\r\nThe choice of execveat() is interesting—it’s a newer syscall that allows executing a program referred to by a file\r\ndescriptor. This further supports the fileless nature of this malware’s execution.\r\nWe have identified that the tgt file is a legitimate cron binary. It is loaded in memory and executed after the\r\nrootkit loader ( wpn ) is executed.\r\nAfter execution, the binary remains active on the host.\r\n\u003e ps aux\r\nroot 2138 ./30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f\r\nBelow is a listing of the file descriptors for this process. These file descriptors show the memory-resident files created\r\nby the dropper.\r\nroot@debian11-rg:/tmp# ls -lah /proc/2138/fd\r\ntotal 0\r\ndr-x------ 2 root root 0 Dec 6 09:57 .\r\ndr-xr-xr-x 9 root root 0 Dec 6 09:57 ..\r\nlr-x------ 1 root root 64 Dec 6 09:57 0 -\u003e /dev/null\r\nl-wx------ 1 root root 64 Dec 6 09:57 1 -\u003e /dev/null\r\nl-wx------ 1 root root 64 Dec 6 09:57 2 -\u003e /dev/null\r\nlrwx------ 1 root root 64 Dec 6 09:57 3 -\u003e '/memfd:tgt (deleted)'\r\nlrwx------ 1 root root 64 Dec 6 09:57 4 -\u003e '/memfd:wpn (deleted)'\r\nlrwx------ 1 root root 64 Dec 6 09:57 5 -\u003e /run/crond.pid\r\nlrwx------ 1 root root 64 Dec 6 09:57 6 -\u003e 'socket:[20433]'\r\nFollowing the references we can see the binaries that are loaded in the sample. We can simply copy the bytes into a\r\nnew file for further analysis using the offset and sizes.\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 4 of 24\n\nEmbedded ELF binary\r\nUpon extraction, we find the following two new files:\r\nWpn : cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe\r\nTgt : 934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136\r\nWe now have the dumps of the two memory files.\r\nStage 2: Memory-resident executables overview\r\nExamining the /memfd:tgt ELF file, it is clear that this is the default Ubuntu Linux Cron binary. There appear to be no\r\nmodifications to the binary.\r\nThe /memfd:wpn file is more interesting, as it is the binary responsible for loading the the LKM rootkit. This rootkit\r\nloader attempts to hide itself by mimicking it as the /usr/sbin/sshd executable. It checks for particular prerequisites,\r\nsuch as whether secure boot is enabled and the required symbols are available, and if all conditions are met, it loads\r\nthe kernel module rootkit.\r\nLooking at the execution in Kibana, we can see that the program checks whether secure boot is enabled by querying\r\ndmesg . If the correct conditions are met, a shell script called script.sh is dropped in the /tmp directory and\r\nexecuted.\r\nExecution flow of the bash script and rootkit loader starting from /dev/fd/4\r\nThis script contains logic for inspecting and processing files based on their compression formats.\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 5 of 24\n\nThe Bash script that is used to decompress the kernel image\r\nHere's what it does:\r\nThe function c() inspects files using the file command to verify whether they are ELF binaries. If not, the\r\nfunction returns an error.\r\nThe function d() attempts to decompress a given file using various utilities like gunzip , unxz , bunzip2 ,\r\nand others based on signatures of supported compression formats. It employs grep and tail to locate and\r\nextract specific compressed segments.\r\nThe script attempts to locate and process a file ( $i ) into /tmp/vmlinux .\r\nAfter the execution of /tmp/script.sh , the file /boot/vmlinuz-5.10.0-33-cloud-amd64 is used as input. The tr\r\ncommand is employed to locate gzip's magic numbers ( \\037\\213\\010 ). Subsequently, a portion of the file starting at\r\nthe byte offset +10957311 is extracted using tail , decompressed with gunzip , and saved as /tmp/vmlinux . The\r\nresulting file is then verified to determine if it is a valid ELF binary.\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 6 of 24\n\nThe process of determining that the decompressing has succeeded\r\nThis sequence is repeated multiple times until all entries within the script have been passed into function d() .\r\nd '\\037\\213\\010' xy gunzip\r\nd '\\3757zXZ\\000' abcde unxz\r\nd 'BZh' xy bunzip2\r\nd '\\135\\0\\0\\0' xxx unlzma\r\nd '\\211\\114\\132' xy 'lzop -d'\r\nd '\\002!L\\030' xxx 'lz4 -d'\r\nd '(\\265/\\375' xxx unzstd\r\nThis process is shown below.\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 7 of 24\n\nAfter running through all of the items in the script, the /tmp/vmlinux and /tmp/script.sh files are deleted.\r\nDeleting the script and unpacked kernel\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 8 of 24\n\nThe script's primary purpose is to verify whether specific conditions are satisfied and, if they are, to set up the\r\nenvironment for deploying the rootkit using a kernel object file.\r\nRootkit loader looking for symbol offsets\r\nAs shown in the image above, the loader looks for __ksymtab and __kcrctab symbols in the Linux Kernel file and\r\nstores the offsets.\r\nSeveral strings show that the rootkit developers refer to their rootkit as “PUMA\" within the dropper. Based on the\r\nconditions, the program outputs messages such as:\r\nPUMA %s\r\n[+] PUMA is compatible\r\n[+] PUMA already loaded\r\nFurthermore, the kernel object file contains a section named .puma-config , reinforcing the association with the\r\nrootkit.\r\nStage 3: LKM rootkit overview\r\nIn this section, we take a closer look at the kernel module to understand its underlying functionality. Specifically, we\r\nwill examine its symbol lookup features, hooking mechanism, and the key syscalls it modifies to achieve its goals.\r\nLKM rootkit overview: symbol lookup and hooking mechanism\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 9 of 24\n\nThe LKM rootkit's ability to manipulate system behavior begins with its use of the syscall table and its reliance on\r\nkallsyms_lookup_name() for symbol resolution. Unlike modern rootkits targeting kernel versions 5.7 and above, the\r\nrootkit does not use kprobes , indicating it is designed for older kernels.\r\nResolving a pointer to the sys_call_table using kallsyms_lookup_name\r\nThis choice is significant because, prior to kernel version 5.7, kallsyms_lookup_name() was exported and could be\r\neasily leveraged by modules, even those without proper licensing.\r\nIn February 2020, kernel developers debated the unexporting of kallsyms_lookup_name() to prevent misuse by\r\nunauthorized or malicious modules. A common tactic involved adding a fake MODULE_LICENSE(\"GPL\") declaration to\r\ncircumvent licensing checks, allowing these modules to access non-exported kernel functions. The LKM\r\nrootkitdemonstrates this behavior, as evident from its strings:\r\nname=audit\r\nlicense=GPL\r\nThis fraudulent use of the GPL license ensures the rootkit can call kallsyms_lookup_name() to resolve function\r\naddresses and manipulate kernel internals.\r\nIn addition to its symbol resolution strategy, the kernel module employs the ftrace() hooking mechanism to\r\nestablish its hooks. By leveraging ftrace() , the rootkit effectively intercepts syscalls and replaces their handlers\r\nwith custom hooks.\r\nThe LKM rootkit leverages ftrace for hooking\r\nEvidence of this is e.g. the usage of unregister_ftrace_function and ftrace_set_filter_ip as shown in the\r\nsnippet of code above.\r\nLKM rootkit overview: hooked syscalls overview\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 10 of 24\n\nWe analyzed the rootkit's syscall hooking mechanism to understand the scope of PUMA's interference with system\r\nfunctionality. The following table summarizes the syscalls hooked by the rootkit, the corresponding hooked functions,\r\nand their potential purposes.\r\nBy viewing the cleanup_module() function, we can see the ftrace() hooking mechanism being reverted by using\r\nthe unregister_ftrace_function() function. This guarantees that the callback is no longer being called. Afterward,\r\nall syscalls are returned to point to the original syscall rather than the hooked syscall. This gives us a clean overview of\r\nall syscalls that were hooked.\r\nCleanup of all the hooked syscalls\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 11 of 24\n\nIn the following sections, we will take a closer look at a few of the hooked syscalls.\r\nLKM rootkit overview: rmdir_hook()\r\nThe rmdir_hook() in the kernel module plays a critical role in the rootkit’s functionality, enabling it to manipulate\r\ndirectory removal operations for concealment and control. This hook is not limited to merely intercepting rmdir()\r\nsyscalls but extends its functionality to enforce privilege escalation and retrieve configuration details stored within\r\nspecific directories.\r\nStart of the rmdir hook code\r\nThis hook has several checks in place. The hook expects the first characters to the rmdir() syscall to be zarya . If\r\nthis condition is met, the hooked function checks the 6th character, which is the command that gets executed. Finally,\r\nthe 8th character is checked, which can contain process arguments for the command that is being executed. The\r\nstructure looks like: zarya[char][command][char][argument] . Any special character (or none) can be placed between\r\nzarya and the commands and arguments.\r\nAs of the publication date, we have identified the following commands:\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 12 of 24\n\nCommand Purpose\r\nzarya.c.0 Retrieve the config\r\nzarya.t.0 Test the working\r\nzarya.k.\u003cpid\u003e Hide a PID\r\nzarya.v.0 Get the running version\r\nUpon initialization of the rootkit, the rmdir() syscall hook is used to check whether the rootkit was loaded\r\nsuccessfully. It does this by calling the t command.\r\nubuntu-rk:~$ rmdir test\r\nrmdir: failed to remove 'test': No such file or directory\r\nubuntu-rk:~$ rmdir zarya.t\r\nubuntu-rk:~$\r\nWhen using the rmdir command on a non-existent directory, an error message “No such file or directory” is\r\nreturned. When using rmdir on zarya.t , no output is returned, indicating successful loading of the kernel module.\r\nA second command is v , which is used to get the version of the running rootkit.\r\nubuntu-rk:~$ rmdir zarya.v\r\nrmdir: failed to remove '240513': No such file or directory\r\nInstead of zarya.v being added to the “failed to remove ‘ directory ’” error, the rootkit version 240513 is\r\nreturned.\r\nA third command is c , which prints the configuration of the rootkit.\r\nubuntu-rk:~/testing$ ./dump_config \"zarya.c\"\r\nrmdir: failed to remove '': No such file or directory\r\nBuffer contents (hex dump):\r\n7ffe9ae3a270 00 01 00 00 10 70 69 6e 67 5f 69 6e 74 65 72 76 .....ping_interv\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 13 of 24\n\n7ffe9ae3a280 61 6c 5f 73 00 2c 01 00 00 10 73 65 73 73 69 6f al_s.,....sessio\r\n7ffe9ae3a290 6e 5f 74 69 6d 65 6f 75 74 5f 73 00 04 00 00 00 n_timeout_s.....\r\n7ffe9ae3a2a0 10 63 32 5f 74 69 6d 65 6f 75 74 5f 73 00 c0 a8 .c2_timeout_s...\r\n7ffe9ae3a2b0 00 00 02 74 61 67 00 08 00 00 00 67 65 6e 65 72 ...tag.....gener\r\n7ffe9ae3a2c0 69 63 00 02 73 5f 61 30 00 15 00 00 00 72 68 65 ic..s_a0.....rhe\r\n7ffe9ae3a2d0 6c 2e 6f 70 73 65 63 75 72 69 74 79 31 2e 61 72 l.opsecurity1.ar\r\n7ffe9ae3a2e0 74 00 02 73 5f 70 30 00 05 00 00 00 38 34 34 33 t..s_p0.....8443\r\n7ffe9ae3a2f0 00 02 73 5f 63 30 00 04 00 00 00 74 6c 73 00 02 ..s_c0.....tls..\r\n7ffe9ae3a300 73 5f 61 31 00 14 00 00 00 73 65 63 2e 6f 70 73 s_a1.....sec.ops\r\n7ffe9ae3a310 65 63 75 72 69 74 79 31 2e 61 72 74 00 02 73 5f ecurity1.art..s_\r\n7ffe9ae3a320 70 31 00 05 00 00 00 38 34 34 33 00 02 73 5f 63 p1.....8443..s_c\r\n7ffe9ae3a330 31 00 04 00 00 00 74 6c 73 00 02 73 5f 61 32 00 1.....tls..s_a2.\r\n7ffe9ae3a340 0e 00 00 00 38 39 2e 32 33 2e 31 31 33 2e 32 30 ....89.23.113.20\r\n7ffe9ae3a350 34 00 02 73 5f 70 32 00 05 00 00 00 38 34 34 33 4..s_p2.....8443\r\n7ffe9ae3a360 00 02 73 5f 63 32 00 04 00 00 00 74 6c 73 00 00 ..s_c2.....tls..\r\nBecause the payload starts with null bytes, no output is returned when running zarya.c through a rmdir shell\r\ncommand. By writing a small C program that wraps the syscall and prints the hex/ASCII representation, we can see\r\nthe configuration of the rootkit being returned.\r\nInstead of using the kill() syscall to get root privileges (like most rootkits do), the rootkit leverages the rmdir()\r\nsyscall for this purpose as well. The rootkit uses the prepare_creds function to modify the credential-related IDs to 0\r\n(root), and calls commit_creds on this modified structure to obtain root privileges within its current process.\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 14 of 24\n\nPrivilege escalation using prepare_creds and commit_creds\r\nTo trigger this function, we need to set the 6th character to 0 . The caveat for this hook is that it gives the caller\r\nprocess root privileges but does not maintain them. When executing zarya.0 , nothing happens. However, when\r\ncalling this hook with a C program and printing the current process’ privileges, we do get a result. A snippet of the\r\nwrapper code that is used is displayed below:\r\n[...]\r\n// Print the current PID, SID, and GID\r\npid_t pid = getpid();\r\npid_t sid = getsid(0); // Passing 0 gets the SID of the calling process\r\ngid_t gid = getgid();\r\nprintf(\"Current PID: %d, SID: %d, GID: %d\\n\", pid, sid, gid);\r\n// Print all credential-related IDs\r\nuid_t ruid = getuid(); // Real user ID\r\nuid_t euid = geteuid(); // Effective user ID\r\ngid_t rgid = getgid(); // Real group ID\r\ngid_t egid = getegid(); // Effective group ID\r\nuid_t fsuid = setfsuid(-1); // Filesystem user ID\r\ngid_t fsgid = setfsgid(-1); // Filesystem group ID\r\nprintf(\"Credentials: UID=%d, EUID=%d, GID=%d, EGID=%d, FSUID=%d, FSGID=%d\\n\",\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 15 of 24\n\nruid, euid, rgid, egid, fsuid, fsgid);\r\n[...]\r\nExecuting the function, we can the following output:\r\nubuntu-rk:~/testing$ whoami;id\r\nruben\r\nuid=1000(ruben) gid=1000(ruben) groups=1000(ruben),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd)\r\nubuntu-rk:~/testing$ ./rmdir zarya.0\r\nReceived data:\r\nzarya.0\r\nCurrent PID: 41838, SID: 35117, GID: 0\r\nCredentials: UID=0, EUID=0, GID=0, EGID=0, FSUID=0, FSGID=0\r\nTo leverage this hook, we wrote a small C wrapper script that executes the rmdir zarya.0 command and checks\r\nwhether it can now access the /etc/shadow file.\r\n#include \u003cstdio.h\u003e\r\n#include \u003cstdlib.h\u003e\r\n#include \u003cunistd.h\u003e\r\n#include \u003csys/syscall.h\u003e\r\n#include \u003cerrno.h\u003e\r\nint main() {\r\n const char *directory = \"zarya.0\";\r\n // Attempt to remove the directory\r\n if (syscall(SYS_rmdir, directory) == -1) {\r\n fprintf(stderr, \"rmdir: failed to remove '%s': %s\\n\", directory, strerror(errno));\r\n } else {\r\n printf(\"rmdir: successfully removed '%s'\\n\", directory);\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 16 of 24\n\n}\r\n // Execute the `id` command\r\n printf(\"\\n--- Running 'id' command ---\\n\");\r\n if (system(\"id\") == -1) {\r\n perror(\"Failed to execute 'id'\");\r\n return 1;\r\n }\r\n // Display the contents of /etc/shadow\r\n printf(\"\\n--- Displaying '/etc/shadow' ---\\n\");\r\n if (system(\"cat /etc/shadow\") == -1) {\r\n perror(\"Failed to execute 'cat /etc/shadow'\");\r\n return 1;\r\n }\r\n return 0;\r\n}\r\nWith success.\r\nubuntu-rk:~/testing$ ./get_root\r\nrmdir: successfully removed 'zarya.0'\r\n--- Running 'id' command ---\r\nuid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd),1000(ruben)\r\n--- Displaying '/etc/shadow' ---\r\nroot:*:19430:0:99999:7:::\r\n[...]\r\nAlthough there are more commands available in the rmdir() function, we will, for now, move on to the next and\r\nmay add them to a future publication.\r\nLKM rootkit overview: getdents() and getdents64() hooks\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 17 of 24\n\nThe getdents_hook() and getdents64_hook() in the rootkit are responsible for manipulating directory listing\r\nsyscalls to hide files and directories from users.\r\nThe getdents() and getdents64() syscalls are used to read directory entries. The rootkit hooks these functions to filter\r\nout any entries that match specific criteria. Specifically, files and directories with the prefix zov_ are hidden from any\r\nuser attempting to list the contents of a directory.\r\nFor example:\r\nubuntu-rk:~/getdents_hook$ mkdir zov_hidden_dir\r\nubuntu-rk:~/getdents_hook$ ls -lah\r\ntotal 8.0K\r\ndrwxrwxr-x 3 ruben ruben 4.0K Dec 9 11:11 .\r\ndrwxr-xr-x 11 ruben ruben 4.0K Dec 9 11:11 ..\r\nubuntu-rk:~/getdents_hook$ echo \"this file is now hidden\" \u003e zov_hidden_dir/zov_hidden_file\r\nubuntu-rk:~/getdents_hook$ ls -lah zov_hidden_dir/\r\ntotal 8.0K\r\ndrwxrwxr-x 2 ruben ruben 4.0K Dec 9 11:11 .\r\ndrwxrwxr-x 3 ruben ruben 4.0K Dec 9 11:11 ..\r\nubuntu-rk:~/getdents_hook$ cat zov_hidden_dir/zov_hidden_file\r\nthis file is now hidden\r\nHere, the file zov_hidden can be accessed directly using its entire path. However, when running the ls command,\r\nit does not appear in the directory listing.\r\nStage 4: Kitsune SO overview\r\nWhile digging deeper into the rootkit, another ELF file was identified within the kernel object file. After extracting this\r\nbinary, we discovered this is the /lib64/libs.so file. Upon examination, we encountered several references to\r\nstrings such as Kitsune PID %ld . This suggests that the SO is referred to as Kitsune by the developers. Kitsune may\r\nbe responsible for certain behaviors observed in the rootkit. These references align with the broader context of how the\r\nrootkit manipulates user-space interactions via LD_PRELOAD .\r\nThis SO file plays a role in achieving the persistence and stealth mechanisms central to this rootkit, and its integration\r\nwithin the attack chain demonstrates the sophistication of its design. We will now showcase how to detect and/or\r\nprevent each part of the attack chain.\r\nPUMAKIT execution chain detection \u0026 prevention\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 18 of 24\n\nThis section will display different EQL/KQL rules and YARA signatures that can prevent and detect different parts of\r\nthe PUMAKIT execution chain.\r\nStage 1: Cron\r\nUpon execution of the dropper, an uncommon event is saved in syslog. The event states that a process has started with\r\nan executable stack. This is uncommon and interesting to watch:\r\n[ 687.108154] process '/home/ruben_groenewoud/30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f' st\r\nWe can search for this through the following query:\r\nhost.os.type:linux and event.dataset:\"system.syslog\" and process.name:kernel and message: \"started with executable\r\nThis message is stored in /var/log/messages or /var/log/syslog . We can detect this by reading syslog through\r\nFilebeat or the Elastic agent system integration.\r\nStage 2: Memory-resident executables\r\nWe can see an unusual file descriptor execution right away. This can be detected through the following EQL query:\r\nprocess where host.os.type == \"linux\" and event.type == \"start\" and event.action == \"exec\" and process.parent.execu\r\nThis file descriptor will remain the parent of the dropper until the process ends, resulting in the execution of several\r\nfiles through this parent process as well:\r\nfile where host.os.type == \"linux\" and event.type == \"creation\" and process.executable like \"/dev/fd/*\" and file.pa\r\n \"/boot/*\", \"/dev/shm/*\", \"/etc/cron.*/*\", \"/etc/init.d/*\", \"/var/run/*\"\r\n \"/etc/update-motd.d/*\", \"/tmp/*\", \"/var/log/*\", \"/var/tmp/*\"\r\n)\r\nAfter /tmp/script.sh is dropped (detected through the queries above), we can detect its execution by querying for\r\nfile attribute discovery and unarchiving activity:\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 19 of 24\n\nprocess where host.os.type == \"linux\" and event.type == \"start\" and event.action == \"exec\" and\r\n(process.parent.args like \"/boot/*\" or process.args like \"/boot/*\") and (\r\n (process.name in (\"file\", \"unlzma\", \"gunzip\", \"unxz\", \"bunzip2\", \"unzstd\", \"unzip\", \"tar\")) or\r\n (process.name == \"grep\" and process.args == \"ELF\") or\r\n (process.name in (\"lzop\", \"lz4\") and process.args in (\"-d\", \"--decode\"))\r\n) and\r\nnot process.parent.name == \"mkinitramfs\"\r\nThe script continues to seek the memory of the Linux kernel image through the tail command. This can be detected,\r\nalong with other memory-seeking tools, through the following query:\r\nprocess where host.os.type == \"linux\" and event.type == \"start\" and event.action == \"exec\" and\r\n(process.parent.args like \"/boot/*\" or process.args like \"/boot/*\") and (\r\n (process.name == \"tail\" and (process.args like \"-c*\" or process.args == \"--bytes\")) or\r\n (process.name == \"cmp\" and process.args == \"-i\") or\r\n (process.name in (\"hexdump\", \"xxd\") and process.args == \"-s\") or\r\n (process.name == \"dd\" and process.args : (\"skip*\", \"seek*\"))\r\n)\r\nOnce /tmp/script.sh is done executing, /memfd:tgt (deleted) and /memfd:wpn (deleted) are created. The\r\ntgt executable, which is the benign Cron executable, creates a /run/crond.pid file. This is nothing malicious but\r\nan artifact that can be detected through a simple query.\r\nfile where host.os.type == \"linux\" and event.type == \"creation\" and file.extension in (\"lock\", \"pid\") and\r\nfile.path like (\"/tmp/*\", \"/var/tmp/*\", \"/run/*\", \"/var/run/*\", \"/var/lock/*\", \"/dev/shm/*\") and process.executable\r\nThe wpn executable will, if all conditions are met, load the LKMrootkit.\r\nStage 3: Rootkit kernel module\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 20 of 24\n\nThe loading of kernel module is detectable through Auditd Manager by applying the following configuration:\r\n-a always,exit -F arch=b64 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules\r\n-a always,exit -F arch=b32 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules\r\nAnd using the following query:\r\ndriver where host.os.type == \"linux\" and event.action == \"loaded-kernel-module\" and auditd.data.syscall in (\"init_m\r\nFor more information on leveraging Auditd with Elastic Security to enhance your Linux detection engineering\r\nexperience, check out our Linux detection engineering with Auditd research published on the Elastic Security Labs\r\nsite.\r\nUpon initialization, the LKM taints the kernel, as it is not signed.\r\naudit: module verification failed: signature and/or required key missing - tainting kernel\r\nWe can detect this behavior through the following KQL query:\r\nhost.os.type:linux and event.dataset:\"system.syslog\" and process.name:kernel and message:\"module verification faile\r\nAlso, the LKM has faulty code, causing it to segfault several times. For example:\r\nDec 9 13:26:10 ubuntu-rk kernel: [14350.711419] cat[112653]: segfault at 8c ip 00007f70d596b63c sp 00007fff9be8136\r\nDec 9 13:26:10 ubuntu-rk kernel: [14350.711422] Code: 83 c4 20 48 89 d0 5b 5d 41 5c c3 48 8d 42 01 48 89 43 08 0f b\r\nThis can be detected through a simple KQL query that queries for segfaults in the kern.log file.\r\nhost.os.type:linux and event.dataset:\"system.syslog\" and process.name:kernel and message:segfault\r\nOnce the kernel module is loaded, we can see traces of command execution through the kthreadd process. The\r\nrootkit creates new kernel threads to execute specific commands. For example, the rootkit executes the following\r\ncommands at short intervals:\r\ncat /dev/null\r\ntruncate -s 0 /usr/share/zov_f/zov_latest\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 21 of 24\n\nWe can detect these and more potentially suspicious commands through a query such as the following:\r\nprocess where host.os.type == \"linux\" and event.type == \"start\" and event.action == \"exec\" and process.parent.name\r\n process.executable like (\"/tmp/*\", \"/var/tmp/*\", \"/dev/shm/*\", \"/var/www/*\", \"/bin/*\", \"/usr/bin/*\", \"/usr/local/\r\n process.name in (\"bash\", \"dash\", \"sh\", \"tcsh\", \"csh\", \"zsh\", \"ksh\", \"fish\", \"whoami\", \"curl\", \"wget\", \"id\", \"nohu\r\n process.command_line like (\r\n \"*/etc/cron*\", \"*/etc/rc.local*\", \"*/dev/tcp/*\", \"*/etc/init.d*\", \"*/etc/update-motd.d*\",\r\n \"*/etc/ld.so*\", \"*/etc/sudoers*\", \"*base64 *\", \"*base32 *\", \"*base16 *\", \"*/etc/profile*\",\r\n \"*/dev/shm/*\", \"*/etc/ssh*\", \"*/home/*/.ssh/*\", \"*/root/.ssh*\" , \"*~/.ssh/*\", \"*autostart*\",\r\n \"*xxd *\", \"*/etc/shadow*\"\r\n )\r\n) and not process.name == \"dpkg\"\r\nWe can also detect the rootkits’ method of elevating privileges by analyzing the rmdir command for unusual\r\nUID/GID changes.\r\nprocess where host.os.type == \"linux\" and event.type == \"change\" and event.action in (\"uid_change\", \"guid_change\")\r\nSeveral other behavioral rules may also trigger, depending on the execution chain.\r\nOne YARA signature to rule them all\r\nElastic Security has created a YARA signature to identify PUMAKIT (the dropper ( cron ), the rootkit\r\nloader( /memfd:wpn ), the LKM rootkit and the Kitsune shared object files. The signature is displayed below:\r\nrule Linux_Trojan_Pumakit {\r\n meta:\r\n author = \"Elastic Security\"\r\n creation_date = \"2024-12-09\"\r\n last_modified = \"2024-12-09\"\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 22 of 24\n\nos = \"Linux\"\r\n arch = \"x86, arm64\"\r\n threat_name = \"Linux.Trojan.Pumakit\"\r\n strings:\r\n $str1 = \"PUMA %s\"\r\n $str2 = \"Kitsune PID %ld\"\r\n $str3 = \"/usr/share/zov_f\"\r\n $str4 = \"zarya\"\r\n $str5 = \".puma-config\"\r\n $str6 = \"ping_interval_s\"\r\n $str7 = \"session_timeout_s\"\r\n $str8 = \"c2_timeout_s\"\r\n $str9 = \"LD_PRELOAD=/lib64/libs.so\"\r\n $str10 = \"kit_so_len\"\r\n $str11 = \"opsecurity1.art\"\r\n $str12 = \"89.23.113.204\"\r\n condition:\r\n 4 of them\r\n}\r\nObservations\r\nThe following observables were discussed in this research.\r\nObservable Type Name Reference\r\n30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f SHA256 cron\r\nPUMAKIT\r\ndropper\r\ncb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe SHA256\r\n/memfd:wpn\r\n(deleted )\r\nPUMAKIT\r\nloader\r\n934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136 SHA256\r\n/memfd:tgt\r\n(deleted)\r\nCron\r\nbinary\r\n8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27 SHA256 libs.so Kitsune\r\nshared\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 23 of 24\n\nObservable Type Name Reference\r\nobject\r\nreference\r\n8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03 SHA256 some2.elf\r\nPUMAKIT\r\nvariant\r\nbbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804 SHA256 some1.so\r\nKitsune\r\nshared\r\nobject\r\nvariant\r\nbc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491 SHA256 puma.ko\r\nLKM\r\nrootkit\r\n1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58 SHA256 kitsune.so Kitsune\r\nsec.opsecurity1[.]art\r\ndomain-namePUMAKIT\r\nC2 Server\r\nrhel.opsecurity1[.]art\r\ndomain-namePUMAKIT\r\nC2 Server\r\n89.23.113[.]204\r\nipv4-\r\naddr\r\nPUMAKIT\r\nC2 Server\r\nConcluding Statement\r\nPUMAKIT is a complex and stealthy threat that uses advanced techniques like syscall hooking, memory-resident\r\nexecution, and unique privilege escalation methods. Its multi-architectural design highlights the growing sophistication\r\nof malware targeting Linux systems.\r\nElastic Security Labs will continue to analyze PUMAKIT, monitor its behavior, and track any updates or new variants.\r\nBy refining detection methods and sharing actionable insights, we aim to keep defenders one step ahead.\r\nSource: https://www.elastic.co/security-labs/declawing-pumakit\r\nhttps://www.elastic.co/security-labs/declawing-pumakit\r\nPage 24 of 24\n\nrmdir: failed Buffer contents to remove '': (hex dump): No such file or directory \n7ffe9ae3a270 00 01 00 00 10 70 69 6e 67 5f 69 6e 74 65 72 76 .....ping_interv\n   Page 13 of 24",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia",
		"MITRE"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://www.elastic.co/security-labs/declawing-pumakit"
	],
	"report_names": [
		"declawing-pumakit"
	],
	"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
		},
		{
			"id": "76d871c3-96cd-41d3-8889-f0396e480e91",
			"created_at": "2023-11-14T02:00:07.093421Z",
			"updated_at": "2026-04-10T02:00:03.449641Z",
			"deleted_at": null,
			"main_name": "Zarya",
			"aliases": [
				"UAC-0109"
			],
			"source_name": "MISPGALAXY:Zarya",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434947,
	"ts_updated_at": 1775826779,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/5b29f671666c9e98a0fb600a486b2ae9dfaf9188.pdf",
		"text": "https://archive.orkl.eu/5b29f671666c9e98a0fb600a486b2ae9dfaf9188.txt",
		"img": "https://archive.orkl.eu/5b29f671666c9e98a0fb600a486b2ae9dfaf9188.jpg"
	}
}