{
	"id": "c0812613-fb2d-448d-a8d6-525d460bd8fe",
	"created_at": "2026-04-06T02:11:48.546357Z",
	"updated_at": "2026-04-10T13:11:27.404864Z",
	"deleted_at": null,
	"sha1_hash": "08071076e5b18d2072622fe738c43f0f47d27190",
	"title": "HTTP iframe Injecting Linux Rootkit - crowdstrike.com",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 96484,
	"plain_text": "HTTP iframe Injecting Linux Rootkit - crowdstrike.com\r\nBy George Kurtz\r\nArchived: 2026-04-06 01:35:10 UTC\r\nOn Tuesday, November 13, 2012, a previously unknown Linux rootkit was posted to the Full Disclosure mailing\r\nlist by an anonymous victim. The rootkit was discovered on a web server that added an unknown iframe into any\r\nHTTP response sent by the web server. The victim has recovered the rootkit kernel module file and attached it to\r\nthe mailing list post, asking for any information on this threat. Until today, nobody has replied on this email\r\nthread. CrowdStrike has performed a brief static analysis of the kernel module in question, and these are our\r\nresults. Our results seem to be in line with Kaspersky's findings; they also already added detection.\r\nKey Findings\r\nThe rootkit at hand seems to be the next step in iframe injecting cyber crime operations, driving traffic to\r\nexploit kits. It could also be used in a Waterhole attack to conduct a targeted attack against a specific target\r\naudience without leaving much forensic trail.\r\nIt appears that this is not a modification of a publicly available rootkit. It seems that this is contract work of\r\nan intermediate programmer with no extensive kernel experience.\r\nBased on the Tools, Techniques, and Procedures employed and some background information we cannot\r\npublicly disclose, a Russia-based attacker is likely.\r\nFunctional Overview\r\nThe kernel module in question has been compiled for a kernel with the version string 2.6.32-5 . The -5 suffix\r\nis indicative of a distribution-specific kernel release. Indeed, a quick Google search reveals that the latest Debian\r\nsqueeze kernel has the version number 2.6.32-5 . The module furthermore exports symbol names for all\r\nfunctions and global variables found in the module, apparently not declaring any private symbol as static in the\r\nsources. In consequence, some dead code is left within the module: the linker can't determine whether any other\r\nkernel module might want to access any of those dead-but-public functions, and subsequently it can't remove\r\nthem. The module performs 6 different tasks during start-up:\r\n1. Resolution of a series of private kernel symbols using a present System.map file or the kernel's run-time\r\nexport of all private symbols through /proc/kallsyms\r\n2. Initialization of the process and file hiding components using both inline hooks and direct kernel object\r\nmanipulation\r\n3. Creating an initial HTTP injection configuration and installing the inline function hook to hijack TCP\r\nconnection contents\r\n4. Starting a thread responsible for updating the injection configuration from a command and control server\r\n(hereafter \"C2\")\r\n5. Ensuring persistence of the rootkit by making sure the kernel module is loaded at system startup\r\n6. Hiding the kernel module itself using direct kernel object manipulation\r\nhttps://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/\r\nPage 1 of 8\n\nThe remainder of this blog post describes those tasks and the components they initialize in detail.\r\nGhetto Private Symbol Resolution\r\nThe rootkit hijacks multiple private kernel functions and global variables that don't have public and exported\r\nsymbols. To obtain the private addresses of these symbols, the rootkit contains code to scan files containing a list\r\nof addresses and private symbols. Those System.map called files are usually installed together with a kernel\r\nimage in most Linux distributions. Alternatively, the kernel exports a pseudo-file with the same syntax via procfs\r\nat /proc/kallsyms to userland.\r\nThe code contains the function search_export_var that receives one parameter: the symbol name to resolve.\r\nThis function merely wraps around the sub-function search_method_export_var that receives an integer\r\nparameter describing the method to use for symbol resolution and the symbol name. It first attempts method 0\r\nand then method 1 if the previous attempt failed. search_method_export_var then is a simple mapping of 1\r\nto search_method_exec_command or 2 to search_method_find_in_file . Any other method input will fail. The\r\nattentive reader will notice that therefore the rootkit will always attempt to resolve symbols using\r\nsearch_method_exec_command , because method 0 is not understood by search_method_export_var and 2 is\r\nnever supplied as input. search_method_exec_command uses the pseudo-file /proc/kallsyms to retrieve a list of\r\nall symbols. Instead of accessing these symbols directly, it creates a usermode helper process with the command\r\nline \"/bin/bash\", \"-c\", \"cat /proc/kallsyms \u003e /.kallsyms_tmp\" to dump the symbol list into a temporary\r\nfile in the root directory. It then uses a function shared with search_method_find_in_file to parse this text\r\nrepresentation of addresses and symbols for the desired symbol. Due to the layout of the call graph, this will\r\nhappen for every symbol to be resolved.\r\nThe alternative (but effectively dead) function search_method_find_in_file is, unfortunately, as ugly. Despite\r\nthe fact that the System.map file is a regular file that could be read without executing a usermode helper process,\r\nthe author found an ingenious way to use one anyway. Since multiple kernels might be installed on the same\r\nsystem, the System.map file(s) (generated at kernel build time) include the kernel version as a suffix. Instead of\r\nusing a kernel API to determine the currently running kernel version, the rootkit starts another usermode helper\r\nprocess executing \"/bin/bash\", \"-c\", \"uname -r \u003e /.kernel_version_tmp\" . uname is a userland helper\r\nprogram that displays descriptive kernel and system information. So instead of using the kernel version this\r\nmodule is built for at build time (it's hardcoded in other places, as we'll see later), or at least just calling the same\r\nsystem call that uname uses to obtain the kernel version, they start a userland program and redirect its output into\r\na temporary file. The kernel version obtained in this way is then appended to the System.map filename so that the\r\ncorrect file can be opened. Recall that this code path is never taken due to a mistake at another place, though.\r\nWhen starting up, the rootkit first iterates over a 13-element array of fixed-length, 0-padded symbol names and\r\nresolves them using the previously described functions. The name of the symbol and its address are then inserted\r\ninto a linked list. Once a symbol's address needs to be used, the code iterates over this linked list, searching for the\r\nright symbol and returning its address.\r\nBerserk Inline Code Hooking\r\nTo hook private functions that are called without indirection (e.g., through a function pointer), the rootkit employs\r\ninline code hooking. In order to hook a function, the rootkit simply overwrites the start of the function with an\r\nhttps://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/\r\nPage 2 of 8\n\ne9 byte. This is the opcode for a jmp rel32 instruction, which, as its only operand, has 4 bytes relative offset\r\nto jump to. The rootkit, however, calculates an 8-byte or 64-bit offset in a stack buffer and then copies 19 bytes (8\r\nbytes offset, 11 bytes uninitialized) behind the e9 opcode into the target function. By pure chance the jump still\r\nworks, because amd64 is a little endian architecture, so the high extra 4 bytes offset are simply ignored. To\r\nfacilitate proper unhooking at unload time, the rootkit saves the original 5 bytes of function start (note that this\r\nwould be the correct jmp rel32 instruction length) into a linked list. However, since in total 19 bytes have been\r\noverwritten, unloading can't work properly:\r\n.text:000000000000A32E\r\n xor\r\n eax, eax\r\n.text:000000000000A330\r\n mov\r\n ecx, 0Ch\r\n.text:000000000000A335\r\n mov\r\n rdi, rbx\r\n.text:000000000000A338\r\n rep stosd\r\n.text:000000000000A33A\r\n mov\r\n rsi, rbp\r\n.text:000000000000A33D\r\n lea\r\n rdi,\r\n.text:000000000000A341\r\n lea\r\n rdx,\r\n.text:000000000000A345\r\n mov\r\n cl, 5\r\n.text:000000000000A347\r\n rep movsd\r\n.text:000000000000A349\r\n mov\r\n , rbp\r\n.text:000000000000A34C\r\n mov\r\n esi, 14h\r\n.text:000000000000A351\r\n mov\r\n rdi, rbp\r\n.text:000000000000A354\r\n mov\r\n rax, cs:splice_func_list\r\n.text:000000000000A35B\r\nhttps://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/\r\nPage 3 of 8\n\nmov\r\n , rdx\r\n.text:000000000000A35F\r\n mov\r\n , rax\r\n.text:000000000000A363\r\n mov\r\n qword ptr , offset splice_func_list\r\n.text:000000000000A36B\r\n mov\r\n cs:splice_func_list, rdx\r\n.text:000000000000A372\r\n call\r\nset_addr_rw_range\r\n.text:000000000000A377\r\n lea\r\n rax,\r\n.text:000000000000A37B\r\n mov\r\n byte ptr , 0E9h\r\n.text:000000000000A37F\r\n lea\r\nrsi,\r\n.text:000000000000A384\r\n mov\r\n ecx, 19\r\n.text:000000000000A389mov\r\n rdi, rax\r\n.text:000000000000A38C\r\n rep movsb\r\n.text:000000000000A38E\r\n mov\r\n rdi, rax\r\nTo support read-only mapped code, the rootkit contains page-table manipulation code. Since the rootkit holds the\r\nglobal kernel lock while installing an inline hook, it could simply have abused the write-protect-enable-bit in cr0\r\nfor the sake of simplicity, though. Since the rootkit trashes the hooked function beyond repair and is not\r\nconsidering instruction boundaries, it can never call the original function again (a feature that most inline hooking\r\nengines normally posses). Instead, the hooked functions have all been duplicated (one function even twice) in the\r\nsourcecode of the rootkit.\r\nFile and Would-be Process Hiding\r\nUnlike many other rootkits, this rootkit has a rather involved logic for hiding files. Most public Linux rootkits\r\ndefine a static secret and hide all files and directories, where this secret is part of the full file or directory name.\r\nThis rootkit maintains a linked list of file or directory names to hide, and it hides them only if the containing\r\nhttps://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/\r\nPage 4 of 8\n\ndirectory is called \".html\" or \"sound\" (the parent directory of temporary files and the module file,\r\nrespectively). The actual hiding is done by inline hooking the vfs_readdir function that's called for enumerating\r\ndirectory contents. The replacement of that function checks if the enumerated directory's name is either \".html\"\r\nor \"sound\" as explained above. If that's the case, the function provides an alternative function pointer to the\r\nnormally used filldir or filldir64 functions. This alternative implementation checks the linked list of file\r\nnames to hide and will remove the entry if it matches. Interestingly, it will also check a linked list of process\r\nnames to hide, and it will hide the entry if it matches, too. That, however, doesn't make sense, since the actual\r\ndirectory name to hide would be the process id. Also, the parent directory for that would be \"/proc\" , which isn't\r\none of the parent directories filtered. Therefore, the process hiding doesn't work at all:\r\nThe list of hidden files is:\r\nsysctl.conf\r\nmodule_init.ko (the actual rootkit filename)\r\nzzzzzz_write_command_in_file\r\nzzzzzz_command_http_inject_for_module_init\r\nThe real module's name gets added to the linked list of file names to hide by the module hiding code. Interestingly,\r\nthe rootkit also contains a list of parent path names to hide files within. However, this list isn't used by the code:\r\n/usr/local/hide/first_hide_file\r\n/ah34df94987sdfgDR6JH51J9a9rh191jq97811\r\nSince only directory listing entries are being hidden but access to those files is not intercepted, it's still possible to\r\naccess the files when an absolute path is specified.\r\nCommand and Control Client\r\nAs part of module initialization, the rootkit starts a thread that connects to a single C2 server. The IP address in\r\nquestion is part of a range registered to Hetzner, a big German root server and co-location provider.\r\nThe rootkit uses the public ksocket library to establish TCP connections directly from the Linux kernel. After the\r\nconnection has been successfully initiated, the rootkit speaks a simple custom protocol with the server. This very\r\nsimple protocol consists of a 1224-byte blob sent by the rootkit to the server as an authentication secret. The blob\r\nis generated from \"encrypting\" 1224 null bytes with a 128-byte static password, the C2 address it's talking to, and,\r\ninterestingly, an IP address registered to Zattoo Networks in Zurich, Switzerland, that is not otherwise used\r\nthroughout the code.\r\nThe server is then expected to respond with the information about whether an iframe or a JavaScript snippet\r\nshould be injected, together with the code to be injected. The server's response must contain a similarly generated\r\nauthentication secret for the response to be accepted. If this check passes, the rootkit then copies the injection\r\ninformation into a global variable. This protocol is obviously vulnerable to simply generating the secret blob once\r\nusing dynamic analysis and replaying it, and therefore it merely serves for a little obfuscation. We didn't invest\r\nfurther time investigating this specific \"encryption\" algorithm.\r\nTCP Connection Hijacking\r\nhttps://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/\r\nPage 5 of 8\n\nIn order to actually inject the iframes (or JavaScript code references) into the HTTP traffic, the rootkit inline\r\nhooks the tcp_sendmsg function. This function receives one or multiple buffers to be sent out to the target and\r\nappends them to a connections outgoing buffer. The TCP code will then later retrieve data from that buffer and\r\nencapsulate it in a TCP packet for transmission. The replacement function is largely a reproduction of the original\r\nfunction included in the kernel sources due to the inline hooking insufficiencies explained above. A single call to\r\nthe function formation_new_tcp_msg was added near the head of the original function; if this function returns\r\none, the remainder of the original function is skipped and internally a replacement message is sent instead. This\r\nfunction always considers only the first send buffer passed, and we'll implicitly exclude all further send buffers\r\npassed to a potential sendmsg call in the following analysis. The formation_new_tcp_msg function invokes a\r\ndecision function that contains 4 tests, determining whether injection on the message should be attempted at all:\r\n1. An integer at +0x2f0 into the current configuration is incremented. Only if its value modulo the integer at\r\n+0x2e8 in the current configuration is equal to zero, this test passes. This ensures that only on every n-th\r\nsend buffer an injection is attempted.\r\n2. Ensure that the size of all the send buffers to be sent is below or equal to 19879 bytes.\r\n3. Verify that originating port (server port for server connections) is :80.\r\n4. Ensure that the destination of this send is not 127.0.0.1.\r\n5. Make sure that none of the following three strings appears anywhere in the send buffer:\r\n\"403 Forbidden\"\r\n\"304 Not Modified\"\r\n\" was not found on this server.\"\r\n6. Make sure the destination of this send is not in a list of 1708 blacklisted IP addresses, supposedly\r\nbelonging to search engines per the symbol name search_engines_ip_array .\r\nThere are several shortcomings in the design of these tests that ultimately led to the discovery of this rootkit as\r\ndocumented in the Full Disclosure post. Since the check to only attempt an inject once every n-th send buffer is\r\nnot performed per every m-th connection and before all other tests, it will trigger on more valid requests than one\r\nmight expect when defining the modulus. Also, doing a negative check on a few selected error messages instead\r\nof checking for a positive \"200\" HTTP status led to the discovery, when an inject in a \"400\" HTTP error response\r\nwas found. The rootkit then tries to parse a HTTP header being sent out by looking for the static header strings\r\n\"Content-Type\", \"Content-Encoding\", \"Transfer-Encoding\" and \"Server\". It matches each of the values of these\r\nheaders against a list of known values, e.g., for Content-Type:\r\ntext/html\r\ntext/css\r\napplication/x-javascript\r\nThe Content-Type of the response and the attacker specified Content-Type of the inject have to match for injection\r\nto continue. The code then searches for an attacker-specified substring in the message and inserts the inject after it.\r\nWhat is notable is the support for both chunked Transfer-Encoding and gzip Content-Encoding. The chunked\r\nencoding handling is limited to handling the first chunk sent because the HTTP headers parsed need to present in\r\nthe same send buffer. However, it will adjust the length of the changed chunk correctly. When encountering a\r\ngzip Content-Encoding, the rootkit will use the zlib kernel module to decompress the response, potentially patch\r\nhttps://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/\r\nPage 6 of 8\n\nit with the inject, and then recompress it. While this is a technically clever way to make sure your inject ends up in\r\neven compressed responses, it will potentially severely degrade the performance of your server.\r\nReboot Persistence\r\nAfter running most of the other initialization tasks, the rootkit creates a kernel thread that continuously tries to\r\nmodify /etc/rc.local to load the module at start-up. The code first tries to open the file and read it all into\r\nmemory. Then it searches for the loading command in the existing file.\r\n\u003eIf it's not found, it appends the loading command \"insmod /lib/modules/2.6.32-5-\r\namd64/kernel/sound/module_init.ko\" by concatenating the \"insmode\" command with the directory path and\r\nfilename. However, all those 3 parts are hardcoded (remember that the kernel version now hardcoded was\r\ndetermined dynamically for symbol resolution earlier?). If opening the file fails, the thread will wait for 5 seconds.\r\nAfter successfully appending the new command, the thread will wait for 3 minutes before checking for the\r\ncommand and potentially re-adding it again. Additionally, the rootkit installs an inline hook for the vfs_read\r\nfunction. If the read buffer (no matter which file it is being read from) contains the fully concatenated load\r\ncommand, the load command is removed from the read buffer by copying the remainder of the buffer over it and\r\nadjusting the read size accordingly. Thereby, the load command is hidden from system administrators if the rootkit\r\nis loaded.\r\nThe screenshot above showcases a problem already with this technique of persistence: since the command is\r\nappended to the end of rc.local, there might actually be shell commands that result in the command not being\r\nexecuted as intended. On a default Debian squeeze install, /etc/rc.local ends in an exit 0 command, so that\r\nthe rootkit is effectively never loaded.\r\nModule Hiding\r\nHiding itself is achieved by simple direct kernel object manipulation. The rootkit iterates about the kernel linked\r\nlist modules and removes itself from the list using list_del . In consequence, the module will never be\r\nunloaded and there will be no need to remove the inline hooks installed earlier. In fact, the\r\nremove_splice_func_in_memory function is unreferenced dead code.\r\nConclusion\r\nConsidering that this rootkit was used to non-selectively inject iframes into nginx webserver responses, it seems\r\nlikely that this rootkit is part of a generic cyber crime operation and not a targeted attack. However, a Waterhole\r\nattack, where a site mostly visited from a certain target audience is infected, would also be plausible. Since no\r\nidentifying strings yielded results in an Internet search (except for the ksocket library), it appears that this is not a\r\nmodification of a publicly available rootkit. Rather, it seems that this is contract work of an intermediate\r\nprogrammer with no extensive kernel experience, later customized beyond repair by the buyer.\r\nAlthough the code quality would be unsatisfying for a serious targeted attack, it is interesting to see the cyber-crime-oriented developers, who have partially shown great skill at developing Windows rootkits, move into the\r\nLinux rootkit direction. The lack of any obfuscation and proper HTTP response parsing, which ultimately also led\r\nto discovery of this rootkit, is a further indicator that this is not part of a sophisticated, targeted attack.\r\nhttps://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/\r\nPage 7 of 8\n\nBased on the Tools, Techniques, and Procedures employed and some background information we cannot publicly\r\ndisclose, a Russia-based attacker is likely. It remains an open question regarding how the attackers have gained the\r\nroot privileges to install the rootkit. However, considering the code quality, a custom privilege escalation exploit\r\nseems very unlikely.\r\nSource: https://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/\r\nhttps://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/\r\nPage 8 of 8",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/"
	],
	"report_names": [
		"http-iframe-injecting-linux-rootkit"
	],
	"threat_actors": [],
	"ts_created_at": 1775441508,
	"ts_updated_at": 1775826687,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/08071076e5b18d2072622fe738c43f0f47d27190.pdf",
		"text": "https://archive.orkl.eu/08071076e5b18d2072622fe738c43f0f47d27190.txt",
		"img": "https://archive.orkl.eu/08071076e5b18d2072622fe738c43f0f47d27190.jpg"
	}
}