有一外国网友贴出了对中国可能通过在硬件中预留的后门对美国进行攻击的担忧(http://hardware.slashdot.org/story/10/10/29/1456242/Hiding-Backdoors-In-Hardware),网友的回帖更是火爆。下面就是在硬件中预留后门的详细方法。(原文:http://blog.ksplice.com/2010/10/hosting-backdoors-in-hardware/)
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_protos
consisting of a struct net_protocol*
for each protocol number. The important field for our purposes is handler
, a pointer to a function which takes a single argument of type struct sk_buff *
. Whenever the Linux kernel receives an IP packet, it looks up the entry in inet_protos
corresponding to the protocol number of the packet, and if the entry is not NULL
, it passes the packet to thehandler
function. The struct sk_buff
type is quite complicated, but the only field we care about is the data
field, 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_usermodehelper
function, 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_protocol
which 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.ko
Now we can use sendip
(available in the sendip
Ubuntu package) to construct and send a packet with protocol number 163 from a second machine (named control
) 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 $targetip
rwbarton@target:~$ ls -l /tmp/x -rw-r--r-- 1 root root 0 2010-10-12 14:53 /tmp/x
Great! It worked. Note that we have to send a null-terminated string in the payload, because that’s what call_usermodehelper
expects to find in argv
and we didn’t add a terminator in exec_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 /boot
directory; 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_packet
function) and data (proto163_protocol
and the string constants inexec_packet
) sections; - it performed relocations, so that, for example,
exec_packet
knows 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_module
function.
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_protocol
in inet_protos[163]
, since we have an exact copy of its code.
The first point is a little tricky. Normally, we would call kmalloc
to 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 call kmalloc
, 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}' | less
One 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 vmlinuz
file 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 in vmlinuz
that decompresses the kernel to jump to our code after doing so. The changes amount to 5 bytes to write that jmp
instruction, 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-generic
image 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 the int 0x13
instruction 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 interrupt 0x13
points 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 the int 0x13
handler 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-generic
that 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 custom int 0x13
handler. 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 0x13
handler so that it will apply our patch to thevmlinuz
file 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
vmlinuz
file from the disk, but our customint 0x13
handler applies our patches to the kernel before returning. - GRUB jumps to the
vmlinuz
entry point, which decompresses the kernel image. Our modifications tovmlinuz
cause it to overwrite a string constant with ourexec_packet
function 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 andcompiler 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.