日韩久久久精品,亚洲精品久久久久久久久久久,亚洲欧美一区二区三区国产精品 ,一区二区福利

使用jprobe建設(shè)鏡面層疊的原則和見解

系統(tǒng) 1913 0
忽然想起的回憶,那是2007上周五在冬季,我看我的老濕調(diào)試Linux堆IP層,只看到他改變路由查找的邏輯,然后直接make install上的立竿見影的效果有點(diǎn),我只知道,,這種邏輯必須再次更改編譯內(nèi)核。再一次,他沒有編譯,就像剛才編譯的文件...時(shí)又無聊的工作阻礙了我對Linux內(nèi)核的探索進(jìn)度,直到今天,我依舊對編譯內(nèi)核有相當(dāng)?shù)目謶郑慌鲁鲥e(cuò),而是怕磁盤空間不夠,initrd的組裝拆解之類,太繁瑣了。我之所以知道2007年的那天是周五,是由于第二天我要加班。沒有誰逼我。我自愿的,由于我想知道師父是怎么做到不又一次編譯內(nèi)核就能改變非模塊的內(nèi)核代碼處理邏輯的。第二天的收獲非常多,不但知道了他使用了“鏡像協(xié)議棧”。還額外賺了一天的加班費(fèi)。我還記得周六加完班我和老婆去吃了一家叫做石工坊的羊排火鍋。人家贈送了一僅僅綠色的兔子玩偶。

如今那個(gè)玩偶還在,我家小小特別喜歡。就是這么一堆看似無關(guān)卻又巧合的事。讓我在這個(gè)周末認(rèn)為必須寫下一點(diǎn)什么。
?????? 好吧。從kprobe開始吧。

假設(shè)我面試一個(gè)搞Linux內(nèi)核的人,問他怎么調(diào)試內(nèi)核,他回答先加入printk然后又一次編譯最后加載新內(nèi)核運(yùn)行,看dmesg,我會讓他先等上幾分鐘,然后人事就會告訴他讓他回去等通知。幸運(yùn)的是,我沒有碰到這樣的人讓我面試來展現(xiàn)我五十步笑百步的半瓶子晃蕩作風(fēng)。也從來沒有碰到過如此不仁慈的面試者,我以前在一次找工作的時(shí)候真的就是這么說的。人家也真的讓我去等通知,然而我真的就等到了通知,通知入職的時(shí)間以及體檢事宜...說這些的目的是想展示一個(gè)調(diào)試內(nèi)核的利器,kprobe。

它能夠動態(tài)改動內(nèi)核地址空間代碼的二進(jìn)制指令,然后運(yùn)行隨意你想讓它運(yùn)行的代碼段,這或許應(yīng)該能夠稱為二進(jìn)制動態(tài)編程!多么黑的技術(shù),全然無視源碼的邏輯。全然無視編譯器的苦功,直接就這么把二進(jìn)制機(jī)器碼給改了。
?????? kprobe的工作原理非常easy,比方你有一個(gè)函數(shù)func,你能夠在func被調(diào)用前和調(diào)用后各插入一段代碼,我們假設(shè)func指令是
begin
go
end

kprobe要做的就是替換掉begin。將其變?yōu)椋?
jmp prefunc
當(dāng)然在替換前還要保存原有的,以便運(yùn)行完我們的鉤子函數(shù)prefunc還能跳回原來的邏輯。至于復(fù)雜的jmp細(xì)節(jié)(長短跳。相對絕對跳之類的)以及Intel的INT 3調(diào)試模式單步模式本文不再贅述,贅這個(gè)字用得好,由于全部這些細(xì)節(jié)都是累贅,你換個(gè)非Intel平臺的話,你就知道這些是多么累贅了,只是對一輩子不換平臺的那些人來講,理解這些細(xì)節(jié)就成了資本,因此想了解這些,還是去看雪吧,找級別高態(tài)度好的問,或者潛水也行。我認(rèn)為看雪的信息量已經(jīng)夠大了,基本上都能找到現(xiàn)成的。
?????? 盡管我不提倡在本文講Intel的細(xì)節(jié),可是有一個(gè)除外。那就是prefunc鉤子函數(shù)的參數(shù)問題,比方我想鉤住vfs_write函數(shù),它的聲明例如以下:

      ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos);
    
假設(shè)這個(gè)prefunc鉤子函數(shù)的參數(shù)和vfs_write的一樣那多好啊,整個(gè)邏輯就成了:
      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的異常/中斷的處理有一套特定的規(guī)程,即保存全部的上下文環(huán)境。因此它的參數(shù)就僅僅有struct pt_regs *regs一個(gè)。即全部的寄存器信息。

要想還原vfs_write的參數(shù),你必須針對這個(gè)regs參數(shù)做一個(gè)“深度解析”才行,而這又一次將你引入了平臺相關(guān)的地獄。假設(shè)你在X86平臺。你就不得不正確它的寄存器使用規(guī)約做一番具體的了解才干還原被鉤函數(shù)的參數(shù),對于X86來講,參數(shù)保存在棧中(也能夠通過寄存器傳參),要想還原被鉤函數(shù)的參數(shù)現(xiàn)場,你要分析的就是regs->sp。以下我就不說了。
?????? 說了上述不幸,來點(diǎn)幸運(yùn)的,那就是Linux內(nèi)核提供了一種kprobe之上的機(jī)制。幫你實(shí)現(xiàn)了上面說的那些本應(yīng)該由你自己完畢的工作,這就是jprobe。總的來講。jprobe的要點(diǎn)在于它實(shí)際上就是一個(gè)kprobe的prefunc。它的prefunc是這么實(shí)現(xiàn)的:

      prefunc(kprobe, regs)
{
    保存regs寄存器現(xiàn)場
    保存棧的內(nèi)容  //由于jprobe使用和被鉤函數(shù)同樣的棧,可能會改變棧的內(nèi)容
    替換regs里面ip指針為jprobe鉤子的指針
    返回
}
    
就這樣一個(gè)kprobe的prefunc鉤子函數(shù)就把INT 3返回正常流,可是請注意,在這個(gè)prefunc中,將regs的ip改變了,改成了jprobe的entry函數(shù),而棧信息一點(diǎn)都沒有變。因此返回正常流之后,棧上的參數(shù)信息沒有變,僅僅是運(yùn)行的函數(shù)變了。變成了entry。等jprobe的entry運(yùn)行完了之后,調(diào)用jprobe_return來還原,這個(gè)return實(shí)際上就是再次進(jìn)入INT 3異常,然后調(diào)用kprobe的還有一個(gè)鉤子函數(shù)來還原現(xiàn)場,即將prefunc保存的regs現(xiàn)場以及棧現(xiàn)場還原。是不是非常像setjmp和longjmp啊。是的,差點(diǎn)兒是一樣的!

到此為止,程序進(jìn)入了被鉤函書,整個(gè)流程就是:
進(jìn)入INT 3--進(jìn)入prefunc保存現(xiàn)場以及ip替換為entry--返回被改動后的正常流在同一棧上運(yùn)行entry--進(jìn)入INT 3--還原原始的reg中的ip以及恢復(fù)原始棧的內(nèi)容--返回原始的運(yùn)行流運(yùn)行被鉤函數(shù)
jprobe的entry鉤子函數(shù)的參數(shù)和原始的被鉤函數(shù)的參數(shù)全然一樣。這是由于它們的棧內(nèi)容一模一樣。以上就是jprobe的全部了。當(dāng)然除了細(xì)節(jié)。
?????? 除了大致原理之外。值得注意的一個(gè)細(xì)節(jié)就是。jprobe的鉤子函數(shù)中是能夠發(fā)生進(jìn)程切換的。由于它實(shí)際上是在一個(gè)正常流中運(yùn)行。僅僅只是這個(gè)正常流被改動了而已,而在kprobe的鉤子函數(shù)中,是不能發(fā)生搶占的。由于本質(zhì)上它還是在INT3的異常/中斷處理函數(shù)中運(yùn)行的。
?????? 那么,我們能用這個(gè)jprobe做些什么呢?假設(shè)你真的看懂了我的意圖。那么我想說的或許正是你所想的,那就是使用jprobe能夠?qū)崿F(xiàn)一個(gè)鏡像協(xié)議棧,我先將代碼片斷貼上:

      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;
}
    
這段代碼或許表達(dá)了我的目的。即從ip_local_deliver開始,數(shù)據(jù)包將不再經(jīng)原生的Linux協(xié)議棧處理。而是被偷到了我的steal_ip_local_deliver,在其內(nèi)部,能夠?qū)崿F(xiàn)自己的協(xié)議棧處理邏輯,當(dāng)然為了簡單,我僅僅是調(diào)用了 ip_local_deliver_finish將數(shù)據(jù)包直接繞過NF_HOOK往上傳遞。


?????? 可是,當(dāng)你真的運(yùn)行上面代碼的時(shí)候,得到的將是無情的panic!

由于在steal函數(shù)調(diào)用ip_local_deliver_finish之后,它一路走到了socket層,skb已經(jīng)被free了。由于共享一個(gè)棧數(shù)據(jù)且skb傳入的僅僅是一個(gè)指向skb數(shù)據(jù)的指針,此時(shí)返回正常的ip_local_deliver之后,skb的字段取值將全不可用。我們須要做的是在steal函數(shù)內(nèi)部阻止掉這個(gè)運(yùn)行流,然而馮.諾依曼機(jī)器是串行處理機(jī),且UNIX/Linux的運(yùn)行流是靠fork分發(fā)的。也就是說你根本就不可能阻止掉不論什么一個(gè)運(yùn)行流,除非調(diào)用exit,可是在softirq中是不能exit的,由于你根本不知道借用了哪個(gè)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的一個(gè)副本。待返回正常的ip_local_deliver后。原始的skb還是可用的。可是這就將一個(gè)數(shù)據(jù)流fork成了兩個(gè),對于TCP協(xié)議而言,TCP邏輯會自己主動丟掉反復(fù)的,可是對于像UDP或者ICMP之類的數(shù)據(jù)流而言。將會收到雙份的數(shù)據(jù),一個(gè)來自正常的協(xié)議棧,還有一個(gè)來自steal的協(xié)議棧。如今的問題在于。怎樣阻止掉正常的協(xié)議棧處理。
?????? 想當(dāng)然的辦法就是讓正常的ip_local_deliver直接返回0。這實(shí)際上也是一種正確的做法。如今我們回到最開始。膜拜一下那個(gè)陰招,那就是二進(jìn)制動態(tài)編程!我能不能將被鉤的函數(shù)也改掉呢?思路非常清晰,接下來就是找解決這個(gè)問題的方法了,我定義了一個(gè)stub函數(shù):
      int stub(struct sk_buff *skb)
{
    return 0;
}
    
我要做的就是將返回原始正常流后原本要調(diào)用ip_local_deliver的指令改為調(diào)用stub,要實(shí)現(xiàn)這個(gè)就要進(jìn)行動態(tài)的二進(jìn)制指令改動。深入到kprobe細(xì)節(jié)的都應(yīng)該知道kprobe結(jié)構(gòu)體包括一個(gè)字段:
          /* copy of the original instruction */
    struct arch_specific_insn ainsn;
    
我連凝視也一并貼上了,由于這省了我解釋了,注意命名。ainsn中的a就是arch的意思,這個(gè)多加的層為上層屏蔽了平臺相關(guān)的細(xì)節(jié),對于X86而言,它就是:
      u8 *insn;
    
是的。一連串的二進(jìn)制指令,非常顯然,這里保存的指令肯定是jmp ip_local_deliver之類的,由于這段指令的目的就是跳轉(zhuǎn)回原始的運(yùn)行流。我僅僅須要將其改為jmp stub就能夠了。就是說。在jprobe的entry鉤子中,將kprobe的ainsn.insn改為jmp stub,然后為了不影響不相關(guān)的興許的運(yùn)行流返回ip_local_deliver,在stub中再將kprobe的ainsn.insn改回去。


?????? 接下拉的任務(wù)就是找指令了,前面說了,與其看大部頭全英文的Intel手冊,不如直接看雪。我并不反對看Intel手冊。可是為了這么一個(gè)簡單的問題一頭扎進(jìn)去也有點(diǎn)太彪了。看雪上的內(nèi)容非常多非常全。我嘗試了兩種方式:
方式1:短跳轉(zhuǎn),指令碼為0xFF 0x04 $小端倒序的stub函數(shù)地址
失敗!不戀戰(zhàn),由于我的目的不是搞清晰Intel的指令集。

只是還是略微有一點(diǎn)想不通。早就開始平坦內(nèi)存模式了,怎么如今還有人用長跳啊!無論怎么樣,換一種方式。
方式2:借助寄存器。

即mov rax $小端倒序的stub函數(shù)地址; jmp rax;指令碼為0x48 0xB8 $小端倒序的stub函數(shù)地址 0xFF 0xE0。
這次成功了。

不歡呼,不慶祝。由于這僅僅是一個(gè)環(huán)節(jié)而已。


?????? 完整的代碼例如以下:

      #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函數(shù)中直接調(diào)用finish,企圖跳過NF_HOOK
#define func 0xffffffff812b70f3

int (*f)(struct sk_buff *);
// 保存全局變量。由于無法從steal鉤子函數(shù)中取到kprobe
struct kprobe *k = NULL;

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

u8 saved[MAX_INSN_SIZE] = {0};

// 注意,不要太在意以下的二進(jìn)制指令碼的具體細(xì)節(jié)!主要含義理解就可以:將地址送入寄存器,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函數(shù)的指令碼。
        memcpy(k->ainsn.insn, jmpcode, JMP_CODE_SIZE);
        // 調(diào)用自己的函數(shù),為了簡單,我僅僅是調(diào)用了ip_local_deliver_finish。
        (*f)(skb);
        // 從這里返回后,由于指令碼已被替換為steal函數(shù)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;
    // 依據(jù)stub函數(shù)的地址填充jmpcode指令碼數(shù)組
    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");
    
這就是幾年前我看到的一個(gè)鏡像協(xié)議棧的原理。盡管Linux非常難直接通過make config將整個(gè)網(wǎng)絡(luò)協(xié)議棧編譯成一個(gè)模塊,可是我們自己能夠手工構(gòu)建一個(gè)網(wǎng)絡(luò)協(xié)議棧模塊,無非就是把net/ipv4文件夾編譯成一個(gè)模塊。然后使用jprobe鉤住netif_receive_skb這個(gè)底層函數(shù),將控制權(quán)導(dǎo)入到我們自己的協(xié)議棧模塊中。說白了在馮.諾依曼這樣的串行處理的機(jī)器中,爭奪的就是控制權(quán),僅僅要你占有了CPU。那控制權(quán)就屬于你,一旦你有了控制權(quán)。你不光能夠增刪改查內(nèi)存中的數(shù)據(jù),還能夠增刪改查內(nèi)存中的代碼,由于數(shù)據(jù)和代碼都在內(nèi)存...
?????? 關(guān)于kprobe的文檔,最好的還是Linux內(nèi)核自帶的Documentation/kprobes.txt。


?????? 本文解讀了一個(gè)鏡像協(xié)議棧的實(shí)現(xiàn)原理。可是同一時(shí)候也展示了一個(gè)Linux內(nèi)核調(diào)試的方法。那就是使用kprobe上面的jprobe進(jìn)行調(diào)試,實(shí)際上基于kprobe的調(diào)試工具非常多,比方SystemTap之類的,可是個(gè)人認(rèn)為。在你親自己主動手step by step編寫一個(gè)原生的jprobe模塊之前。還是不用那些工具為好,由于光是僅僅熟悉工具本身的使用方法就要花費(fèi)不少時(shí)間和精力。并且假設(shè)底層原理還不理解的話,即使學(xué)會了工具的使用方法也會非常快忘記。或許是我太老土了。可是我一直都記得教計(jì)算機(jī)編程的老師說過的。在親自用命令行編譯一個(gè)完整的程序之前,不要用IDE,是這個(gè)道理。

等親自己主動手玩轉(zhuǎn)了kprobe和jprobe,再去學(xué)習(xí)基于它們封裝的工具,那就簡單多了。一旦學(xué)會便更加難以忘記。


后記:關(guān)于panic
編程和生活相比,其快感在于panic后的reset!無論你犯了多大的錯(cuò)誤(段錯(cuò)誤?棧溢出?被滲透?被抹屎?),無論你有多大的遺憾(/etc/sysctl.conf文件加入了kernel.panic = 1以后忘了sysctl -p...關(guān)鍵是我是遠(yuǎn)程連的公司的機(jī)器...可恨沒有ipmi的支持!!),reset后一切成云煙!

假設(shè)有什么過不去的坎,panic吧,然后reset!


?????? 時(shí)間過得太快了,從2007年至今,也就彈指一揮間。當(dāng)時(shí)的我多么希望能成為像我的老濕那樣的人。其實(shí),我也正是憑著這樣的簡單的崇拜與對網(wǎng)絡(luò)技術(shù)的好奇而一步步走到了如今的,水平談不上什么登峰造極,但起碼也是從菜鳥一步步來的,如今充其量是區(qū)區(qū)肥胖退伍軍人。

沒有理由,突然,我從過去的回憶,歲月不饒人啊!

版權(quán)聲明:本文博客原創(chuàng)文章。博客,未經(jīng)同意,不得轉(zhuǎn)載。

使用jprobe建設(shè)鏡面層疊的原則和見解


更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯(lián)系: 360901061

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發(fā)表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 德保县| 百色市| 肇州县| 淳安县| 福海县| 原平市| 成安县| 连南| 甘孜| 阳江市| 石台县| 海城市| 卓资县| 沧源| 深水埗区| 凤山县| 神池县| 宾阳县| 平泉县| 日喀则市| 丰原市| 临安市| 东乡县| 富源县| 金塔县| 绥化市| 修武县| 湘阴县| 皋兰县| 谢通门县| 滦南县| 连山| 景洪市| 田林县| 双峰县| 凉城县| 靖边县| 渭南市| 阿拉尔市| 遂宁市| 台安县|