如何在内核模块中读取其他进程的堆内存?
内核态读取用户态进程内存的可行方法
以下是几种内核模块中常用的用户态进程内存读取方案,覆盖从高层封装到底层操作的不同场景:
1. 使用 copy_from_user() 基础接口(需配合地址空间切换)
这是内核访问用户态内存最基础的函数,但需要手动处理地址空间切换和内存区域合法性验证:
- 核心逻辑:先获取目标进程的
task_struct和mm_struct,切换到用户态地址空间视角后,调用copy_from_user()完成数据读取。 - 示例代码:
#include <linux/uaccess.h> #include <linux/sched.h> #include <linux/mm.h> int read_user_memory(struct task_struct *target_task, unsigned long user_addr, void *kernel_buf, size_t len) { struct mm_struct *mm = get_task_mm(target_task); if (!mm) return -ESRCH; mmap_read_lock(mm); // 切换到用户态地址空间,让内核能识别用户态虚拟地址 set_fs(USER_DS); // 先验证地址是否可读 if (!access_ok(VERIFY_READ, (void __user *)user_addr, len)) { set_fs(KERNEL_DS); mmap_read_unlock(mm); mmput(mm); return -EFAULT; } // 执行读取,返回未成功读取的字节数 long ret = copy_from_user(kernel_buf, (void __user *)user_addr, len); // 切回内核地址空间 set_fs(KERNEL_DS); mmap_read_unlock(mm); mmput(mm); return ret == 0 ? 0 : -EFAULT; }
- 注意:必须锁定目标进程的内存描述符(
mmap_read_lock),防止读取过程中进程内存区域被修改;get_task_mm会增加mm_struct的引用计数,用完后需调用mmput释放。
2. 使用 access_process_vm() 高层封装接口
内核提供了封装好的 access_process_vm() 函数,已经内置了地址空间切换、权限验证和内存锁定逻辑,是更安全便捷的选择:
- 核心逻辑:直接传入目标进程的
task_struct、用户态地址、内核缓冲区、读取长度和访问标志即可完成读取。 - 示例代码:
#include <linux/mm.h> #include <linux/sched.h> ssize_t read_process_memory(struct task_struct *target_task, unsigned long user_addr, void *kernel_buf, size_t len) { // FOLL_FORCE 标志可强制读取部分特殊内存区域,根据需求调整 ssize_t bytes_read = access_process_vm(target_task, user_addr, kernel_buf, len, FOLL_FORCE); return bytes_read >= 0 ? bytes_read : -EFAULT; }
- 注意:该函数会自动处理
mm_struct的引用和锁定,无需手动操作;返回值为成功读取的字节数,失败返回负数错误码。
3. 遍历进程 VMA 结构(模拟 /proc/pid/maps 逻辑)
如果需要先获取进程的合法内存区域再读取,可以遍历目标进程的 vm_area_struct 链表(对应用户态 /proc/pid/maps 的内容),筛选出可读区域后进行读取:
- 核心逻辑:遍历
mm->mmap链表,检查每个 VMA 的权限标志(VM_READ),对合法区域执行内存读取。 - 示例代码片段:
#include <linux/mm.h> #include <linux/sched.h> #include <linux/uaccess.h> void scan_and_read_process_memory(struct task_struct *target_task) { struct mm_struct *mm = get_task_mm(target_task); if (!mm) return; mmap_read_lock(mm); struct vm_area_struct *vma; // 遍历所有内存区域 for (vma = mm->mmap; vma; vma = vma->vm_next) { if (vma->vm_flags & VM_READ) { char buf[64]; unsigned long read_addr = vma->vm_start; size_t read_len = min((size_t)(vma->vm_end - vma->vm_start), sizeof(buf)); set_fs(USER_DS); copy_from_user(buf, (void __user *)read_addr, read_len); set_fs(KERNEL_DS); // 处理读取到的buf数据 } } mmap_read_unlock(mm); mmput(mm); }
- 注意:遍历过程中必须持有
mmap_read_lock,避免进程内存区域被动态修改导致遍历出错。
4. 底层页表操作(特殊场景使用)
如果上述方法无法满足需求(比如读取未被映射的物理页),可以直接操作进程页表找到用户地址对应的物理页,再映射到内核空间访问:
- 核心步骤:
- 获取目标进程的
pgd(页全局目录) - 遍历页表(pgd -> pud -> pmd -> pte)找到用户地址对应的物理页框号(PFN)
- 使用
kmap()将物理页映射到内核虚拟地址 - 直接访问内核地址读取数据,完成后调用
kunmap()解除映射
- 获取目标进程的
- 注意:该方法复杂度极高,需要处理页表转换异常、大页、页锁定等问题,仅推荐在特殊场景下使用;可以配合
get_user_pages()函数辅助获取用户页的引用。
内容的提问来源于stack exchange,提问作者user24572871
相关产品推荐
相关产品推荐

