如今那個玩偶還在,我家小小特別喜歡。就是這么一堆看似無關卻又巧合的事。讓我在這個周末認為必須寫下一點什么。
?????? 好吧。從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)同意,不得轉載。
更多文章、技術交流、商務合作、聯(lián)系博主
微信掃碼或搜索:z360901061

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