[00:00.000 --> 00:04.160] Hi there, my name is Bill and thanks for coming out to my talk. [00:05.120 --> 00:11.340] So, I'm 19 years old. I'm a rising sophomore at the Rochester Institute of Technology. [00:11.340 --> 00:17.060] I love Windows internals. I'm mostly self-taught with some guidance and I have a strong game [00:17.060 --> 00:22.640] hacking background, which is why I'm so interested in the Windows kernel. So, what is this talk [00:22.640 --> 00:29.980] about? Well, in this talk we'll be going over loading a rootkit, communicating with a rootkit, [00:30.660 --> 00:35.110] abusing legitimate network communications in order to communicate with our malware, [00:35.820 --> 00:42.020] an example rootkit I made and the design choices behind it, executing commands from kernel, [00:42.020 --> 00:46.000] and tricks to cover up the file system trace of your rootkit. [00:46.900 --> 00:52.120] So, when you hear me say rootkit, I'm going to be referring to kernel level rootkits for Windows. [00:52.560 --> 00:56.560] So, why would you want to use a rootkit? Well, there's a lot of reasons. [00:56.560 --> 01:01.360] Kernel drivers have significant access to the machine. Unlike in user mode, [01:01.360 --> 01:05.280] you pretty much have the same, you have the access to anything and everything. [01:06.220 --> 01:10.580] Kernel drivers have the same privilege level as a typical kernel antivirus. [01:10.600 --> 01:14.720] Generally speaking, you share the same access to the same resources. [01:15.300 --> 01:19.540] There are less mitigations and security solutions targeting kernel malware. [01:19.540 --> 01:23.820] If you can load kernel code, there's a lot you can do to cover up your tracks. [01:24.700 --> 01:29.420] Antivirus often have less visibility into the operations performed by kernel drivers. [01:29.860 --> 01:32.820] This is because a lot of antivirus rely on user mode hooks [01:32.820 --> 01:37.380] in order to gain visibility into potentially suspicious operations. [01:37.380 --> 01:42.800] In kernel, however, you can't directly hook the NTOS kernel functions due to patch guard, [01:42.800 --> 01:46.420] reducing the visibility antivirus has significantly. [01:46.980 --> 01:52.620] And finally, kernel drivers are treated with a significant amount of trust by antivirus. [01:52.620 --> 01:54.880] Let's take a look at an example of that. [01:55.820 --> 02:00.160] So whether you're running a consumer antivirus or corporate EDR, [02:00.160 --> 02:05.480] chances are whatever solution you run treats kernel drivers with a significant amount of trust. [02:05.500 --> 02:08.820] Here I have two examples from Malwarebytes and Carbon Black, [02:08.820 --> 02:12.680] specifically their pre-processed thread callback functions. [02:12.740 --> 02:17.780] These functions are called whenever a handle is created to a process or a thread. [02:17.780 --> 02:22.280] In the case of Malwarebytes, they check to see if the process ID is less than 8, [02:22.280 --> 02:27.740] and if the operation is for a kernel handle, then it will not continue processing that handle [02:27.740 --> 02:33.660] creation. In the case of Carbon Black, if your handle is for a kernel handle, [02:33.660 --> 02:39.180] or if the caller is not from user mode, it will not process that handle being created. [02:40.060 --> 02:43.040] Let's talk a little bit about loading a rootkit. [02:43.500 --> 02:46.680] So the first option you have is to abuse legitimate drivers. [02:47.020 --> 02:51.800] There are a lot of publicly available vulnerable drivers out there. [02:51.800 --> 02:56.800] And with some reversing knowledge, finding your own zero day in one of these drivers can be trivial. [02:57.580 --> 03:03.320] Examples of vulnerable drivers include Capcom's anti-cheat driver, Intel's NAL driver, [03:03.320 --> 03:08.660] and even Microsoft themselves. Now the reason I put vulnerable and zero day in quotes [03:08.660 --> 03:13.980] is because oftentimes these drivers require administrative privileges to communicate with [03:13.980 --> 03:20.840] them. Following Microsoft standards, Ring 3 administrator to Ring 0 is not a security boundary, [03:20.840 --> 03:26.040] so technically they're not vulnerabilities, but that doesn't mean we can't abuse them. [03:27.400 --> 03:31.160] Abusing legitimate drivers has quite a few benefits as well. [03:31.160 --> 03:36.240] You only need a few primitives to escalate privileges. Finding a vulnerable driver is [03:36.240 --> 03:41.560] relatively trivial. A good place to start is OEM drivers. And it's difficult to detect [03:41.560 --> 03:46.760] due to compatibility reasons. For example, let's say a driver had a vulnerable component [03:46.760 --> 03:52.880] that could potentially be exploited. The problem antivirus face is that if the legitimate application [03:52.880 --> 03:59.340] also uses that vulnerable component, how do you detect a malicious program requesting [04:00.360 --> 04:06.080] this vulnerable component versus the legitimate application requesting that vulnerable component? [04:06.080 --> 04:12.060] It becomes a tricky problem. There are some drawbacks for this method as well though. [04:12.060 --> 04:17.640] The biggest issue I've had with this method is compatibility. Oftentimes the primitives you get [04:17.640 --> 04:23.900] might not be a lot and even then can have stability issues such as race conditions. [04:23.900 --> 04:29.780] Now this is a significant threat if you're writing red team tooling because you do not want your [04:29.780 --> 04:34.300] malware to blue screen a victim. That's probably the worst case scenario. It would be a pretty big [04:34.300 --> 04:39.420] indicator of compromise and you're probably going to get caught. This is why I generally stay away [04:39.420 --> 04:47.340] from this method. Now another method is to just buy a code signing certificate. This is completely [04:47.340 --> 04:53.140] valid for some red teams especially for targeted attacks and primarily because you're not going to [04:53.140 --> 04:57.940] have any stability issues. It's the normal way of getting a driver signed is just to buy your own [04:57.940 --> 05:05.300] certificate under your company's name. But it will reveal your identity and also it can be blacklisted. [05:05.300 --> 05:10.460] Now this blacklisting doesn't happen so much nowadays. This is something that I know is being [05:10.460 --> 05:16.740] worked on by antivirus vendors and even Microsoft themselves. It's not widely spread yet so [05:16.740 --> 05:23.160] you're probably not going to see too much blacklisting happening nowadays. Another option is just to use [05:23.160 --> 05:27.920] someone else's certificate. You'd be surprised at the number of publicly available leaked certificates [05:27.920 --> 05:34.020] online. A good place to start if you're looking for one is game hacking forums. Now leaked [05:34.020 --> 05:38.840] certificates have almost all of the benefits of just buying your own certificate without the [05:39.660 --> 05:46.740] anonymization. But the leaked certificate you use can be blacklisted in the future. It goes back [05:46.740 --> 05:53.000] to the same point about buying your own certificate. And if the certificate was issued after July 29th [05:53.000 --> 06:00.780] 2015, you can't use it on secure boot machines running Windows 10 1607 or higher [06:01.640 --> 06:05.820] unless it is an EV certificate, which is probably not going to happen. [06:07.120 --> 06:13.500] In most cases, Windows doesn't even care if the certificate appears to be expired or even revoked. [06:13.560 --> 06:18.680] This is because what you see in the digital signature section of a driver is not what the [06:18.680 --> 06:23.980] kernel checks when determining whether or not to load a driver. So this view you see here [06:23.980 --> 06:29.760] when it says a certificate was explicitly revoked, that's what WinVerify Truster is returning, not [06:29.760 --> 06:35.040] necessarily what the kernel cares about. So if you do come across a certificate that's expired [06:35.040 --> 06:41.840] or revoked, chances are you can still use it for signing a driver. Now if you don't want to use one [06:41.840 --> 06:46.540] of the publicly available certificates out there, you can also try to find your own. One method I [06:46.540 --> 06:52.300] found interesting was to use OpenS3Buckets to search for private keys. I used a site called [06:52.300 --> 06:57.860] GreyhatWarfare which searches these OpenS3Buckets and I was able to find over 6,000 results [06:58.560 --> 07:04.600] for the pfx and p12 extensions, which is definitely a place to look at if you're trying to get your own [07:04.600 --> 07:11.540] certificate. And the best part about this method is that it's undetected by the bulk of antivirus. [07:11.540 --> 07:16.320] Now I'd understand if antivirus had trouble detecting a certificate that was released [07:16.780 --> 07:21.280] a month ago because that's recent, but some of these certificates have been out there for years [07:21.280 --> 07:27.840] and yet even the most next generation cutting-edge solutions fail to detect this basic threat. [07:28.920 --> 07:34.240] Let's talk about communicating with a rootkit. A tried and true method of communicating with [07:34.240 --> 07:40.040] your malware is to just call home to a C2. Now firewalls can block or flag these requests [07:40.040 --> 07:48.640] that go to suspicious IPs or ports, and even for more complex methods such as DNS exfiltration, [07:48.640 --> 07:52.300] there are some solutions being developed such as advanced network inspection [07:52.300 --> 07:56.820] that can try to catch some of these methods that attempt to blend in with the noise. [07:59.180 --> 08:05.620] Some malware takes the route that the C2 connects to the victim directly to control it. Now this is [08:05.620 --> 08:12.240] extremely easy to set up, but at the same time it's extremely easy for a firewall to block this, [08:12.240 --> 08:17.380] and it's very difficult to blend in with the noise given that you're using one port exclusively. [08:18.760 --> 08:24.900] A more advanced method I've seen is to hook into an application's network communications directly. [08:24.900 --> 08:30.140] Now this is very hard to detect, especially if you're mimicking a legitimate protocol, [08:30.140 --> 08:35.260] but it's not very flexible because in a lot of environments you're going to have different [08:35.260 --> 08:39.960] ports exposed, different services running, and so if that one port or service isn't exposed, [08:39.960 --> 08:43.560] you can't use that line of communication, which isn't very reliable. [08:44.960 --> 08:51.540] So what I wanted when choosing a communication method was limited detection vectors, [08:51.540 --> 08:58.200] flexibility for a variety of environments, while making the assumptions that some services will be [08:58.200 --> 09:03.380] exposed, which is especially true in corporate environments that have active domain services [09:03.380 --> 09:08.780] running. But inbound and outbound access may be monitored, so it's going to have to be a method [09:08.780 --> 09:17.520] that has a low detection vector opportunity. Now application-specific hooking was perfect for my [09:17.520 --> 09:23.520] needs, except for the flexibility. Is there any way we could change application-specific hooking [09:23.520 --> 09:29.600] to where it isn't dependent on any single application? Well, it turns out there might [09:29.600 --> 09:35.960] be a way. So what if instead of hooking one application directly, we hooked network communication [09:35.960 --> 09:41.720] similar to tools such as Wireshark? So this means that we would hook every packet that reaches any [09:41.720 --> 09:48.980] port on the system, and we're able to inspect it. And then what we did is on our C2, we created a [09:48.980 --> 09:55.540] custom packet that had a magic constant value. And right after that magic constant, there was [09:55.540 --> 09:59.860] information we want to pass to our malware. So what we could do is we could send this malicious [09:59.860 --> 10:06.200] packet to any port on the victim machine, and then the victim machine running our malware would check [10:06.200 --> 10:12.180] every packet incoming and see, hey there's a packet with a magic constant, and we'll be able to use [10:12.180 --> 10:18.680] it as a reference point to extract information from our C2. So using this method, we could [10:18.680 --> 10:25.880] communicate from our C2 to our malware by abusing any legitimate port on the victim machine. Let's [10:25.880 --> 10:30.810] talk about how we would actually do this, and specifically hooking to user mode network stack. [10:30.810 --> 10:37.550] So a significant amount of services on Windows rely in user mode. And how can we [10:38.060 --> 10:44.930] globally intercept this traffic? Well, networking relating to Winsock is handled by afd.sys, [10:44.930 --> 10:51.270] otherwise known as the ancillary function driver for Winsock. Reversing a few functions inside of [10:51.270 --> 10:56.860] mswsock.dll revealed that a bulk of the communication was performed through ioctals. [10:56.860 --> 11:02.780] If we could somehow intercept these ioctals, we could snoop in on the data being received. [11:03.200 --> 11:10.140] So how do ERPs know where to go? So how does a kernel determine what function to call of what [11:10.140 --> 11:16.600] driver? Well, first it'll obtain the device object associated with the file object by calling [11:16.600 --> 11:23.160] ioget related device object. Now for our purposes, this will just be retrieving the device object [11:23.160 --> 11:29.420] member of the file object structure. If the driver associated with the device supports FastIO, [11:29.420 --> 11:34.800] it'll dispatch the request using the FastIO dispatch table, part of the driver object [11:34.800 --> 11:41.640] structure. If the driver does not support FastIO, then it'll allocate and fill out an ERP and [11:41.640 --> 11:48.800] forward that ERP to the driver by calling iocalldriver. Now there's a few standard methods [11:48.800 --> 11:57.420] of intercepting ERPs, and the first method is to replace the major functions table or array [11:57.420 --> 12:04.060] that is part of the driver object structure. Now this major function array contains pointers to [12:04.060 --> 12:10.720] dispatch functions, and the index for this array directly corresponds to the major function code, [12:11.320 --> 12:16.920] which is relevant for that dispatch function. So for example, what we can do is if we wanted to hook [12:17.400 --> 12:22.800] a certain major function code, we could replace that index in the array with a pointer to our [12:22.800 --> 12:29.680] own function, which would redirect ERPs for that major function to our driver instead. Another [12:29.680 --> 12:36.920] option is just to perform a code hook directly on the dispatch function itself. So when picking a [12:36.920 --> 12:41.340] method, there's a few questions you want to ask yourself. How many detection vectors am I exposed [12:41.340 --> 12:47.880] to? How usable is the method, both from a compatibility and stability perspective? And [12:47.880 --> 12:54.780] how expensive would it be to detect the method? Well, for hooking a driver object in memory, [12:54.780 --> 12:59.860] you're going to be exposing yourself to memory artifacts. And from a usability perspective, [12:59.860 --> 13:04.360] you're going to be quite stable, mostly because driver objects are well documented. [13:05.100 --> 13:09.160] And from a detectionist perspective, though, it wouldn't be crazy difficult for [13:10.120 --> 13:15.080] antivirus to detect because there's only going to be a handful of driver objects out there because [13:15.080 --> 13:20.940] there's only a handful of drivers loaded at once. And all an antivirus would need to do is enumerate [13:20.940 --> 13:26.660] these driver objects and check the major functions array for signs that the dispatch function is [13:26.660 --> 13:34.240] outside of the driver's bounds. For hooking a driver's dispatch function directly with a code [13:34.240 --> 13:39.160] hook, you're going to be exposing yourself to memory artifacts. But unless the function is [13:39.160 --> 13:43.940] exported, you're going to need to find it yourself. And there's some a couple of ways to do this, [13:43.940 --> 13:48.840] but it's something to especially consider if the underlying driver file might change between [13:48.840 --> 13:54.800] Windows operating system versions, which aft.sys does. And also, not all drivers are going to be [13:54.800 --> 14:00.560] compatible with this method due to patch guard. And this is also HVCI incompatible. [14:00.560 --> 14:06.580] Now, how expensive would it be to detect? Well, that can vary. So there's quite simple ways of [14:06.580 --> 14:11.320] detecting hooking that are varying inexpensive. There's other methods that can get pretty [14:11.320 --> 14:17.200] expensive. Now, one inexpensive method could be if they know that you're hooking this one function [14:17.200 --> 14:22.480] in this one driver, they could check the bytes of that driver function to check for tampering. [14:22.480 --> 14:30.540] That's pretty inexpensive. But another method is if the antivirus can enumerate every driver loaded, [14:30.560 --> 14:37.260] and specifically, the executable sections of that driver module to check for differences between [14:37.260 --> 14:46.920] what's on disk and what is on the actual what's in memory. Now, I wanted a method that was [14:46.920 --> 14:54.700] undocumented, stable, yet relatively expensive to detect. So what if instead of hooking the [14:54.700 --> 15:01.420] original driver object, we hook the file object structure instead? Well, if you recall to one of [15:01.420 --> 15:07.720] our previous slides, the way that the kernel determines what device is associated with a [15:07.720 --> 15:14.180] file object is by calling IO get related device object. And for our purposes, this is the device [15:14.180 --> 15:20.020] object member of the file object structure. Now, what is stopping us from overriding this [15:20.020 --> 15:25.200] device object pointer inside of the file object with our own device? Well, it turns out absolutely [15:25.200 --> 15:32.600] nothing. So what we can do is we can create our own driver and device objects, patch our copy of [15:32.600 --> 15:38.580] the driver object using a common method such as replacing the major function table. And then we [15:38.580 --> 15:44.680] can replace the device object pointer inside of the file object with our own device. So let's [15:44.680 --> 15:50.200] talk about how we would do this. Well, first, we need to find file objects that are to the device [15:50.200 --> 15:58.160] AFD device object. But how can we actually find these objects? Well, the Windows NTOS kernel [15:58.160 --> 16:03.320] exposes this great function called ZW query system information, which allows you to query a lot of [16:03.320 --> 16:08.060] different information about the system. And one of the classes you can query for is called system [16:08.060 --> 16:13.940] handle information, which allows us to enumerate every handle opened on the system, including the [16:13.940 --> 16:19.860] process ID that the handle is associated with, and the pointer to the kernel object associated with [16:19.860 --> 16:26.300] that handle. Now, if we open the AFD device ourselves, we can easily determine if a file [16:26.300 --> 16:31.740] object is for the AFD device by comparing the device object member with the previously opened [16:31.740 --> 16:38.500] AFD device. So then we can see, okay, this file object is associated with the AFD device. Now, [16:38.500 --> 16:44.320] before we can overwrite the device object member of the file object, we need to do some preparation [16:44.320 --> 16:50.280] first. And specifically, we need to create our fake objects. Fortunately, the kernel exports [16:50.280 --> 16:56.920] the function we can use to create our own objects. We can call obcreate object passing in an IO [16:56.920 --> 17:02.620] driver object type and IO device object type respectively, and then copy over the existing [17:02.620 --> 17:09.200] object data using a function like mem copy. Now with our fake objects created, we're almost ready [17:09.200 --> 17:15.360] to set the device object of the file object. But first, we need to hook our driver object. And [17:15.360 --> 17:20.720] the way we can do this is by using the standard major function hook method. Except remember, [17:20.720 --> 17:25.640] we're performing this on our own copy of the driver object, not what a normal antivirus [17:25.640 --> 17:32.300] could retrieve. So it's actually relatively safe. Now to prevent race conditions between [17:32.300 --> 17:39.180] our hook function and the device object member, we need to replace it using an interlocked exchange. [17:40.000 --> 17:46.040] Now, one thing to remember here is that if when you replace the file objects device object, [17:46.040 --> 17:54.320] you can have a normal the Windows kernel call that file object at any time. So you're going [17:54.320 --> 17:59.080] to want to use an interlocked exchange in order to make sure the device object you use in your [17:59.080 --> 18:04.960] hook is set at the same time the device object is replaced inside of the file object. [18:05.620 --> 18:11.000] So now that we've actually hooked the file object, there's not much work left. [18:11.020 --> 18:16.740] Inside of our dispatch hook, we need to check to see if the major function code being called [18:16.740 --> 18:22.440] is hooked. And if so, we need to pass the original dispatch function, the original device object, [18:22.440 --> 18:28.500] and the ERP to our hook function. Now the trick here is also if we receive the major function [18:28.500 --> 18:34.640] code cleanup, we need to replace the device object member of the file object with the original [18:34.640 --> 18:42.140] device object. This is to prevent issues during teardown. So from a detection perspective, [18:42.140 --> 18:46.980] we're going to be exposing ourselves to memory artifacts. And from a usability perspective, [18:47.400 --> 18:50.900] most of the functions we're going to be using for this is semi-documented [18:50.900 --> 18:57.500] and unlikely to change significantly. And finally, how expensive is it to detect the method? [18:57.500 --> 19:02.160] It's going to be pretty expensive because an antivirus would need to replicate our hooking [19:02.160 --> 19:08.160] procedure. And so they would have to enumerate every file object to detect if the device object [19:08.160 --> 19:15.060] has been tampered with, which is also add some complications. So now let's talk about how to [19:15.060 --> 19:23.020] spec Darukin. Darukin I wrote abuses the user mode network stack. So now using our file object hook, [19:23.020 --> 19:29.240] we can intercept ERPs to the AFD driver. This will allow us to inspect all user mode traffic [19:29.240 --> 19:35.460] and send and receive our own data over any socket. To review our existing plan, we're going to hook [19:35.460 --> 19:40.600] user mode communication similar to tools such as Wireshark. And then we're going to use our C2 [19:40.600 --> 19:48.160] to place a special indicator inside of a packet we sent to any port on the victim machine. [19:48.200 --> 19:55.340] And this way, our C2 can then communicate with our malware through any port, any service that [19:55.340 --> 20:01.800] is exposed. Now, how can we actually retrieve the contents of the packets that are received [20:02.620 --> 20:08.400] when the WCA receive function is called? Well, when you call that function, it's going to send [20:08.400 --> 20:14.800] an IOCTL called IOCTL AFD receive. And specifically, it'll pass the AFD receive info structure in the [20:14.800 --> 20:20.860] input buffer. Now this buffer, now this structure contains some flags, but what we really care about [20:20.860 --> 20:27.160] is the WCA buffers, which is essentially an array of arrays. And these buffers actually [20:27.160 --> 20:33.400] contain the bytes that are received from the packet, which we can use then to look into. [20:33.740 --> 20:37.780] So let's talk a little bit more about how the Spectre Rookit was designed. [20:37.860 --> 20:43.620] Starting with our packet structure, you can prepend any data you'd like, as long as it [20:43.620 --> 20:50.240] doesn't contain the magic constant at the start of your packet. And after any prepended data you'd [20:50.240 --> 20:55.800] like, you can place a magic constant, which will act as a reference point for the malware. [20:56.040 --> 21:02.240] And after the magic constant, you can add a base packet structure, which will have basic [21:02.240 --> 21:08.260] information, as you can see on the right, about the packet length and the type of operation being [21:08.260 --> 21:14.680] requested. After the base packet structure, you have an optional custom structure. Now this, [21:14.680 --> 21:18.680] again, custom structure is optional, so it might not be there in the first place. [21:18.680 --> 21:24.460] But this will vary depending on the operation being requested. And after this optional custom [21:24.460 --> 21:30.900] structure, you can have any data you'd like. Now this model allows for quite a bit of flexibility, [21:30.900 --> 21:35.980] because you can first send any packets you'd like, even one that doesn't contain the magic. [21:35.980 --> 21:41.740] And then you can send a packet that has any prepended data or any appended data with the [21:41.740 --> 21:47.560] magic content inside, malicious packet inside. And then you can, after that packet, send any [21:47.560 --> 21:53.240] packet you'd like. So the key, what I was trying to get at here, is that the Specter Rootkit design [21:53.240 --> 21:59.980] allows for quite a bit of flexibility in how you structure your packets. So what happens when the [21:59.980 --> 22:04.660] Specter Rootkit receives a packet? Well, it's pretty simple. First, it'll search the buffers [22:04.660 --> 22:11.220] for the magic constant. If the buffer contains the magic, it'll go ahead into the processing stage. [22:11.320 --> 22:16.540] And if the buffer does not contain the magic, then it'll just ignore the packet. [22:16.540 --> 22:23.400] Now, before dispatching the packet, there's a few steps we need to take. First, if we have enough [22:23.400 --> 22:28.420] bytes to fill out a base packet structure, we'll try to fill out a custom structure as well, using [22:28.420 --> 22:34.160] the bytes we already received. In any case, if we do not have enough bytes for either the base packet [22:34.160 --> 22:40.160] or the optional custom structure, we'll receive the rest. And finally, we'll dispatch the packet. [22:42.140 --> 22:47.880] Now, before we go any further, let's talk about the concept of packet handlers inside of the [22:47.880 --> 22:54.900] Specter Rootkit. So the Specter Rootkit contains this general packet handler class that exposes a [22:54.900 --> 23:00.980] virtual process packet function. Now, this base packet handler class has a default constructor [23:00.980 --> 23:06.280] that receives a pointer to the current packet dispatcher instance. And the process packet [23:06.280 --> 23:12.230] function receives a pointer to the packet itself. We'll talk more about the dispatcher later. [23:13.320 --> 23:20.440] Now, an example of a packet handler included with the Specter Rootkit is the ping packet handler. [23:20.440 --> 23:26.180] This handler is quite simple. It is just used to determine if a port slash machine [23:26.180 --> 23:33.160] is infected. All it is, is a bare bone magic and then base packet structure. And this base [23:33.160 --> 23:38.640] packet structures has its type or operation set to ping. And when the Specter Rootkit, or [23:38.640 --> 23:44.580] specifically the ping packet handler, receives a packet, all it will do is send back an empty [23:44.580 --> 23:53.760] base packet with the operation set to ping. Now, once a packet is completely populated, [23:53.760 --> 23:59.580] the packet dispatcher will allocate a packet handler depending on the requested operation. [23:59.580 --> 24:05.480] Then it'll call that packet handler's process packet function. And finally, it'll free the [24:05.480 --> 24:10.900] packet handler. Now, the reason the packet dispatcher model is really nice is because [24:10.900 --> 24:18.780] by passing a pointer to itself, to any packet handler, any packet handler can then recursively [24:18.780 --> 24:26.160] dispatch a brand new packet. Now, before we get into how this dispatching works and recursively [24:26.160 --> 24:32.820] dispatching, to give you an example of how the flow of a ping packet goes, is first the [24:32.820 --> 24:37.800] Specter Rootkit receives that packet. It'll recognize that there's a magic constant there. [24:37.800 --> 24:43.820] It'll then fill out the base packet and the optional custom structure for that packet. Now, [24:43.820 --> 24:47.900] since it's a ping packet, there's no optional custom structure, and it'll only make sure to [24:47.900 --> 24:52.680] fill out the base packet. Then, during the dispatching phase, it'll allocate the ping [24:52.680 --> 24:59.160] packet handler. It'll call the ping packet handler process packet function, and then it'll free it. [24:59.160 --> 25:05.280] Now, the process packet function, the ping packet handler, will send back just a simple base packet [25:05.280 --> 25:11.920] containing the ping operation, which will indicate to the C2 that this port is infected with the [25:11.920 --> 25:18.060] Specter Rootkit. So, the best way I can explain the recursive nature of the packet dispatcher [25:18.060 --> 25:23.980] is through another example called the XOR packet handler. Now, the XOR packet handler takes in an [25:23.980 --> 25:29.520] optional custom structure called the XOR packet structure, and this XOR packet structure has [25:29.520 --> 25:37.620] the XOR key and a XOR content array. Now, what happens is that if the C2 wants to request a [25:37.620 --> 25:43.180] certain operation, but it doesn't want to send the exact same packet as it did previously, [25:43.180 --> 25:48.580] it can take the packet, the base packet structure it wants to send over, and actually put it into [25:48.580 --> 25:54.760] the XOR content array. Then, it'll generate a random byte key, which it'll use to perform a [25:54.760 --> 26:01.540] XOR operation on every byte of the XOR content array. Then, it'll send this XOR packet to the [26:01.540 --> 26:09.320] Specter Rootkit. When the XOR packet handler receives this packet, it'll use the XOR key in [26:09.320 --> 26:16.580] order to de-obfuscate the XOR content array. And then, it'll take that XOR content and call [26:16.580 --> 26:23.800] the dispatch function recursively to dispatch that brand new packet. Essentially, this model of the [26:23.800 --> 26:29.640] Specter Rootkit allows you to create infinite layers of encapsulation, or layers of obfuscation, [26:29.640 --> 26:33.340] allowing you to create variants, even if you're requesting the same operation [26:34.300 --> 26:37.360] by applying a layer of XOR obfuscation. [26:39.660 --> 26:46.040] So next, let's talk about executing commands, a common feature seen in a ton of Windows malware. [26:46.680 --> 26:52.240] Now, before we get into starting a process from a kernel driver, we need to understand how do you [26:52.240 --> 26:57.220] actually do that from user mode in order to see what functions we have to re-implement in the [26:57.220 --> 27:05.260] kernel. So, in user mode, the first thing we need to do is create an unnamed pipe. We'll use this [27:05.260 --> 27:12.360] pipe in order to obtain the output of our process. Then, we'll set the startup info structure, and [27:12.360 --> 27:18.640] specifically the standard out and standard error handles, to our names pipe. And we'll set here, [27:18.640 --> 27:23.660] we can also set window flags, such as hide to window, so that the victim doesn't see [27:23.660 --> 27:29.340] the process being created. Next, after the startup info structure is populated, [27:29.340 --> 27:34.360] we can forward this to create process, to actually create the command prompt process. [27:34.360 --> 27:39.620] And then we can wait for it to exit using wait for single object. And finally, we can read the [27:39.620 --> 27:45.520] output of the command by simply calling read file on the unnamed pipe we created before. [27:46.100 --> 27:51.100] Now, let's talk about how we would do this from kernel mode. Something important to remember is [27:51.100 --> 27:55.940] inside of a kernel driver, you don't have access to many of the same function you have access to [27:55.940 --> 28:01.580] in user mode, because the kernel 32 DLL doesn't exist in kernel mode. Instead, you have to call [28:01.580 --> 28:09.480] the NT or ZW variants of functions. And kernel 32 DLL inside of user mode also calls these [28:09.480 --> 28:17.060] functions, but it acts as a simplified layer. So first, we need to actually create our pipe. And [28:17.060 --> 28:23.260] the way we can do this is by replicating what the kernel 32 DLL does itself. So what create pipe [28:23.260 --> 28:30.100] does is first, it'll open the names pipe device, if it hasn't done it before, then it'll use this [28:30.100 --> 28:36.820] names pipe device and set the root directory object attribute to the handle to the names pipe [28:36.820 --> 28:43.660] device, then it'll call NT create names by file to actually create that pipe. And here, it'll only [28:43.660 --> 28:49.620] create a read handle to pass in as a handle strictly used for reading from the pipe. It'll [28:49.620 --> 28:55.560] then call NT open file on that read handle in order to open a write handle as well. [28:57.080 --> 29:03.020] So now that we've re implemented the create pipe function in kernel mode, we need to create the [29:03.020 --> 29:10.500] actual process. And we'll use the same function ZW create user process that kernel base uses itself. [29:10.900 --> 29:17.300] Now, we're going to need to replicate the entire process of entire process that kernel base does [29:17.300 --> 29:22.760] itself. And so we'll need to first pass in an attribute list. And the only attribute we need [29:22.760 --> 29:28.340] really pass is the PS attribute image name attribute, which will specify the image file [29:28.340 --> 29:35.840] name for the new process. Next, we have to fill out an RTL process parameters structure for the [29:35.840 --> 29:42.180] process. In this structure, we need to set our window flags and the output handles to our pipes, [29:42.180 --> 29:48.380] similar to what we did with the user mode startup info structure. But we also need to specify the [29:48.380 --> 29:54.940] current directory, the command line arguments, the process image path, and the default desktop name. [29:55.200 --> 30:01.900] From there, all it takes is a call to ZW create user process in order to start the command prompt [30:01.900 --> 30:07.860] process. Once the process has exited, we can easily read the output of the command by calling [30:07.860 --> 30:14.660] ZW read file on the read handle we obtained for the pipe. Now let's talk about what you can do [30:14.660 --> 30:21.280] to hide your rootkit. So introduction to many filters. So many filters driver allow you to [30:21.280 --> 30:26.880] attach to volumes and intercept certain file operations. This is performed by registering [30:26.880 --> 30:34.000] your mini filter with the filter manager driver. So this reference from Microsoft documentation [30:34.000 --> 30:40.660] shows an example of user requesting file IO. First, the IO manager forwards the request to the file [30:40.660 --> 30:46.880] system. While this request is being forwarded, the filter manager driver intercepts this request [30:46.880 --> 30:54.480] and calls the registered mini filters that I've registered with it in order to, for example, modify [30:54.480 --> 31:01.440] the request being performed before being sent to the file system itself. So many filters essentially [31:01.440 --> 31:09.460] allow you to edit file operations before they actually happen. And many filters can be used to [31:09.460 --> 31:16.000] mask the presence of our rootkit on the file system. For example, a mini filter can redirect all [31:16.000 --> 31:21.820] access to a certain file to another file. We can use this to redirect access to our rootkit file [31:21.820 --> 31:27.460] to another legitimate driver file. Now again, going back to picking a method, there's a few [31:27.460 --> 31:32.800] questions you want to ask yourself. How many detection vectors am I exposed to? How usable [31:32.800 --> 31:37.460] is the method from both a stability and compatibility perspective? And how expensive [31:37.460 --> 31:43.320] would it be to detect the method? Well, the easiest way to abuse the functionality of a mini filter [31:43.320 --> 31:48.740] is to follow the documented procedure and just become a mini filter yourself. So here are the [31:48.740 --> 31:54.860] requirements for the function fltRegisterFilter. You're going to need to create an instances registry [31:54.860 --> 32:01.380] key under your service key. And under that instances key, you need to create an instance name key, [32:01.380 --> 32:06.580] which can be whatever name you'd like. This is going to be the name for your filter instance. [32:06.580 --> 32:13.860] Then under the instances key, you need to add a default instance value, which is a string and is [32:13.860 --> 32:21.080] set to the instance name you created in step 2. Then under your instance name key, you need to [32:21.080 --> 32:28.440] add the altitude and flags values. And the altitude is pretty much what the ordering is [32:28.440 --> 32:35.520] that many filters get called. The higher your altitude is, the first in line you are [32:35.520 --> 32:42.740] for your mini filter to get called. So how many detection vectors are you exposed to? [32:42.740 --> 32:47.880] You're going to be exposing yourself to registry and memory artifacts. How usable is the method? [32:47.880 --> 32:52.040] Well, you don't really have concerns from a stability or usability perspective, because [32:52.040 --> 32:58.560] this is just how legitimate drivers register as a mini filter. But at the same time, it's pretty [32:58.560 --> 33:05.520] easy to detect this method, because besides the registry artifacts, an antivirus could easily [33:05.520 --> 33:10.720] enumerate registered filters and their instances through documented functions, such as FLT [33:10.720 --> 33:18.920] enumerate filters or FLT enumerate instances. So another option is to just hook an existing [33:18.920 --> 33:24.160] mini filter. And there's a couple of routes you can take. First, you can just do a basic code hook [33:24.160 --> 33:30.940] on an existing mini filters function. You can overwrite the FLT registration structure, [33:30.940 --> 33:38.980] which is passed into FLT register filter before the victim driver calls it. Or you can edit a [33:39.200 --> 33:44.900] existing mini filter instance through DCOM to replace the function that gets called with your [33:44.900 --> 33:52.240] own. Now one of the easiest ways to intercept callbacks to an existing mini filter is just [33:52.240 --> 33:57.180] again a code hook. Now this can be as simple as a jump hook, but it comes with quite a few [33:57.180 --> 34:02.940] drawbacks similar to what we saw when we were talking about intercepting ERPs. You're going [34:02.940 --> 34:07.100] to be exposing yourself to memory artifacts, and unless the function is exported, you're going to [34:07.100 --> 34:12.620] need to find yourself, which can be difficult if the underlying image changes between operating [34:12.620 --> 34:18.920] system versions. Not all drivers are compatible with this method due to patch card. And finally, [34:18.920 --> 34:25.740] this is also HVCI incompatible. Now similar to what we saw with hooking ERPs, it's going to be [34:25.740 --> 34:31.600] potentially inexpensive because if a antivirus knows you're hooking a specific function in a [34:31.600 --> 34:37.100] driver, it can easily check the bytes of that driver function to determine if there's any [34:37.100 --> 34:45.260] patching performed. Now another way of obtaining access to a mini filter, existing mini filters, [34:45.260 --> 34:50.060] Now what you can do is you can enumerate filters and instances, as I mentioned before, [34:50.060 --> 34:56.620] by calling the functions fltenumeratefilters and fltenumerateinstances. Now the function that gets [34:56.620 --> 35:02.680] called for a certain filtered operation, for a mini filter, is specified in an array called [35:02.680 --> 35:08.400] callback nodes, which is part of the filter instance structure, which you can find by using [35:08.400 --> 35:14.680] the previously mentioned functions. Now the callback nodes array index is directly associated [35:14.680 --> 35:21.080] with the operation that is being filtered. If you find a callback node array entry for the [35:21.080 --> 35:26.100] operation you want to target, you can replace the pre-operation and post-operation pointers [35:26.100 --> 35:32.900] to your own function. Now one note here is that you will also want to replace the FLT registration [35:32.900 --> 35:40.300] structure that is part of the FLT filter structure. Now this is because the FLT registration structure [35:40.300 --> 35:46.600] will contain the original function pointer. And what an antivirus could then do is check to see [35:46.600 --> 35:51.860] if there's any discrepancies between the callback nodes array and the FLT registration structure, [35:51.860 --> 35:59.160] part of the filter instance, sorry, the filter itself. Now from a detection vectors perspective, [35:59.160 --> 36:04.760] you're going to be exposing yourself to primarily memory artifacts. And from a stability perspective, [36:04.760 --> 36:11.380] the only concern you should have is that the FLT instance structure itself is undocumented. Now [36:11.380 --> 36:17.200] finding an FLT instance is easy through documented functions, but the structure at the FLT instance [36:17.200 --> 36:22.280] structure itself may change across operating system versions. Now how expensive would it be [36:22.280 --> 36:26.860] to detect the method? Well it would be inexpensive because all an antivirus would need to do [36:26.860 --> 36:32.960] is occasionally enumerate registered filters and their instances in the callback nodes array. [36:33.680 --> 36:38.920] So let's say you want to protect a certain file, such as our rootkit file. What's an example of [36:38.920 --> 36:45.660] redirecting access to it? Well first, I'm assuming that you hooked a minifilter's pre-create operation [36:45.660 --> 36:52.680] callback. Inside of that pre-create operation callback, you can use FLT get file name [36:52.680 --> 36:58.660] information in order to get a normalized path to the file being accessed. Then if the path contains [36:58.820 --> 37:06.100] a protected file, such as the path to our rootkit, you can replace the file path being accessed by [37:06.100 --> 37:12.880] calling io replace file object name on that file object. Then you need to set the status being [37:12.880 --> 37:20.420] returned to status reparse and return FLT pre-operation complete so that the file system [37:20.420 --> 37:27.940] will then redirect access to the legitimate driver file. So for example, you could use this [37:27.940 --> 37:33.760] to change file access to your rootkit file to another legitimate driver that might be in the [37:33.760 --> 37:39.100] same directory so that when a user or program inspects it, it'll actually be looking at the [37:39.100 --> 37:46.520] contents of a completely different file. Okay, wrapping up. I'd like to give thanks to Alex [37:46.520 --> 37:53.240] Ionescu, a longtime mentor who's very experienced with Windows internals. The React OS [37:53.240 --> 37:58.900] is an amazing reference for Windows internals, especially for undocumented functions, functions, [37:58.900 --> 38:04.380] and structures. And thanks to Nemanja Mlazmajic and Vlad Ionescu for helping me review this [38:04.380 --> 38:11.600] presentation. Thanks for sticking around. You'll be able to ask questions in my DEF CON Q&A session, [38:11.600 --> 38:17.360] which will be later today. Make sure to follow me on Twitter and look at my blog. [38:17.360 --> 38:23.080] And you can check out the Spectre rootkit, assuming everything went well, at the URL below. [38:24.220 --> 38:26.920] Thanks for coming out to my talk again. Have a great one.