 Alright, so this is Omri Misgav and he is going to talk about running root kits 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 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 we'll also run some live demos and everything will go as planned. So what is DST? 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 its 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 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 call 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 CIDLL. 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 on. Next up when a driver will be loaded you can see the call stuck on the middle. Basically when it's going to be mapped into memory the couple of wrapper functions that will be called. Basically those are just very simple wrappers for the callbacks that CIDLL 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 header and validate image data. And that's pretty much how it works. There have been a few changes along the years. 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 CIDLL. And there are more callbacks that CIDLL now provides but they are not really important for our case. And because there are more callbacks that are used then some offsets in the structure change basically from Windows 8 till 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 loaded. So 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. They'll just write to it a value that will disable it and then they're gonna load the driver and restore the original state. The way it's being done is that all of those variables, all of those callbacks are basically 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 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 pros to it because it means that we can just reuse this capability no matter when we want to. So a few steps that Microsoft tried to implement in order to fight this thing is basically it's first apply patch guard to it from Windows 8 and Windows 8.1 there were improvements. 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 VBS 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 and all 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 P 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 first two points here with our main goal is basically be able to change some data and not touch any 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 try 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 tried to verify it but since it's periodically we can still make some short-lived changes. Instead 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 change the page table and basically we only write the address of the PTE, 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, the page we allocate is in the kernel 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 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 Ntos as we can see here 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 only 4 bytes, page size is 4 kilobyte we need to modify the value in order to set it to turn it off and then basically just switch the PFS as we showed earlier load the driver and restore the PTE so this is pretty complicated when I walked 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 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 we're going to show you is that we see the build number and we'll 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 rootkit over here and just show you that it's unsigned and we're 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 rootkit itself and I stopped it and now we'll try to run it again and 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 basically on how the callbacks work so there is some address that basically calls CI DLL and this callback needs to return as 0 okay, sorry it needs to return 0 and now instead of just calling to the entry point of the function maybe we can just replace this functionality and get some other way to return 0 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 Initialize is imported by the kernel so we can just find a reference and 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 uninitialized memory in the data section now we need to find a new replacement callback which is pretty easy to do because all we need to return is 0 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 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 right now I disabled DC and then we see that our rootkit is back running after we didn't manage to run it we re-enable DC sorry I can't 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 these two next slides we can see that basically we managed to find some 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 IO 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 DC 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 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 permission 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