All right. So this is Omri Misgav, and he is going to talk about running rootkits like a nation state hacker. Hi, everyone. Good morning. As I was introduced, my name is Omri. I work at Fortinet in Israel. Most of my work is around OS security and OS internals. Today I'm going to talk about a bit about driver signing enforcement and basically how it works, how it works, the internals of it, how attackers abuse it, what Microsoft tried to do in order to prevent it, and then we'll talk about some new tradecraft. Hopefully that will also run some live demos and everything will go as planned. So what is DSC? Basically, there is a feature in Windows that is called code integrity. It is used for threat protection. Basically, it's used to sign code, and some aspects of it means that in order to sign, in order to run drivers in the kernel, now you have to sign them, to digitally sign them. And every time you want to execute a driver, basically the operating system will check the signature, will verify it, and by that we can improve the security of the OS. We can know from where drivers originated, who are the authors, and if we trust them, and make sure that they are not modified on the way sometime after they were built. Moreover, the signing is not something that just everybody can do. You used to need a specific code signing certificate that came only from a handful of approved sources. And over the years, there was another limitation that was added by Microsoft that you had to give them your driver for them to sign it. And without their signature, it won't work. You won't be able to run it. Which, if you think about it, for attackers, is not really ideal. You just hand over the code, and it's not going to work. It's not going to be able to run it. You hand over your payload to the defenders. It's not really a place you want to be in. So how does it work in practice? Basically, when the system boots up, if you can see the code stuck on the left, at a certain point, the kernel will mark the CI enabled flag as set to true. And it will call another library in the kernel that is called CI DLL. And it will give this initialization function that is exported by the DLL. It will give it a callback structure that the DLL itself will fill with callbacks for the kernel to use later on. And the DLL will also set its internal state to be set as own. Next up, when the driver will be loaded, you can see the call stuck on the middle. Basically, when it's going to be mapped into memory, there are a couple of functions that will be called. Basically, those are just very simple wrappers for the callbacks that CI DLL provides. And what they do, we can see on the right, on the right side on the disassembly, basically, they check the flags. They check the global flags to see if the code integrity is enabled. And then they just check that the callback is valid, that it's not a null pointer. And that's pretty much it. Both functions, both callbacks that will be called is the validate image data and validate image data. And that's pretty much how it works. There have been a few changes along the years, but the changes, what I just described was mainly for Windows 7. The changes are really, really small. So, from Windows 8, basically, the enable flag was removed. So, right now, the state of the driver signing enforcement is only set in the CI DLL. And there are more callbacks that CI DLL now provides. But they are not really important for our case. So, we have to make sure that we have more callbacks in the case. And because there are more callbacks that are used, then some offsets in the structure change. But basically, from Windows 8 until today, there wasn't any critical change for us. One other thing that is important here is that, basically, right now, there is only a single function that will be called, a single callback that will be called when the driver will be logged in. So, basically, we have to make sure that we have a single callback that will be loaded. So, how is, how it's been abused in the wild? Basically, we go, we, or mainly attackers go in order to load their driver, which is usually unsigned or untrusted. And they just go and flip the bits of the flag, depends on the operating system build. And they'll just write to it a value that will disable it. And then, they, you know, they, you know, they are going to load their driver and restore the original state. The way it's being done is that all of those variables, all of those callbacks are basically internals, internal structures. They are private. They are not exported in any way. But they can be easily found with the simple pattern matching. Again, with barely any change between all the Windows builds. And in order to be able to do that, we have to make sure that we have a single function that will be able to write to the kernel. Basically, they bring their own driver, or bring their own vulnerable driver. And they just leverage it in order to gain right permissions to the kernel. It has a lot of, a lot of pros to it, because it means that we can just reuse this capability no matter when we want to. One of the steps that Microsoft tried to implement in order to fight this thing is basically is first apply patch guard to it from Windows 8 and Windows 8.1. There were improvements. They tried to block, now they try to block attackers from gaining the right primitive. Basically, they deploy driver block lists. Which is effective, but there is a problem with it, because it doesn't cover what we don't know. If we find a new driver that is vulnerable, if the attackers find it, so they can use it, and everything will just go on. And the major change that they tried to introduce with Windows 11 was basically using KDP, Kernel Data Protection. It's a new feature based on the VMware architecture. Basically, leverages the hypervisor in order to enforce read-only permissions on specific pages. And they let drivers, other drivers use it as well. Or other kernel components. And that's pretty much what they did with Windows 11. Now CIDLL was tweaked a bit. Basically, we can see that CI options is now being provided as a parameter to the KDP API. And the global variable was moved to its own page, which is called now CI policy. It's like a unique section in the PE that it's found in. So just to recap for a second, before we get into the new stuff. Basically, how the DSC tampering works is we locate something that we need to change. Then we overwrite it. We load the driver and revert back. We're going to focus on the main, on the first two points here. With our main goal is basically be able to change some data. And not touch any code that we don't want to touch. So we're going to modify code pages or modify any executable pages whatsoever. The first technique, it has a very creative name for it. It's called page swapping. Basically, because the protected page, we cannot write to it. Because there is another set of permissions that is being enforced on in the slot PTE. Basically, we can't write to it because there is another set of permissions that is being enforced on in the slot PTE. Basically, we can't write to it no matter what we'll do. We can see the illustration here of the mapping. But still, KDP doesn't really enforce the way that the virtual memory is being mapped to the physical memory at the end. It tried to do something like patch guard that it goes on periodically and try to verify it. But since it's periodically, we can still make some short-lived changes. So instead, we're going to use the same thing. We're going to create our own page, our own copy of the page. And we'll make it writable. So we can set whatever value we want to it. And then we'll set DSC as off in this page. And then we just need to somehow make the same virtual address for the original variable to change its value to it. For that, we just need to make the same virtual address for the original variable. And then we just need to change the page table. And basically, we only write the address of the PTE, of the PFN there. And then the next time the virtual address will be used, it will go to the page that we control and set the value. So just to go through the steps by steps here, we allocate a new page. We need to find the page we allocate is in the kernel. And we need to find the CI options flags just as it's currently done in the wild. There's nothing new about it here. We need to read the PTE base. Just a short explanation on this. Basically, the page tables are also found in the virtual address space in the kernel. With simple bitwise calculation, we can basically convert the virtual address that we need to use in the kernel. That we want to access and find it's matching PTE for it. There is a problem with it. Basically, the address space is now randomized. But it's a very good thing that I already had some research in the past that covered it. So we can just read the new PTE base with a single read from kernel using an exported function in the kernel. Then we go on and read the PTEs that we need to change. The PTE for the original page and the PTE for the page we allocated. Then we need to copy the page because if we're going to swap the page in order for anything else not to break, we need to copy the entire values there because we don't really know what other things are in the page itself. The flag itself is also only four bytes. The page size is four kilobytes. We need to modify the value in order to set it as to turn it off. And then basically just switch the PFNs as we showed earlier. Load the driver and restore the PTE. So this is pretty complicated. When I worked on it, I wasn't really happy. So I tried to figure out some other way to solve it and make it a bit more feasible to use. And with a few very slight changes, it's very easy to do. Basically instead of allocating a page in kernel, we're going to allocate a page in user space. And then everything we need to locate just remains the same. Instead of copying the page and doing a lot of kernel reads, we can just in the case of CIDLL, since the variable was moved to its own unique page, we can just like use the default values for this page. Most of them are just zeros. And just set the value that we need for the flag. And then again, change the PFN, load the driver, and restore it. So demo time. I have a Windows 11 build here, an inside preview build from like two weeks ago. We'll boot up in a second. The first thing I'm going to show you is that we see the build number and we see the code. You see that VBS is running. But the only thing that is being enabled is credential guard. KDP doesn't appear here for some reason. So the first thing we're going to show is basically that the current technique in the wild doesn't work. So here we're going to expect a blue screen. Yay. We can see that it's blocked on the right. And let's see. Back. Second. Okay. So now I have my root kit over here. And just show you that it's unsigned. And I'm going to quickly install it. We'll try and run it. And see it doesn't run. We get an error. We'll run the page swapping. And now it runs. . . . You can see the output here. You can see print by the root kit itself. And I stopped it. And now I'll try to run it again. And I'll get an error. Okay. So this is the first technique. It's pretty complicated to implement. It involves both reads and writes to the kernel. And we wanted to find a different method to do it that won't be as complicated as that. So we found another way. If we look on the basically how the callbacks work. So there is some address that basically calls CI DLL. And this callback needs to return us zero. Okay. Sorry. It needs to return zero. And now instead of just calling to the entry point of the function, maybe we can just replace this functionality and get like some other way to return zero. And then it will just work as the same. . The authorization of the driver will work. So we're basically going to swap the callbacks. But we need to find out how to do it. So first we need to find the global callback structure. It's very easy to do because CI initializes is imported by the kernel. So we can just find a reference in the import address table for it. From there we need to go back and find the register assignment for it. And basically when we do, we see that the address will point to initialize, to uninitialize memory in data section. And now we need to find a new replacement callback, which is pretty easy to do because all we need to return is zero. There are exported functions in the kernel for this. And if we don't want to go that far, we can just use the IDLL itself and look for gadgets or exported functions in it. Now we need to find the way to restore the original callbacks. And that's important so we can just do this entire technique without any kernel reads. . So the pattern that is used to initialize the callback structure. Is pretty much the same in every build. So we can just look for it. And then we get a list of addresses that we need to actually verify that they are functions. So we can basically traverse the exception directory. And look for entries that are basically function start addresses. And then we know that we got to the right place. So then we need to set the callback, which is pretty easy. Load the driver. And restore the callbacks again. Go back for a quick demo. So. So right now I disabled the AC. And then. We see that our rootkit is back running after we didn't manage to run it. We'll re-enable the AC. Sorry. So I can see it. So it's stopped as we can see now. And we can run it again. So I'm a bit out of time. So I'll try to go quickly about this next slide. . We can see that basically we managed to find a new technique that is pretty synonymous to the existing one in the wild. There is a way to mitigate it. Basically what we suggest is like try and find all of these internal states, internal variables ourselves just as attackers do. There is no reason not to do it. It was proven as stable. Till now at least. We can copy the state of the variables and then just re-test them again and again every time a driver is loaded. It's also easy to prevent because once we see that there is a change in the variables, we can either block the I operation or just restore the variables to the original state and let the operating system do the work for us. And just to sum it up, basically DEC tampering is still feasible even with everything Microsoft attempted to do here. Doing like providing data oriented mitigations are pretty difficult as you can see. The real solution for it at the end is called HVCI. It's another feature that Microsoft provided. On top of VBS. It's a very old feature. It's like there from the beginning. And it pretty much covers every aspect of it because what it does is basically have the entire validation process being done twice. Once in the kernel itself by CIDLL. And then it holds a copy of the policy in the secure area that the kernel can touch. And it basically does the entire validation there. And only then it provides the execution permissions. So without that validation, nothing will work. And that's it. You can check out our blog for additional data. And we're done. Thank you.