不同的内核源码编译出来的ko文件,区别到底是什么?

时间:2024-04-02 09:23:42

不同的内核源码编译出来的ko文件,区别到底是什么?

之前一直在考虑,不同的内核源码编译出来的ko文件,区别到底是什么?

能不能不编译内核加载内核模块呢?最近逆向分析了linux内核ko模块的结构,事实证明,是可以的。

我在这里给大家分享一些我的心得。

 

不同的内核源码编译出来的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文件,区别到底是什么?

不同的内核源码编译出来的ko文件,区别到底是什么?

不同的内核源码编译出来的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结构网上资料很多,这里就不赘述了。

不同的内核源码编译出来的ko文件,区别到底是什么?

可以看到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,把它改成这样。

不同的内核源码编译出来的ko文件,区别到底是什么?

可以看到,删掉了汇编段所有代码,把汇编段第一条命令改成了09 F0 A0 E3,字节序问题,也就是上面手算的e3a0f009,mov pc, #9指令了。

我们继续尝试,只有一行汇编的ko文件能否成功加载。

Insmod hello.ko

结果什么都没有发生,哪怕是崩溃,都没有,成功加载,一点反应都没有。

 

看样子不是汇编的问题,这后面紧跟着的是只读常量段,C语言里写的hello exit和hello init也在。这后面一堆零,是预留给堆栈区的空间。具体应该跳过多少呢?

 

不同的内核源码编译出来的ko文件,区别到底是什么?

 只读常量段

 翻到上面,从上面readelf的输出可以看到,

 Start of p headers:          904 (bytes into file)

去掉52个elf头,汇编段大小是852字节,让我们直接跳到目的地。

 

不同的内核源码编译出来的ko文件,区别到底是什么?

可以看到光标所在位置就是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内容变成了现在这样。

不同的内核源码编译出来的ko文件,区别到底是什么?

我们重新逆向一下修改后的汇编代码

不同的内核源码编译出来的ko文件,区别到底是什么?

嗯,很好,就剩一行汇编了,干净多了。

 

让我们再逐渐把无关的东西清理的更干净一些。(逐渐忘记最初的目的,→_→)

不同的内核源码编译出来的ko文件,区别到底是什么?

rel.init.text段标注了汇编段指定位置的动态预留地址,因为现在已经没有printk了,删!对应的二进制位置在这里。

.symtab段里有大量的标号,除init_module、cleanup_module、__this_module等一些有symbol的位置信息外,其他不用的,全部删除。

这下面紧接着的是symtab的位置信息,这是真正保存命名的字符串数据,这部分都是寻找字符串命名的,在此就不赘述了。

不同的内核源码编译出来的ko文件,区别到底是什么?

 

这里多截取了一些,但是可以看到,这个ko文件打开二进制,在修改的情况下,基本整个读完了。

不同的内核源码编译出来的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文件,区别到底是什么?

终于到了最后时刻,有兴趣的朋友可以测试一下,现在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文件出来,读一下二进制。

不同的内核源码编译出来的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模块了。

 

不同的内核源码编译出来的ko文件,区别到底是什么?

不同的内核源码编译出来的ko文件,区别到底是什么?

1.人工智能:嵌入式技术的机遇与挑战

2.厉害了!用6个芯片打造复古经典计算机

3.国产操作系统这盘棋不简单

4.做嵌入式必须知道的国产CPU之路,里面有良机!

5.Linux是否能在8位MCU上运行?

6.为了适合你的项目,rt-thread有时候需要裁剪!不同的内核源码编译出来的ko文件,区别到底是什么?