內(nèi)核熱補(bǔ)丁是一種無(wú)需重啟操作系統(tǒng),動(dòng)態(tài)為內(nèi)核打補(bǔ)丁的技術(shù)。系統(tǒng)管理員基于該技術(shù),可以在不重啟系統(tǒng)的情況下,修復(fù)內(nèi)核BUG或安全漏洞,可以在最大程度上減少系統(tǒng)宕機(jī)時(shí)間,增加系統(tǒng)的可用性。
一直很好奇內(nèi)核熱補(bǔ)丁這個(gè)黑科技,今天終于可以揭露它的真容了。當(dāng)然這章的內(nèi)容強(qiáng)烈依賴(lài)于前一章探秘ftrace[1]。有需要的小伙伴請(qǐng)自取。
從一個(gè)例子開(kāi)始
作為一個(gè)小白,當(dāng)然是從一個(gè)例子開(kāi)始入手會(huì)比較簡(jiǎn)單。感謝內(nèi)核社區(qū)開(kāi)發(fā)著貼心的服務(wù),在內(nèi)核代碼中,就有熱補(bǔ)丁的例子在samples/livepatch目錄下。
我們來(lái)看一個(gè)非常簡(jiǎn)單的例子,因?yàn)樘?jiǎn)單了,我干脆就把整個(gè)代碼都貼上來(lái)了。
我想,有一些內(nèi)核開(kāi)發(fā)經(jīng)驗(yàn)的小伙伴,從這個(gè)例子中就可以猜出這個(gè)代碼的作用。
將函數(shù)cmdline_proc_show替換成livepatch_cmdline_proc_show
怎么樣,是不是炒雞簡(jiǎn)單?
來(lái)點(diǎn)難的
上面的代碼實(shí)在是太沒(méi)有難度了,讓我們來(lái)點(diǎn)挑戰(zhàn)。看看這個(gè)klp_enable_patch究竟做了點(diǎn)什么。
怎么樣,是不是有點(diǎn)傻眼了?這么多調(diào)用都是點(diǎn)啥?別急,其實(shí)這么多調(diào)用大多是花架子。如果你了解了klp_patch這個(gè)數(shù)據(jù)結(jié)構(gòu),我想一切都迎刃而解了。
klp_patch的數(shù)據(jù)結(jié)構(gòu)
所以說(shuō)大學(xué)時(shí)候?qū)W習(xí)算法和數(shù)據(jù)結(jié)構(gòu)是非常有道理的,只可惜當(dāng)年我壓根就沒(méi)有好好學(xué)習(xí),以至于工作后不得不拼命補(bǔ)課。瞧,這時(shí)候又能用上了。
想要了解上面列出的klp_enable_patch這個(gè)函數(shù)的邏輯,還是要從klp_patch這個(gè)結(jié)構(gòu)體入手。
大家可以對(duì)照這例子代碼中的klp_patch和這個(gè)圖來(lái)幫助理解。
這個(gè)klp_patch就好像是一個(gè)二維數(shù)組
第一維是klp_object
第二維是klp_func
最后落實(shí)到klp_func標(biāo)注了要替換的目標(biāo)函數(shù)和替換成的新的函數(shù)。
知道了這個(gè)后,再回過(guò)去看剛才那一坨初始化的代碼是不是會(huì)簡(jiǎn)單點(diǎn)?其實(shí)就是做了幾個(gè)循環(huán),把這個(gè)二維數(shù)組上所有的klp_object和klp_patch都初始化好。所有的初始化,大部分是創(chuàng)建對(duì)應(yīng)的kobj,這樣在/sys/kernel/livepatch/目錄下就能控制每個(gè)熱補(bǔ)丁點(diǎn)了。
真正的干貨
到此為止,看了半天其實(shí)都沒(méi)有看到熱不定究竟是怎么打到內(nèi)核代碼上的。別急,小編這就給您娓娓道來(lái)。
在前面初始化的代碼中,大家有沒(méi)有看到一個(gè)函數(shù)–klp_patch_func?這個(gè)函數(shù)會(huì)對(duì)每個(gè)klp_func數(shù)據(jù)執(zhí)行一遍。對(duì)了,魔鬼就在這里。
這幾個(gè)可以說(shuō)都是重量級(jí)的選手,讓我慢慢給您一一講解。
klp_get_ftrace_location
這個(gè)函數(shù)呢,就是要給出被替換的函數(shù)地址。首先我們?cè)诙x中并沒(méi)有給出這個(gè)old_func的地址,所以第一步是要算出這個(gè)old_func。這部分工作在函數(shù)klp_init_object_loaded中通過(guò)klp_find_object_symbol查找symbol來(lái)得到。
ops->fops.func = klp_ftrace_handler
這是什么呢?對(duì)了,如果你對(duì)ftrace還有印象,這就是我們會(huì)替換掉ftrace探針的那個(gè)函數(shù)。也就是說(shuō),當(dāng)我們的想要修改的函數(shù)被執(zhí)行到時(shí),這個(gè)klp_ftrace_handler就會(huì)被調(diào)用起來(lái)干活了。
ftrace_set_filter_ip
在探秘ftrace中,我們并沒(méi)有展開(kāi)這個(gè)ftrace_ops結(jié)構(gòu)體。那這里我們就來(lái)展開(kāi)看一下。
每個(gè)ftrace_ops上都有兩個(gè)哈希表,還記得我們操作ftrace時(shí)候有兩個(gè)文件 set_ftrace_filter / set_ftrace_notrace么?這兩個(gè)文件分別用來(lái)控制我們想跟蹤那個(gè)函數(shù)和不想跟蹤那個(gè)函數(shù)。這兩個(gè)集合在代碼中就對(duì)應(yīng)了ftrace_ops中的兩個(gè)哈希表 filter_hash / notrace_hash。
所以 ftrace_set_filter_ip 就是用來(lái)將我們想要補(bǔ)丁的函數(shù)加到這個(gè)哈希表上的。
register_ftrace_function
這個(gè)函數(shù)的功效在探秘ftrace中已經(jīng)描述過(guò)了一部分,這里我們將從另一個(gè)角度再次闡述。
register_ftrace_function函數(shù)的功效之一是將ftrace_ops結(jié)構(gòu)體添加到全局鏈表ftrace_ops_list上,這么做有什么用呢?我們來(lái)看一下被ftrace插入到代碼中的函數(shù)ftrace_ops_list_func。
可以看到,每一個(gè)被ftrace改變的函數(shù),如果在有多個(gè)ftrace_ops的情況下,會(huì)通過(guò)ftrace_ops_test()來(lái)判斷當(dāng)前函數(shù)是否符合這個(gè)ftrace_ops。如果符合才會(huì)執(zhí)行op->func。(注意,這個(gè)func就是剛才設(shè)置的klp_ftrace_handler了。
而這個(gè)ftrace_ops_test()是怎么做判斷的呢?對(duì)了,我想你已經(jīng)猜到了,咱不是有兩個(gè)哈希表么?
驚人一躍
到此為止,我們還是圍繞著熱補(bǔ)丁怎么利用ftrace的框架,讓自己在特定的探針上執(zhí)行,還沒(méi)有真正看到所謂的補(bǔ)丁是怎么打上去的。是時(shí)候來(lái)揭開(kāi)這層面紗了。
通過(guò)上述的操作,klp成功的在某個(gè)探針上嵌入了函數(shù)klp_ftrace_handler。那就看看這個(gè)函數(shù)吧。
klp_ftrace_handler(ip, parent_ip, fops, regs) klp_arch_set_pc(regs, func->new_func) regs->ip = ip;
怎么樣,是不是有點(diǎn)吃驚,所謂的熱補(bǔ)丁就是這么一個(gè)語(yǔ)句?理論上講到這里,意思上也明白了,但是我依然想要弄清楚這個(gè)究竟是怎么一回事兒。
這一切還是要從ftrace的探針開(kāi)始說(shuō)起。
因?yàn)閗lp在設(shè)置ftrace_ops時(shí)添加了FTRACE_OPS_FL_SAVE_REGS,所以對(duì)應(yīng)的探針是ftrace_reg_caller。經(jīng)過(guò)一番刨根問(wèn)底,終于發(fā)現(xiàn)了秘密。
在探針執(zhí)行ftrace_ops_list_func的前,會(huì)將調(diào)用探針的rip保存到堆棧上的regs參數(shù)中。然后在返回探針前,將rges->ip上的內(nèi)容再恢復(fù)到函數(shù)返回地址上。此時(shí)如果有klp的探針函數(shù),那么這個(gè)值就改變?yōu)榱宋覀兿敫淖兂傻暮瘮?shù)了。
怎么樣,原來(lái)黑科技是這么玩的!
這事兒有點(diǎn)抽象,讓我畫(huà)一個(gè)簡(jiǎn)易的堆棧示意一下。
一切的秘密都在這個(gè)堆棧上的return address里了。
到這里我才反應(yīng)過(guò)來(lái),原來(lái)黑科技就是黑客用的科技啊 :)
補(bǔ)充知識(shí) – 函數(shù)返回地址
上面的這個(gè)黑科技運(yùn)用到了一個(gè)x86架構(gòu)下,如何保存函數(shù)返回是運(yùn)行的地址的原理。也就是指令callq/retq是如何改變堆棧的。
那先說(shuō)一下原理:
callq指令在跳轉(zhuǎn)到目標(biāo)代碼前,會(huì)將自身的下一條指令的地址放到堆棧上。retq執(zhí)行返回時(shí),會(huì)從堆棧上取出目標(biāo)地址然后跳轉(zhuǎn)到那里。
這么說(shuō)有點(diǎn)抽象了,咱們可以用gdb做一個(gè)簡(jiǎn)單的實(shí)驗(yàn)。
實(shí)驗(yàn)代碼
一個(gè)再簡(jiǎn)單不過(guò)的add函數(shù)。
#include
驗(yàn)證返回地址在堆棧上
使用gdb在add返回前停住,然后用下面的指令查看狀態(tài)。
(gdb) disassembleDump of assembler code for function add: 0x00000000004004ed <+0>: push %rbp 0x00000000004004ee <+1>: mov %rsp,%rbp 0x00000000004004f1 <+4>: mov %edi,-0x4(%rbp) 0x00000000004004f4 <+7>: mov %esi,-0x8(%rbp) 0x00000000004004f7 <+10>: mov -0x8(%rbp),%eax 0x00000000004004fa <+13>: mov -0x4(%rbp),%edx 0x00000000004004fd <+16>: add %edx,%eax 0x00000000004004ff <+18>: pop %rbp=> 0x0000000000400500 <+19>: retqEnd of assembler dump.(gdb) info registers rsprsp 0x7fffffffe2e8 0x7fffffffe2e8(gdb) x/1xw 0x7fffffffe2e80x7fffffffe2e8: 0x00400523
首先我們看到在執(zhí)行retq前,堆棧上的內(nèi)容是0x00400523。
接著我們?cè)賵?zhí)行一次stepi。
(gdb) stepimain () at main.c:1313 return 0;(gdb) info registers rsprsp 0x7fffffffe2f0 0x7fffffffe2f0(gdb) info registers riprip0x4005230x400523
然后再反匯編一下,看到此時(shí)正要執(zhí)行的指令就是callq后面的一條指令。
(gdb) disassembleDump of assembler code for function main: 0x0000000000400501 <+0>: push %rbp 0x0000000000400502 <+1>: mov %rsp,%rbp 0x0000000000400505 <+4>: sub $0x10,%rsp 0x0000000000400509 <+8>: movl $0x3,-0x4(%rbp) 0x0000000000400510 <+15>: addl $0x3,-0x4(%rbp) 0x0000000000400514 <+19>: mov -0x4(%rbp),%eax 0x0000000000400517 <+22>: mov $0x2,%esi 0x000000000040051c <+27>: mov %eax,%edi 0x000000000040051e <+29>: callq 0x4004ed
修改返回地址
接下來(lái)我們還能模擬熱補(bǔ)丁,來(lái)修改這個(gè)返回值。(當(dāng)然比較簡(jiǎn)陋些。)
我們?cè)赼dd函數(shù)執(zhí)行retq前停住,用gdb改變堆棧上的值,讓他指向mov的下一條指令leaveq。
(gdb) disassembleDump of assembler code for function add: 0x00000000004004ed <+0>: push %rbp 0x00000000004004ee <+1>: mov %rsp,%rbp 0x00000000004004f1 <+4>: mov %edi,-0x4(%rbp) 0x00000000004004f4 <+7>: mov %esi,-0x8(%rbp) 0x00000000004004f7 <+10>: mov -0x8(%rbp),%eax 0x00000000004004fa <+13>: mov -0x4(%rbp),%edx 0x00000000004004fd <+16>: add %edx,%eax 0x00000000004004ff <+18>: pop %rbp=> 0x0000000000400500 <+19>: retqEnd of assembler dump.(gdb) info registers rsprsp 0x7fffffffe2e8 0x7fffffffe2e8(gdb) x/1xw 0x7fffffffe2e80x7fffffffe2e8: 0x00400523(gdb) set *((int *) 0x7fffffffe2e8) = 0x00400528(gdb) x/1xw 0x7fffffffe2e80x7fffffffe2e8: 0x00400528然后我們?cè)賵?zhí)行stepi
(gdb) stepimain () at main.c:1414 }(gdb) info registers riprip 0x400528 0x400528
瞧,這下是不是直接走到了leaveq,而不是剛才的mov?我們輕松的黑了一把。
好了,到這里就真的結(jié)束了,希望大家有所收獲。
-
內(nèi)核
+關(guān)注
關(guān)注
3文章
1377瀏覽量
40338 -
補(bǔ)丁
+關(guān)注
關(guān)注
0文章
27瀏覽量
8554 -
黑科技
+關(guān)注
關(guān)注
14文章
127瀏覽量
37379
原文標(biāo)題:揭露內(nèi)核黑科技 - 熱補(bǔ)丁技術(shù)真容
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論