NOWSECURE NOW AVAILABLE IN THE MICROSOFT AZURE MARKETPLACE

Microsoft Azure customers gain access to NowSecure Mobile App Security and Privacy Testing for scalability, reliability, and agility of Azure to drive mobile appdev and shape business strategies.

Media Announcement
NOWSECURE NOW AVAILABLE IN THE MICROSOFT AZURE MARKETPLACE NOWSECURE NOW AVAILABLE IN THE MICROSOFT AZURE MARKETPLACE Show More
magnifying glass icon

Getting sys_call_table on Android

Posted by
Sebastián Guerrero Selma

Sebastian Guerrero

Senior Mobile Security Analyst at NowSecure
Sebastián's work includes mobile and web security research, developing tools and techniques for assessing vulnerabilities and performing post-exploitation of mobile devices and apps, and reverse engineering embedded and mobile platforms.

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:

exploiting-sys-android-table

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:&lt;/strong&gt;

&lt;strong&gt;__kuser_memory_barrier: @ 0xffff0fa0

#if <strong>LINUX_ARM_ARCH</strong> _&gt;;= 6 &amp;&amp; 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(&amp;vector_table_instruction, vector_table_address, sizeof(vector_table_instruction));
printk(KERN_INFO &quot;_&gt;; 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 &quot;„&gt;; Loading Modulenî);
printk(KERN_INFO &quot;„&gt;; Done.nî);
vector_table();
}

static int __exit debug_stop() {
printk(KERN_INFO &quot;„&gt;; 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&quot;
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(&amp;vector_swi_instruction, swi_address, sizeof(vector_swi_instruction));
printk(KERN_INFO &quot;„&gt;;;DEBUG: Vector SWI Instruction: %lxnî, vector_swi_instruction);

vector_swi_offset = vector_swi_instruction &amp; (unsigned long)0x00000FFF;
printk(KERN_INFO &quot;„&gt;;;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 &quot;„&gt;;;DEBUG: Vector SWI Address Pointer %p, Value: %lxnî, vector_swi_pointer, </em>vector_swi_pointer);

ptr = *vector_swi_pointer;

printk(KERN_INFO &quot;„„„-&gt;;;DEBUG: Vector SWI Handlernî);
while(ptr != 0xc0026fb4) {
memcpy(&amp;vector_swi_instruction, ptr, sizeof(vector_swi_instruction));
printk(KERN_INFO &quot;„&gt;;;DEBUG: Vector SWI Address Pointer %p, Value: %lxnî, ptr, *ptr);
ptr++;
}

memcpy(&amp;vector_swi_instruction, ptr, sizeof(vector_swi_instruction));
printk(KERN_INFO &quot;„&gt;;;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(&amp;vector_swi_instruction, swi_address, sizeof(vector_swi_instruction));
printk(KERN_INFO &quot;„-&gt;;DEBUG: Vector SWI Instruction: %lxnî, vector_swi_instruction);

vector_swi_offset = vector_swi_instruction &amp; (unsigned long)0x00000FFF;
printk(KERN_INFO &quot;„-&gt;;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 &quot;„-&gt;;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 &amp; (unsigned long)0xFFFFFF000) == 0xE28F8000) {
syscall_table_offset = </em>ptr &amp; (unsigned long)0x00000FFF;
syscall = (unsigned long)ptr+8+syscall_table_offset;
printk(KERN_INFO &quot;„-&gt;;;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.