如何在任意进程中修改内存保护属性
最近,我们在进行一项安全研究时,需要在任意进程中修改内存空间的保护标志。起初,我们发现这项任务看起来很简单,但在实际操作中,却发现困难重重,还好这些都不是什么大问题。在解决这些问题的过程中,我们还学到了一些新的东西,主要是关于Linux机制和内核开发的。在以下的详解中,我们会介绍我们所采取的三种方法以及每次寻求更好解决方案的原因。
背景介绍
在现代操作系统中,每个进程都有自己的虚拟地址空间(从虚拟地址到物理地址的映射)。此虚拟地址空间由内存页面(某些固定大小的连续内存块)组成,且每个页面都有保护标志,这些保护标志决定了允许对该页面的访问类型(读取、写入和执行)。不过,这种机制依赖于架构页表(architecture page table)。不过要注意的是,在x64的架构中,你不能只进行页面写入,即使你是特意从操作系统请求的,也都同时具有页面写入和可读的功能。
在Windows中,你可以使用API函数VirtualProtect或VirtualProtectEx修改内存空间的保护。VirtualProtectEx使我们的修改任务变得非常简单:因为它的第一个参数hProcess是“要修改其内存保护的进程的句柄”。
不过,在Linux中,修改过程就没有这么简单了,因为修改内存保护的API是系统调用mprotect或pkey_mprotect的结果,并且这两个函数始终在当前进程的地址空间上运行。现在让我们想办法解决一下如何在x64架构上的Linux中解决修改的问题,不过前提条件是,我们具有修改设备的root权限。
方法一:代码注入
如果mprotect总是在当前进程中运行,我们需要让目标进程从它自己的上下文中调用它。这时就要用到代码注入了,该方法可以通过许多不同的方式实现。我们可以选择使用ptrace机制实现它,该机制允许一个进程“观察和控制另一个进程的执行”,包括修改目标进程的内存和寄存器的能力。这种机制用于调试器(如gdb)和跟踪实用程序(如strace),使用ptrace注入代码所需的步骤如下:
1.使用ptrace附加到目标进程,如果进程中有多个线程,那么最好停止所有其他线程;
2.找到一个可执行的内存空间(通过检查/ proc / PID / maps),并在这个空间编写操作码syscall(十六进制:0f05);
3.根据调用约定来修改寄存器,首先,将rax修改为mprotect的系统调用号(即10);然后,前三个参数(即起始地址、长度和所需的保护)分别存储在rdi、rsi和rdx中;最后,将rip修改为步骤2中使用的地址;
4.继续这个过程,直到系统调用返回(ptrace允许你跟踪系统调用的进入和退出);
5.恢复被修改的内存和寄存器,从进程中将其分离并恢复正常执行;
这种方法是我们的采用的第一个也是最直观的方法,并且非常有效。不过在我们发现了Linux中的另一种完全破坏机制:利用seccomp进行破坏之后,该方法就不是我们的最优选择了。基本上,它是Linux内核中的一个安全工具,允许进程输入某种形式的“监狱”,除了read,write,_exit和sigreturn之外,它不能进行任何系统调用。还有一个选项,可以指定任意的系统调用及针对它们的过滤参数。
因此,如果进程启用了seccomp模式并且我们尝试将一个对mprotect的调用注入其中,那么内核将终止进程,因为该进程是不允许使用此系统调用的。因此,要对这些进程进行调用,就要采用方法二。
方法二:在内核模块中模拟mprotect系统调用
seccomp(全称securecomputing mode)是linuxkernel从2.6.23版本开始所支持的一种安全机制。
在Linux系统里,大量的系统调用直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。通过seccomp,我们限制程序使用某些系统调用,这样可以减少系统的暴露面,同时是程序进入一种“安全”的状态。
由于Linux中存在另一种完全破坏机制:利用seccomp进行破坏,因此这个方法肯定要在内核模式中进行。在Linux内核中,每个线程(包括用户线程和内核线程)都由一个名为task_struct的结构表示,并且当前线程(任务)可以通过pointer current访问。内核中mprotect的内部实现使用了pointer current,因此我们的第一个想法是,只要将mprotect的代码复制粘贴到内核模块中,并将每次出现的current替换为指向目标线程task_struct的指针,不就可以了吗?
接下来的事情你可能已经猜到了,就是复制C代码,不过复制过程并不是你想的那么简单,因为其中存在大量使用我们无法访问的未导出的函数、变量和宏。某些函数说明会在标头文件中导出,但是它们的实际地址不是由内核导出的。如果内核是用linux内核符号表kallsyms编译的,那么通过文件/ proc / kallsysm导出所有内部符号,这个特定的问题就可以解决。因为kallsyms在进行源码调试时具有相当重要的作用,它可以描述所有不处在堆栈上的内核符号。linux内核在编译的过程中,将内核中所有的符号(所有的内核函数以及已经装载的模块)及符号的地址以及符号的类型信息都保存在了/proc/kallsyms文件中。
尽管存在这个特定问题,我们仍然试图实现mprotect调用。为此,我们特意编写一个内核模块,利用该模块获取目标PID和参数以进行mprotect,并模仿其调用行为。首先,我们需要获取所需的内存映射对象,用它表示线程的地址空间:
/* Find the task by the pid */
pid_struct = find_get_pid(params.pid);
if (!pid_struct)
return -ESRCH;
task = get_pid_task(pid_struct, PIDTYPE_PID);
if (!task) {
ret = -ESRCH;
goto out;
}
/* Get the mm of the task */
mm = get_task_mm(task);
if (!mm) {
最近,我们在进行一项安全研究时,需要在任意进程中修改内存空间的保护标志。起初,我们发现这项任务看起来很简单,但在实际操作中,却发现困难重重,还好这些都不是什么大问题。在解决这些问题的过程中,我们还学到了一些新的东西,主要是关于Linux机制和内核开发的。在以下的详解中,我们会介绍我们所采取的三种方法以及每次寻求更好解决方案的原因。
背景介绍
在现代操作系统中,每个进程都有自己的虚拟地址空间(从虚拟地址到物理地址的映射)。此虚拟地址空间由内存页面(某些固定大小的连续内存块)组成,且每个页面都有保护标志,这些保护标志决定了允许对该页面的访问类型(读取、写入和执行)。不过,这种机制依赖于架构页表(architecture page table)。不过要注意的是,在x64的架构中,你不能只进行页面写入,即使你是特意从操作系统请求的,也都同时具有页面写入和可读的功能。
在Windows中,你可以使用API函数VirtualProtect或VirtualProtectEx修改内存空间的保护。VirtualProtectEx使我们的修改任务变得非常简单:因为它的第一个参数hProcess是“要修改其内存保护的进程的句柄”。
不过,在Linux中,修改过程就没有这么简单了,因为修改内存保护的API是系统调用mprotect或pkey_mprotect的结果,并且这两个函数始终在当前进程的地址空间上运行。现在让我们想办法解决一下如何在x64架构上的Linux中解决修改的问题,不过前提条件是,我们具有修改设备的root权限。
方法一:代码注入
如果mprotect总是在当前进程中运行,我们需要让目标进程从它自己的上下文中调用它。这时就要用到代码注入了,该方法可以通过许多不同的方式实现。我们可以选择使用ptrace机制实现它,该机制允许一个进程“观察和控制另一个进程的执行”,包括修改目标进程的内存和寄存器的能力。这种机制用于调试器(如gdb)和跟踪实用程序(如strace),使用ptrace注入代码所需的步骤如下:
1.使用ptrace附加到目标进程,如果进程中有多个线程,那么最好停止所有其他线程;
2.找到一个可执行的内存空间(通过检查/ proc / PID / maps),并在这个空间编写操作码syscall(十六进制:0f05);
3.根据调用约定来修改寄存器,首先,将rax修改为mprotect的系统调用号(即10);然后,前三个参数(即起始地址、长度和所需的保护)分别存储在rdi、rsi和rdx中;最后,将rip修改为步骤2中使用的地址; 无奈人生安全网
4.继续这个过程,直到系统调用返回(ptrace允许你跟踪系统调用的进入和退出);
5.恢复被修改的内存和寄存器,从进程中将其分离并恢复正常执行;
这种方法是我们的采用的第一个也是最直观的方法,并且非常有效。不过在我们发现了Linux中的另一种完全破坏机制:利用seccomp进行破坏之后,该方法就不是我们的最优选择了。基本上,它是Linux内核中的一个安全工具,允许进程输入某种形式的“监狱”,除了read,write,_exit和sigreturn之外,它不能进行任何系统调用。还有一个选项,可以指定任意的系统调用及针对它们的过滤参数。
因此,如果进程启用了seccomp模式并且我们尝试将一个对mprotect的调用注入其中,那么内核将终止进程,因为该进程是不允许使用此系统调用的。因此,要对这些进程进行调用,就要采用方法二。
方法二:在内核模块中模拟mprotect系统调用
seccomp(全称securecomputing mode)是linuxkernel从2.6.23版本开始所支持的一种安全机制。
在Linux系统里,大量的系统调用直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。通过seccomp,我们限制程序使用某些系统调用,这样可以减少系统的暴露面,同时是程序进入一种“安全”的状态。
由于Linux中存在另一种完全破坏机制:利用seccomp进行破坏,因此这个方法肯定要在内核模式中进行。在Linux内核中,每个线程(包括用户线程和内核线程)都由一个名为task_struct的结构表示,并且当前线程(任务)可以通过pointer current访问。内核中mprotect的内部实现使用了pointer current,因此我们的第一个想法是,只要将mprotect的代码复制粘贴到内核模块中,并将每次出现的current替换为指向目标线程task_struct的指针,不就可以了吗?
接下来的事情你可能已经猜到了,就是复制C代码,不过复制过程并不是你想的那么简单,因为其中存在大量使用我们无法访问的未导出的函数、变量和宏。某些函数说明会在标头文件中导出,但是它们的实际地址不是由内核导出的。如果内核是用linux内核符号表kallsyms编译的,那么通过文件/ proc / kallsysm导出所有内部符号,这个特定的问题就可以解决。因为kallsyms在进行源码调试时具有相当重要的作用,它可以描述所有不处在堆栈上的内核符号。linux内核在编译的过程中,将内核中所有的符号(所有的内核函数以及已经装载的模块)及符号的地址以及符号的类型信息都保存在了/proc/kallsyms文件中。
尽管存在这个特定问题,我们仍然试图实现mprotect调用。为此,我们特意编写一个内核模块,利用该模块获取目标PID和参数以进行mprotect,并模仿其调用行为。首先,我们需要获取所需的内存映射对象,用它表示线程的地址空间: 内容来自无奈安全网
/* Find the task by the pid */
pid_struct = find_get_pid(params.pid);
if (!pid_struct)
return -ESRCH;
task = get_pid_task(pid_struct, PIDTYPE_PID);
if (!task) {
ret = -ESRCH;
goto out;
}
/* Get the mm of the task */
mm = get_task_mm(task);
if (!mm) {
本文来自无奈人生安全网