实战Linux内核模块:终止ptrace跟踪程序与被跟踪进程

在Linux系统中,ptrace(进程跟踪)是调试、分析程序的核心能力------比如我们常用的GDB调试器,就是靠ptrace系统调用来实现断点调试、查看进程内存、单步执行等功能。但凡事有两面性,恶意程序也可能通过ptrace跟踪系统中的关键进程,窃取数据或篡改执行逻辑。今天我们就通过一个极简的Linux内核模块,聊聊如何从内核层面识别并终止所有存在ptrace跟踪关系的进程对(跟踪者与被跟踪者),同时拆解背后的内核编程思路和核心知识点。

一、先搞懂:ptrace跟踪关系的内核本质

要写终止ptrace跟踪的模块,首先得知道内核里怎么记录"谁在跟踪谁"。

Linux内核中,每个进程都对应一个叫task_struct的结构体(可以理解成进程的"身份证"),里面藏着所有和进程相关的信息。其中有个关键的链表字段ptraced------只要进程A是跟踪者(tracer),那么所有被A跟踪的进程(tracee),都会被挂到A的ptraced链表上

简单说:想找谁是跟踪者,只要看它的ptraced链表有没有内容就行;想找被跟踪的进程,直接遍历这个链表就能全部揪出来。这也是我们这个模块的核心判断依据。

二、模块核心功能:简单直接的"终止逻辑"

这个内核模块的目标没有花里胡哨的设计,核心就四件事:

  1. 模块加载后,周期性扫描系统中所有进程;
  2. 逐个检查进程的ptraced链表,识别出"跟踪者"进程;
  3. 先给所有被跟踪进程发送无法拦截的SIGKILL信号(必杀信号),终止被跟踪者;
  4. 再终止跟踪者进程;
  5. 模块卸载时,安全停止扫描并清理占用的内核资源。

整个逻辑"简单粗暴",但胜在高效------毕竟内核层操作不需要经过用户态的权限校验,能直接对进程下"终止指令"。

三、代码拆解

我们把核心代码拆成几个关键部分,用普通人能懂的话解释,不堆砌专业术语:

1. 基础准备:宏定义与头文件

c 复制代码
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/list.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/sched/signal.h>
#include <linux/workqueue.h>
  • pr_fmt宏:让内核打印日志时自动带上模块名,比如日志会显示模块名: Loaded!,方便我们定位日志来源;
  • 头文件:list.h用来操作内核链表(比如ptraced链表),sched.h用来访问进程的task_struct结构体,workqueue.h用来实现"周期性扫描"的功能。

2. 核心工具函数:杀进程+判断跟踪者

(1)kill_task:给进程发"致命信号"
c 复制代码
static void kill_task(struct task_struct *task)
{
    send_sig(SIGKILL, task, 1);
}

send_sig是内核里发送信号的函数,这里发的是SIGKILL(信号9)------这是Linux里最"狠"的信号,进程无法忽略、无法捕获,收到就只能终止。参数里的1表示"向整个进程组发送",确保彻底杀死进程,不留残留。

(2)is_tracer:判断是不是"跟踪者"
c 复制代码
static bool is_tracer(struct list_head *children)
{
    struct list_head *list;
    list_for_each (list, children) {
        struct task_struct *task =
            list_entry(list, struct task_struct, ptrace_entry);
        if (task)
            return true;
    }
    return false;
}

这个函数接收一个链表(也就是进程的ptraced链表),用list_for_each遍历链表:如果链表里有元素(说明有被跟踪的进程),就返回"是跟踪者"(true),否则返回false。

这里的list_entry是内核链表的核心宏------简单说就是"从链表节点,找到对应的进程信息(task_struct)",是内核链表操作的必学知识点。

(3)kill_tracee:杀死所有被跟踪者
c 复制代码
static void kill_tracee(struct list_head *children)
{
    struct list_head *list;
    list_for_each (list, children) {
        struct task_struct *task_ptraced =
            list_entry(list, struct task_struct, ptrace_entry);
        pr_info("ptracee -> comm: %s, pid: %d, gid: %d, ptrace: %d\n",
                task_ptraced->comm, task_ptraced->pid, task_ptraced->tgid,
                task_ptraced->ptrace);
        kill_task(task_ptraced);
    }
}

遍历跟踪者的ptraced链表,逐个取出被跟踪进程,先打印进程名、PID等信息(方便排查),再调用kill_task发SIGKILL终止它。

3. 核心扫描逻辑:check函数

c 复制代码
static void check(void)
{
    struct task_struct *task;
    for_each_process (task) {
        if (!is_tracer(&task->ptraced))
            continue;

        kill_tracee(&task->ptraced);
        kill_task(task); /* Kill the tracer once all tracees are killed */
    }
}

for_each_process是内核提供的宏,能遍历系统中所有进程。对每个进程:

  • 先用is_tracer判断是不是跟踪者;
  • 如果是,先杀所有被它跟踪的进程(kill_tracee);
  • 最后再杀这个跟踪者本身。

4. 周期性扫描:工作队列的妙用

模块需要"一直扫描",但内核里不能用死循环(会占满CPU),所以用了"延迟工作队列"实现周期性执行:

c 复制代码
static void periodic_routine(struct work_struct *ws)
{
    if (likely(loaded))
        check();
    queue_delayed_work(wq, &dont_trace_task, JIFFIES_DELAY);
}

periodic_routine是每次要执行的函数:

  • 先看模块是否还加载(loaded为true),如果是就执行check扫描进程;
  • 然后把自己重新加入工作队列,延迟1个内核节拍(JIFFIES_DELAY=1,约10ms)后再次执行------这样就实现了"循环扫描",既不卡CPU,又能实时监控。

5. 模块的加载与卸载:生命周期管理

(1)加载函数(dont_trace_init)
c 复制代码
static int __init dont_trace_init(void)
{
    wq = create_workqueue(...);
    queue_delayed_work(wq, &dont_trace_task, JIFFIES_DELAY);

    loaded = true;
    pr_info("Loaded!\n");
    return 0;
}

模块加载时:创建工作队列 → 把扫描任务加入队列(1个节拍后首次执行) → 标记模块已加载。

(2)卸载函数(dont_trace_exit)
c 复制代码
static void __exit dont_trace_exit(void)
{
    loaded = false;

    cancel_delayed_work(&dont_trace_task);
    flush_workqueue(wq);
    destroy_workqueue(wq);

    pr_info("Unloaded.\n");
}

模块卸载时:先标记"不再扫描" → 取消还没执行的扫描任务 → 等正在执行的任务完成 → 销毁工作队列释放资源------这是内核模块的通用规范,避免内存泄漏或任务残留。

四、设计思路与执行流程

我们用流程图把模块的核心逻辑梳理清楚,一看就懂:




模块加载
创建工作队列
提交首次延迟扫描任务(10ms后执行)
重新提交延迟任务,等待下一次扫描
模块是否已加载?
终止本次任务
遍历系统所有进程
进程的ptraced链表非空?
遍历ptraced链表,杀死所有被跟踪进程
杀死该跟踪者进程
模块卸载
标记模块未加载
取消未执行的扫描任务
等待正在执行的任务完成
销毁工作队列释放资源
模块卸载完成

If you need the complete source code, please add the WeChat number (c17865354792)

这个模块的设计思路,其实是内核编程的通用思路:

  1. 找对数据结构 :依托task_structptraced链表,精准定位跟踪关系------内核编程的核心就是"找对数据结构,问题就解决了一半";
  2. 异步周期性执行:用工作队列实现非阻塞的循环扫描,既保证实时性,又不占用过多CPU资源;
  3. 安全的资源管理:加载时创建资源,卸载时彻底清理,遵循"谁创建谁销毁"的原则。

五、核心知识点总结:从这个模块学到什么?

这个小模块虽然代码量少,但覆盖了Linux内核编程的三大核心领域,是入门内核开发的绝佳案例:

1. 进程管理领域

  • task_struct:Linux内核描述进程的"核心结构体",包含PID、进程名、链表、状态等所有进程信息;
  • for_each_process:遍历系统所有进程的基础宏,是进程监控、进程管理类模块的必备工具;
  • 内核态信号发送:send_sig比用户态的kill系统调用权限更高,能直接对所有进程发送信号。

2. 内核链表领域

  • list_head:Linux内核的"通用链表",几乎所有内核数据结构(比如进程链表、文件链表)都基于它实现;
  • list_for_each/list_entry:遍历链表、转换链表节点的核心宏------掌握这两个,就能操作内核中90%的链表结构。

3. 工作队列领域

  • 工作队列是内核实现"异步任务"的核心机制,适合处理需要延迟、周期性执行的任务;
  • 模块卸载时必须用flush_workqueue等待任务完成,这是避免内核资源泄漏的关键。

六、模块测试运行完整步骤

制造ptrace跟踪场景(测试靶场)

先开两个终端 ,手动用gdb做ptrace跟踪,模拟调试/跟踪场景:

终端1:启动一个常驻进程(被跟踪者)
bash 复制代码
# 启动sleep进程,休眠300秒
sleep 300

记一下这个sleep的PID(比如后续用ps查)。

终端2:用gdb附加sleep进程(跟踪者)

bash 复制代码
# 查sleep的PID
ps aux | grep sleep

# gdb附加该PID(替换为实际sleep的PID)
gdb -p 你的sleep进程PID

gdb成功attach后,就建立了ptrace跟踪关系:gdb是跟踪者(tracer),sleep是被跟踪者(tracee)。

加载模块,验证查杀效果

1. 加载内核模块

回到第三个root终端,加载模块:

bash 复制代码
insmod dont_trace.ko

加载成功无报错,查看内核日志确认加载:

bash 复制代码
dmesg -w

日志会输出:模块名: Loaded!

2. 观察查杀结果

加载后模块会立即周期性扫描,瞬间发生两件事:

  1. 终端2的gdb进程直接被杀死,退回命令行;
  2. 终端1的sleep进程也被杀死,进程退出;
  3. dmesg会打印被杀死的跟踪者/被跟踪者的进程名、PID等日志。

3. 验证无残留

bash 复制代码
# 查gdb和sleep是否还存在
ps aux | grep -E "gdb|sleep"

正常情况两个进程都已消失,说明模块生效。

卸载模块(测试完成必做)

测试结束后安全卸载模块,避免持续查杀:

bash 复制代码
rmmod dont_trace

查看dmesg日志,会输出模块名: Unloaded.,模块停止扫描、资源释放完成。

七、最后说两句

这个模块虽然功能简单,但它是"从0到1"理解内核进程管理的好例子。你可以基于它扩展更多功能:比如只终止特定PID的跟踪者、记录跟踪行为日志、或者把"SIGKILL"换成"SIGSTOP"(暂停进程而非终止)。

内核编程的核心,从来不是写复杂的代码,而是"理解内核的设计逻辑和数据结构"------只要找对了task_struct里的关键字段,再配合内核提供的通用工具(链表、工作队列),就能写出简洁又有效的内核模块。

总结

  1. 该模块的核心是利用task_structptraced链表识别跟踪者,通过send_sig发送SIGKILL终止跟踪者和被跟踪者;
  2. 采用工作队列实现周期性扫描,遵循内核模块生命周期规范确保安全卸载;
  3. 模块覆盖了进程管理、内核链表、工作队列三大内核编程核心领域,是入门内核开发的优质实战案例。

Welcome to follow WeChat official account【程序猿编码

相关推荐
网安情报局5 小时前
除了 CDN,DDoS 攻击还有哪些更有效的防护方式?
网络
Promise微笑5 小时前
2026年国产替代油介损测试仪:油介损全场景解决方案与技术演进
大数据·网络·人工智能
蜡台6 小时前
Python包管理工具pip完全指南-----2
linux·windows·python
Ujimatsu6 小时前
虚拟机安装Debian 13.x及其常用软件(2026.4)
linux·运维·ubuntu
千百元6 小时前
zookeeper启不来了
linux·zookeeper·debian
AnalogElectronic8 小时前
linux 测试网络和端口是否连通的命令详解
linux·网络·php
Edward111111119 小时前
4月28日防火墙问题
linux·运维·服务器
Rust研习社9 小时前
使用 Axum 构建高性能异步 Web 服务
开发语言·前端·网络·后端·http·rust
灰子学技术9 小时前
Envoy HTTP 流量层面的 Metric 指标分析
网络·网络协议·http
上海云盾-小余9 小时前
海外恶意 UDP 攻击溯源:分层封禁策略与业务兼容平衡方案
网络·网络协议·udp