深入Linux | 如何在任意进程中修改内存保护(含PoC)

2019 年 1 月 1 日 FreeBuf

前言

最近,我们遇到一个非常具体的问题:改变任意进程的内存区域的保护标志。这项任务看似微不足道,但是我们着实遇到了一些麻烦,在此过程中也学到了关于Linux机制和内核开发相关的东西。以下是一些简要概述,其中包括了当时采取的三种方案,每次也在寻求更好解决方案。

概述

在现代操作系统中,每个进程都有自己的虚拟地址空间(从虚拟地址映射到物理地址)。此虚拟地址空间由内存页(某些固定大小的连续内存块)组成,每个页都有保护标志,用于确定允许此页面访问的类型(读取,写入和执行)。这种机制依赖于架构页表(有趣的是,在x64架构中,你不能使页面只写(write-only),就算你特意从操作系统请求,它也总是可读的)。

在Windows中,你可以使用VirtualProtect或VirtualProtectEx这两个API更改内存区域的保护。后者让我们的任务变得非常简单:它的第一个参数hProcess是“要改变内存保护的进程的句柄”(参见MSDN.aspx))。

另一方面,在Linux中,我们并不那么幸运:更改内存保护的API是系统调用mprotect或pkey_mprotect,并且两者始终在当前进程的地址空间上运行。 我们现在回顾一下在x64架构上的Linux中解决此任务的方法(我们假设是root权限)。

而在Linux中,我们就没那么幸运了,更改内存保护的API是系统调用(mprotect或pkey_mprotect),并且两者始终在当前进程的地址空间上运行。所以现在我们来回顾一下在Linux x64架构上解决此问题的方法(假设是root权限)。

方案一:代码注入

如果mprotect总是作用于当前进程,那么我们就需要让目标进程从它自己的上下文中调用它。这称为代码注入,可以通过许多不同的方式实现。我们选择使用ptrace机制实现它,其允许一个进程“观察并控制另一个进程的执行”(参见手册),包括更改目标进程的内存的能力。此机制用于调试器(如gdb)和跟踪程序(如strace)。使用ptrace注入代码所需的步骤如下:

1. 通过ptrace附加到目标进程。如果进程中有多个线程,那就终止所有其他线程

2. 找到可执行内存区域(通过检查/proc/PID/maps)并在那里写操作码(hex:0f 05)

3.根据调用约定修改寄存器:首先将rax更改为mprotect的系统调用号(即10)。然后三个参数(起始地址,长度和所需的保护)分别存储在rdi,rsi和rdx中。最后,将rip更改为步骤2中使用的地址

4. 恢复进程直到系统调用返回(ptrace允许你跟踪系统调用的进入和退出)

5. 恢复被覆盖的内存和寄存器,从进程中分离并恢复正常执行

这种方法是第一个也是最直观的方法,但是我们之后发现Linux中的另一种叫seccomp的机制会工作得更好。它是Linux内核中的一个安全工具,允许进程自己进入某种封闭状态,除了read,write,_exit和sigreturn之外,它不能调用任何系统调用。不过也可以选择任意系统调用及其参数来仅仅过滤指定的系统调用。

因此,如果进程启用了seccomp模式并且我们尝试将mprotect调用到其中,那么内核将终止进程,因为不允许此系统调用。所以我们要寻求更好的解决方案……

方案二:模仿内核模块中的mprotect

由于seccomp,用户态中每个解决方案都不可行,因此下一个方法肯定存在于内核态中。在Linux内核中,每个线程(用户线程和内核线程)都由名为task_struct的结构表示,并且当前线程(任务)可通过指针访问。内核中mprotect的内部实现使用指针current,所以我们首先想到的是将mprotect的代码复制粘贴到我们的内核模块,并用指向目标线程的task_struct的指针替换每次出现的current。

可能你已经猜到了,复制C代码并不是那么简单,其中有大量我们无法访问的,未导出的函数,变量和宏。某些函数声明在头文件中导出,但内核不会导出它们的实际地址。如果内核是由kallsyms支持编译的,那么这个特定的问题就可以解决,然后通过文件/proc/kallsysm导出所有内部符号。

尽管存在这些问题,我们仍以mprotect的本质进行尝试,甚至仅用于教育目的。因此,我们开始编写一个内核模块,它获取mprotect目标PID和参数,并模仿其行为。首先,我们需要获取所需的内存映射对象,它表示线程的地址空间:

现在我们有了内存映射对象,就需要深入挖掘。Linux内核实现了一个抽象层来管理内存区域,每个区域由结构vm_area_struct表示。为了找到正确的内存区域,我们使用函数find_vma,它通过所需的地址搜索内存映射。

vm_area_struct包含字段vm_flags,其以与结构无关的方式表示存储器区域的保护标志,vm_page_prot以体系结构相关的方式表示。单独更改这些字段不会真正地影响页表(但会影响proc/PID/maps的输出,我们已经尝试过)。 你可以点击这里获取更多内容。

在深入研究内核代码之后,我们发现了真正改变内存区域保护所需的最基本工作:

1. 将字段vm_flags更改为所需的保护

2. 调用函数vma_set_page_prot_func来根据vm_flags字段更新vm_page_prot

3. 调用函数change_protection_func更新页表中的保护位。

这段代码虽然有效,但它有很多问题,首先,我们只实现了mprotect的基本部分,但原始函数比我们做的要多得多(例如通过保护标志分割和连接内存区域)。其次,我们使用两个内核函数,这些函数不是由内核导出的(vma_set_page_prot_func和change_protection_func)。我们可以使用kallsyms来调用它们,但是这很容易出问题(将来可能会更改它们的名称,或者会改变内存区域的整个内部实现)。所以我们想要一个更通用的解决方案,不考虑内部结构。

方案三:使用目标进程的内存映射

这种方法与第一种方法非常相似,因为我们希望在目标进程的上下文中执行代码。但在这里,我们会用自己的线程中执行代码,同时使用目标进程的“内存上下文”,这意味着:我们会使用其地址空间。

通过几个API可以在内核态下更改地址空间,我们使用了use_mm。如文档明确指出的那样,“此例程仅用于从内核线程上下文中调用”。这些是在内核中创建的线程,不需要任何用户地址空间,因此可以更改其地址空间(地址空间内的内核区域在每个任务中以相同的方式映射)。

在内核线程中运行代码有一种简单方法,就是内核的工作队列接口,它允许你使用特定例程和特定参数来安排工作。我们的例程获取所需进程的内存映射对象和mprotect的参数,并执行以下操作(do_mprotect_pkey是内核中实现mprotect和pkey_mprotect系统调用的内部函数):

当我们的内核模块在某个进程(通过一个特殊的IOCTL)获得更改保护的请求时,它首先找到所需的内存映射对象,然后使用正确的参数来调度工作。这个方案仍有一个小问题:函数do_mprotect_pkey_func不由内核导出,需要使用kallsyms获取。与前一个解决方案不同,这个内部函数不太容易发生变化,因为它与系统调用pkey_mprotect有关,而且我们无需处理内部结构。

如果你有兴趣,可以在github中找到这个PoC内核模块的源代码。

*参考来源:perception-point,FB小编Covfefe编译,转载请注明来自FreeBuf.com

登录查看更多
0

相关内容

Linux 是一系列类 Unix 计算机操作系统的统称。该操作系统的核心为 Linux 内核。Linux 操作系统也是软件和开放源代码发展中最著名的例子之一。
【2020新书】实战R语言4,323页pdf
专知会员服务
100+阅读 · 2020年7月1日
【实用书】流数据处理,Streaming Data,219页pdf
专知会员服务
76+阅读 · 2020年4月24日
深度神经网络实时物联网图像处理,241页pdf
专知会员服务
76+阅读 · 2020年3月15日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
VS Code Remote发布!真·远程开发
开源中国
6+阅读 · 2019年5月3日
一个牛逼的 Python 调试工具
机器学习算法与Python学习
15+阅读 · 2019年4月30日
Linux挖矿病毒的清除与分析
FreeBuf
14+阅读 · 2019年4月15日
支持多标签页的Windows终端:Fluent 终端
Python程序员
7+阅读 · 2019年4月15日
如何编写完美的 Python 命令行程序?
CSDN
5+阅读 · 2019年1月19日
逆向 | C++ 加壳程序的编写思路
计算机与网络安全
9+阅读 · 2019年1月1日
10个深度学习软件的安装指南(附代码)
数据派THU
17+阅读 · 2017年11月18日
Universal Transformers
Arxiv
5+阅读 · 2019年3月5日
Doubly Attentive Transformer Machine Translation
Arxiv
4+阅读 · 2018年7月30日
Arxiv
7+阅读 · 2018年6月1日
Arxiv
6+阅读 · 2018年4月4日
Arxiv
3+阅读 · 2018年3月13日
VIP会员
相关VIP内容
【2020新书】实战R语言4,323页pdf
专知会员服务
100+阅读 · 2020年7月1日
【实用书】流数据处理,Streaming Data,219页pdf
专知会员服务
76+阅读 · 2020年4月24日
深度神经网络实时物联网图像处理,241页pdf
专知会员服务
76+阅读 · 2020年3月15日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
相关资讯
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
VS Code Remote发布!真·远程开发
开源中国
6+阅读 · 2019年5月3日
一个牛逼的 Python 调试工具
机器学习算法与Python学习
15+阅读 · 2019年4月30日
Linux挖矿病毒的清除与分析
FreeBuf
14+阅读 · 2019年4月15日
支持多标签页的Windows终端:Fluent 终端
Python程序员
7+阅读 · 2019年4月15日
如何编写完美的 Python 命令行程序?
CSDN
5+阅读 · 2019年1月19日
逆向 | C++ 加壳程序的编写思路
计算机与网络安全
9+阅读 · 2019年1月1日
10个深度学习软件的安装指南(附代码)
数据派THU
17+阅读 · 2017年11月18日
Top
微信扫码咨询专知VIP会员