Good morning, DEFCON. How's everyone doing? Right. So hope you got a good night's sleep because today we'll embark on a journey. A journey into the realm of Ring Hopper. But first we'll tell you the story of how we almost zero-dayed the world. So usually a timeline is presented towards the end of the talk, but we'll make an exception. It all started on June 1st, 2021, when we initially reported the findings we'll be talking about today. On January 19th, 2022, Search CC started working on the coordinated disclosure. We'll soon see why this disclosure had to be coordinated. By the way, I'd like to use this stage to send a big shout out to VGI and the folks for mastering a challenging disclosure. Right about now, a year ago, we should have presented our findings in the Black Hat and DEFCON conferences, but unfortunately we were notified that the issues will not be fixed in time. So we had to withdraw our talks. But we did it rather close to the conferences themselves, so our abstract was still printed in the booklet, which might have caused some confusion. By the way, from the bottom of our hearts, we appreciate the concern. Fast forward to November 8th, 2022, when the last embargo set by Search CC has expired. And a note was published, which cleared pretty much everything up. And now that we're out of the all-day danger zone, where we couldn't talk about our findings, let's dive in. So at the beginning, there were four privileged rings. The most notable are the user mode, user land, user space. Ring three goes by many names. And the kernel. The lower the number of the ring, the more privileges it has. And it works in the manner that is opaque to all the rings above it. For instance, the user mode has no notion whatsoever of what's going on in the kernel. Over the years, new additional rings were in our introduced for instance the VMM ring minus one is usually where your hypervisor runs and SMM ring minus two so let's talk about SMM it started it stands for system management mode and it started as a small and humble processor operating mode that's mostly provided low-level system functionalities such as power management system hardware control and OEMs ran their proprietary device specific code there over the years the SMM got bigger and bigger and turned it to huge monstrosity that performed all kind of operations for instance it exposed and in exposes an interface to the SM BIOS does ps2 mouse keyboard emulation handles USB events and boot and runtime and the list is long together with SMM grew the attack surface and it has become so much more interesting for the attack but this is fine because numerous registers and MSRs along with some pretty awesome mitigations were created with the sole purpose to keep SMM safe so in order to switch to SMM the kernel has to trigger a special kind of interrupt called system management interrupt or SMI for short once an SMI is triggered the processor switches to running SMM and the kernel is no longer active and just like with any other interrupt there is an interrupt handler that corresponds to the interrupt number that was triggered and in our case it's aptly named the SMI handler once the SMI handler is done running the processor switches back to running the kernel and the SMM is no longer active the SMM resides in a special well-guarded Polish portion of the of the RAM system management RAM while all the other entities have no access to the SMRAM whatsoever for instance if the kernel tries to read a bunch um tries to read from the SMRAM it'll get a bunch of effects uh overall looks like a worthy target target right so in our research we started with an agent running in user mode escalated through the kernel and from there we helped to the SMM and we did all that on this Intel NUC. But as we'll see later on, our research goes way beyond this recently discontinued little fella. So this is a good time to dive into our findings, but before we do so we need to understand what is a time of check time of use vulnerability in SMM context. So let's say we have an SMI handler that provides the awesome service of overriding a buffer with the value of coffee. Since we are SMM, we do not trust the operating system, so the first thing we have to do is to validate the input and basically make sure that the output buffer points to a valid location. This will be a time of check. Then, once it was validated we can use it and overwrite its content with the value of coffee. This will be the time of use. Even attacker can modify this output buffer to point there to lead to another location in between the time of check and that's it. and the time of use, it will basically gain a write primitive of the value coffee. So, let's see how it works in SMM context. So, in the normal scenario, we have an input buffer, which would lead to an OS-owned memory, and the SMI handler will simply override the content of it with the value of coffee. However, an attacker may try to modify this output buffer to lead to the SMRAM in between the time of check and time of use. And by doing so, gain a write primitive of the value coffee to the SMRAM. By the way, this is exactly what a validation function does. It makes sure that the output buffer is not located inside the SMRAM. So, let's discuss how we exploit such an issue. In a classic time of check, time of use exploitation, it will work as follows. Let's say we have a system with four different cores, and let's assume that the first core does the critical SMI handling. It does the validation and use. So, within the other cores, we will try to corrupt the data. Sometimes, we may do this before the validation, and if so, it will simply not pass the validation phase. Sometimes, after the usage, and in such scenario, nothing would happen. But, eventually, with some luck, we may corrupt the data exactly in between the validation and usage, and exploit this issue. However, when an SMI is triggered, all the other cores are going idle, and we cannot use them to corrupt the data. So, how did we do that? The answer was DMA. DMA stands for Direct Memory Access, and this is the way for your peripheral devices to talk with the CPU, the operating system. And, in precise, those devices could be your Ethernet adapter, or your graphics card, and etc. So, to test this hypothesis, we took something that is called PCI-LH. It's a cool tool by Ulf Rysk. This is the small red FPGA device that you see here, which is connected through the M2 bus. And, basically, it allows you to initiate arbitrary read and write DMA transactions to the RAM. And, using it, we were actually able to exploit this time-of-check, time-of-fuse issue, which was amazing, but, as some of you may say, it's not good enough. It requires physical access, right? And, it could be considered only as an evil main attack, or supply chain attack at best. And, we thought so as well, and we wanted to make things even more powerful. We wanted to operate them remotely. So, to do that, we utilized something that exists on any platform. We utilized a storage device. And, we did it using a cool technique by Rafal, that was presented a couple of years ago, which allows you to generate DMA transactions using a storage device. So, let's see how it works now. Remember the previous diagram? Now, we add another component into the equation. We add your storage device. And, on top of it, we place some malicious file, which contains our payload. Or, basically, the modified address. So, once again, we will start by generating a DMA request. Due to the asynchronous nature of the storage device, it will take it some time to process the request. Then, we will also hook this request such that it will be redirected back to the input buffer. And, we will trigger an SMI. Once we trigger an SMI, all the cores are going idle. Remember? Then, eventually, the SMI handler will enter the critical validation and usage steps. And, with some luck, the data or the file will be returned exactly in between the validation and usage and will allow us to exploit this time-of-check, time-of-use issue. Okay. So, we talked a lot. And, it's probably a good time for a recap. So, we discussed what is SMM and how we can communicate with it. We also discussed how we can turn those time-of-check, time-of-use issues into write primitives to the SMRAM. And, we also discussed how to manipulate DMA transactions to exploit exactly those time-of-check, time-of-use issues. And, we also discussed how to execute code inside SMM. Oh, well, not exactly. So, yeah. We don't currently have the ability to execute code in SMM, but we have something pretty powerful. We have a write primitive to the SMRAM. So, let's put that to some good use. One of the modules we analyzed was the SMBIOS DMA edit Dixie driver. And, our analysis was not in vain. It bore fruits in the image of potential write primitives. It's worth noting that this list is not exhaustive and it's far from being the only vulnerable module. However, with a bunch of write primitives in a classic scenario, you would probably try to find an executable memory region, right? And, we thought that it would apply to SMM as well. But, as we discovered, SMM has a paging mechanism of its own where all pages containing code are read-only and all pages containing data are non-executable. Then, you would probably try to forge some arbitrary payload out of your write primitives. And, even though we have quite a few of them, their variety and number does not let us craft anything decent. And, lastly, you would strive to get unrestricted memory access. And, here, once again, the paging mechanism in the SMM hinders our exploitation. So, for starters, most of the non-SMRAM RAM is simply not mapped to the page table. And, if that's not annoying enough, and it's annoying, the page table resides in read-only pages, meaning no new pages can be allocated and all attributes are permanent. Meaning that if we do manage to execute code in SMM, it would be pretty hard, not impossible, but pretty hard to do something, for instance, to the OS. So, as you all probably agree, I hope by now, the classic approach might not be very applicable here. So, let's try to think out of the box and leverage some mechanisms that are unique and internal to SMM. So, at this point, Jonathan and I turned to reading some works from the past. And one of the works we found was by Rafael Waschuk and Corey Kallenberg from 2015, where they write, by forcing S3 suspend resume cycle, an attacker can run an arbitrary code and take control over SMM. Sounds promising, right? But what's S3 suspend resume? And what is S3? So, well, S3 is a sleep state where the CPU is idle, but the memory keeps on working. And simply put, when you put your machine to sleep, it usually enters S3. Let's continue with some more background. So, a normal boot process is rather time-consuming. But when we come back from S3, some of this process can be skipped because, well, the machine has already booted once. However, some configurations still needs to be restored. And here, the S3 boot script comes to our aid. During the normal boot, the configuration of the platform is encoded into the S3 boot script, and then when the machine wakes up, there is the code boot, and the code boot engine simply executes the script and restores the configuration. And this boot script consists of rather primitive operations of codes like reading, writing, to I.O., to memory, to PCI, etc. And here's the interesting bit. Previously, it was stored in a location that was accessible to the OS, which is exactly the fact that the paper we saw earlier abuses. They modified the boot script from the kernel, put the machine to sleep, woke it up. At this point, their malicious S3 boot script was executed, and eventually they ran code in SMM. So, as you could probably guess, this paper was from 2015, so it's already mitigated. And nowadays, the S3 boot script resides in a container aptly named Logbox. And the Logbox is a data structure that provides integrity of the data. It is located in the SM RAM, has a pretty standard image implementation, and there is a rather convenient API for reading, writing, and updating it from ring zero. So, let's have a look at the update procedure. So, here we see the function that updates the Logbox, and it either returns access denied error or some internal status. The only way we do not get access denied, and, well, we don't want to get access denied, is if the M locked global variable is not set, which is kind of a nice way of preventing the kernel from updating the Logbox. And this global variable unconveniently resides in the SM RAM, but, hey, we have write primitive there, right? So, let's utilize all the information we got and try to execute unauthorized code in SMM. So, the plan is as follows. We use our write primitive to zero out the M locked global variable. Then we invoke the update procedure of the Logbox, and by that, modify the S3 boot script with our malicious version of it. We put the machine to sleep, wake it up, and at this point, the S3 boot script will be executed and will be eventually running code in SMM. So, it's a great idea, but it doesn't, it doesn't really work, because right after the machine wakes up and just before the S3 boot script is executed, the code will close and lock SM RAM and only then jump to the boot script, meaning that even though we managed to execute code, rather privileged code, at rather early stages of S3 resume, the SM RAM is locked and most of the RAM is simply not mapped to the page table. Yeah, we should have read the documentation. All right, so our quest continues. And let's take another attempt. And let's take another attempt where we emerge victorious. But as usual, some background. So, each core has its own region of the SM RAM pointed to by the SM base MSR, model-specific register. Each core has an MSR of its own. Each of these regions contains a lot of interesting things, but we'll be focusing on two. The SMI handler entry point, which is the first code that will be executed upon entering SMM, and the save state, which contains, among other things, the value of the SM base MSR. And as a matter of fact, the proper way to modify the SM base MSR is by changing its value in the save state. And this whole operation, this question is pretty special. How special is it, you might wonder. It's so special that it has a name. SM base relocation. From a developer's point of view, SM base relocation is pretty straightforward. They modify the value, the SM base value in the save state, and some magic behind the scene relocates it, or relocates the whole region to the new, to the new address. Which leads to the following attack idea. So let's allocate some user-controlled memory that is accessible to both kernel and SMM. There we'll craft our own SMI handler entry point, the first code that will be executed upon entering SMM. And now we'll simply use our write primitive to modify the SM base value in the save state. This will trigger an SM base relocation, and eventually we'll execute code in SMM, right? Huh? It's a great idea, but it doesn't work. It doesn't work at least out of the box, and it doesn't work thanks to this MSR. And in particular, the SMM code check enable bit. So when this bit is set, and the machine is running in SMM, if someone tries to execute code outside of SMRAM, it will simply trigger an exception. You can think of it as some sort of a SMAP mechanism. But that's not all. There is a log bit here that if set, this MSR cannot be modified or changed in any way until the next reboot. And while rebooting the machine or cutting power to the CPU kind of defeats the purpose. So at this point, Jonathan and I sat and thought, what could be the closest thing to rebooting the machine without rebooting the machine? And then it hit us. We'll just put the machine to sleep, and in particular to S3 sleep. Recall that the S3 sleep state is a processor state where the CPU is idle, but the memory keeps on working. And as the CPU loses power, all of the entries stored on the CPU are cleared when going into S3. So if you care about all these entries, for instance, the MSR we just saw, all the data should be copied to the memory prior to entering S3. And after the machine wakes up, it should be restored to the CPU. So let's have a look at our beloved MSM code check enable bit. In normal execution, it is set. When going into S3, it is still set. When the machine wakes up and we come back from S3 as the CPU lost its power, it is cleared. And at the very early stage of the boot process, there is some initialization code that initializes this bit. So let's have a look at the initialization code. So clearly we see the write MSR over here with a certain value. You'd have to believe us that it comes from the memory. And here's the interesting bit. This write MSR happens only if the condition in the if statement holds, meaning the MSMM feature control supported global variable should not be zero. And remember we have a write primitive to the SMRAM, to the place where this global variable is stored. This leads to the full recipe of unauthorized code execution in SMM from kernel. So first, we use our write primitive to annul the MSMM feature control supported global variable. Then we put the machine to sleep, wake it up. At this point, the function you saw earlier is executed. The condition in the if statement does not hold. And the SMAP-like mechanism is not enabled. Hooray. Now we create our own SMI handler entry point, the first code that will be executed upon entering SMM. And we use our write primitive once again to modify the value of the SMBase MSR in the safe state. And thus, we trigger an SMBase relocation. And we trigger an SMBase release of the SMI. And drum rolls, please. We have code execution in SMM. All right. Great success. So frankly and humbly speaking, this method is pretty awesome. But it gets even better. So the SMI handler entry point starts running in real mode, meaning that the page tables enforcements have not kicked in yet because, well, there is no paging. And as we crafted our own SMI handler entry point, we have full and unrestricted memory access to all of the RAM. So for instance, we do not mind the non-executable or read-only pages, or the fact that the paging mechanism is static, or the read-only, or the fact that the page table resides in read-only pages, or the non-executable stack, or any of these mitigations. This is pretty awesome. However, newer mitigations do hinder our exploitation. In particular, the S3 and SMBase log bits. When these bits are set, the CR3 register and the SMBase MSR cannot be modified until next reboot. So while those mitigations are not fully adopted yet in all platforms, we wanted to deal with them as well. And to do that, we had no lead at this moment. So we decided to dig up once again through the pages of history. And we stumbled upon this cache poisoning attack that was presented in 2009 by Rafal and Joanna. And using the cache poisoning technique, you basically are able to arbitrarily read and write to and from the SMRAM. So let's see how this mechanism works. When we try to write to the SMRAM from the kernel, we will stumble upon a barrier, which basically makes sense, right? We are not allowed to write to the SMRAM, so the memory transactions will be discarded. However, this was a little bit misleading. In practice, when we try to write to the SMRAM, we'll go through the cache first, and the data will be stored there first, and only then it will be written back to the SMRAM. So what if, before the data is written back, we will trigger an SMI, and the CPU will enter SMM state, the barrier will be removed, and with some luck, only then the writeback will occur, and will modify the content of the SMRAM. So this would work. But obviously, this is already mitigated. And this is due to a special MSR that is called SMRR, the System Management Range Register, and it does a simple thing. It basically disables the caching of the SMRAM. But we already know how to disable MSRs, right? So let's see the code that restores it from the sleep state. And this probably looks familiar, and this is why. So this is the line of code that actually restores the MSR, and once again, it is dependent upon a global. So what if we annul this global, set it to zero using one of our write primitives, we'll put the machine into sleep, we'll wake it up, and when we wake up, we will be able to write to the SMRAM directly by resurrecting from the dead the cache poisoning attack. So once again, success, and we can deal with even those newer mitigations. So this is a great time for a demo, and let's hope it will work. Okay. Okay. Perfect. So before we begin, please notice that Intel NUC logo. This logo... Okay. This logo is stored on the SPI flash along with the UEFI firmware. So if we are able to execute code inside SMM, we will be able to modify this logo. The reason for that is because, only SMM is accessible to the SPI flash. It is the only one that is able to write to it. So now it's time to elevate our privileges. We'll begin by crafting a new write primitive of the value of zero. Remember, we had a rather complex write primitives, and none of them was an actual zero. So by chaining a couple of them and modifying a couple of globals inside the SMRAM, we were able to craft the value of zero. And this will help up in the... in the next step of our attack. So now we will try to annul the MSMM feature control supported global, which is responsible for the MSR restoration. And if you are actually able to disable it, when we return from sleep state, the MSR will be annulled. So now that we have annulled this global, we will read the MSR before we go to sleep state. We will see that the value of the MSR is five, which stands for two enabled bits, both the enable bit and the lock bit. And we'll put the machine into sleep. So at this moment, we count on the user to return from the sleep. I guess no one was ever concerned that he's being SMM mega breached when his machine suddenly went to sleep. And now that he is back, we will read the MSR once again, and it is zero. So now we are able to execute code outside the SMRAM. But how we will do this? We will do it using the SMBase relocation attack. So at this point, we'll execute the SMBase relocation attack and execute our own shellcode outside the SMRAM. So what the shellcode actually does, it simply replaces one of the SMI handlers. And now we are only left with triggering it. So now that we have triggered it, we will reboot the machine and see the result of what our SMI handler actually did. And voila, we managed to change the logo. So to better understand the implications of the issues we have found, we need to understand the UEFI ecosystem first. So everything begins with the Tiana core project, or what some of you may refer to as the EDK2, or practically the source code for any of your modern UEFI frameworks. Then comes the IBVs, the independent BIOS vendors. They take this source code, EDK2, and practically add the major portion of functionality. They add all the different SMI handlers along the way. Then they deliver it to the OEMs, to the original equipment manufacturers, such as Acer, Asus, Dell, Gigabyte, HP, Intel in the case of the Intel NUCs, Lenovo, and MSI. Then those OEMs deliver it to you guys, the end customers. So in the attempt to understand how many devices are vulnerable to RingCopper, we found out that only in 2020 there were manufactured more than 200 million devices that are vulnerable to our attack, which is absolutely crazy, and probably results in a couple of billion devices that are vulnerable. And obviously it means tons of CVEs. So if things were not bad enough, we want to make things easier. Even worse. Up until now, we attacked the machine from kernel to SMM, aka Ring-2. But we want to make our attack work from Ring-3 as well, from user space. And to do that, we require three different things. We need to be able to generate DMA transactions, we need to be able to trigger SMIs, and we need to be able to write to specific physical memory. So let's begin by generating DMA transactions. So remember the previous diagram where we hooked inside a kernel the DMA response, and we made some very complicated DMA, generated very complicated DMA requests? So figure that out. It is way, way simpler. In order to generate DMA requests, you only have to read the file. The data will be returned to us with DMA directly. And to make sure that it is being redirected to the input buffer, we do not need a hook. We simply read it into the input buffer. So this was it, and how we generated DMA transactions from user space. It was rather simple, but triggering SMIs is way complicated. And this is because we need to execute a special opcode, outb2, that can be executed only from kernel. And at this point, we got very frustrated. And like every frustrated millennial, we went on a social media. And in specific, the ones that if you triple its name by, by three, by three, you'll probably get the wrong results. So anyway, we stumbled upon one of Alex Matrosov's tweets, which says that, practically, any BIOS update tools that you'll find out there, probably has some low-level functionality that can be reused for offensive purposes. And indeed there was. So we, AMI provides two drivers. A Linux driver and a signed Windows driver. And both of them expose an API for triggering any arbitrary SMIs that we desire. Basically a octal. So now that we have done with triggering SMIs, we are only left with writing to specific physical memory. And I remind you, we wanted to make it work both from Windows and Linux. So let's start with Windows. So the communication with SMM is a little bit more complicated. It has a special structure that is called the mailbox, which has a description of all the things related with the communication with SMM. And among others, it contains a physical address of a row buffer, which would actually contain the input. So in order to access it from SMM, we require a physical address, which is basically the virtual address within SMM context. And to access it from user space, we require a virtual address that maps exactly this physical memory region. So let's look at the buffer mapping the octal inside one of the Windows drivers. So it begins by triggering SMID9, which basically populates the mailbox and along the way the row pointer to the buffer. And then in order to access this mailbox, it needs to map this physical address. So the next thing that you need to do is basically map this physical address into an accessible virtual address. Then at offset eight, it access the pointer to this physical buffer, the row buffer, and once again map it into a user accessible virtual address, and then returns everything into a user accessible memory so the user will be able to access those virtual addresses. So some of you may notice there is a race condition here. In between the time of the address that the SMI is being triggered and the mailbox is being populated with the pointer to this row buffer and the times it is being mapped to user virtual address. So what if we change this physical address in between the time that the mailbox is being populated and the times that this physical address is being mapped, basically we'll be able to map any arbitrary physical address that we desire. So let's see how it works. We will trigger the buffer mapping the octal twice. On the first attempt, we will get an access to the mailbox virtual address. So we will be able to modify the mailbox at offset eight with our own desired physical address. And then we will modify the mailbox to exploit this race condition and basically change this address with our own arbitrary physical address and we will get the corresponding virtual address. So this looks great, but reality makes things even harder. In order to avoid the remapping of the same physical addresses over and over again, those globals are being checked, which basically makes sense, right? We do not want to map the same address over and over again. So at this point, we decided to look at the cleanup octal. And we noticed that the cleanup octal, all it does is to annul those globals. And I will repeat it, it only annuls the globals. There is no unmapping of this virtual address once or ever. So it is still accessible to user space. So we will use this fact and we will make the following exploitation. So we will trigger the buffer mapping octal to get a mailbox virtual address. We will trigger the cleanup octal to annul the globals. We will exploit the file and then we will exploit the following race condition. From two cores, we will do as follows. On the first core, we will trigger the buffer mapping octal once again to populate the mailbox and to map the physical address inside it. From within the other cores, we will use our previous virtual address of the mailbox to modify the top state with our own desired physical address. Then we will check if the newly mapped physical address is the one that we wanted. If not, we will simply repeat the entire process. We will trigger the cleanup octal, repeat the entire process once again until we succeed. So you folks are pretty tired by now, I guess. We presented a lot of info, but do you have a moment to talk about Linux? AMI lets us do some pretty amazing things from user space. Here we open the kernel module and trigger the mmap syscall with the kernel module's file descriptor to map any physical address, which is pretty awesome. At this point, we have to somehow be able to generate DMA transactions for our exploit to work. So just as we did on Windows, we'll simply open a file, read it to the previously mapped memory, and hope for the best. From the Linux module's side, they initialize the mmap attribute of the file operations struct with their own function, which actually does the physical-to-virtual mapping using the remap-pfn range, which is pretty straightforward. It gets a physical address and returns a user-accessible virtual address. But there are some bad news. When we execute this code, we get an e-fault. And the reason for that is in the way remap-pfn range works. When running this function, page ranges are managed without struct page, just pure pfn. Simply put, this will not work. No DMA transactions will be generated here. However, we have the ability to map any physical address. Any physical address. So let's take the code from before, and instead of mapping some arbitrary physical address, let's map, for instance, the address of the Linux kernel or a kernel module. At this point, that we have the user-accessible mapping, we will be able to patch ring-zero code and gain code execution in kernel, which is pretty awesome. But it sounds kind of familiar, right? Do we remember how we generated DMA transactions from kernel? So now that we have all three bullets working from ring-three, we have a full exploitation chain from user-land to SMM, which concludes our journey. We first got the ability to write to the SMRAM. Then we tried to leverage it into code execution in SMM using the S3 boot script, and we failed spectacularly. And we successfully ran unauthorized code in SMM using and abusing the SMBase relocation technique. And lastly, we elevated the attack to work from user-land, and thus we managed to hop from user-space to SMM. We were Jonathan and Benny. Thank you so much for taking part in this journey.