
之前一直在考虑,不同的内核源码编译出来的ko文件,区别到底是什么? 能不能不编译内核加载内核模块呢?最近逆向分析了linux内核ko模块的结构,事实证明,是可以的。 我在这里给大家分享一些我的心得。 首先分析一个最简单的hello.ko,Makefile就不写了,因为需要尽可能简单,加一行去除调试信息的objcopy -g hello.ko就好。 hello.c #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> static int __init hello_init(void){ printk(KERN_EMERG "\nhello init.\n"); return 0; } static void __exit hello_exit(void){ printk(KERN_EMERG "\nhello exit.\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); 可以看到编译后的ko文件只有2.4Kb。 我这里准备好一个基于arm架构板子交叉编译好的linux3.6.33的linux内核,正经的烧进去,后续简称now kernel。 在胡乱修改make menuconfig的模块结构之后,重新在另一个无关的目录编译另外一个linux内核,后续简称fake kernel。 首先我先把基于fake kernel编译的hello.ko拷贝到我的板子上正在使用的now kernel上,然后执行:insmod hello.ko。 结果什么都没有发生,没有报错,没有执行,没有打印??? 首先确定了一点,不基于同一套内核源码编译的内核模块是无法直接加载的。那是什么导致了加载失败呢?我们先用二进制编辑器打开刚刚编译的ko文件。 ![]() ![]() ![]() ![]() 可以看到内核ko文件就是个标准的elf格式的文件,那么我们用readelf读一下ko文件的结构。 readelf -a hello.ko ELF Header: Magic: 7f 45 4c 46 01 01 01 6100 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: ARM ABIVersion: 0 Type: REL (Relocatable file) Machine: ARM Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 904 (bytes into file) Flags: 0x600, GNU EABI, software FP, VFP Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 19 Section header string table index: 16 Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [0] NULL 00000000 000000 000000 00 0 0 0 [1] .text PROGBITS 00000000 000034 000000 00 AX 0 0 1 [2] .exit.text PROGBITS 00000000 000034 00001c 00 AX 0 0 4 [3] .rel.exit.text REL 00000000 000904 000010 08 17 2 4 [4] .init.text PROGBITS 00000000 000050 000020 00 AX 0 0 4 [5] .rel.init.text REL 00000000 000914 000010 08 17 4 4 [6] .modinfo PROGBITS 00000000 000070 000060 00 A 0 0 4 [7] .rodata.str1.4 PROGBITS 00000000 0000d0 000028 01 AMS 0 0 4 [8] .data PROGBITS 00000000 0000f8 000000 00 WA 0 0 1 [9] .gnu.linkonce.thi PROGBITS 00000000 0000f8 000150 00 WA 0 0 4 [10].rel.gnu.linkonce REL 00000000 000924 000010 08 17 9 4 [11] .note.gnu.build-i NOTE 00000000 000248 000024 00 A 0 0 4 [12] .bss NOBITS 00000000 00026c 000000 00 WA 0 0 1 [13] .comment PROGBITS 00000000 00026c 000056 00 0 0 1 [14] .note.GNU-stack PROGBITS 00000000 0002c2000000 00 0 0 1 [15] .ARM.attributes ARM_ATTRIBUTES 00000000 0002c2000010 00 0 0 1 [16] .shstrtab STRTAB 00000000 0002d2 0000b6 00 0 0 1 [17] .symtab SYMTAB 00000000 000680 0001f0 10 18 27 4 [18] .strtab STRTAB 00000000 000870 000091 00 0 0 1 Key to Flags: W(write), A (alloc), X (execute), M (merge), S (strings) I(info), L (link order), G (group), x (unknown) O(extra OS processing required) o (OS specific), p (processor specific) There are no section groups in this file. There are no program headers in this file. Relocation section '.rel.exit.text' atoffset 0x904 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000010 00001e01 R_ARM_PC24 00000000 printk 00000018 00000102 R_ARM_ABS32 00000000 .rodata.str1.4 Relocation section '.rel.init.text' atoffset 0x914 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000010 00001e01 R_ARM_PC24 00000000 printk 0000001c 00000102 R_ARM_ABS32 00000000 .rodata.str1.4 Relocation section'.rel.gnu.linkonce.this_module' at offset 0x924 contains 2 entries: Offset Info Type Sym.Value Sym. Name 000000d4 00001d02 R_ARM_ABS32 00000000 init_module 00000140 00001c02 R_ARM_ABS32 00000000 cleanup_module There are no unwind sections in this file. Symbol table '.symtab' contains 31 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1:00000000 0 SECTION LOCAL DEFAULT 7 2: 00000000 0 NOTYPE LOCAL DEFAULT 2 $a 3: 00000000 28 FUNC LOCAL DEFAULT 2 hello_exit 4: 00000018 0 NOTYPE LOCAL DEFAULT 2 $d 5: 00000000 0 NOTYPE LOCAL DEFAULT 4 $a 6: 00000000 32 FUNC LOCAL DEFAULT 4 hello_init 7: 0000001c 0 NOTYPE LOCAL DEFAULT 4 $d 8: 00000000 0 NOTYPE LOCAL DEFAULT 6 $d 9: 00000000 12 OBJECT LOCAL DEFAULT 6 __mod_license18 10:00000000 0 NOTYPE LOCAL DEFAULT 7 $d 11: 0000000c 0 NOTYPE LOCAL DEFAULT 6 $d 12: 0000000c 35 OBJECT LOCAL DEFAULT 6 __mod_srcversion23 13: 00000030 9 OBJECT LOCAL DEFAULT 6 __module_depends 14: 0000003c 34 OBJECT LOCAL DEFAULT 6 __mod_vermagic5 15: 00000000 0 NOTYPE LOCAL DEFAULT 9 $d 16: 00000000 0 SECTIONLOCAL DEFAULT 1 17: 00000000 0 SECTIONLOCAL DEFAULT 2 18: 00000000 0 SECTIONLOCAL DEFAULT 4 19: 00000000 0 SECTIONLOCAL DEFAULT 6 20: 00000000 0 SECTIONLOCAL DEFAULT 8 21: 00000000 0 SECTIONLOCAL DEFAULT 9 22: 00000000 0 SECTIONLOCAL DEFAULT 11 23: 00000000 0 SECTIONLOCAL DEFAULT 12 24: 00000000 0 SECTIONLOCAL DEFAULT 13 25: 00000000 0 SECTIONLOCAL DEFAULT 14 26: 00000000 0 SECTIONLOCAL DEFAULT 15 27: 00000000 336 OBJECT GLOBAL DEFAULT 9 __this_module 28: 00000000 28 FUNC GLOBAL DEFAULT 2 cleanup_module 29: 00000000 32 FUNC GLOBAL DEFAULT 4 init_module 30: 00000000 0 NOTYPE GLOBAL DEFAULT UND printk No version information found in this file. Notes at offset 0x00000248 with length 0x00000024: Owner Data size Description GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring) Attribute Section: aeabi File Attributes 下面我们读一下这个二进制文件,elf头52个字节,也就是读到0x34。elf结构网上资料很多,这里就不赘述了。 ![]() 可以看到0x34个字节的elf头之后紧跟着0D C0 A0 E1 00 D8 2D E9 04 B0 4C E2 04 00 9F E5 FE FF FF EB 对照上面readelf的输出,开头零地址对应的是.text段,相对文件的跳转位置是0x34, 接下来使用objdump工具逆向读取一下后面的arm汇编段。 objdump -S hello.ko 00000000 <cleanup_module>: 0: e1a0c00d mov ip, sp 4: e92dd800 push {fp, ip, lr, pc} 8: e24cb004 sub fp, ip, #4 ;0x4 c: e59f0004 ldr r0, [pc, #4] ;18 <cleanup_module+0x18> 10: ebfffffe bl 0 <printk> 14: e89da800 ldm sp, {fp, sp, pc} 18: 00000000 .word 0x00000000 Disassembly of section .init.text: 00000000 <init_module>: 0: e1a0c00d mov ip, sp 4: e92dd800 push {fp, ip, lr, pc} 8: e24cb004 sub fp, ip, #4 ;0x4 c: e59f0008 ldr r0, [pc, #8] ;1c <init_module+0x1c> 10: ebfffffe bl 0 <printk> 14: e3a00000 mov r0, #0 ;0x0 18: e89da800 ldm sp, {fp, sp, pc} 1c: 00000014 .word 0x00000014 可以看到,这个项目的汇编代码其实只有60个字节。开头的 e1a0c00d, e92dd800,跟阅读ko二进制文件的0D C0 A0 E1 00 D8 2D E9 04 B0 4C E2 04 00 9F E5 FE FF FF EB也是一一对应的。开头是 cleanup_module,机器码跟汇编是一一对应的。 那么我们的ko模块加载无效,是不是汇编段导致的问题呢?printk这种代码还是太复杂了,我们把这个汇编段精简一下,只保留一行arm汇编:mov pc, #9 汇编语言跟机器码是一一对应的,查表手算一下,可以知道这行命令对应的机器码是:e3a0f009 让整个程序精简到4个字节,上来就把pc指针置为9,触发内核panic,通过查看内核panic的寄存器状态,看pc指针的值是不是9,判断程序是否执行。 我们直接修改文件二进制,找到init_module对应的汇编代码入口位置e1a0c00d,把它改成这样。 ![]() 可以看到,删掉了汇编段所有代码,把汇编段第一条命令改成了09 F0 A0 E3,字节序问题,也就是上面手算的e3a0f009,mov pc, #9指令了。 我们继续尝试,只有一行汇编的ko文件能否成功加载。 Insmod hello.ko 结果什么都没有发生,哪怕是崩溃,都没有,成功加载,一点反应都没有。 看样子不是汇编的问题,这后面紧跟着的是只读常量段,C语言里写的hello exit和hello init也在。这后面一堆零,是预留给堆栈区的空间。具体应该跳过多少呢? ![]() 只读常量段 翻到上面,从上面readelf的输出可以看到, Start of section headers: 904 (bytes into file) 去掉52个elf头,汇编段大小是852字节,让我们直接跳到目的地。 ![]() 可以看到光标所在位置就是elf格式的section header了,前面紧接着的都是些字符串、模块信息之类了。 可以继续翻上去看readelf的输出,section header一共0-18,也就是19个段。一个加载就崩溃的模块,我们不需要exit段,那让我们仅保留下面几个段: .text,.init.text,.rel.init.text,.modinfo(模块信息,内核会读取识别这个段的数据),.symtab(保存了很多symbol信息,还是有必要留一下的),.shstrtab(段的名字,需要保留一下),.gnu.linkonce.this_module,.rel.gnu.linkonce.this_module这几个段。暴力一点,把其他的段全都删了吧。 在elf格式里,每个section header是40字节,就从光标所在位置往下数,仅保留需要的段部分,其他的全都删除。 重新readelf看一下seciton内容变成了现在这样。 ![]() 我们重新逆向一下修改后的汇编代码 ![]() 嗯,很好,就剩一行汇编了,干净多了。 让我们再逐渐把无关的东西清理的更干净一些。(逐渐忘记最初的目的,→_→) ![]() rel.init.text段标注了汇编段指定位置的动态预留地址,因为现在已经没有printk了,删!对应的二进制位置在这里。 .symtab段里有大量的标号,除init_module、cleanup_module、__this_module等一些有symbol的位置信息外,其他不用的,全部删除。 这下面紧接着的是symtab的位置信息,这是真正保存命名的字符串数据,这部分都是寻找字符串命名的,在此就不赘述了。 ![]() 这里多截取了一些,但是可以看到,这个ko文件打开二进制,在修改的情况下,基本整个读完了。 ![]() 最后,还剩余 48字节没读,也是最关键的48字节了,这里先卖个关子。 让我们把删的面目全非的ko文件扔到内核里加载一下看看,还能用不。。。 insmod hello.ko 嗯,依然没有任何反应,ismod可以看到模块成功加载了,但是如果pc置为9,应该会触发panic才对,但是依然是一行汇编都没有执行的状态。 我们回来继续读这关键的48字节所对应的Relocation section段。首先简单介绍一下这一段是干什么用的,为什么是ko模块对接最关键的段。 内核ko模块加载的时候一定会调用外部的函数,比如printk函数,这个printk函数的汇编代码在内核的某个位置,执行期加载到了内存的某个位置。我的模块怎么找到这个函数的真实汇编调用呢?内核在加载ko模块的时候,会读取Relocation section段,你需要什么函数,symbol名字是什么。内核在动态寻找这个printk函数对应的正在运行的内核的内存位置,然后在加载ko模块的时候,将printk在内核里运行时的真实内存地址覆盖到这个ko模块的指定位置,这样在ko模块执行到调用printk这行ebfffffe汇编的时候,调用的就是内核printk真实的地址了。 首先,exit段被我删了,这里readelf显示的对应位置开始缺失了,请滚动到最上方查看最初的readelf的exit段打印 [3].rel.exit.text REL 00000000 000904 000010 08 17 2 4 Relocation section '.rel.exit.text' atoffset 0x904 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000010 00001e01 R_ARM_PC24 00000000 printk 00000018 00000102 R_ARM_ABS32 00000000 .rodata.str1.4 0x0940的位置是exit段的Relocation section段,可以看到00000010 00001e01,00000018 00000102,这个开头跟最后48字节的开头完全一致,既然exit段都没了,可以删! Relocation section '.rel.init.text' atoffset 0x914 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000010 00001e01 R_ARM_PC24 00000000 printk 0000001c 00000102 R_ARM_ABS32 00000000 init段里还有个printk的动态入口?现在就一行汇编,这个Relocation section段已经没用了,也可以删。 ![]() 终于到了最后时刻,有兴趣的朋友可以测试一下,现在ko模块依然能正常加载成功,但是依然不执行没有反应。 我们还剩下16字节没有修改,这16字节分为两组,读readelf打印可以看到,分别表示: 000000d4 00001d02 R_ARM_ABS32 00000000 init_module 00000140 00001c02 R_ARM_ABS32 00000000 cleanup_module 因为cleanup_module对应的exit段已经不存在了,我们继续删。好了,还剩余8字节。 明显前4字节表示的是offset位置,后4字节表示的是info信息,但这4字节到底是怎么来的呢?笔者尝试修改后4个字节info信息,发现加载ko的时候开始失败了,提示信息为unknown symbol。这个info是如何算出来的呢,我暂时也没有找到相关信息,希望有知道的好心读者能告知一下。 最后让我们看一下offset这4字节,其实就是内核加载ko模块时候的入口相对地址,我们先从上述now kernle里面找一个正常能用的ko文件出来,读一下二进制。 ![]() 我这里使用了能正常使用的ebtables.ko模块,可以看到对应的init_module的offset地址是BC 00 00 00,而我的hello.ko的offset地址是D4 00 00 00。让我们手动把入口地址改为BC 00 00 00。 insmod hello.ko 成功触发kernel panic,查看pc指针值,就是9,终于成功加载了。 已经写的够长了,后面的就不赘述了,因为内核ko模块的地址全部是按照相对地址计算的,除了这一行汇编。类似printk,nf_register_hook,register_sysctl_table等常用的调用测试,均不影响正常使用。 所以当无法完美使用之前内核代码的情况下,编译一个magic code一致的假的fake kernel,只要版本基本一致,头文件没有什么区别,编译出来的ko文件,修改一下.rel.gnu.linkonce.this_module段的offset地址,info信息不用改动,就能在没有编译过的内核上完美正常运行自己编译的内核ko模块了。 ![]() |
谢谢分享 |