From SMM to userland in a few bytes
10 Jan 2016In 2014, @coreykal, @xenokovah, @jwbutterworth3 @ssc0rnwell gave a talk entitled Extreme Privilege Escalation on Windows 8/UEFI Systems at Black Hat USA. They introduced the idea of a SMM rootkit called The Watcher slides (57 to 63). To sum it up:
- The Watcher lives in SMM (where you can’t look for him)
- It has no build-in capability except to scan memory for a magic signature
- If it finds the signature, it treats the data immediately after the signature as code to be executed
- In this way the Watcher performs arbitrary code execution on behalf of some controller.
This idea is awesome, and I wanted to try to implement it on Linux. Actually, it was far more easier than expected, thanks to QEMU and SeaBIOS.
SMM rootkit
The BIOS flash is initially modified in order to have a malicious code executed in SMM by the SMI handler, which scans memory for a magic signature on a regular basis.
Periodic SMI generation
In order to scan memory regularly, the SMI handler must be called
regularly. There’s a mechanism that’s exactly meant for this: the Periodic SMI#
Rate Select
, through GEN_PMCON_1
register
(slide 18).
It guarantees the generation of a SMI at least at least once every 8, 16, 32, or
64 seconds. On a modern CPU, it’s straightforward to use this register to
ensures that the SMM handler is called regularly. Nevertheless, it doesn’t seem
to exist on the emulated CPU (Intel® 440FX) of the virtual machine.
But this isn’t the only manner to call the SMI handler regularly. The APIC can be configured to redirect IRQ to SMM. In fact, the I/O APIC defines a redirection table for this purpose. This technique was already used by chpie and SMM-Rootkits-Securecom08.pdf to implement a keylogger in 2008. Actually, it can also be used to call a SMI handler regularly.
For instance, IRQs on ata_piix
occurs quite frequently (about every 2 seconds)
in my VM:
$ grep ata_piix /proc/interrupts
14: 4357 IO-APIC-edge ata_piix
ATA PIIX seems to be the Intel PATA/SATA controller. This looks like a great opportunity to let our SMI handler be called regularly by redirecting IRQ #14 to SMM.
The Intel® 82093AA I/O APIC Datasheet describes how to use the I/O redirection table registers. The registers are 64 bits wide. Basically, they’re just set to the interrupt vector.
For example, IOREDTBL 14 = 0x000000000000003e
. But one can change the
Delivery Mode to another value, for example 0b010
to deliver a SMI:
Once the SMI handler is called, the interrupt must be sent to the Local APIC through an IPI. Simply write to the Local APIC registers:
#define LOCAL_APIC_BASE (void *)0xfee00000
#define INTERRUPT_VECTOR 0x3e
static void forward_interrupt(void)
{
uint32_t volatile *a, *b;
unsigned char *p;
p = LOCAL_APIC_BASE;
b = (void *)(p + 0x310);
a = (void *)(p + 0x300);
*b = 0x00000000;
*a = INTERRUPT_VECTOR << 0;
}
The IRQ redirection must be created once the OS finished to configure APIC, otherwise it may be overwritten.
If you’re too lazy to read MSRs to get IOAPIC and Local APIC memory addresses, they’re located here:
$ grep APIC /proc/iomem
fec00000-fec003ff : IOAPIC 0
fee00000-fee00fff : Local APIC
Memory scan from the SMM handler
Since the SMM handler is called regularly, it must be as fast as possible. The memory scan is easy: just walk through all the physical pages. Even if not bullet-proof, the techniques from OS Dev.org are sufficient for a proof of concept. If one page starts with the magic signature, the payload located just after is executed.
Payloads
SMM payload
The SMM payload must be as simple as possible since SMM handlers execute in with paging disabled, no interruptions, etc.
Fortunately, the VDSO library is mmaped in every userland processes. A few
syscalls (on x64:
clock_gettime
, getcpu
, gettimeofday
, time
) use VDSO to be
faster. One can dump the VDSO of a random process to take a look at it:
$ gdb /bin/ls
gdb$ b __libc_start_main
gdb$ r
Breakpoint 1, __libc_start_main
gdb$ dump_binfile vdso 0x7ffff7ffa000 0x7ffff7ffc000
gdb$ q
and notice that clock_gettime
is right at the entrypoint address:
$ readelf -a vdso | grep Entry
Entry point address: 0xffffffffff700700
$ gdb vdso
gdb$ x/5i 0xffffffffff700700
0xffffffffff700700 <clock_gettime>: push rbp
0xffffffffff700701 <clock_gettime+1>: mov rbp,rsp
0xffffffffff700704 <clock_gettime+4>: push r15
0xffffffffff700706 <clock_gettime+6>: push r14
0xffffffffff700708 <clock_gettime+8>: push r13
gdb$
Moreover, there is about 2800 bytes of free space to put some code at the end of the mapping:
$ hd vdso | tail -3
00001510 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00002000
All of this make VDSO looks like a promising target. VDSO is easy to fingerprint
in memory because the first bytes are the ELF header, and a few strings are
always present. Since clock_gettime
is the entrypoint of the library,
there’s no need to implement an ELF parser in ring-2. Finally, VDSO library is
mapped on 2 consecutive physical pages. One don’t have mess with the page
tables of the current process to find the second page.
Here’s the plan: once the magic is found by the SMI handler at the beginning of
a physical page, the code located after it is executed. The code executed in SMM
walk (again) through the physical pages to find the 2 VDSO’s consecutive
physical pages. Finally, the prologue of clock_gettime
is hijacked to a custom
userland payload written at the end of VDSO.
This SMM payload written in C and compiled with metasm is about 750 bytes long.
Once again, this is sufficient for a proof of concept, but a fully weaponized exploit should parse the VDSO ELF carefully. One could note that once this SMM payload is executed, all present and future userland processes will be backdoored and the IRQ redirection can be safely removed.
Userland payload
The userland payload will be called every time a process makes a call to one of
the previously mentioned system call. Since clock_gettime()
might be called
frequently by different processes, we don’t want to overload the machine with a
lot of Python processes. Thus, the payload checks if the file /tmp/.x
exists,
and in that case does nothing because it has already been executed. On the other
hand if /tmp/.x
doesn’t exist, the payload forks:
- the parent restore registers before returning to
clock_gettime()
, - the child executes a python one-liner which creates
/tmp/.x
and does a TCP reverse shell to the attacker.
This userland payload, written in assembly, is 280 bytes long.
Trigger
For a local attacker, it’s trivial to execute code in SMM: just mmap a page and
copy MAGIC+CODE
. That’s it.
For a remote attacker, there are several ways to reach one’s goal. For example,
send a lot of UDP packets containing MAGIC+CODE
prefixed with random padding
(between 0
and PAGE_SIZE-sizeof(CODE)
bytes) to any UDP port. With a bit
of luck, it triggers the SMM handler in a few seconds.
Waiting for the shell
Once the SMM payload is executed, the attacker only have to wait for the reverse shell. It can take a few seconds to a few minutes given the running processes on the machine.
An impatient attacker may want to check if everything went well by triggering
one of the VDSO syscalls. For instance, one can try to login to a ssh server
with an invalid account. OpenSSH server calls clock_gettime()
before writing
the log entry, which triggers the execution of the userland payload by any
process.
A non-root process may however call clock_gettime()
, and the reverse shell
would not have root privileges. The uid of the process may be checked in the
payload, but to save a few bytes, the attacker may also remove /tmp/.x
and
wait for a new reverse shell.
Eventually, here’s a screenshot of the backdoor. On the left, the debug log
/tmp/bios.log
of the virtual machine whose SMM is backdoored. On the right a
Python script wich send in a loop the payload to be executed by the SMI handler
in the hope of being mmaped at the very beginning of a page. And at the bottom,
the reverse shell from a root process of the VM whose VDSO has been hijacked:
Conclusion
The idea of The Watcher is practical on Linux: it’s pretty straightforward to execute code in userland from SMM reliably, thanks to VDSO. The code of SeaBIOS is modified to include a malicious SMI handler, and the memory of the OS is never altered until the attacker manages to put the payload in memory. The payload size is no longer than 1084 bytes, and can be injected through the network even if not port is open.
Nevertheless, SeaBIOS’ SMM support is basic, and I didn’t find a way to
automatically install The Watcher at the boot of the machine (there should be
a more elegant way than a bootkit). At present, a SMI must be issued
(outb(0xXY, 0xb2)
) to start The Watcher.
The code of this proof-of-concept is available on github: the-sea-watcher.
On a more encouraging note,
spender mentioned
a simple trick to potentially determine if a machine is infected by a SMM
rootkit: just count the number of SMIs since the last reset of the machine, with
MSR_SMI_COUNT
(since Nehalem):
$ sudo aptitude install linux-tools-common linux-tools-generic
$ wget http://kernel.ubuntu.com/git/cking/debug-code/.git/tree/smistat/smistat.c http://kernel.ubuntu.com/git/cking/debug-code/.git/tree/smistat/Makefile
$ make
$ sudo modprobe msr
$ sudo ./smistat
Time SMIs
02:21:41 268
02:21:42 268
02:21:43 268
Update: as
pointed out by @XenoKovah,
the MSR_SMI_COUNT
isn’t a meaningful detector.