Below is a detailed HOWTO of getting ‘sys_call_table’ on an Android device.
About me: My name is Sebastiàn Guerrero, and I’m a mobile researcher with viaForensics. You can find me on twitter @0xroot.
After a few days of looking at the handling of interrupts and exceptions in ARM in order to understand how it works, I’ve come to implement a small module that can be used to get the sys_call_table and use it for hooking the syscalls and implementing a rootkit.
The purpose of this article is to provide a brief introduction and share step-by-step how I conducted my research. At the end, I’ll show two pieces of code – one to retrieve a reverse TCP shell when a device receives an SMS from a known number, the other to get the syscalls used by an application.
I’ve only scratched the surface of this topic, so if you want to go deeper I suggest reading this paper: “Exploiting ARM Linux Systems“.
Exceptions in ARM
An exception is classified as any condition that halts normal execution of the instructions set. Examples of exceptions include failures in fetching instructions or memory access, when an external interrupt is raised, or when a software interrupt instruction is executed.
Usually, after each exception there is a program function commonly named an ‘exception handler’. Each of the ARM exceptions causes the ARM core to enter a certain mode, inferring in its behavior.
Without going into too much detail on ARM architecture, we find the following handlers with their associated mode of operation for every ARM processor:
Vector table
When an exception or interrupt occurs, usually in ARM processors, the execution flow is passed to the Exception Vector Table (EVT), where we can find an exception handler associated to each type of exception. There, we can find different definite routines and instructions that determine what the behavior will be in the following steps.
Through the constant ‘CONFIG_VECTOR_BASE‘ , we force the EVT to be loaded in the low vector address (0xFFFF0000) or high vector address (0x0000FFFF), in the current case, the first one.
In Android, this content is declared in the file ‘entry-armv.S‘, processed and copied to the EVT by the method ‘early_trap_init()‘, then declared in the file ‘traps.c‘.
void <strong>init early_trap_init(void) { unsigned long vectors = CONFIG_VECTORS_BASE; extern char </strong>stubs_start[], <strong>stubs_end[]; extern char </strong>vectors_start[], <strong>vectors_end[]; extern char </strong>kuser_helper_start[], <strong>kuser_helper_end[]; int kuser_sz = </strong>kuser_helper_end - __kuser_helper_start; /* <ul> <li>Copy the vectors, stubs and kuser helpers (in entry-armv.S)</li> <li>into the vector page, mapped at 0xffff0000, and ensure these</li> <li>are visible to the instruction stream. <em>/ memcpy((void </em>)vectors, <strong>vectors_start, </strong>vectors_end - <strong>vectors_start); memcpy((void *)vectors + 0x200, </strong>stubs_start, <strong>stubs_end - </strong>stubs_start); memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);</li> </ul> /* <ul> <li>Copy signal return handlers into the vector page, and</li> <li>set sigreturn to be a pointer to these. <em>/ memcpy((void </em>)KERN_SIGRETURN_CODE, sigreturn_codes, sizeof(sigreturn_codes));</li> </ul> flush_icache_range(vectors, vectors + PAGE_SIZE); modify_domain(DOMAIN_USER, DOMAIN_CLIENT); }
As you can see, the first thing that is done is to copy the exception vectors, and helpers for ‘stubs’and ‘kuser’on vectorÍs page mapped at 0xFFFF0000.
Looking in the kernel source, we can extract the values assigned to the constants appearing in the code:
- __vectors_start : 0xC000F1E4
- __vectors_end : 0xC000F204
- __stubs_start : 0xC000EFC0
- __stubs_end : 0xC000F1E4 (Fix this)
- __kuser_helper_start : 0xC000EF60
- __kuser_helper_end : 0xC000EFC0
Looking now at the ‘entry-armv.S‘ with the new information we have, we note that the content loaded into 0xFFFF0000 with:
[C]memcpy((void *)vectors, vectors_start, vectors_end – vectors_start);[/C]
Corresponds to:
vectors_start: swi SYS_ERROR0 b vector_und + stubs_offset ldr pc, .LCvswi + stubs_offset b vector_pabt + stubs_offset b vector_dabt + stubs_offset b vector_addrexcptn + stubs_offset b vector_irq + stubs_offset b vector_fiq + stubs_offset .globl <strong>vectors_end </strong>vectors_end:
Furthermore, from the address 0xFFFF0200
until the offset defined at 0x224
( stubs_end - stubs_start
):
memcpy((void *)vectors + 0x200, <strong>stubs_start, </strong>stubs_end - <strong>stubs_start);
It will be filled with:
line 1057: </strong>stubs_start: /* <ul> <li>Interrupt dispatcher */ vector_stub irq, IRQ_MODE, 4</li> </ul> .long <strong>irq_usr @ 0 (USR_26 / USR_32) .long </strong>irq_invalid @ 1 (FIQ_26 / FIQ_32) .long <strong>irq_invalid @ 2 (IRQ_26 / IRQ_32) .long </strong>irq_svc @ 3 (SVC_26 / SVC_32) .long <strong>irq_invalid @ 4 .long </strong>irq_invalid @ 5 .long <strong>irq_invalid @ 6 .long </strong>irq_invalid @ 7 .long <strong>irq_invalid @ 8 .long </strong>irq_invalid @ 9 .long <strong>irq_invalid @ a .long </strong>irq_invalid @ b .long <strong>irq_invalid @ c .long </strong>irq_invalid @ d .long <strong>irq_invalid @ e .long </strong>irq_invalid @ f line 1185: <strong>stubs_end:
Similarly, the same happens with the following case from the address 0xFFFF0FA0 until the offset defined at 0x60 ( kuser_helper_end – kuser_helper_start ):
memcpy((void *)vectors + 0x1000 – kuser_sz, kuser_helper_start, kuser_sz);
It’s filled with:
line 769: __kuser_helper_start:</strong> <strong>__kuser_memory_barrier: @ 0xffff0fa0 #if <strong>LINUX_ARM_ARCH</strong> _>;= 6 && defined(CONFIG_SMP) mcr p15, 0, r0, c7, c10, 5 @ dmb #endif usr_ret lr .align 5 line 1007: __kuser_helper_end:
Getting sys_call_table
Our goal so far has been to introduce and explain how the EVT is filled with instructions. As we discussed previously, every time an exception is produced it is high and controlled by predefined handlers for it.
The technique used to perform our task described below is based on obtaining the sys_call_table address. The sys_call_table address can be collected from the routines defined for the exception handler for interrupts committed by software, also known as the handler for “vector_swiî.
Looking at the first EVT addresses, using the following code as a LKM loaded and used by the phone:
/<em> LKM - debug_evt Author: Sebastiàn Guerrero </em>/ #include #include #include #include void vector_table(); void vector_table(){ unsigned long* vector_table_address = 0xFFFF0000; unsigned long vector_table_instruction; while(vector_table_address != 0xFFFF1000) { memcpy(&vector_table_instruction, vector_table_address, sizeof(vector_table_instruction)); printk(KERN_INFO "_>; DEBUG: Vector Table Address: %lx, Vector Table Instruction; %lxnî, vector_table_address, vector_table_instruction); vector_table_address += 1; } } static int __init debug_start() { printk(KERN_INFO ">; Loading Modulenî); printk(KERN_INFO ">; Done.nî); vector_table(); } static int __exit debug_stop() { printk(KERN_INFO ">; Bye Byenî); } module_init (debug_start); module_exit (debug_stop);
Relying again on radare2:
>; Loading Module >; Done. _>; DEBUG: Vector Table Address: ffff0000, Vector Table Instruction; ef9f0000 _>; DEBUG: Vector Table Address: ffff0004, Vector Table Instruction; ea0000dd _>; DEBUG: Vector Table Address: ffff0008, Vector Table Instruction; e59ff410 _>; DEBUG: Vector Table Address: ffff000c, Vector Table Instruction; ea0000bb _>; DEBUG: Vector Table Address: ffff0010, Vector Table Instruction; ea00009a _>; DEBUG: Vector Table Address: ffff0014, Vector Table Instruction; ea0000fa _>; DEBUG: Vector Table Address: ffff0018, Vector Table Instruction; ea000078 _>; DEBUG: Vector Table Address: ffff001c, Vector Table Instruction; ea0000f7 rasm2 -e -d -a arm ef9f0000: svc 0x009f0000 rasm2 -e -d -a arm ea0000dd: b 0x0000037c rasm2 -e -d -a arm e59ff410: ldr pc, [pc, 0x410] rasm2 -e -d -a arm ea0000bb: b 0x000002f4 rasm2 -e -d -a arm ea00009a: b 0x00000270 rasm2 -e -d -a arm ea0000fa: b 0x000003f0 rasm2 -e -d -a arm ea000078: b 0x000001e8 rasm2 -e -d -a arm ea0000f7: b 0x000003e4
We know that every time there is a software interrupt, an instruction of 4 bytes will be executed at the address 0xFFFF0008
, adding to the current value of PC the offset 0x410
and jumping to the handler for “vector_swi” at 0xFFFF0420
. After this, the instructions set defined in the file entry-common.S
will be executed.
At this point, if you look at the different exceptions introduced at the beginning of this article and compare its values with the first one stored in the EVT, what we have is:
0xFFFF0000 - RESET : svc 0x009F0000 0xFFFF0004 - Undefined Instruction : b 0x0000037C 0xFFFF0008 - Software Interrupt : ldr pc, [pc, 0x410] 0xFFFF000C - Abort (prefetch) : b 0x000002F4 0xFFFF0010 - Abort (data) : b 0x00000270 0xFFFF0014 - Reserved : b 0x000003F0 0xFFFF0018 - IRQ : b 0x000001E8 0xFFFF001C - IFQ : b 0x000003E4
Moreover, there are several ways in ARM to perform a jump to other memory addresses, namely:
- b
-This instruction is used to make branching to the memory location with “addressî relative to the current location of the PC. - LDR pc, [pc, #offset] – This instruction is used to load in the program counter register its old value plus an offset value equal to ‘offsetÍ.
- LDR pc, [pc, #-0xFF0] – This instruction is used only when an interrupt controller is available, to load a specific ISR address from the vector table.
- MOV pc, #immediate – Load in the program counter the value “immediate”.
Returning to the previous point, if we analyze the source code defined in entry-common.S
, specifically the part relating to ENTRY (vector_swi)
:
ENTRY(vector_swi) sub sp, sp, #S_FRAME_SIZE stmia sp, {r0 - r12} @ Calling r0 - r12 add r8, sp, #S_PC stmdb r8, {sp, lr}^ @ Calling sp, lr mrs r8, spsr @ called from non-FIQ mode, so ok. str lr, [sp, #S_PC] @ Save calling PC str r8, [sp, #S_PSR] @ Save CPSR str r0, [sp, #S_OLD_R0] @ Save OLD_R0 zero_fp /* <ul> <li>Get the system call number. */</li> </ul> #if defined(CONFIG_OABI_COMPAT) /* <ul> <li>If we have CONFIG_OABI_COMPAT then we need to look at the swi</li> <li>value to determine if it is an EABI or an old ABI call. */ #ifdef CONFIG_ARM_THUMB tst r8, #PSR_T_BIT movne r10, #0 @ no thumb OABI emulation ldreq r10, [lr, #-4] @ get SWI instruction #else ldr r10, [lr, #-4] @ get SWI instruction A710( and ip, r10, #0x0f000000 @ check for SWI ) A710( teq ip, #0x0f000000 ) A710( bne .Larm710bug ) #endif</li> </ul> #elif defined(CONFIG_AEABI) /* <ul> <li>Pure EABI user space always put syscall number into scno (r7). */ A710( ldr ip, [lr, #-4] @ get SWI instruction ) A710( and ip, ip, #0x0f000000 @ check for SWI ) A710( teq ip, #0x0f000000 ) A710( bne .Larm710bug )</li> </ul> #elif defined(CONFIG_ARM_THUMB) /<em> Legacy ABI only, possibly thumb mode. </em>/ tst r8, #PSR_T_BIT @ this is SPSR from save_user_regs addne scno, r7, #__NR_SYSCALL_BASE @ put OS number in ldreq scno, [lr, #-4] #else /<em> Legacy ABI only. </em>/ ldr scno, [lr, #-4] @ get SWI instruction A710( and ip, scno, #0x0f000000 @ check for SWI ) A710( teq ip, #0x0f000000 ) A710( bne .Larm710bug ) #endif #ifdef CONFIG_ALIGNMENT_TRAP ldr ip, __cr_alignment ldr ip, [ip] mcr p15, 0, ip, c1, c0 @ update control register #endif enable_irq get_thread_info tsk adr tbl, sys_call_table @ load syscall table pointer ldr ip, [tsk, #TI_FLAGS] @ check for syscall tracing #if defined(CONFIG_OABI_COMPAT) /* <ul> <li>If the swi argument is zero, this is an EABI call and we do nothing. *</li> <li>If this is an old ABI call, get the syscall number into scno and</li> <li>get the old ABI syscall table address. */ bics r10, r10, #0xff000000 eorne scno, r10, #<strong>NR_OABI_SYSCALL_BASE ldrne tbl, =sys_oabi_call_table #elif !defined(CONFIG_AEABI) bic scno, scno, #0xff000000 @ mask off SWI op-code eor scno, scno, #</strong>NR_SYSCALL_BASE @ check OS number #endif</li> </ul> stmdb sp!, {r4, r5} @ push fifth and sixth args tst ip, #_TIF_SYSCALL_TRACE @ are we tracing syscalls? bne __sys_trace cmp scno, #NR_syscalls @ check upper syscall limit adr lr, ret_fast<em>syscall @ return address ldrcc pc, [tbl, scno, lsl #2] @ call sys</em>* routine add r1, sp, #S_OFF 2: mov why, #0 @ no longer a real syscall cmp scno, #(<strong>ARM_NR_BASE - </strong>NR_SYSCALL_BASE) eor r0, scno, #__NR_SYSCALL_BASE @ put OS number back bcs arm_syscall b sys_ni_syscall @ not private func ENDPROC(vector_swi)
We can see that in line 254, an instruction is executed to load the pointer containing the ‘syscall_table‘.
line:245 adr tbl, sys_call_table - load syscall table pointer
Our goal is to get that subroutine, and obtain the value that is being loaded, that way we can look to the real sys_call_table and achieve our objective.
At this point we are presented with a problem. We know the initial address where the ‘vector_swi‘ starts, but donÍt know where it ends since it is impossible to get access to the content directly. Furthermore, there is not an ARM instruction like ‘ret’implemented, so we canÍt directly reference the content returned by the subroutine.
We could reiterate the whole EVT using the address 0xFFFF0420 as an entry point and start searching from there, but itÍs a tedious and unnecessary process. Instead, I suggest finding out the end and then proceed to narrow the vector.
If we look carefully at the source code ‘entry-common.S‘ again, we can observe after the statement:
ENDPROC(vector_swi)
We load a new operative:
sys_trace
If we look for this value in the memory address mapped in the file System.map
, we will get the following value:
cat System.map | grep '</strong>sys_trace" c0026fb4 t <strong>sys_trace c0026fe0 t </strong>sys_trace_return
Modifying the snippet code I put previously for a dump of EVT and adding a few lines:
void vector_swi() { unsigned long <em>swi_address = 0xFFFF0008; unsigned long vector_swi_offset = 0; unsigned long vector_swi_instruction = 0; unsigned long </em>vector_swi_pointer = NULL; unsigned long *ptr = NULL; memcpy(&vector_swi_instruction, swi_address, sizeof(vector_swi_instruction)); printk(KERN_INFO ">;;DEBUG: Vector SWI Instruction: %lxnî, vector_swi_instruction); vector_swi_offset = vector_swi_instruction & (unsigned long)0x00000FFF; printk(KERN_INFO ">;;DEBUG: Vector SWI Offset: 0x%lxnî, vector_swi_offset); vector_swi_pointer = (unsigned long <em>)((unsigned long)swi_address+vector_swi_offset+8); printk(KERN_INFO ">;;DEBUG: Vector SWI Address Pointer %p, Value: %lxnî, vector_swi_pointer, </em>vector_swi_pointer); ptr = *vector_swi_pointer; printk(KERN_INFO "->;;DEBUG: Vector SWI Handlernî); while(ptr != 0xc0026fb4) { memcpy(&vector_swi_instruction, ptr, sizeof(vector_swi_instruction)); printk(KERN_INFO ">;;DEBUG: Vector SWI Address Pointer %p, Value: %lxnî, ptr, *ptr); ptr++; } memcpy(&vector_swi_instruction, ptr, sizeof(vector_swi_instruction)); printk(KERN_INFO ">;;DEBUG: Vector SWI Address Pointer %p, Value: %lxnî, ptr, <em>ptr); }
We’ve managed to solve one of the problems we mentioned above by finding the end of ‘vector_swi‘ and ignoring everything that was of no interest to us. In fact, if we look at the output this is considerably reduced in comparison with the full log
>;; Loading Module >; Done. >;DEBUG: Vector SWI Instruction: e59ff410 >;DEBUG: Vector SWI Offset: 0x410 >;DEBUG: Vector SWI Address Pointer ffff0420, Value: c0026f40 >;DEBUG: Vector SWI Handler >;DEBUG: Vector SWI Address Pointer c0026f40, Value: e24dd048 >;DEBUG: Vector SWI Address Pointer c0026f44, Value: e88d1fff >;DEBUG: Vector SWI Address Pointer c0026f48, Value: e28d803c >;DEBUG: Vector SWI Address Pointer c0026f4c, Value: e9486000 >;DEBUG: Vector SWI Address Pointer c0026f50, Value: e14f8000 >;DEBUG: Vector SWI Address Pointer c0026f54, Value: e58de03c >;DEBUG: Vector SWI Address Pointer c0026f58, Value: e58d8040 >;DEBUG: Vector SWI Address Pointer c0026f5c, Value: e58d0044 >;DEBUG: Vector SWI Address Pointer c0026f60, Value: e3a0b000 >;DEBUG: Vector SWI Address Pointer c0026f64, Value: e59fc094 >;DEBUG: Vector SWI Address Pointer c0026f68, Value: e59cc000 >;DEBUG: Vector SWI Address Pointer c0026f6c, Value: ee01cf10 >;DEBUG: Vector SWI Address Pointer c0026f70, Value: e321f013 >;DEBUG: Vector SWI Address Pointer c0026f74, Value: e1a096ad >;DEBUG: Vector SWI Address Pointer c0026f78, Value: e1a09689 >;DEBUG: Vector SWI Address Pointer c0026f7c, Value: e28f8080 >;DEBUG: Vector SWI Address Pointer c0026f80, Value: e599c000 >;DEBUG: Vector SWI Address Pointer c0026f84, Value: e92d0030 >;DEBUG: Vector SWI Address Pointer c0026f88, Value: e31c0c01 >;DEBUG: Vector SWI Address Pointer c0026f8c, Value: 1a000008 >;DEBUG: Vector SWI Address Pointer c0026f90, Value: e3570f5b >;DEBUG: Vector SWI Address Pointer c0026f94, Value: e24fef47 >;DEBUG: Vector SWI Address Pointer c0026f98, Value: 3798f107 >;DEBUG: Vector SWI Address Pointer c0026f9c, Value: e28d1008 >;DEBUG: Vector SWI Address Pointer c0026fa0, Value: e3a08000 >;DEBUG: Vector SWI Address Pointer c0026fa4, Value: e357080f >;DEBUG: Vector SWI Address Pointer c0026fa8, Value: e2270000 >;DEBUG: Vector SWI Address Pointer c0026fac, Value: 2a000f9d >;DEBUG: Vector SWI Address Pointer c0026fb0, Value: ea00b836 >;DEBUG: Vector SWI Address Pointer c0026fb4, Value: e1a02007
The next point in this output is how to detect and resolve identify the opcode in charge to load the sys_call_table. We know that the instruction we seek is ‘adr‘, which is really a combination of ‘add‘ and ‘*ldr‘.
If we use radare2 again, in order to transform opcodes into instructions:
e24dd048 sub sp, sp, 0x48 e88d1fff stm sp, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip} e28d803c add r8, sp, 0x3c e9486000 stmdb r8, {sp, lr} e14f8000 mrs r8, SPSR e58de03c str lr, [sp, 0x3c] e58d8040 str r8, [sp, 0x40] e58d0044 str r0, [sp, 0x44] e3a0b000 mov fp, 0x0 e59fc094 ldr ip, [pc, 0x94] e59cc000 ldr ip, [ip] ee01cf10 mcr 15, 0, ip, cr1, cr0, {0} e321f013 msr CPSR_c, 0x13 e1a096ad lsr r9, sp, 13 e1a09689 lsl r9, r9, 13 e28f8080 add r8, pc, 0x80 e599c000 ldr ip, [r9] e92d0030 push {r4, r5} e31c0c01 tst ip, 0x100 1a000008 bne 0x00000028 e3570f5b cmp r7, 0x16c e24fef47 sub lr, pc, 0x11c 3798f107 ldrcc pc, [r8, r7, lsl 2] e28d1008 add r1, sp, 0x8 e3a08000 mov r8, 0x0 e357080f cmp r7, 0xf0000 e2270000 eor r0, r7, 0x0 2a000f9d bcs 0x00003e7c ea00b836 b 0x0002e0e0 e1a02007 mov r2, r7
The opcode we are looking for specifically is E28F8080
, corresponding to the instruction add r8, pc, 0x80
, which is adding 0x80
as an offset to the current value of PC
.
After this process we have found the solution weÍre looking for by implementing the following code snippet:
unsigned long<em> syscall_table() { unsigned long </em>swi_address = 0xFFFF0008; unsigned long vector_swi_offset = 0; unsigned long vector_swi_instruction = 0; unsigned long <em>vector_swi_pointer = NULL; unsigned long </em>ptr = NULL; unsigned long *syscall = NULL; unsigned long syscall_table_offset = 0; memcpy(&vector_swi_instruction, swi_address, sizeof(vector_swi_instruction)); printk(KERN_INFO "->;DEBUG: Vector SWI Instruction: %lxnî, vector_swi_instruction); vector_swi_offset = vector_swi_instruction & (unsigned long)0x00000FFF; printk(KERN_INFO "->;DEBUG: Vector SWI Offset: 0x%lxnî, vector_swi_offset); vector_swi_pointer = (unsigned long <em>)((unsigned long)swi_address+vector_swi_offset+8); printk(KERN_INFO "->;DEBUG: Vector SWI Address Pointer %p, Value: %lxnî, vector_swi_pointer, </em>vector_swi_pointer); ptr = *vector_swi_pointer; while(syscall == NULL) { if((<em>ptr & (unsigned long)0xFFFFFF000) == 0xE28F8000) { syscall_table_offset = </em>ptr & (unsigned long)0x00000FFF; syscall = (unsigned long)ptr+8+syscall_table_offset; printk(KERN_INFO "->;;DEBUG: Syscall Table Found at %pnî, syscall); break; } ptr++; } return syscall; }
We will get as output:
>; Loading Module >; Done. >;DEBUG: Vector SWI Instruction: e59ff410 >;DEBUG: Vector SWI Offset: 0x410 >;DEBUG: Vector SWI Address Pointer ffff0420, Value: c0026f40 >;DEBUG: Syscall Table Found at c0027004
Showing the sys_call_table address at 0xC0027004.
Related Content

