Header Ads Widget

Responsive Advertisement

How CPUs Access Hardware - SerenityOS Exploit

How CPUs Access Hardware - SerenityOS Exploit

In a previous article, we looked at a kernel exploit in SerenityOS, which was the intended solution for the wisdom2 challenge from the hxp CTF. But of course, SerentiyOS is a big software project, so there might have been more security issues. In this article, I want to look at another solution for this challenge. So basically another kernel exploit. Actually our team ALLES! was the only team that solved this challenge during the CTF. But as always I have to mention, this was not me.

This team is insane, and I did not contribute to this success. It just blows my mind how skilled the ALLES! members are. Especially when you know how young they are. In this case, it was incredible Linus Henze again solving it alone.


Base on experience

It’s crazy to think back at how much I knew over 5 years ago when I started with my binary exploitation course. At the time I felt I already had some nice experience. But you should always keep in mind that overall those years since then, I kept learning more stuff and accumulated more knowledge and skills.

And I think that’s important for you to remember, you can also learn all that stuff, it just takes time. Literally years. And so in this article, I want to share with you one new thing I learned. I just learned something new from Linus’s SerenityOS exploit. And it makes me really excited, because it’s a thing that I never really understood, and it finally clicked for me.

Like with the previous exploit, Andreas Kling, the developer of SerenityOS, made a whole video going over the write-up of the exploit, explaining the vulnerability, looking at the kernel source code, and fixing the issue. Because that video is already very technical and detailed, I thought I will try to have a bit of a different angle with my article - I want to focus on my eureka moment instead.


Computers are simple and hard

So let’s head in. Sometimes when I think about how computers work, I get this feeling of “it’s kinda simple”. I can see how complex code is compiled to very simple instructions, which then get executed by a CPU. Also maybe you know that I have spent many hours working on Ben Eater’s 8-bit computer - and when you develop each part of a CPU by hand, you really start to feel like you understand it.

I have also played a lot with very simple electronics like Arduinos, which have an Atmega AVR microprocessor.

But the thing is: on one side I understand how higher-level C programs can be compiled to machine code, and how they run. I understand how an ELF binary is loaded into memory and then executed. I know my way around reverse-engineering them, I know how to debug them, and so forth. And on the other side, I understand very low-level code running on a microprocessor.

For example, this basic Arduino code where I can simply set a PIN to high or low. And that makes an LED blink. I understand that. I understand that there are transistors that decode some instruction that will then lead to a toggle of wire that goes to the outside. To this pin. Thus turning this on or off. But there is something in the middle of those two worlds that I have very little experience with.

And as soon as I think about this middle part, I suddenly get this feeling that I actually ''do not understand a single thing about computers.'' And this thing in the middle is basically the world where kernel code lies. It’s what makes modern CPUs like an Intel CPU so much more complex than a microprocessor.


What I know about kernels

What I’m basically referring to are the privilege levels. Ring 0,1,2,3. You probably have heard those terms before. But of course, I have some knowledge about operating system kernels, so what do I already know? I have actually made a few videos where I explained some stuff about the Linux kernel. For example, in the binary exploitation playlist, I introduced syscalls.

I have made articles about Docker containers, and the Linux kernel namespace feature that enables that. In those articles, we also looked at kernel source code to better understand how that works. I also made an article about the Linux Device Drivers book, which was a huge “AHA!” moment for me when I understood some stuff about it.

And in the last SerenityOS article, you can also see how I feel comfortable reading the syscall source code of the Serenity kernel. But all those topics that I have prior experience with, have one thing in common.

They are very close to the userland. It is kernel code, it is high privilege code, and there are some quirks to that, but it still feels and looks like relatively “simple” code. There is nothing really hardware-specific in those areas. And that’s the crux of the matter.


I’m missing the link between this software world and the hardware world. How exactly does Linux talk to a hard drive? How exactly do keypresses on this physical thing end up as the input to a program?

When you look at very simple microcontrollers, like looking at an Arduino or ben eater’s 8-bit computer that I was building, I can understand how this microprocessor can “talk” to hardware. I can understand that there is a wire going out of this chip, driven by transistor logic, and it can “talk to this LED” to turn it on or off.

But I don’t understand how a modern Intel CPU is connected to the physical world. How do drivers talk to a physical hard drive? And this is where we come back to Linus’s SerenityOS exploit. It’s a really creative exploit, in some ways a super simple exploit, but it offered me an opportunity to learn about this missing link.



So let’s see how Andreas summarises the kernel

Vulnerability abused in this exploit. ''The vulnerability they found is quite interesting. But basically, they discovered another flaw in our ptrace implementation.'' Quick reminder, ptrace is a syscall, so a kernel feature, that allows one process to debug another process. Read-write the other processes memory.

Single-step execution. Change registers. So the kind of stuff gdb implements. ''And the issue is, that we just accept the e-flags coming from one process to the other. That makes it possible to overwrite certain CPU flags that you really shouldn’t let user programs change. Like the I/O privilege register for example. And that’s how they exploit it. So they change the IOPL.

The IO Privilege level. And elevate the IO Privilege of their own process, making it possible for it to talk to the hardware. And then once they have hardware access they can talk to the hard disk and extract information that way. And that’s actually what their exploit does. So the exploit contains a small IDE hard disk drive. Very small. Just reads one sector.''

And you know the typical objective of a CTF. read a flag from a file. And the wisdom2 challenge actually had a second hard drive connected, that contained this flag. So if your program can directly talk to hardware, they can just directly ask the hard drive to give them data started on it.

And you completely bypass any of the permission checks that the operating system might have implemented in the kernel. Theoretically, you could also directly talk to the main drive, overwrite any setuid binary, and then you can easily get root.

But here it was enough just to read some data from the hard drive. ''What they do is, attach with ptrace to a child, then they get the registers of the remote process. And then they write back the registers, after modifying the flags, setting the IOPL to 3. This means that Ring3 is allowed to have IO access I suppose.''



So what are the Eflags?

Let’s fire up gdb run a basic program like /bin/ls, let the program single-step run for a bit. And then let’s look at the info registers output. Here are all the registers of the process we are debugging. Under the hood, GDB used ptrace to ask the kernel, “please give me the registers of this process”. And then gdb displayed it here. So here is the stack pointer. Here is the instruction pointer that points to the next code being executed. You have other general-purpose registers.

So these are just small memory cells in the CPU that your code can use to calculate stuff. And if you followed binary exploitation, you are pretty familiar with those. But what’s up with the registers down here? They are a bit weird registers, and for your regular program, you usually don’t deal with them. They sometimes differ between operating systems how they are used.

And they are actually another thing I don’t fully understand myself yet. But I want to focus for now on the eflags. And the eflags are a bit like internal CPU “housekeeping” flags. Some of them you might be familiar with. For example ZF. It’s the ZERO FLAG. “Set by most instructions if the result of an operation is binary zero.”

That’s basically how if-cases are implemented in assembly. Let’s say you want to jump if two values are equal. Well, jump equal has no additional operand parameters which values to compare.

Instead, the actual implementation of that instruction is to jump IF THE ZERO FLAG is set. And before that jump-equal instruction, you usually have a compare instruction. And here is what compare does. “The comparison is performed by subtracting the second operand from the first operand and then setting the status flags in the same manner as the SUB instruction”

If you subtract the same value, the result is zero. If the values differ, the result is not zero. So if it’s zero, the zero flag is set in the eflags. And then Jump equal can check if the zero flag was set. As you can see you might not directly set them yourself. It’s like CPU housekeeping stuff, where the CPU wants to remember something. In this case the result of a comparison or subtraction. But there are more flags, and I want to focus on an eflag I didn’t know existed.

And it’s the IOPL flags. It’s two bits. Input/Output privilege level flags. The IOPL (I/O Privilege level) flag is a flag found on all IA-32 compatible x86 CPUs. It occupies bits 12 and 13 in the FLAGS register. And it’s used, in order for the task or program to access I/O ports. But it’s important, the IOPL can only be changed when the current privilege level is Ring 0.


So what are IO ports?

Memory-mapped I/O (MMIO) and port-mapped I/O (PMIO) are two complementary methods of performing input/output (I/O) between the CPU and peripheral devices in a computer. So IO Ports are the secret to how intel CPUs access hardware. I am familiar with memory-mapped IO from microprocessors.

Again, I’d like to reference my hardware wallet series, where I explain memory-mapped IO on an ARM processor. But ARM and INTEL are different processor architectures, so they handle hardware differently. And this concept of PORTS was weird to me. Maybe it’s the name and when I hear “ports” think of network ports. But that’s of course something completely different.

But this is where the Serenity exploit comes into play. As you now know, there was a vulnerability in the Serenity kernel ptrace handler for setting registers in another process. This exploit simply set the 12th and 13th bit, and write the changed eflags with ptrace back to the other process.



Playing with Eflags

As you know, gdb uses ptrace to debug other processes, so we could try the same on Linux. As you can see, the zero flag is currently not set. But with set $eflags and 0x40, we can set the 7th bit, which is the zero flag. And when we now check the registers, we can see the zero flag is set. We could also just try to set all flags to zero. Let’s do that.

But when you check the registers, you see that apparently IF is still set. IF, Interrupt Enable Flag, is also a privileged flag you are not supposed to change. A user program cannot just disable all interrupts on the system. Now we could also try to set the IOPL flags like the exploit did. 0x3000.

But as you can see, it didn’t do anything. Of course, we shouldn’t be allowed to change those flags. But because serenity OS didn’t account for this, and the kernel runs in ring0, this code ''kernel_regs.eflags = ptrace_regs.eflegs;'' can set the flags for us, when we call PTRACE Set Registers with the modified eflags.


Talking to hardware via IO ports

So now we have the IOPL flags set. Apparently, we are now allowed to talk to hardware via I/O ports. And as Andreas said, this exploit implements a very basic IDE hard disk driver which reads out a sector from the attached drive. And when you look into this code, specifically the inline assembly, you can find here port_byte_in, and port_byte_out using the in and out assembly instruction. And this assembly instruction takes a port number. The instruction reads a byte from a port. And the out instruction writes a byte to that port. 


And before we continue with this code, I want to quickly jump back to my super simple Arduino blinking lights.

Because when you write Arduino C code, you use functions like digitalWrite, to write to a pin. But under the hood, in actual AVR assembly, there is an instruction to set a bit in a port. And there are also IN and OUT instructions. These instructions also write or read a byte to or from a port. Here is another blinking example using inline assembly with the OUT instruction, writing 0 or 1111s as a byte to port number 5.

And the port number references these entire 8 pins on the Arduino. 8 pins, 8 bit, 1 byte. Another port number corresponds to the other 8 bits of pins. The AVR architecture of the Arduino, and Intel architecture of modern CPUs, are of course completely different. But they both have a concept of I/O Ports.


Access harddisk controller with ports

And so the exploit here, uses the OUT instruction, to write to specific ports. And the port number we are using is based on this base 0x1f0. And when we look up a list of the strictly defined x86 intel ports, we can see that the ports 0x1f0 to 0x1f7, are connected to The primary hard disk controller. You can imagine this as if a hard disk is connected to those Arduino pins. Isn’t this incredible? When I saw this list I also saw those ports at 0x60 dealing with the ps/2 controller.


Detecting keyboard key press

So keyboards and mice connected to that old-school ps/2 plug. And I found this awesome repository by Ciro Santilli, x86 bare metal examples. And here in the ps2 keyboard assembly file, you can see that it reads a byte from the port 0x60. So it reads whatever key you pressed on your connected ps2 keyboard.


And this was a eureka moment for me

I realized that an x86 CPU is not so much different from an Arduino microcontroller. Both need somehow instructions to write and read bits or bytes from wires connected to their chips. The difference is only that modern desktop CPUs have privilege-level features so that only kernel code in ring0 can use instructions like IN and OUT.

And the IOPL flags are not set for ring3 processes, so they can’t directly talk to the hardware. That’s why they need to ask the kernel to read data from a hard disk using syscalls like reading and write instead. And the kernel can then check permissions like file permissions, to make sure you are allowed to read or not. Really really cool. I think now I understand the link I was missing. 

Post a Comment

0 Comments

class='back-top' title='Back to Top'/>