之前一直在考虑,不同的内核源码编译出来的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 p 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 p headers: 40 (bytes)
Number of p 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 p groups in this file.
There are no program headers in this file.
Relocation p '.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 p '.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 p'.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 ps 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 p .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 p headers: 904 (bytes into file)
去掉52个elf头,汇编段大小是852字节,让我们直接跳到目的地。
可以看到光标所在位置就是elf格式的p header了,前面紧接着的都是些字符串、模块信息之类了。
可以继续翻上去看readelf的输出,p 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格式里,每个p 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 p段。首先简单介绍一下这一段是干什么用的,为什么是ko模块对接最关键的段。
内核ko模块加载的时候一定会调用外部的函数,比如printk函数,这个printk函数的汇编代码在内核的某个位置,执行期加载到了内存的某个位置。我的模块怎么找到这个函数的真实汇编调用呢?内核在加载ko模块的时候,会读取Relocation p段,你需要什么函数,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 p '.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 p段,可以看到00000010 00001e01,00000018 00000102,这个开头跟最后48字节的开头完全一致,既然exit段都没了,可以删!
Relocation p '.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 p段已经没用了,也可以删。
终于到了最后时刻,有兴趣的朋友可以测试一下,现在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模块了。