使用jprobe建设镜面层叠的原则和见解

忽然想起的回忆,那是2007上周五在冬季,我看我的老湿调试Linux堆IP层,只看到他改变路由查找的逻辑,然后直接make install上的立竿见影的效果有点,我只知道,,这种逻辑必须再次更改编译内核。再一次,他没有编译,就像刚才编译的文件...时又无聊的工作阻碍了我对Linux内核的探索进度,直到今天,我依旧对编译内核有相当的恐惧,不怕出错,而是怕磁盘空间不够,initrd的组装拆解之类,太繁琐了。我之所以知道2007年的那天是周五,是由于第二天我要加班。没有谁逼我。我自愿的,由于我想知道师父是怎么做到不又一次编译内核就能改变非模块的内核代码处理逻辑的。第二天的收获非常多,不但知道了他使用了“镜像协议栈”。还额外赚了一天的加班费。我还记得周六加完班我和老婆去吃了一家叫做石工坊的羊排火锅。人家赠送了一仅仅绿色的兔子玩偶。

如今那个玩偶还在,我家小小特别喜欢。就是这么一堆看似无关却又巧合的事。让我在这个周末认为必须写下一点什么。
       好吧。从kprobe開始吧。

假设我面试一个搞Linux内核的人,问他怎么调试内核,他回答先加入printk然后又一次编译最后加载新内核运行,看dmesg,我会让他先等上几分钟,然后人事就会告诉他让他回去等通知。幸运的是,我没有碰到这样的人让我面试来展现我五十步笑百步的半瓶子晃荡作风。也从来没有碰到过如此不仁慈的面试者,我以前在一次找工作的时候真的就是这么说的。人家也真的让我去等通知,然而我真的就等到了通知,通知入职的时间以及体检事宜...说这些的目的是想展示一个调试内核的利器,kprobe。

它能够动态改动内核地址空间代码的二进制指令,然后运行随意你想让它运行的代码段,这或许应该能够称为二进制动态编程!多么黑的技术,全然无视源码的逻辑。全然无视编译器的苦功,直接就这么把二进制机器码给改了。
       kprobe的工作原理非常easy,比方你有一个函数func,你能够在func被调用前和调用后各插入一段代码,我们假设func指令是
begin
go
end

kprobe要做的就是替换掉begin。将其变为:
jmp prefunc
当然在替换前还要保存原有的,以便运行完我们的钩子函数prefunc还能跳回原来的逻辑。至于复杂的jmp细节(长短跳。相对绝对跳之类的)以及Intel的INT 3调试模式单步模式本文不再赘述,赘这个字用得好,由于全部这些细节都是累赘,你换个非Intel平台的话,你就知道这些是多么累赘了,只是对一辈子不换平台的那些人来讲,理解这些细节就成了资本,因此想了解这些,还是去看雪吧,找级别高态度好的问,或者潜水也行。我认为看雪的信息量已经够大了,基本上都能找到现成的。
       尽管我不提倡在本文讲Intel的细节,可是有一个除外。那就是prefunc钩子函数的參数问题,比方我想钩住vfs_write函数,它的声明例如以下:

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos);

假设这个prefunc钩子函数的參数和vfs_write的一样那多好啊,整个逻辑就成了:

ssize_t prefunc(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
    todo_something(.....);
    return vfs_write(file, buf, count, pos);
}

可是不幸的是。kprobe做不到。由于它是基于INT 3异常/中断来处理的,而Intel的异常/中断的处理有一套特定的规程,即保存全部的上下文环境。因此它的參数就仅仅有struct pt_regs *regs一个。即全部的寄存器信息。

要想还原vfs_write的參数,你必须针对这个regs參数做一个“深度解析”才行,而这又一次将你引入了平台相关的地狱。假设你在X86平台。你就不得不正确它的寄存器使用规约做一番具体的了解才干还原被钩函数的參数,对于X86来讲,參数保存在栈中(也能够通过寄存器传參),要想还原被钩函数的參数现场,你要分析的就是regs->sp。以下我就不说了。
       说了上述不幸,来点幸运的,那就是Linux内核提供了一种kprobe之上的机制。帮你实现了上面说的那些本应该由你自己完毕的工作,这就是jprobe。总的来讲。jprobe的要点在于它实际上就是一个kprobe的prefunc。它的prefunc是这么实现的:

prefunc(kprobe, regs)
{
    保存regs寄存器现场
    保存栈的内容  //由于jprobe使用和被钩函数同样的栈,可能会改变栈的内容
    替换regs里面ip指针为jprobe钩子的指针
    返回
}

就这样一个kprobe的prefunc钩子函数就把INT 3返回正常流,可是请注意,在这个prefunc中,将regs的ip改变了,改成了jprobe的entry函数,而栈信息一点都没有变。因此返回正常流之后,栈上的參数信息没有变,仅仅是运行的函数变了。变成了entry。等jprobe的entry运行完了之后,调用jprobe_return来还原,这个return实际上就是再次进入INT 3异常,然后调用kprobe的还有一个钩子函数来还原现场,即将prefunc保存的regs现场以及栈现场还原。是不是非常像setjmp和longjmp啊。是的,差点儿是一样的!

到此为止,程序进入了被钩函书,整个流程就是:
进入INT 3--进入prefunc保存现场以及ip替换为entry--返回被改动后的正常流在同一栈上运行entry--进入INT 3--还原原始的reg中的ip以及恢复原始栈的内容--返回原始的运行流运行被钩函数
jprobe的entry钩子函数的參数和原始的被钩函数的參数全然一样。这是由于它们的栈内容一模一样。以上就是jprobe的全部了。当然除了细节。
       除了大致原理之外。值得注意的一个细节就是。jprobe的钩子函数中是能够发生进程切换的。由于它实际上是在一个正常流中运行。仅仅只是这个正常流被改动了而已,而在kprobe的钩子函数中,是不能发生抢占的。由于本质上它还是在INT3的异常/中断处理函数中运行的。
       那么,我们能用这个jprobe做些什么呢?假设你真的看懂了我的意图。那么我想说的或许正是你所想的,那就是使用jprobe能够实现一个镜像协议栈,我先将代码片断贴上:

static struct jprobe steal_jprobe = {
    .entry   = steal_ip_local_deliver,
    .kp = {
    .symbol_name    = "ip_local_deliver",
    }
};

int steal_ip_local_deliver(struct sk_buff *skb)
{
    if (skb && skb->mark == 1004) {
        ip_local_deliver_finish(skb);
    }
    jprobe_return();
    return 0;
}

这段代码或许表达了我的目的。即从ip_local_deliver開始,数据包将不再经原生的Linux协议栈处理。而是被偷到了我的steal_ip_local_deliver,在其内部,能够实现自己的协议栈处理逻辑,当然为了简单,我仅仅是调用了 ip_local_deliver_finish将数据包直接绕过NF_HOOK往上传递。

可是,当你真的运行上面代码的时候,得到的将是无情的panic!

由于在steal函数调用ip_local_deliver_finish之后,它一路走到了socket层,skb已经被free了。由于共享一个栈数据且skb传入的仅仅是一个指向skb数据的指针,此时返回正常的ip_local_deliver之后,skb的字段取值将全不可用。我们须要做的是在steal函数内部阻止掉这个运行流,然而冯.诺依曼机器是串行处理机,且UNIX/Linux的运行流是靠fork分发的。也就是说你根本就不可能阻止掉不论什么一个运行流,除非调用exit,可是在softirq中是不能exit的,由于你根本不知道借用了哪个task_struct!为了不再panic,你仅仅能:

int steal_ip_local_deliver(struct sk_buff *skb)
{
    if (skb && skb->mark == 1004) {
        ip_local_deliver_finish(skb_copy(skb, GFP_ATOMIC));
    }
    jprobe_return();
    return 0;
}

这样做之后,在steal中传入 ip_local_deliver_finish的仅仅是skb的一个副本。待返回正常的ip_local_deliver后。原始的skb还是可用的。可是这就将一个数据流fork成了两个,对于TCP协议而言,TCP逻辑会自己主动丢掉反复的,可是对于像UDP或者ICMP之类的数据流而言。将会收到双份的数据,一个来自正常的协议栈,还有一个来自steal的协议栈。如今的问题在于。怎样阻止掉正常的协议栈处理。
       想当然的办法就是让正常的ip_local_deliver直接返回0。这实际上也是一种正确的做法。如今我们回到最開始。膜拜一下那个阴招,那就是二进制动态编程!我能不能将被钩的函数也改掉呢?思路非常清晰,接下来就是找解决这个问题的方法了,我定义了一个stub函数:

int stub(struct sk_buff *skb)
{
    return 0;
}

我要做的就是将返回原始正常流后原本要调用ip_local_deliver的指令改为调用stub,要实现这个就要进行动态的二进制指令改动。深入到kprobe细节的都应该知道kprobe结构体包括一个字段:

    /* copy of the original instruction */
    struct arch_specific_insn ainsn;

我连凝视也一并贴上了,由于这省了我解释了,注意命名。ainsn中的a就是arch的意思,这个多加的层为上层屏蔽了平台相关的细节,对于X86而言,它就是:

u8 *insn;

是的。一连串的二进制指令,非常显然,这里保存的指令肯定是jmp ip_local_deliver之类的,由于这段指令的目的就是跳转回原始的运行流。我仅仅须要将其改为jmp stub就能够了。就是说。在jprobe的entry钩子中,将kprobe的ainsn.insn改为jmp stub,然后为了不影响不相关的兴许的运行流返回ip_local_deliver,在stub中再将kprobe的ainsn.insn改回去。

接下拉的任务就是找指令了,前面说了,与其看大部头全英文的Intel手冊,不如直接看雪。我并不反对看Intel手冊。可是为了这么一个简单的问题一头扎进去也有点太彪了。看雪上的内容非常多非常全。我尝试了两种方式:
方式1:短跳转,指令码为0xFF 0x04 $小端倒序的stub函数地址
失败!不恋战,由于我的目的不是搞清晰Intel的指令集。

只是还是略微有一点想不通。早就開始平坦内存模式了,怎么如今还有人用长跳啊!无论怎么样,换一种方式。
方式2:借助寄存器。

即mov rax $小端倒序的stub函数地址; jmp rax;指令码为0x48 0xB8 $小端倒序的stub函数地址 0xFF 0xE0。
这次成功了。

不欢呼,不庆祝。由于这仅仅是一个环节而已。

完整的代码例如以下:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/hardirq.h>

#include <linux/skbuff.h>

// 从/proc/kallsyms中找出的ip_local_deliver_finish地址
// 我仅仅是想在jprobe函数中直接调用finish,企图跳过NF_HOOK
#define func 0xffffffff812b70f3

int (*f)(struct sk_buff *);
// 保存全局变量。由于无法从steal钩子函数中取到kprobe
struct kprobe *k = NULL;

#define JMP_CODE_SIZE   12
#define ADDR_SIZE       sizeof(void *)

u8 saved[MAX_INSN_SIZE] = {0};

// 注意,不要太在意以下的二进制指令码的具体细节!主要含义理解就可以:将地址送入寄存器,jmp到该处
u8 jmpcode[JMP_CODE_SIZE] = {0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xe0};

int stub(struct sk_buff *skb)
{
    memcpy(k->ainsn.insn, saved, MAX_INSN_SIZE);
    return 0;
}

int steal_ip_local_deliver(struct sk_buff *skb)
{

    if (skb && skb->mark == 1234) {
        // 先保存原始的替换指令码。
        memcpy(saved, k->ainsn.insn, MAX_INSN_SIZE);
        // 替换为jmp到steal函数的指令码。
        memcpy(k->ainsn.insn, jmpcode, JMP_CODE_SIZE);
        // 调用自己的函数,为了简单,我仅仅是调用了ip_local_deliver_finish。
        (*f)(skb);
        // 从这里返回后,由于指令码已被替换为steal函数stub,因此就不会
        // 再返回正常的ip_local_deliver了。
    }
    jprobe_return();
    return 0;
}

static struct jprobe steal_jprobe = {
    .entry   = steal_ip_local_deliver,
    .kp = {
    .symbol_name    = "ip_local_deliver",
    }
};

static int __init jprobe_init(void)
{
    int ret;
    int i = 0, j = 9;
    unsigned long addr = (unsigned long)&stub;
    ret = register_jprobe(&steal_jprobe);
    if (ret < 0) {
        printk("register_jprobe failed:%d\n", ret);
        return -1;
    }
    k = &steal_jprobe.kp;
    f = func;
    // 依据stub函数的地址填充jmpcode指令码数组
    for (i = 0; i < ADDR_SIZE; i++, j--) {
        jmpcode[j] = (addr&0xff00000000000000)>>56;
        addr <<= 8;
    }
    return 0;
}

static void __exit jprobe_exit(void)
{
    unregister_jprobe(&steal_jprobe);
}

module_init(jprobe_init)
module_exit(jprobe_exit)
MODULE_LICENSE("GPL");

这就是几年前我看到的一个镜像协议栈的原理。尽管Linux非常难直接通过make config将整个网络协议栈编译成一个模块,可是我们自己能够手工构建一个网络协议栈模块,无非就是把net/ipv4文件夹编译成一个模块。然后使用jprobe钩住netif_receive_skb这个底层函数,将控制权导入到我们自己的协议栈模块中。说白了在冯.诺依曼这样的串行处理的机器中,争夺的就是控制权,仅仅要你占有了CPU。那控制权就属于你,一旦你有了控制权。你不光能够增删改查内存中的数据,还能够增删改查内存中的代码,由于数据和代码都在内存...
       关于kprobe的文档,最好的还是Linux内核自带的Documentation/kprobes.txt。

本文解读了一个镜像协议栈的实现原理。可是同一时候也展示了一个Linux内核调试的方法。那就是使用kprobe上面的jprobe进行调试,实际上基于kprobe的调试工具非常多,比方SystemTap之类的,可是个人认为。在你亲自己主动手step by step编写一个原生的jprobe模块之前。还是不用那些工具为好,由于光是仅仅熟悉工具本身的使用方法就要花费不少时间和精力。并且假设底层原理还不理解的话,即使学会了工具的使用方法也会非常快忘记。或许是我太老土了。可是我一直都记得教计算机编程的老师说过的。在亲自用命令行编译一个完整的程序之前,不要用IDE,是这个道理。

等亲自己主动手玩转了kprobe和jprobe,再去学习基于它们封装的工具,那就简单多了。一旦学会便更加难以忘记。

后记:关于panic
编程和生活相比,其快感在于panic后的reset!无论你犯了多大的错误(段错误?栈溢出?被渗透?被抹屎?),无论你有多大的遗憾(/etc/sysctl.conf文件加入了kernel.panic = 1以后忘了sysctl -p...关键是我是远程连的公司的机器...可恨没有ipmi的支持!!),reset后一切成云烟!

假设有什么过不去的坎,panic吧,然后reset!

时间过得太快了,从2007年至今,也就弹指一挥间。当时的我多么希望能成为像我的老湿那样的人。其实,我也正是凭着这样的简单的崇拜与对网络技术的好奇而一步步走到了如今的,水平谈不上什么登峰造极,但起码也是从菜鸟一步步来的,如今充其量是区区肥胖退伍军人。

没有理由,突然,我从过去的回忆,岁月不饶人啊!

版权声明:本文博客原创文章。博客,未经同意,不得转载。

时间: 2024-10-29 19:11:44

使用jprobe建设镜面层叠的原则和见解的相关文章

[转载]CSS三大特性(继承、优先级、层叠)之个人见解

首先声明一下CSS三大特性——继承.优 先级和层叠.继承即子类元素继承父类的样式,比如font-size,font-weight等f开头的css样式以及text-align,text- indent等t开头的样式以及我们常用的color.简单的就不演示了,强调一下font-size这个东东(虽然也有继承,但是标签不同继承的效果也 不一样),比如下面的代码: <!DOCTYPE html> <html lang="en"> <head> <met

CSS三大特性(继承、优先级、层叠)之个人见解

首先声明一下CSS三大特性——继承.优先级和层叠.继承即子类元素继承父类的样式,比如font-size,font-weight等f开头的css样式以及text-align,text-indent等t开头的样式以及我们常用的color.简单的就不演示了,强调一下font-size这个东东(虽然也有继承,但是标签不同继承的效果也不一样),比如下面的代码: <!DOCTYPE html> <html lang="en"> <head> <meta c

数字化校园建设方案

第一章 总体目标 1.1 总体目标 建设一个运行于统一平台.统一技术框架下的学校门户系统.管理信息系统和教学资源管理系统及应用平台. 根据国示范建设中有关数字化校园建设的要求及我校实际情况,采用走出去请进来,遵循面向服务架构(SOA)的设计原则进行系统的需求分析.总体设计,实现技术先进.高效稳定.安全可靠.信息规范.数据完整的一体化数字化校园系统.消除信息孤岛和应用孤岛:支持师生教学与学习:支持学校决策和管理:支持数据信息共建共享:为学校师生提供一站式管理服务:提高工作效率,提高管理效率,提高决

0017 CSS 三大特性:层叠性、继承性、优先级

目标: 理解 能说出css样式冲突采取的原则 能说出那些常见的样式会有继承 应用 能写出CSS优先级的算法 能会计算常见选择器的叠加值 5.1 CSS层叠性 概念: 所谓层叠性是指多种CSS样式的叠加. 是浏览器处理冲突的一个能力,如果一个属性通过两个相同选择器设置到同一个元素上,那么这个时候一个属性就会将另一个属性层叠掉 原则: 样式冲突,遵循的原则是就近原则. 那个样式离着结构近,就执行那个样式. 样式不冲突,不会层叠 CSS层叠性最后的执行口诀: 长江后浪推前浪,前浪死在沙滩上. 5.2

基于三维GIS平台的智慧园区建设方案

随着Web3. 0在城市生活应用中的不断深入,智慧城市建设也在不断深入的发展,智慧园区作为智慧城市重要组成部分,存在着建设内容多.周期长,运用存在延续性.技术不断的更新性,而且在建设的过程中涉及到的人员多与投资规模大等特征,需要将智慧停车.餐饮.安防.信息发布.环境监控等融合在一体,并能为用户提供个性化的支持服务,这就需要对智慧园区的建设做好整体规划与设计.1智慧园区建设的系统架构分析基于三维GIS平台的智慧园区建设主要目标是为用户提供高效.便捷.舒适.生态和谐的居住环境,通过以感知技术为核心智

HTML5和CSS3基础教程(第8版)-读书笔记(2)

第7章 CSS构造模块 7.1 构造样式规则 样式表中包含了定义网页外观的规则.样式表中的每条规则都有两个主要部分:选 择 器(selector) 和 声 明 块(declaration block). 选择器决定哪些元素受到影响:声明块由一个或多个属性 - 值对(每个属性 -值对构成一条声明,declaration)组成,它们指定应该做什么. 声明块内的每条声明都是一个由冒号隔开.以分号结尾的属性- 值对. 声明的顺序并不重要,除非对相同的属性定义了两次. 在样式规则中可以添加额外的空格.制表

CSS样式与选择器

CSS构造块的样式: 1.  h1{color:red;background-color:yellow} 其中:h1是选择器,花括号内是声明部分.多个声明之间用分号隔开. 2.为样式规则添加注释:/*...*/.注意不能将一个注释嵌套在另一注释中.如:/*这样做/*是不对的*/因为嵌套在外层注释内*/.注释可以放在样式规则内部.如:img{border:4px solid red;/*margin-right:12px;*/} ,浏览器会显示的只有border样式,因为margin-right

CS考研_统考大纲

序号 政治 外语 业务课一 业务课二 1 (101)思想政治理论 (201)英语一 (301)数学一 (408)计算机学科专业基础综合 以上是计算机全国统考考试科目,三门公共课非统考基本也都是这三个,大家如果看到非统考的科目如果是三个1,就可以直接来参考我这里列出的大纲了!所以在此,我就直接列出最近的2015年考研这四个的考试大纲: 政治101: Ⅰ.考试性质 思想政治理论考试是为高等院校和科研院所招收硕士研究生而设置的具有选拔性质的全国招生考试科目,其目的是科学.公平.有效地测试考生掌握大学本

高项:2016年3月7日作业(第1章、第2章)

第1章       信息化基础知识 1.1.1信息 1.信息的概念存在两个基本的层次,即本体论层次和认识论层次. 2.事件的本体论:就是事物的运动状态和状态变化方式的自我表述. 3.主体关于某个事物的认识论信息,就是主体对于该事物的运动状态以及状态变化方式的具体描述,包含对于它的"状态和方式"的形式.含义和价值的描述. 1.1.3国家信息化体系要素 1.国家信息化体系包括:(信息技术应用).(信息资源).(信息网络).(信息技术和产业).(信息化人才).(信息化法国政策和标准规范)6个