Home System Programming Embedding in the Linux kernel:intercepting system calls

Embedding in the Linux kernel:intercepting system calls

by admin

The term "system call" in programming and computer science refers to an application program’s call to the operating system (OS) kernel to perform some operation. Since this is a basic interaction, intercepting system calls is a very important stage of the embedding process because it allows controlling the key component of the OS kernel which is the system call interface and this in turn allows inspecting the application software’s requests to the kernel services.
This article is a continuation of the of the previously announced series dedicated to specific issues of implementation of superimposed protections and, in particular, of embedding them into software systems.

I. Approaches to embedding

There are different ways to intercept Linux kernel system calls. First of all, it is worth mentioning that for intercepting single system calls the following can be used previously method of intercepting kernel functions. Indeed, by virtue of the fact that most system calls are represented by the corresponding functions (e.g, sys_open ), the task of intercepting them is equivalent to the task of intercepting these functions. However, as the number of intercepted system calls increases and "business logic" becomes more complex, this approach may be limited.
A more universal way to modify entries in system call tables (more about tables below) that contain pointers to functions that implement the logic of a particular system call. The tables are used by the kernel for dispatching, when the handler function’s pointer is selected from the corresponding table by the number of the system call requested by your application and then executed. Replacing such a pointer allows you to modify the kernel’s logic when it comes to handling system calls. Getting ahead of ourselves, it should be noted that the tables themselves will have to be found somehow in order to successfully implement this method, as they are not exported. Ultimately, intercepting a system call will consist of simply redefining the table element.
The most universal way to intercept system calls has been and still is to modify the system calls manager’s code to provide pre- and postprocessing of the context of the thread requesting some system service. This variant gives more flexibility in comparison to previous ones, because it introduces single state control points before and after the handler function.
Further on example will show in detail how to embed the Linux kernel system call interface by modifying the dispatcher code.

II. Dispatching system calls in the Linux kernel

The dispatching of system calls is a rather complicated process with many nuances, but within this article we omit many details, because apart from the dispatching process itself (the selection and execution of the corresponding system call function), there is nothing else you need to know for embedding.
Traditionally, the Linux kernel supports the following system call options for the x86 architecture:

  • instruction INT 80h (32-bit interface, native call or emulation);
  • SYSENTER instruction (32-bit interface, native call or emulation);
  • SYSCALL instruction (64-bit interface, native call or emulation).

Below is borrowed from by me an excellent illustration of the implementation of a system call depending on the variant used :
Embedding in the Linux kernel:intercepting system calls
As you can see, 32-bit applications execute system calls using the INT 80h and SYSENTER mechanisms while 64-bit ones do it using SYSCALL. Moreover, there is support of executing 32-bit code in the 64-bit environment (the so called compatibility mode; the kernel option CONFIG_IA32_EMULATION ). Because of this, there are 2 non-exportable tables in the kernel sys_call_table and ia32_sys_call_table (only available for emulation mode), which contain the addresses of the system call handlers.
In the general case, when all possible mechanisms are represented in the 64-bit kernel, there are 4 points of input that determine what the logic of the corresponding dispatcher will be :

Either way, when an application makes a system call, the kernel takes control. The system call manager for each of the cases considered has differences from the others, but without loss of generality their general structure can be considered on the example system_call :

0xffffff81731670 <+0> : swapgs0xffffff81731673 <+3> : mov %rsp, %gs:0xc0000xffffff8173167c <+12> : mov %gs:0xc830, %rsp0xffffff81731685 <+21> : sti0xffffff81731686 <+22> : data32 data32 xchg %ax, %ax0xffffff8173168a <+26> : data32 xchg %ax, %ax0xffffff8173168d <+29> : sub $0x50, %rsp0xffffff81731691 <+33> : mov %rdi, 0x40(%rsp)0xffffff81731696 <+38> : mov %rsi, 0x38(%rsp)0xffffff8173169b <+43> : mov %rdx, 0x30(%rsp)0xffffff817316a0 <+48> : mov %rax, 0x20(%rsp)0xffffff817316a5 <+53> : mov %r8, 0x18(%rsp)0xffffff817316aa <+58> : mov %r9, 0x10(%rsp)0xffffff817316af <+63> : mov %r10, 0x8(%rsp)0xffffff817316b4 <+68> : mov %r11, (%rsp)0xffffff817316b8 <+72> : mov %rax, 0x48(%rsp)0xffffff817316bd <+77> : mov %rcx, 0x50(%rsp)0xffffff817316c2 <+82> : testl $0x100801d1, -0x1f78(%rsp)0xffffff817316cd <+93> : jne 0xffffff8173181e <tracesys>0xffffff817316d3 <+0> : and $0xbfffff, %eax0xffffff817316d8 <+5> : cmp $0x220, %eax /* <-------- cmp $__NR_syscall_max, %eax */0xffffff817316dd <+10> : yes 0xffffff817317a5 <badsys>0xffffff817316e3 <+16> : mov %r10, %rcx0xffffff817316e6 <+19> : callq *-0x7e7fec00(, %rax, 8) /* <-------- call *sys_call_table(, %rax, 8)*/0xffffff817316ed <+26> : mov %rax, 0x20(%rsp)0xffffff817316f2 <+0> : mov $0x1008feff, %edi0xffffff817316f7 <+0> : cli0xffffff817316f8 <+1> : data32 data32 xchg %ax, %ax0xffffff817316fc <+5> : data32 xchg %ax, %ax0xffffff817316ff <+8> : mov -0x1f78(%rsp), %edx0xffffff81731706 <+15> : and %edi, %edx0xffffff81731708 <+17> : jne 0xffffff81731745 <sysret_careful>0xffffff8173170a <+19> : mov 0x50(%rsp), %rcx0xffffff8173170f <+24> : mov (%rsp), %r110xffffff81731713 <+28> : mov 0x8(%rsp), %r100xffffff81731718 <+33> : mov 0x10(%rsp), %r90xffffff8173171d <+38> : mov 0x18(%rsp), %r80xffffff81731722 <+43> : mov 0x20(%rsp), %rax0xffffff81731727 <+48> : mov 0x30(%rsp), %rdx0xffffff8173172c <+53> : mov 0x38(%rsp), %rsi0xffffff81731731 <+58> : mov 0x40(%rsp), %rdi0xffffff81731736 <+63> : mov %gs:0xc000, %rsp0xffffff8173173f <+72> : swapgs0xffffff81731742 <+75> : sysretq

As you can see, the first instruction ( swapgs ) switches the data structures (from user to kernel). Then the stack is configured, interrupts are allowed, and the register context of the thread is formed on the stack (the structure pt_regs ), which is necessary in the process of processing. Returning to the above listing, special attention should be paid to the following commands :

0xffffff817316d8 <+5> : cmp $0x220, %eax /* <-------- cmp $__NR_syscall_max, %eax */0xffffff817316dd <+10> : yes 0xffffff817317a5 <badsys>0xffffff817316e3 <+16> : mov %r10, %rcx0xffffff817316e6 <+19> : callq *-0x7e7fec00(, %rax, 8) /* <-------- call *sys_call_table(, %rax, 8)*/0xffffff817316ed <+26> : mov %rax, 0x20(%rsp)

The first line checks whether the number of the requested system call (the register %rax ), the maximum permissible value ( __NR_syscall_max ). If the check succeeds, the dispatching of the system call will be done, i.e., the control will go to the function that implements the corresponding logic.
Thus, the key point of the system call processing is the dispatch command, which is a function call ( call *sys_call_table(, %rax, 8) ). Further embedding will be done by modifying this command.

III. Methodology for implementing embedding

As noted, a universal way to embed the dispatcher would be to modify its code so that it can control the context of the thread before it executes the system call logic function (pre-processing) as well as after it executes it (post-processing).
In order to implement embedding in the described way, it is proposed to slightly patch the dispatcher by modifying the dispatch command ( call *sys_call_table(, %rax, 8) ), and over it write the unconditional jump command ( JMP REL32 ) to the handler service_stub The overall structure of such a handler will look like the following (further pseudocode):

system_call: swapgs..jmp service_stub /*<-------- THERE WAS A call *sys_call_table(, %rax, 8) */mov %rax, 0x20(%rsp) /*<-------- HERE IS THE RETURN FROM service_stub */...swapgssysretqservice_stub:...call ServiceTraceEnter /* void ServiceTraceEnter(struct pt_regs*) */...call sys_call_table[N](args)...call ServiceTraceLeave(regs) /* void ServiceTraceLeave(struct pt_regs *) */...jmp back

Here ServiceTraceEnter() and ServiceTraceLeave() – are pre- and postprocessing functions, respectively. Their parameters are the pointer to pt_regs – register structure that represents the context of the thread. The final instruction is a command to pass control to the system call manager code from which this handler was previously called.
Below is the handler code service_syscall64 used as an example to implement the interception system_call (SYSCALL instruction):

global service_syscall64service_syscall64:SAVE_RESTmovq %rsp, %rdicall ServiceTraceEnterRESTORE_RESTLOAD_ARGS0movq %r10, %rcxmovq ORIG_RAX - ARGOFFSET(%rsp), %raxcall *0x00000000(, %rax, 8) // origin callmovq %rax, RAX - ARGOFFSET(%rsp)SAVE_RESTmovq %rsp, %rdicall ServiceTraceLeaveRESTORE_RESTmovq RAX - ARGOFFSET(%rsp), %raxjmp 0x00000000

As you can see, it has the structure discussed above. The exact values of the pointers and offsets are set up in the process of loading the module (this will be discussed later). Moreover, this fragment contains additional elements ( SAVE_REST , RESTORE_REST , LOAD_ARGS ), the purpose of which is mainly to form the context of the stream ( pt_regs ) before calling the functions ServiceTraceEnter and ServiceTraceLeave

IV.Features of the implementation of embedding

The implementation of embedding in the system call scheduling mechanisms of the Linux kernel in one way or another implies the need to solve the following practical problems :

  • Defines the addresses of the system call managers;
  • defines the addresses of the system call dispatch tables;
  • modification of dispatch code;
  • handler configuration;
  • module unloading.

Defining system call manager addresses
The presence of several dispatchers in the system implies the need to define their addresses.It was noted above that each dispatcher corresponds to a different "way" of making a system call request.Therefore, the appropriate mechanisms will be used to determine the required addresses :

  • INT 80h, reading the vector of the IDT table ( more info );
  • SYSENTER, read the contents of the MSR register number MSR_IA32_SYSENTER_EIP ( more info );
  • SYSCALL32, read the contents of the MSR register with the number MSR_CSTAR ( more info );
  • SYSCALL, read the contents of the MSR register with the number MSR_LSTAR ( more info ).

Thus, it is easy to determine each of the addresses you are looking for.
Defining the addresses of the system call dispatch tables
As noted above, the tables sys_call_table and ia32_sys_call_table are not exported. There are different ways to determine their addresses, but having determined the addresses of the dispatchers in the previous step, the table addresses are also determined simply by searching for a dispatch instruction of the form call sys_call_table[N]
For these purposes it is rational to use the disassembler ( udis86 ). By sequentially going through the instructions, starting from the first one, you can get to the instruction you are looking for, the argument of which is the address of the corresponding table. Since the dispatcher structure is well established, it is possible to uniquely identify the instruction (CALL with 7 bytes length) and get the desired table address from it with a high degree of reliability.
If for some reason this is not enough, you can enhance the check of the received address. To do this, for example, you can check if the value in the cell with number __NR_open of the assumed table is equal to the address of the sys_open function. In the example under consideration, however, no such additional checks are made.
Modification of dispatcher code
When modifying the system call managers’ code, you should keep in mind that their code is ReadOnly. Besides, modifying code on the working system should be done atomically, i.e. in such a way that there are no undefined states during modification, when any of the threads sees a partially completed record.
In one of the previous articles discussed the correct way to write to write-protected pages using temporary mappings. There’s no need to repeat anything here. As for atomicity, this has also been covered previously , when the topic of kernel function hijacking was addressed.
Thus, it is reasonable to modify write-protected code using temporary mappings as well as a special interface of the Linux kernel stop_machine
Setting up handlers
According to the embedding method presented, the code of each of the dispatchers is modified so that the 7-byte dispatch command CALL MEM32 is replaced by a 5-byte unconditional transition command to the corresponding handler JMP REL32 This imposes some restrictions on the range of the transition. The handler must be located within ± 2 Gb of the JMP REL32
According to the structure of the handlers, there are commands (JMP and CALL) that require precise arguments (e.g., return address or system call table address). Since these values are not available at compile time or module load time, they have to be set "manually", after loading, before the start.
Another important feature when configuring handlers is the need to enable unloading of a module while keeping the system up and running. For this purpose, the handler code must remain in the system even after the main module has been unloaded (about this later).
Unloading the module
The module must be unloaded while keeping the system operational. This means that after the module has been unloaded, the system must operate normally. This is not a trivial task, because unloading a module unloads all the code it uses.
For example, you can imagine a situation where some thread making a system call is "asleep" in the kernel. Until the moment it wakes up, someone is trying to unload the module. Basically, nothing prevents the system from doing that. Eventually, when the thread in question wakes up and completes the requested system call, control will go back to the corresponding handler (which is why it should not be unloaded).
However, not unloading handler code is not the only way to keep the system running after the module has been unloaded. It is worth recalling that the actual system call in the handler was "wrapped" in a pair of trace function calls ServiceTraceEnter and ServiceTraceLeave The code of which was located in the module to be unloaded.
So to avoid getting into a situation where on return from a system call the thread would try to call a function that physically doesn’t exist anymore, you need to modify the code of each handler again, excluding invalid more calls from there (simply put, nailing them with NOP’s).

V. Features of kernel module implementation

Below we consider the structure of the kernel module which implements the system call dispatch mechanism of the Linux kernel.
The key structure of the module is struct scentry – structure that contains the necessary information to be embedded in the corresponding dispatcher. The structure contains the following fields :

typedef struct scentry {const char *name;const void *entry;const void *table;const void *pcall;void *pcall_map;void *stub;const void *handler;void (*prepare)(struct scentry *);void (*implant)(struct scentry *);void (*restore)(struct scentry *);void (*cleanup)(struct scentry *);} scentry_t;

The structures are combined in array which defines how and with which parameters to embed :

scentry_t elist[] = {...{.name = "system_call", /* SYSCALL: MSR(LSTAR), kernel/entry_64.S (1) */.handler = service_syscall64, .prepare= prepare_syscall64_1}, {.name = "system_call", /* SYSCALL: MSR(LSTAR), kernel/entry_64.S (2) */.handler = service_syscall64, .prepare = prepare_syscall64_2}, ...};

Except for the marked fields, all other elements of the structure are filled in automatically – this is the responsibility of the function prepare Below is example implementation of a function to prepare for embedding the SYSCALL command in the dispatcher:

extern void service_syscall64(void);static void prepare_syscall64_1(scentry_t *se){/** searching for -- 'call *sys_call_table(, %rax, 8)'* http://lxr.free-electrons.com/source/arch/x86/kernel/entry_64.S?v=3.13#L629*/se-> entry= get_symbol_address(se-> name);se-> entry = se-> entry ? se-> entry : to_ptr(x86_get_msr(MSR_LSTAR));if (!se-> entry) return;se-> pcall= ud_find_insn(se-> entry, 512, UD_Icall, 7);if (!se-> pcall) return;se-> table = to_ptr(*(int *)(se-> pcall + 3));}

As you can see, first of all an attempt is made to resolve the name of a character to its corresponding address ( se-> entry ). If the address cannot be resolved this way, the mechanisms specific to each dispatcher come into play (in this case, reading the MSR register with the MSR_LSTAR number).
Next, for the dispatcher found, the dispatch command is searched for ( se-> pcall ) and, if successful, the address of the system call table used by the dispatcher is determined.
The completion of the preparation phase is the creation of the handler code used by the dispatcher after it has been modified. The following is a function stub_fixup that does this :

static void fixup_stub(scentry_t *se){ud_t ud;memset(se-> stub, 0x90, STUB_SIZE);ud_initialize(ud, BITS_PER_LONG, \UD_VENDOR_ANY, se-> handler, STUB_SIZE);while (ud_disassemble(ud)) {void *insn = se-> stub + ud_insn_off(ud);const void *orig_insn = se-> handler + ud_insn_off(ud);memcpy(insn, orig_insn, ud_insn_len(ud));/* fixup sys_call_table dispatcher calls (FF.14.x5.xx.xx.xx.xx) */if (ud.mnemonic == UD_Icall ud_insn_len(ud) == 7) {x86_insert_call(insn, NULL, se-> table, 7);continue;}/* fixup ServiceTraceEnter/Leave calls (E8.xx.xx.xx.xx) */if (ud.mnemonic == UD_Icall ud_insn_len(ud) == 5) {x86_insert_call(insn, insn, orig_insn + (long)(*(int *)(orig_insn + 1)) + 5, 5);continue;}/* fixup jump back (E9.xx.xx.xx.xx) */if (ud.mnemonic == UD_Ijmp ud_insn_len(ud) == 5) {x86_insert_jmp(insn, insn, se-> pcall + 7);break;}}se-> pcall_map = map_writable(se-> pcall, 64);}

As you can see, the main role of this function is to create a copy of the handlers and then tune them to the actual addresses. The disassembler is also actively used here. The simple structure of the handlers allows us to avoid any complex logic here. The signal to exit the loop is the detection of the JMP REL32 that returns control to the dispatcher.
The preparation phase is followed by the phase of implantation code into the kernel code. This phase is quite simple and consists in writing a single instruction ( JMP REL32 ) into the code of each of the system services.
When a module is unloaded, first the phase restore which consists in restoring the code of the system call managers, as well as modifying the handler code :

static void generic_restore(scentry_t *se){ud_t ud;if (!se-> pcall_map) return;ud_initialize(ud, BITS_PER_LONG, \UD_VENDOR_ANY, se-> stub, STUB_SIZE);while (ud_disassemble(ud)) {if (ud.mnemonic == UD_Icall ud_insn_len(ud) == 5) {memset(se-> stub + ud_insn_off(ud), 0x90, ud_insn_len(ud));continue;}if (ud.mnemonic == UD_Ijmp)break;}debug(" [o] restoring original call instruction %p (%s)\n", se-> pcall, se-> name);x86_insert_call(se-> pcall_map, NULL, se-> table, 7);}

As you can see, in the handlers code, all found 5-byte CALL commands will be replaced by a sequence of NOP’s, which will eliminate attempts to execute non-existent code on return from a system call. This was discussed earlier.
The functions corresponding to the implant and restore phases are performed in the context of stop_machine , so all mappings used must be prepared in advance.
The final phase in the unloading phase is the clenup , where internal resources are released ( pcall_map ).
It is worth noting again that after unloading a module in kernel memory permanently area, which contains the handler code, remains. As noted earlier, this is a prerequisite for the correct operation of the system after a module has been unloaded.
Thus, on the example the basic principles of embedding in the kernel system calls mechanism are disambiguated, and also the possibility of their interception is illustrated.

VI. Testing and debugging

For testing purposes, we intercept the system call open(2) Below is the function trace_syscall_entry which implements this interception using the handler ServiceTraceEnter :

static void trace_syscall_entry(int arch, unsigned long major, \unsigned long a0, unsigned long a1, unsigned long a2, unsigned long a3){char *filename = NULL;if (major == __NR_open || major == __NR_ia32_open) {filename = kmalloc(PATH_MAX, GFP_KERNEL);if (!filename || strncpy_from_user(filename, (const void __user *)a0, PATH_MAX) < 0)goto out;printk("%s open(%s) [%s]\n", arch ? "X86_64" : "I386", filename, current-> comm);}out:if (filename) kfree(filename);}void ServiceTraceEnter(struct pt_regs *regs){if (IS_IA32)trace_syscall_entry(0, regs-> orig_ax, \regs-> bx, regs-> cx, regs-> dx, regs-> si);#ifdef CONFIG_X86_64elsetrace_syscall_entry(1, regs-> orig_ax, \regs-> di, regs-> si, regs-> dx, regs-> r10);#endif}

Assembly and download module is done with the standard means :

$ git clone https://github.com/milabs/kmod_hooking_sct$ cd kmod_hooking_sct$ make$ sudo insmod scthook.ko

As a result, the kernel log (command dmesg ) the following information should appear :

[5217.779766] [scthook] # SYSCALL hooking module[ 5217.780132] [scthook] # prepare[ 5217.785853] [scthook] [o] prepared stub ffffffffa000c000 (ia32_syscall)[ 5217.785856] [scthook] entry:ffffffff81731e30 pcall:ffffffff81731e92 table:ffffffff81809cc0[5217.790482] [scthook] [o] prepared stub ffffffffa000c200 (ia32_sysenter_target)[ 5217.790484] [scthook] entry:ffffffff817319a0 pcall:ffffffff81731a36 table:ffffffff81809cc0[5217.794931] [scthook] [o] prepared stub ffffffffa000c400 (ia32_cstar_target)[ 5217.794933] [scthook] entry:ffffffff81731be0 pcall:ffffffff81731c75 table:ffffffff81809cc0[5217.797517] [scthook] [o] prepared stub ffffffffa000c600 (system_call)[ 5217.797518] [scthook] entry:ffffffff8172fcb0 pcall:ffffffff8172fd26 table:ffffffff81801400[5217.800013] [scthook] [o] prepared stub ffffffffa000c800 (system_call)[ 5217.800014] [scthook] entry:ffffffff8172fcb0 pcall:ffffffff8172ff38 table:ffffffff81801400[ 5217.800014] [scthook] # prepare OK[ 5217.800015] [scthook] # implant[ 5217.800052] [scthook] [o] implanting jump to stub handler ffffffffa000c000 (ia32_syscall)[ 5217.800054] [scthook] [o] implanting jump to stub handler ffffffffa000c200 (ia32_sysenter_target)[ 5217.800054] [scthook] [o] implanting jump to stub handler ffffffffa000c400 (ia32_cstar_target)[ 5217.800055] [scthook] [o] implanting jump to stub handler ffffffffa000c600 (system_call)[ 5217.800056] [scthook] [o] implanting jump to stub handler ffffffffa000c800 (system_call)[ 5217.800058] [scthook] # implant OK

Correct interception handling open(2) will cause messages like :

[ 5370.999929] X86_64 open(/usr/share/locale-langpack/en_US.utf8/LC_MESSAGES/libc.mo) [perl][ 5370.999930] X86_64 open(/usr/share/locale-langpack/en_US/LC_MESSAGES/libc.mo) [perl][ 5370.999932] X86_64 open(/usr/share/locale-langpack/en.UTF-8/LC_MESSAGES/libc.mo) [perl][ 5370.999934] X86_64 open(/usr/share/locale-langpack/en.utf8/LC_MESSAGES/libc.mo) [perl][ 5370.999936] X86_64 open(/usr/share/locale-langpack/en/LC_MESSAGES/libc.mo) [perl][ 5371.001308] X86_64 open(/etc/login.defs) [cron][ 5372.422399] X86_64 open(/home/ilya/.cache/awesome/history) [awesome][ 5372.424013] X86_64 open(/dev/null) [awesome][ 5372.424682] I386 open(/etc/ld.so.cache) [skype][ 5372.424714] I386 open(/usr/lib/i386-linux-gnu/libXv.so.1) [skype][ 5372.424753] I386 open(/usr/lib/i386-linux-gnu/libXss.so.1) [skype][ 5372.424789] I386 open(/lib/i386-linux-gnu/librt.so.1) [skype][ 5372.424827] I386 open(/lib/i386-linux-gnu/libdl.so.2) [skype][ 5372.424856] I386 open(/usr/lib/i386-linux-gnu/libX11.so.6) [skype][ 5372.424896] I386 open(/usr/lib/i386-linux-gnu/libXext.so.6) [skype][ 5372.424929] I386 open(/usr/lib/i386-linux-gnu/libQtDBus.so.4) [skype][ 5372.424961] I386 open(/usr/lib/i386-linux-gnu/libQtWebKit.so.4) [skype][ 5372.425003] I386 open(/usr/lib/i386-linux-gnu/libQtXml.so.4) [skype][ 5372.425035] I386 open(/usr/lib/i386-linux-gnu/libQtGui.so.4) [skype][ 5372.425072] I386 open(/usr/lib/i386-linux-gnu/libQtNetwork.so.4) [skype][ 5372.425103] I386 open(/usr/lib/i386-linux-gnu/libQtCore.so.4) [skype][ 5372.425151] I386 open(/lib/i386-linux-gnu/libpthread.so.0) [skype][ 5372.425191] I386 open(/usr/lib/i386-linux-gnu/libstdc++.so.6) [skype][ 5372.425233] I386 open(/lib/i386-linux-gnu/libm.so.6) [skype][ 5372.425265] I386 open(/lib/i386-linux-gnu/libgcc_s.so.1) [skype][ 5372.425292] I386 open(/lib/i386-linux-gnu/libc.so.6) [skype][ 5372.425338] I386 open(/usr/lib/i386-linux-gnu/libxcb.so.1) [skype][ 5372.425380] I386 open(/lib/i386-linux-gnu/libdbus-1.so.3) [skype][ 5372.425416] I386 open(/lib/i386-linux-gnu/libz.so.1) [skype][ 5372.425444] I386 open(/usr/lib/i386-linux-gnu/libXrender.so.1) [skype][ 5372.425475] I386 open(/usr/lib/i386-linux-gnu/libjpeg.so.8) [skype][ 5372.425510] I386 open(/lib/i386-linux-gnu/libpng12.so.0) [skype][ 5372.425546] I386 open(/usr/lib/i386-linux-gnu/libxslt.so.1) [skype][ 5372.425579] I386 open(/usr/lib/i386-linux-gnu/libxml2.so.2) [skype]

Note that capture for 32-bit applications (for example, Skype) works correctly too, which is confirmed by the presence of messages starting with I386 and not with X86_64. Thus, on the example of open(2) illustrates the ability to make system calls intercepts.

VII. Conclusion

The presented in the article method of embedding into the dispatch mechanism of Linux kernel’s system calls allows to solve not only the task of intercepting specific system calls, but the dispatch mechanism as a whole. The proposed approach to realization of loading and unloading of the module allows to build correctly in the system and also gives an opportunity, which is not unimportant, to provide operability of the system after unloading. Active use of the disassembler allows to reliably solve the problem of searching for hidden and non-exportable symbols.
Traditionally, the kernel module code that implements the actions necessary to intercept functions is available at github

You may also like