Linux内核编程——进程内存窥探修改器
关键词
内核模块法;带参数的系统调用;虚拟内存管理;虚拟地址映射为物理地址;私有内存的入侵读写;
主要功能
- 使用内核模块法向Linux内核增加两个系统调用,系统调用号分别为335和336 。
- 335号系统调用:使用此系统调用能够读取任意进程的任意虚拟地址范围内的内容。
- 336号系统调用:使用此系统调用能够修改任意进程的任意虚拟地址范围内的内容。
- 两者都能获取进程的私有内存空间结构。
设计思路
-
Linux虚拟内存管理实现了进程之间的地址访问隔离,因此要想让一个进程访问另一个进程的私有内存(非共享)地址中的内容,就需要从用户态转变为内核态,让内核去完成此项任务,这一过程本质上就是使用系统调用,因此需要理解系统调用的基本原理[1],然后向内核增加系统调用。
-
增加系统调用的常见的两种方法:
- 修改内核源码[4],配置内核,然后编译,最后安装新内核,此方法耗时费力。
- 内核模块法[5,6,7],编写内核模块动态加载到内核中,即插即用,实现热插拔。
-
由于我们的目标是任意进程,因此对进程管理[2],如进程控制块进程描述符要有一定的了解。
-
另外对于我们想要完成的功能,Linux虚拟内存管理[3]带来的另一个问题是进程所使用的都是虚拟地址(Linux中由于没有段的概念,虚拟地址等同于线性地址),CPU对内存的访问需要经过MUM(Memory Management Unit)和Linux内核共同来完成虚拟地址到物理地址的转换,MMU是一种硬件实现,操作系统用于管理填充页表,MMU则利用页表进行地址转换。因此要完成此项人物还需要理解Linux内核关于虚拟内存管理的源码,并对自己的计算机的体系架构和页表机制了如指掌。
-
综上所述,总的设计思路为:
- 理解系统调用、进程管理、虚拟内存管理三大知识点
- 学会怎么用内核模块法增加带参数传递的系统调用**(搭好框架)**
- 设计实现系统调用的逻辑内容**(填充框架)**——即读取(修改)任意进程任意虚拟地址空间的思路逻辑是什么。
- 首先根据pid进程号获取进程描述符
- 根据用户空间的虚拟地址寻找其所在页的物理页框的物理地址
- 利用page_address( )函数将物理页转变为内核空间的页虚拟地址[8]
- 再加上页偏移值便可得到用户空间虚拟地址对应的内核空间的虚拟地址[9]
- 然后就可以在内核态下对该地址的内容进行读取或者修改
-
本文模拟黑客入侵,设计了四个模块:
- **hacker模块:**该模块对应
invade_process.c
源文件,该程序主要执行进程内存窥探修改的整体控制逻辑。 - **335号系统调用模块:**该模块对应
my_syscall335.c
源文件,该模块会被hacker模块所调用,在调用前需要将其sudo insmod my_syscall335.ko
加载到内核中,使用结束后sudo rmmod my_syscall335
将模块卸载防止污染内核。 - **336号系统调用模块:**该模块对应
my_syscall336.c
源文件,该模块会被hacker模块所调用,在调用前需要将其sudo insmod my_syscall336.ko
加载到内核中,使用结束后sudo rmmod my_syscall336
将模块卸载防止污染内核。 - **defender模块:**该模块对应
target_process.c
源文件,该程序向外暴露虚拟地址printf("%ld",vaddr)
,然后将自己阻塞getchar()
,这样保证该进程一直存在,以供hacker模块入侵(读取 or 修改)。
- **hacker模块:**该模块对应
代码解读
my_syscall335.c源代码
/*
my_syscall335.c
此模块主要功能:提供一个系统调用,其他进程可利用此系统调用(读取)任意进程任意虚拟地址空间的内容。
需要注意以下几点:
系统调用号335,使用前需确认335是否被占用,若占用则修改下面的__NR_syscall宏即可;
Linux 内核自5.7后为了安全起见 unexport kallsyms_lookup_name() and kallsyms_on_each_symbol() 因此为了获得sys_call_table的地址需用以下方法:
sudo cat /proc/kallsyms | grep sys_call_table 然后将其硬编码为源文件中的宏,每次开机地址发生变化,需要修改源文件,重新编译后再insmod入内核;
或者重新编译内核,在配置内核时修改sys_call_table地址为固定值,这样就不会变化了;
Author: Pcyslist
*/
#include <linux/kernel.h> //包含内和打印函数或其他重要宏定义
#include <linux/init.h> //模块初始化宏定义在此
#include <linux/module.h> //内核模块
#include <linux/unistd.h> //系统调用
#include <linux/sched.h> //进程管理,调度程序头文件,定义了任务结构task_struct
#include<linux/mm.h> //内存管理
#include<linux/mm_types.h>
#include<linux/export.h>
#include <linux/kallsyms.h> //曾包含了kallsyms_lookup_name(const char *)
#include <linux/time.h> //时间头文件
#include <linux/uaccess.h> //核访问用户进程内存地址的函数定义
#define __NR_syscall 335//系统调用号为335
#define SYS_CALL_TABLE_ADDRESS 0xffffffffa1000300 //sys_call_table对应的地址 0xffffffffa1000300 sudo cat /proc/kallsyms | grep sys_call_table
int orig_cr0; //用来存储cr0寄存器原来的值
unsigned long *sys_call_table=0; //保存系统调用表指针
static int(*anything_saved)(void); //定义一个函数指针,用来保存一个系统调用
typedef asmlinkage long (*sys_call_ptr_t)(const struct pt_regs *); //结构体指针类型用于传参
static int clear_cr0(void) //使cr0寄存器的第17位设置为0(内核空间可写)
{
unsigned int cr0=0;
unsigned int ret;
asm volatile("movq %%cr0,%%rax":"=a"(cr0));//将cr0寄存器的值移动到eax寄存器中,同时d输出到cr0变量中
ret=cr0;
cr0&=0xfffffffffffeffff;//将cr0变量值中的第17位清0,将修改后的值写入cr0寄存器
asm volatile("movq %%rax,%%cr0"::"a"(cr0));//将cr0变量的值作为输入,输入到寄存器eax中,同时移动到寄存器cr0中
return ret;
}
static void setback_cr0(int val) //使cr0寄存器设置为内核不可写
{
asm volatile("movq %%rax,%%cr0"::"a"(val));
}
// 根据一个进程的进程描述符和虚拟地址找到虚拟地址所在页的页表项
static pte_t* get_pte(struct task_struct *task, unsigned long vaddr)
{
pgd_t* pgd; //PGD: Page Global Directory
p4d_t* p4d; //P4D: 第四级页表
pud_t* pud; //PUD: Page Upper Directory
pmd_t* pmd; //PMD: Page Middle Directory
pte_t* pte; //PTE: Page Table Entry
struct mm_struct *mm = task->mm;
//五级页表层层索引
pgd = pgd_offset(mm, vaddr); //获得pgd的地址
if(pgd_none(*pgd) || pgd_bad(*pgd))
return NULL;
p4d = p4d_offset(pgd, vaddr); //获得p4d的地址
if(p4d_none(*p4d) || p4d_bad(*p4d))
return NULL;
pud = pud_offset(p4d, vaddr); //获得pud的地址
if(pud_none(*pud) || pud_bad(*pud))
return NULL;
pmd = pmd_offset(pud, vaddr); //获得pmd的地址
if(pmd_none(*pmd) || pmd_bad(*pmd))
return NULL;
pte = pte_offset_kernel(pmd, vaddr); //获得pte的地址
if(pte_none(*pte))
return NULL;
return pte;
}
static int sys_mycall(const struct pt_regs * regs) //定义自己的系统调用 参数为一个结构体指针regs,详情请看参考文献[6]
{
/*regs结构体指针内的四个成员,本质上是四个寄存器,分别存着 pid vaddr buffer bytes*/
unsigned long obj_pid=(unsigned long )regs->di; //待入侵的进程pid
unsigned long obj_vaddr=(unsigned long )regs->si; //待入侵的进程的虚拟地址,此处的虚拟地址vaddr是用户态下的
unsigned long crrt_buffer=(unsigned long )regs->dx; //将[obj_vaddr,obj_vaddr+obj_bytes]地址区间的内容拷贝到crrt_buffer中实现读取
unsigned long obj_bytes=(unsigned long )regs->r10; //读取的字节数
int crrt_pid=current->pid; //调用本服务例程的进程pid
struct task_struct *obj_task; //目标进程描述符
struct task_struct *crrt_task; //当前进程描述符
pte_t* obj_pte; //目标进程页表项
pte_t* crrt_pte; //当前进程页表项
struct page* obj_page; //页
struct page* crrt_page;
// int i;
unsigned long obj_pgvaddr=0; //虚拟地址vaddr所在页的虚拟地址
unsigned long crrt_pgvaddr=0;
unsigned long obj_page_offset = 0; //虚拟地址vaddr的页偏移值
unsigned long crrt_page_offset = 0;
unsigned long obj_vaddr_kernel=0; //vaddr的内核态的虚拟地址
unsigned long crrt_vaddr_kernel=0;
printk("module my_syscall335 is calling by process %d...\npid=%ld vaddr=0x%lx %lu bufferaddr=0x%lx %lu \n",
current->pid,obj_pid,obj_vaddr,obj_vaddr,crrt_buffer,crrt_buffer);
// 根据pid找到进程描述符
obj_task = pid_task(find_pid_ns(obj_pid, &init_pid_ns), PIDTYPE_PID);
// 根据进程描述符和虚拟地址找到虚拟地址vaddr所在页的页表项
if(!(obj_pte = get_pte(obj_task, obj_vaddr)))
return -2; //入侵的地址所在的页暂时不在内存中 此处返回-2是因为系统调用失败会返回-1,依据返回值告诉调用进程失败的原因
//由页表项找到所在页page
obj_page = pte_page(*obj_pte);
//由页返回页的虚拟地址
obj_pgvaddr = (unsigned long)page_address(obj_page);
printk("page virtual address=0x%lx %lu\n",obj_pgvaddr,obj_pgvaddr);
obj_page_offset = obj_vaddr & ~PAGE_MASK;//获得页偏移地址
obj_vaddr_kernel=obj_pgvaddr+obj_page_offset; //内核态的虚拟地址
printk("kernel virtual address=0x%lx %lu\n",obj_vaddr_kernel,obj_vaddr_kernel);
crrt_task=pid_task(find_pid_ns(crrt_pid, &init_pid_ns), PIDTYPE_PID);
if(!(crrt_pte = get_pte(crrt_task, crrt_buffer)))
return -3; //buffer分配失败时buffer所在页就不会在内存中,此处返回-3,一般来说传入的buffer是否是空要做检查,此处为了防止不做检查就传入的情况
crrt_page = pte_page(*crrt_pte);
crrt_pgvaddr = (unsigned long)page_address(crrt_page);
printk("buffer page virtual address=0x%lx %lu\n",crrt_pgvaddr,crrt_pgvaddr);
crrt_page_offset = crrt_buffer & ~PAGE_MASK;
crrt_vaddr_kernel=crrt_pgvaddr+crrt_page_offset;
printk("kernel virtual BUFFER address=0x%lx %lu\n",crrt_vaddr_kernel,crrt_vaddr_kernel);
//进行读取
memcpy((void *)crrt_vaddr_kernel,(void *)obj_vaddr_kernel,obj_bytes);
return 0; //调用成功
}
/*初始化*/
static int __init init_mycall(void)
{
printk("module my_syscall335 is initializing......\n");
sys_call_table=(unsigned long*)(SYS_CALL_TABLE_ADDRESS);
printk("sys_call_table: 0x%p\n", sys_call_table); //会发现输出值不等于SYS_CALL_TABLE_ADDRESS
anything_saved=(int(*)(void))(sys_call_table[__NR_syscall]);//保存系统调用表中的NUM位置上的系统调用
orig_cr0=clear_cr0();//使内核地址空间可写
sys_call_table[__NR_syscall]=(unsigned long) &sys_mycall;//用自己的系统调用替换NUM位置上的系统调用
setback_cr0(orig_cr0);//使内核地址空间不可写
return 0;
}
/*卸载*/
static void __exit exit_mycall(void)
{
printk("module my_syscall335 is exiting......\n");
orig_cr0=clear_cr0();
sys_call_table[__NR_syscall]=(unsigned long)anything_saved;//将系统调用恢复
setback_cr0(orig_cr0);
}
MODULE_LICENSE("GPL");
module_init(init_mycall);
module_exit(exit_mycall);
my_syscall336.c 源代码和上面的代码很相似,不同在于memcpy(dest,src,size)
这一行代码。
/*
my_syscall336.c
此模块主要功能:提供一个系统调用,其他进程可利用此系统调用(读取)任意进程任意虚拟地址空间的内容。
需要注意以下几点:
系统调用号336,使用前需确认336是否被占用,若占用则修改下面的__NR_syscall宏即可;
Linux 内核自5.7后为了安全起见 unexport kallsyms_lookup_name() and kallsyms_on_each_symbol() 因此为了获得sys_call_table的地址需用以下方法:
sudo cat /proc/kallsyms | grep sys_call_table 然后将其硬编码为源文件中的宏,每次开机地址发生变化,需要修改源文件,重新编译后再insmod入内核;
或者重新编译内核,在配置内核时修改sys_call_table地址为固定值,这样就不会变化了;
Author: Pcyslist
*/
#include <linux/kernel.h> //包含内和打印函数或其他重要宏定义
#include <linux/init.h> //模块初始化宏定义在此
#include <linux/module.h> //内核模块
#include <linux/unistd.h> //系统调用
#include <linux/sched.h> //进程管理,调度程序头文件,定义了任务结构task_struct
#include<linux/mm.h> //内存管理
#include<linux/mm_types.h>
#include<linux/export.h>
#include <linux/kallsyms.h> //曾包含了kallsyms_lookup_name(const char *)
#include <linux/time.h> //时间头文件
#include <linux/uaccess.h> //核访问用户进程内存地址的函数定义
#define __NR_syscall 336//系统调用号为336
#define SYS_CALL_TABLE_ADDRESS 0xffffffffa1000300 //sys_call_table对应的地址 0xffffffffa1000300 sudo cat /proc/kallsyms | grep sys_call_table
int orig_cr0; //用来存储cr0寄存器原来的值
unsigned long *sys_call_table=0;
static int(*anything_saved)(void); //定义一个函数指针,用来保存一个系统调用
typedef asmlinkage long (*sys_call_ptr_t)(const struct pt_regs *); //结构体指针类型用于传参
static int clear_cr0(void) //使cr0寄存器的第17位设置为0(内核空间可写)
{
unsigned int cr0=0;
unsigned int ret;
asm volatile("movq %%cr0,%%rax":"=a"(cr0));//将cr0寄存器的值移动到eax寄存器中,同时d输出到cr0变量中
ret=cr0;
cr0&=0xfffffffffffeffff;//将cr0变量值中的第17位清0,将修改后的值写入cr0寄存器
asm volatile("movq %%rax,%%cr0"::"a"(cr0));//将cr0变量的值作为输入,输入到寄存器eax中,同时移动到寄存器cr0中
return ret;
}
static void setback_cr0(int val) //使cr0寄存器设置为内核不可写
{
asm volatile("movq %%rax,%%cr0"::"a"(val));
}
// 根据一个进程的进程描述符和虚拟地址找到虚拟地址所在页的页表项
static pte_t* get_pte(struct task_struct *task, unsigned long vaddr)
{
pgd_t* pgd; //PGD: Page Global Directory
p4d_t* p4d; //P4D: 第四级页表
pud_t* pud; //PUD: Page Upper Directory
pmd_t* pmd; //PMD: Page Middle Directory
pte_t* pte; //PTE: Page Table Entry
struct mm_struct *mm = task->mm;
//五级页表层层索引
pgd = pgd_offset(mm, vaddr); //获得pgd的地址
if(pgd_none(*pgd) || pgd_bad(*pgd))
return NULL;
p4d = p4d_offset(pgd, vaddr); //获得p4d的地址
if(p4d_none(*p4d) || p4d_bad(*p4d))
return NULL;
pud = pud_offset(p4d, vaddr); //获得pud的地址
if(pud_none(*pud) || pud_bad(*pud))
return NULL;
pmd = pmd_offset(pud, vaddr); //获得pmd的地址
if(pmd_none(*pmd) || pmd_bad(*pmd))
return NULL;
pte = pte_offset_kernel(pmd, vaddr); //获得pte的地址
if(pte_none(*pte))
return NULL;
return pte;
}
static int sys_mycall(const struct pt_regs * regs) //定义自己的系统调用 参数为一个结构体指针regs,详情请看参考文献[6]
{
/*regs结构体指针内的四个成员,本质上是四个寄存器,分别存着 pid vaddr buffer bytes*/
unsigned long obj_pid=(unsigned long )regs->di; //待入侵的进程pid
unsigned long obj_vaddr=(unsigned long )regs->si; //带入侵进程的虚拟地址,此处的虚拟地址vaddr是用户态下的
unsigned long crrt_buffer=(unsigned long )regs->dx; //将[crrt_buffer,crrt_buffer+obj_bytes]地址区间的内容拷贝到obj_vaddr中实现修改
unsigned long obj_bytes=(unsigned long )regs->r10; //修改的字节数
int crrt_pid=current->pid; //调用本服务例程的进程pid
struct task_struct *obj_task;
struct task_struct *crrt_task;
pte_t* obj_pte;
pte_t* crrt_pte;
struct page* obj_page;
struct page* crrt_page;
// int i;
unsigned long obj_pgvaddr=0; //虚拟地址vaddr所在页的虚拟地址
unsigned long crrt_pgvaddr=0;
unsigned long obj_page_offset = 0; //虚拟地址vaddr的页偏移值
unsigned long crrt_page_offset = 0;
unsigned long obj_vaddr_kernel=0; //vaddr的内核态的虚拟地址
unsigned long crrt_vaddr_kernel=0;
printk("module my_syscall336 is calling by process %d...\npid=%ld vaddr=0x%lx %lu bufferaddr=0x%lx %lu \n",
current->pid,obj_pid,obj_vaddr,obj_vaddr,crrt_buffer,crrt_buffer);
// 根据pid找到进程描述符
obj_task = pid_task(find_pid_ns(obj_pid, &init_pid_ns), PIDTYPE_PID);
// 根据进程描述符和虚拟地址找到虚拟地址vaddr所在页的页表项
if(!(obj_pte = get_pte(obj_task, obj_vaddr)))
return -2; //入侵的地址所在的页暂时不在内存中 此处返回-2是因为系统调用失败会返回-1,依据返回值告诉调用进程失败的原因
//由页表项找到所在页page
obj_page = pte_page(*obj_pte);
//由页返回页的虚拟地址
obj_pgvaddr = (unsigned long)page_address(obj_page);
printk("page virtual address=0x%lx %lu\n",obj_pgvaddr,obj_pgvaddr);
obj_page_offset = obj_vaddr & ~PAGE_MASK;//获得页偏移地址
obj_vaddr_kernel=obj_pgvaddr+obj_page_offset; //内核态的虚拟地址
printk("kernel virtual address=0x%lx %lu\n",obj_vaddr_kernel,obj_vaddr_kernel);
crrt_task=pid_task(find_pid_ns(crrt_pid, &init_pid_ns), PIDTYPE_PID);
if(!(crrt_pte = get_pte(crrt_task, crrt_buffer)))
return -3; //buffer分配失败时buffer所在页就不会在内存中,此处返回-3,一般来说传入的buffer是否是空要做检查,此处为了防止不做检查就传入的情况
crrt_page = pte_page(*crrt_pte);
crrt_pgvaddr = (unsigned long)page_address(crrt_page);
printk("buffer page virtual address=0x%lx %lu\n",crrt_pgvaddr,crrt_pgvaddr);
crrt_page_offset = crrt_buffer & ~PAGE_MASK;
crrt_vaddr_kernel=crrt_pgvaddr+crrt_page_offset;
printk("kernel virtual BUFFER address=0x%lx %lu\n",crrt_vaddr_kernel,crrt_vaddr_kernel);
//执行修改
memcpy((void *)obj_vaddr_kernel,(void *)crrt_vaddr_kernel,obj_bytes);
return 0; //调用成功
}
static int __init init_mycall(void)
{
printk("module my_syscall336 is initializing......\n");
sys_call_table=(unsigned long*)(SYS_CALL_TABLE_ADDRESS);
printk("sys_call_table: 0x%p\n", sys_call_table);
anything_saved=(int(*)(void))(sys_call_table[__NR_syscall]);//保存系统调用表中的NUM位置上的系统调用
orig_cr0=clear_cr0();//使内核地址空间可写
sys_call_table[__NR_syscall]=(unsigned long) &sys_mycall;//用自己的系统调用替换NUM位置上的系统调用
setback_cr0(orig_cr0);//使内核地址空间不可写
return 0;
}
static void __exit exit_mycall(void)
{
printk("module my_syscall336 is exiting......\n");
orig_cr0=clear_cr0();
sys_call_table[__NR_syscall]=(unsigned long)anything_saved;//将系统调用恢复
setback_cr0(orig_cr0);
}
MODULE_LICENSE("GPL");
module_init(init_mycall);
module_exit(exit_mycall);
为了编译以上两个.c源文件,需要写Makefile文件,这里文件名必须为"Makefile"注意大小写,文件中的内容如下:(注意第一行obj-m:=****.o要与****.c名字对应),编译后会生成两个对应的.ko
文件,命令行终端执行sudo insmod ***.ko
将模块插入内核。
#Makefile
obj-m:=my_syscall335.o # Pay attention here 'obj-m:=my_syscall336.o'
PWD:= $(shell pwd)
KERNELDIR:= /lib/modules/$(shell uname -r)/build
all:
make -C $(KERNELDIR) M=$(PWD) modules
clean:
make -C $(KERNELDIR) M=$(PWD) clean
invade_process.c源代码,实现展示进程私有内存整体布局,并通过调用以上两个系统调用完成进程内存窥探和修改的整体逻辑。
/*
invade_process.c
*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int disp_mem(int pid); //展示任意进程内存空间结构
int main(){
unsigned long pid,vaddr,bytes; //读取(并修改)任意进程pid的虚拟地址vaddr的bytes个字节
char *buffer=0; //读取时用于存放读取到的内容,修改时用于存放要修改的内容
int i=0; //用于迭代buffer
printf("Current pid=%d\n",getpid()); //输出当前进程pid
printf("Input the infomation of process you want invade : pid vaddr bytes\n");
scanf("%lu%lu%lu",&pid,&vaddr,&bytes); //输入要入侵(读取或修改)的进程的pid、虚拟地址vaddr、读取(修改)的字节数bytes
printf("Info of invaded process: pid=%lu vaddr=%lu bytes=%lu\n",pid,vaddr,bytes);
if(disp_mem(pid)==-1) //展示任意进程pid的内存映射布局,返回-1代表该pid进程不存在
return -1;
/*调用335系统调用(读取)指定进程指定虚拟地址空间的内容*/
if(buffer=(char *)malloc(bytes)){
int sign=syscall(335,pid,vaddr,buffer,bytes); //调用335号系统调用,335系统调用实现了对任意进程任意虚拟地址空间内容的读取
if(sign==0){ //调用成功
printf("Intercepted content: \n0x");
//逐字节以16进制输出读取到的bytes个字节
for(i=0;i<bytes;i++)
printf("%02x ",*(buffer+i));
printf("\n");
printf("%s\n",buffer);
}
else if(sign==-2){ //调用失败,原因:当前要读取的虚拟地址所在页不在内存中(即缺页)
printf("Failed: The page where the virtual address is located is not in memory \n");
free(buffer); //中途结束程序前,释放buffer内存
return 0;
}
else if(sign==-1){ //系统调用335内核模块未加载,或者其他系统故障
printf("syscall 335 failed !\n");
free(buffer); //中途结束程序前,释放buffer内存
return 0;
}
//正常释放buffer内存
free(buffer);
}else
printf("Buffer allocation failed !");
/*调用336系统调用(修改)指定进程指定虚拟地址空间的内容*/
printf("Do you want something more exciting ? y/n\n");
getchar(); //接受前面格式输入scanf的回车键,因为下面的choice读取只读一个字符
char choice='n';
scanf("%c",&choice);
if(choice=='n'|| choice=='N'){
printf("So boring you are , bye !\n");
}else if(choice=='y'||choice=='Y'){
printf("Let's modify the contents of the specified memory address of the process: vaddr bytes\n");
scanf("%lu%lu",&vaddr,&bytes); //此处只需输入要修改的内存虚拟地址和字节数,而不用输入pid
if(buffer=(char *)malloc(bytes)){
printf("input the modifed content < %ld bytes:\n",bytes);
getchar();
fgets(buffer,bytes,stdin); //此处以修改的内容是字符串为例,当然也可以是任意类型比如整形int,则申请4字节的buffer 然后 scanf("%d",(int*)buffer);
//以下调用336系统调用来修改指定虚拟地址空间的内容为buffer的内容
int sign=syscall(336,pid,vaddr,buffer,bytes);
if(sign==0)
printf("Modify successfully !\n");
else if(sign==-2)
printf("Failed: The page where the virtual address is located is not in memory \n");
else if(sign==-1)
printf("syscall 336 failed !");
free(buffer);
}
}
return 0;
}
/*展示任意进程的内存布局*/
int disp_mem(int pid){
char file_name[50]={0};
sprintf(file_name,"/proc/%d/maps",pid); //读取任意进程的/proc/$pid/maps
FILE * f=fopen(file_name,"r");
if(f==NULL){
printf("process %d doesn't exist !\n",pid);
return -1;
}else{
char c;
while((c = fgetc(f)) != EOF)
{
printf("%c", c);
}
printf("\n");
fclose(f);
}
return 0;
}
target_process.c源代码,被入侵的进程,主要任务为暴露自己的相关信息出去,并阻塞自己让其他进程入侵。
/*
target_process.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
int main()
{
char* vaddr = (char*)malloc(100); //待其它进程入侵读取的内容,将其地址暴露出去
char* buffer=(char *)malloc(100); //待其他进程入侵修改的内容,将其地址也暴露出去,此时的buffer没有任何内容
strcpy(vaddr, "Who are you ?");
printf("pid:%d vaddr: %lu buufer: %lu\n", getpid(),(unsigned long)vaddr, (unsigned long)buffer); //暴露进程pid和两个待入侵的地址
printf("host:%s \n", vaddr);
getchar(); //阻塞自身
printf("hacker:%s\n", buffer); //黑客入侵成功后,将会在此留下他想对主人说的话:I am your father hahaha...
//入侵失败则输出为空
free(vaddr);
free(buffer);
return 0;
}
运行结果
运行平台:Ubuntu20.04 (kernel 5.13.0-27-generic)
首先查看当前系统调用表sys_call_table在内存中的地址,可以看到第一行R
是只读的:
执行命令行 sudo cat /proc/kallsyms | grep sys_call_table
修改两个系统调用内核模块源文件中的宏定义:
#define SYS_CALL_TABLE_ADDRESS 0xffffffff82c00300
命令行终端执行sudo make
编译两个系统调用源文件(执行两次,第一次执行完需要修改Makefile文件的第一行)
加载两个内核模块到内核中sudo insmod ***.ko
命令行终端输入lsmod | grrep my_syscall
可以看到模块成功插入到Linux内核中。
编译invade_process.c和target_process.c
启动defender进程,准备迎接入侵,可以看到第一次./defender
直接按回车停止阻塞,hacker没有入侵,输出为空,第二次./defender
后保持阻塞,接下来我们将进行入侵。
启动hacker进程入侵defender进程,输入要入侵的进程pid 12395、虚拟地址、读取字节数20,输出如下,中间的密密麻麻的为内存布局,Intercepted content就是我们读取到的20字节内容(16进制一行,字符形式一行),“ Who are you ? "说明我们确实读取到defender进程该虚拟地址的内容了。
继续输入 y 干点大事,我们要告诉他我是谁,于是就有了下面的输出,修改字节数小于25字节,输入要修改的内容 “i am a hacker named Pcyslist…”。
打开defender进程,输入回车让它停止阻塞,看看我们是否真的入侵成功!
emm…这…输入的25字节太少了,我们输入的内容多余25个字节了,有一部分没有修改进去,不过程序逻辑是没问题的,下次输入字节选项时输大点即可随心所欲的修改了。
在target_process.c源文件中,待修改的地址的内容是一个通过malloc()函数申请的buffer内存,经测试,这个地址可以是任意的,比如一个整形变量int a,结构体中的某个成员变量,只要将其虚拟地址暴露出去,就能够成功读取和修改。
遇到的问题
-
系统调用的问题:
- **sys_call_table的内存地址的获取:**该地址是动态的,每次系统启动其地址都是不一样的,本来计划用kallsyms_lookup_name函数来获取,Linux 内核自5.7后为了安全起见取消了该函数,因此只能通过
sudo cat /proc/kallsyms | grep sys_call_table
来获取然后硬编码到代码中。这种方法很机械,也很无奈,若有更好的解决办法,欢迎大家评论区指正。 - **对于sys_call_table的写入:**系统对sys_call_table进行了写保护,对CR0寄存器进行修改,以改变它的只读状态,经查资料修改CR0寄存器的第17位为0即可,修改完后即可将自己的系统调用函数sys_mycall这个函数指针挂在相应的系统调用号的位置上。
- **含有参数传递的系统调用的设计:**从 内核4.17 版本开始,64 位(x86-64)的内核会采用包装的方式来调用系统调用实际的函数,如果直接简单粗暴地像编写普通函数那样的话,在后面调用自定义系统调用时,并不能正确地返回结果——实际上如果有输出调试的话,会发现根本不会触发我们增加的系统调用。[6] 为此需要传入一个结构体指针,这一点在下面的参考文献[6]中有详细介绍,是一篇很不错的文章。
- **sys_call_table的内存地址的获取:**该地址是动态的,每次系统启动其地址都是不一样的,本来计划用kallsyms_lookup_name函数来获取,Linux 内核自5.7后为了安全起见取消了该函数,因此只能通过
-
虚拟内存管理的问题:
-
**从虚拟地址到物理地址的转换:**因为我们要访问任意进程的虚拟地址空间的内容,因此就需要找到存放其内容的具体物理地址在那,为实现这一点需要理解Linux内存管理的基本原理以及
mm.h
头文件中的各种函数,还有自己的计算机的分页机制是几级页表。 -
**用户空间的虚拟地址VS内核空间的虚拟地址:**在实验过程中发现,同一个物理地址在用户态下的虚拟地址和内核态下的虚拟地址是不一样的,可以对比以下两对地址:
obj_vaddr 和 obj_vaddr_kernel
或者crrt_buffer 和 crrt_vaddr_kernel
第一对儿地址均指向的是defender进程的vaddr(读取时) buffer(修改时)
;第二对二地址均指向hacker进程的buffer(无论读取还是修改)
,可以发现每一对地址都是不一样的,这一点很让人迷惑不解。在运行完hacker进程后,我们执行命令dmesg
可以看到hacker进程执行系统调用时留下的内核输出记录:可以看到pid为12600的进程(hacker进程)分别调用335号系统调用和336号系统调用各一次,对pid为12395的进程(defender进程)进行入侵。在进行读取时,obj_vaddr=vaddr=0x55ced8d8a2a0 obj_vaddr_kernel=kernel virtual address=0xffff9e31694432a0
是不相等的,他们均指向的是defender进程的vaddr内存空间;crrt_buffer=bufferaddr=0x5603c4ac80b0 crrt_vaddr_kernel=kernel virtual BUFFER address=0xffff9e3186fce0b0
同样也是不相同的。在进行修改时,调用336系统调用中也是不同的。 -
对于暂时还没有加入到内存的页,当我们用其他进程入侵访问该虚拟地址时会发生缺页(不会产生缺页中断),此时系统调用会失败,因为缺页中断是进程自己正常执行时才会触发的。当然我们或许可以设计一种方法让它加载到内存?这是一个不错的想法。
-
内容总结
本文设计了一个小工具——“进程内存窥探修改器”:通过内核模块法向Linux内核增加了两个带参数传递的系统调用,借用此系统调用**能够完成对任意进程任意虚拟地址空间内容的读取和修改。**并通过hacker 入侵 defender的情景模式进行演示,实验结果证明该算法程序的真实有效性。若要访问的虚拟地址所在的页未在内存中,则无法对其进行读取和修改,这种情况可能是这个程序可以改进的地方。
参考文献
特别感谢以下文献作者对本文的帮助!
[1] 系统调用原理 https://linux.fasionchan.com/zh_CN/latest/system-programming/syscall/principle.html
[2] Linux 进程管理 https://www.starky.ltd/2018/08/17/Linux-Process-Management/
[3] Linux虚拟内存管理 https://blog.csdn.net/u010150046/article/details/72630262
[4] Ubuntu20.04编译内核并添加一个系统调用 https://blog.csdn.net/BAR_WORKSHOP/article/details/111647568
[5] 使用内核模块添加系统调用(无需编译内核)https://blog.csdn.net/qq_34258344/article/details/103228607
[6] 内核模块法增加带参数的系统调用 https://moefactory.com/3041.moe#header-id-8
[7] ERROR: modpost: “kallsyms_lookup_name” undefined https://github.com/anbox/anbox-modules/issues/49
[8] Linux内核私闯进程地址空间 https://blog.csdn.net/dog250/article/details/102292288
[9] 虚拟地址转换得到实际的物理地址 https://blog.csdn.net/hpu11/article/details/52600726