RwyKit

Context

A few months ago we stumbled upon a rootkit: signed, not revoked, with a single detection on VirusTotal. Digging into it, we uncovered a remarkably clever targeted network-traffic redirection system. Here we present a previously unknown kernel implant: RwyKit.

This implant isn't meant to drop a RAT like the ones we're used to seeing. What we have here is a "minimalist" tool, and that is precisely the point: performing as few actions as possible in order to stay as stealthy as possible.

We also identified about ~30 other related samples, however as they all date from 2015 we will not cover them here.

The Dropper

The dropper is a very classic executable, with most of its code taken up by the embedded libcurl, which, as you might expect, it uses to perform HTTP requests. We'll come back to that in a few lines.

After initialization, the malware tries to open c:\windows\system32\Rwymoudle.dat and c:\windows\system32\RwyFeemoudle.dat. These are configuration files, probably written by the attacker beforehand, prior to executing the malware. If they are not found, every value meant to be extracted from them is treated as "0".

A connection is then made to the website tt.51wanyx[.]net. This communication is analyzed in a dedicated section, as the operations performed on this data are fairly numerous despite its modest size.

But the part that interests us most is the loading of a driver. It happens after the configuration has been decoded, and we assume this is why the driver hadn't been studied more closely before: it simply wasn't observed on victims (or was observed too late). Its deployment also involves anti-forensic/anti-EDR actions that are rather rarely encountered. The driver is stored in the binary's SYS resource, in two versions: one targeting Windows XP and the other Windows 7.

Communication with the C2

First of all, from a purely reverse-engineering standpoint, note that the binary embeds libCurl, which accounts for more than 50% of its code size. When executed, the malware tries to reach the web page hxxp://tt.51wanyx[.]net/xstf/allport.asp?RealName=<>&sBarID=<>&BigID=<>&SmallID=<>. The parameters are read directly from c:\windows\system32\Rwymoudle.dat or c:\windows\system32\RwyFeemoudle.dat, and have the following format:

RealName=test123
sBarID=1
BigID=2
SmallID=3

These values are not written during the malware's initialization, which implies a prior action, either by the actor themselves or by another dropper.

In response to this request, a buffer is received. This buffer is the concatenation of a 36-byte XOR key followed by Zlib-compressed data encrypted with that key. We can therefore represent the buffer as follows: [XOR key (36 bytes)][ xor( Zlib(DATA) ) ]

DATA is actually a JSON document made up of several parameters:

  • report_server: Server to which the malware will send its activity reports
  • report_server_port: The port used for this server
  • report_interval_time: How often, in seconds, the report is sent
  • report_max_url: Maximum number of accumulated URLs before a report
  • report_type: Type passed on to the kernel
  • rules: A list (array) of rules, each made of a hostname, a port, a match_string, a response, a status_code and a rate.

This configuration is passed to the driver through its device \\.\rwy_001. To prepare the transmissions to the "notification server", the malware gathers information to identify the right network interface to use (along with its MAC address), as well as the MAC address of the gateway to contact. We can therefore already anticipate that the malware will most likely try to forge packets at very low network layers.

Rootkit installation

Several aspects of the rootkit's deployment are particularly interesting and were not left to chance. First, its installation: the dropper creates the file C:\Windows\usbcore.sys, which is of course a fake USB driver, and in the wrong location to boot, then loads it with CreateServiceA/StartServiceA.

For the driver's removal, however, the author anticipated that deleting a .sys file would look suspicious, so they rename it to c:\windows\s.tmp, and to c:\1.s, before deleting both 1.s and the driver's original name. See for yourself in our emulator's trace:

del_trace

If all of this seems confusing, don't worry, it does to us too, and it reveals several technical problems. Broadly speaking, the developer wanted to change the file name to evade EDR detections of driver deletion. But they renamed the driver to a name that is never used afterwards (so if you find a c:\windows\s.tmp on your machine… that's not a good sign), and on top of that they tried to move it a second time and delete it from 2 paths it doesn't occupy. So they ended up performing the very action they were trying to avoid. The kind of operational mistake that costs dearly.

The driver

The rootkit first tries to determine its execution context. It naturally starts with a KdDisableDebugger() call to disable any kernel debugger that may be present, then uses NtQueryInformationProcess with the ProcessDebugPort class on the current process to determine whether user mode is being debugged. If the environment is not being debugged, it creates a communication device \Device\rwy_001 and sets up its network instrumentation.

NDIS insertion

The driver registers a protocol in the NDIS stack, under the name Tcpip7, so it can receive/send packets at the lowest level of the network. It then enumerates the network interfaces present under HKLM\System\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318} and binds to each one via its Linkage key and Export value. If Export is not found, a fallback is available: the System\CurrentControlSet\Services\Tcpip\Linkage key with the Bind value. The rootkit thus sits alongside the TCP/IP stack.

HTTP traffic processing

Incoming traffic is handled by a kernel thread initialized when the driver loads. For each incoming packet, the driver decodes the Ethernet/IPv4/HTTP headers and decides whether to process it based on a ratio (which may differ from one rule to another), so only a portion of the received requests is taken into account. This ratio is applied via KeQueryPerformanceCounter modulo 100. If the request is selected, it is then compared against the patterns received from user mode, which itself retrieved them from the C2.

If the HTTP packet matches the target, a response is specifically crafted, either of type 200 or 302. The content used for this response was also provided by the server, with the "$" in the response field specifying the name of the data to use.

For a 200 response, the following header is forged:

HTTP/1.1 200 OK\r\n
Server: Nginx/1.2.0\r\n
Content-Type: text/html\r\n
Expires: Thu, 30 Jan 2016 04:18:06 GMT\r\n
Cache-Control: max-age=31536000\r\n
"Content-Length: %d\r\n
"Connection: close\r\n\r\n

And for a 302:

HTTP/1.1 307 Temporary Redirect\r\n
Server: Nginx/1.2.0\r\n
[DEST]
Connection: close\r\n\r\n

Note that for the 302, what is actually sent back is a 307 response ;)

In both cases the page is replaced, and in one case it is redirected. This makes it possible to host malicious content on another server and avoid storing the payload directly on the server.

And on every check, a KdDisableDebugger call is made to ensure that no kernel debugger can remain attached.

Given the configuration, the malware targets servers in order to spoof part of their communications.

An interesting point is that the driver sits on NDIS but does not divert the TCP/IP traffic. As a result, for a hijacked response the victim receives 2 responses, and only the first one is taken into account. That will most likely be the malware's, since it was forged as close as possible to the network layer.

Another interesting point is the presence of a function, at address 0x156d0, that looks up the TCPIP driver and replaces its IAT entry for NdisSendNetBufferLists with an internal function, i.e. IAT hooking. We believe this function is a leftover predating the NDIS hijacking currently in place.

Hypotheses

Based on our analysis, two possible profiles emerge for this malware: mass advertising (probably illegal) and a sophisticated malicious actor.

In both cases, we take into account the effort invested in obfuscating the data, the C2 communication, the driver renaming, checking that the program isn't being debugged, and its self-deletion, all of which leads us to believe the malware did not arrive on the machine legitimately. Furthermore, the attacker went to great lengths to hijack incoming traffic while redirecting only part of it. Since we did not have access to a real exchange with the server, it is just as possible that this is a very aggressive and probably illegal ad-redirection system as it is the work of an attacker seeking to redirect part of the traffic in a targeted and random way to limit detection. An analysis of the other related samples should provide more context.

This case remains a particularly instructive example of implants able to operate for a long time without administrators noticing, all the more so with a signed driver (and not revoked at the time of writing).

IOC

IOC list

IOC type
tt.51wanyx[.]net domain
c:\windows\system32\Rwymoudle.dat path
c:\windows\system32\RwyFeemoudle.dat path
c:\windows\s.tmp path
C:\Windows\usbcore.sys path
rwy_001 device
0642ac2213f7e13e737d1b75bed02c6c0459ec59911a7d9f87876fead294eaff dropper sha256
2b237c9af84831150ea027ee9d35b20a1669c8e515a324fff58b8fd751460f8a rootkit sha256
85a52b92e240700e2f3ab62357f6f565b55de202e90a6032c9b732df92e2d6cf dropper (UPX) sha256

Yara rule

rule RwyKit_driver_01 {
    meta:
        author = "Heurs"
        date =   "2026-10-08"
        update =   "2026-10-08"
        description = "Rootkit HTTP interceptor"
        score =   80
        tlp =  "CLEAR"
        source =  "Exatrack"
        type = "pe"
        sample_hash = "2b237c9af84831150ea027ee9d35b20a1669c8e515a324fff58b8fd751460f8a"

    strings:
        $pdb_str_01 = "E:\\source\\rwy_source\\trunk\\src\\http_redirector\\" ascii
        $pdb_str_02 = "\\rwy_http_win" ascii
        $device_str_01 = "\\Device\\rwy_001" wide fullword
        $info_str_01 = "Pkt Size It is too big!" ascii
        $info_str_02 = "Tcpip7" wide fullword

    condition:
        1 of them
}