亚洲免费在线-亚洲免费在线播放-亚洲免费在线观看-亚洲免费在线观看视频-亚洲免费在线看-亚洲免费在线视频

使用jprobe建設鏡面層疊的原則和見解

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

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

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

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

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

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

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

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

到此為止,程序進入了被鉤函書,整個流程就是:
進入INT 3--進入prefunc保存現(xiàn)場以及ip替換為entry--返回被改動后的正常流在同一棧上運行entry--進入INT 3--還原原始的reg中的ip以及恢復原始棧的內(nèi)容--返回原始的運行流運行被鉤函數(shù)
jprobe的entry鉤子函數(shù)的參數(shù)和原始的被鉤函數(shù)的參數(shù)全然一樣。這是由于它們的棧內(nèi)容一模一樣。以上就是jprobe的全部了。當然除了細節(jié)。
?????? 除了大致原理之外。值得注意的一個細節(jié)就是。jprobe的鉤子函數(shù)中是能夠發(fā)生進程切換的。由于它實際上是在一個正常流中運行。僅僅只是這個正常流被改動了而已,而在kprobe的鉤子函數(shù)中,是不能發(fā)生搶占的。由于本質(zhì)上它還是在INT3的異常/中斷處理函數(shù)中運行的。
?????? 那么,我們能用這個jprobe做些什么呢?假設你真的看懂了我的意圖。那么我想說的或許正是你所想的,那就是使用jprobe能夠實現(xiàn)一個鏡像協(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;
}
    
這段代碼或許表達了我的目的。即從ip_local_deliver開始,數(shù)據(jù)包將不再經(jīng)原生的Linux協(xié)議棧處理。而是被偷到了我的steal_ip_local_deliver,在其內(nèi)部,能夠實現(xiàn)自己的協(xié)議棧處理邏輯,當然為了簡單,我僅僅是調(diào)用了 ip_local_deliver_finish將數(shù)據(jù)包直接繞過NF_HOOK往上傳遞。


?????? 可是,當你真的運行上面代碼的時候,得到的將是無情的panic!

由于在steal函數(shù)調(diào)用ip_local_deliver_finish之后,它一路走到了socket層,skb已經(jīng)被free了。由于共享一個棧數(shù)據(jù)且skb傳入的僅僅是一個指向skb數(shù)據(jù)的指針,此時返回正常的ip_local_deliver之后,skb的字段取值將全不可用。我們須要做的是在steal函數(shù)內(nèi)部阻止掉這個運行流,然而馮.諾依曼機器是串行處理機,且UNIX/Linux的運行流是靠fork分發(fā)的。也就是說你根本就不可能阻止掉不論什么一個運行流,除非調(diào)用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還是可用的。可是這就將一個數(shù)據(jù)流fork成了兩個,對于TCP協(xié)議而言,TCP邏輯會自己主動丟掉反復的,可是對于像UDP或者ICMP之類的數(shù)據(jù)流而言。將會收到雙份的數(shù)據(jù),一個來自正常的協(xié)議棧,還有一個來自steal的協(xié)議棧。如今的問題在于。怎樣阻止掉正常的協(xié)議棧處理。
?????? 想當然的辦法就是讓正常的ip_local_deliver直接返回0。這實際上也是一種正確的做法。如今我們回到最開始。膜拜一下那個陰招,那就是二進制動態(tài)編程!我能不能將被鉤的函數(shù)也改掉呢?思路非常清晰,接下來就是找解決這個問題的方法了,我定義了一個stub函數(shù):
      int stub(struct sk_buff *skb)
{
    return 0;
}
    
我要做的就是將返回原始正常流后原本要調(diào)用ip_local_deliver的指令改為調(diào)用stub,要實現(xiàn)這個就要進行動態(tài)的二進制指令改動。深入到kprobe細節(jié)的都應該知道kprobe結構體包括一個字段:
          /* copy of the original instruction */
    struct arch_specific_insn ainsn;
    
我連凝視也一并貼上了,由于這省了我解釋了,注意命名。ainsn中的a就是arch的意思,這個多加的層為上層屏蔽了平臺相關的細節(jié),對于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手冊。可是為了這么一個簡單的問題一頭扎進去也有點太彪了。看雪上的內(nèi)容非常多非常全。我嘗試了兩種方式:
方式1:短跳轉,指令碼為0xFF 0x04 $小端倒序的stub函數(shù)地址
失敗!不戀戰(zhàn),由于我的目的不是搞清晰Intel的指令集。

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

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

不歡呼,不慶祝。由于這僅僅是一個環(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};

// 注意,不要太在意以下的二進制指令碼的具體細節(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");
    
這就是幾年前我看到的一個鏡像協(xié)議棧的原理。盡管Linux非常難直接通過make config將整個網(wǎng)絡協(xié)議棧編譯成一個模塊,可是我們自己能夠手工構建一個網(wǎng)絡協(xié)議棧模塊,無非就是把net/ipv4文件夾編譯成一個模塊。然后使用jprobe鉤住netif_receive_skb這個底層函數(shù),將控制權導入到我們自己的協(xié)議棧模塊中。說白了在馮.諾依曼這樣的串行處理的機器中,爭奪的就是控制權,僅僅要你占有了CPU。那控制權就屬于你,一旦你有了控制權。你不光能夠增刪改查內(nèi)存中的數(shù)據(jù),還能夠增刪改查內(nèi)存中的代碼,由于數(shù)據(jù)和代碼都在內(nèi)存...
?????? 關于kprobe的文檔,最好的還是Linux內(nèi)核自帶的Documentation/kprobes.txt。


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

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


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

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


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

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

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

使用jprobe建設鏡面層疊的原則和見解


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

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯(lián)系: 360901061

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

【本文對您有幫助就好】

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

發(fā)表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 99在线观看精品免费99 | 久久亚洲精品中文字幕二区 | 高清欧美一区二区免费影视 | 一区二区精品久久 | 国内精品伊人久久久影视 | 久久精品国产半推半就 | 日本高清在线精品一区二区三区 | 中文字幕在线看视频一区二区三区 | 亚洲一级生活片 | 亚洲国产欧美国产综合一区 | 四虎色姝姝影院www 四虎色影院 | 四虎影视永久在线 | 久热综合 | 愉拍自拍 | 日韩综合一区 | 天天干天天射天天操 | 亚洲福利视频一区二区三区 | 四虎影院永久在线 | 成人亚洲欧美综合 | 国产一国产一级毛片视频在线 | 欧美日韩国产一区二区三区欧 | 亚洲欧美日韩成人网 | 精品国产区一区二区三区在线观看 | 中文字幕色婷婷在线视频 | 久草首页在线 | 国产成人在线小视频 | 中文字幕 亚洲精品 | 性生活免费网站 | 国内欧美一区二区三区 | 久久99热只有视精品6国产 | 黄色综合网 | 羞羞视频免费在线观看 | 91久久亚洲精品国产一区二区 | 国产亚洲精品久久麻豆 | 国产91精品系列在线观看 | 黄页网址大全免费观看美女 | 国产午夜影院 | 国产精品蜜臀 | 四虎影院在线免费观看视频 | 91av久久| 久久精品国产99久久 |