晚上回去有朋友在朋友圈回復(fù)了我關(guān)于 “函數(shù)開頭5字節(jié)跳轉(zhuǎn)” 的事。今天正好要確認一個與此相關(guān)的細節(jié),就順便又把這問題重新擼了一遍。
其實起初我也很納悶,以前不都是0x55開頭的指令嗎?怎么現(xiàn)在這種call self或者lea 0x0(%rsp),%rsp套路卻都成了慣例。…
關(guān)于5字節(jié)跳轉(zhuǎn),說的是下面的情況:
請注意函數(shù)最開頭的5個字節(jié):
可見,它實際上call的是緊接著它下面的地址,所以說這個5字節(jié)的call指令其實是 沒有用 的!
仔細看一下這5個字節(jié),思考一下它到底有什么用。
我們可以任意將它替換成 jmp $4字節(jié)相對偏移
這樣,代碼指令流就會進入我們自己的HOOK函數(shù)里了。
當然了,關(guān)于 “e8 00 00 00 00 callq …” 這個有很多話可以講,比如和Link相聯(lián)系的時候就比較有意思了,它可是作為一個樁指令存在。這個和HOOK無關(guān),也不再說太多。
讓我覺得最有意思的是,昨天那位朋友提到了微軟的/hotpatch編譯選項,我大致看了一下:
/hotpatch (Create Hotpatchable Image):https://docs.microsoft.com/en-us/cpp/build/reference/hotpatch-create-hotpatchable-image?view=vs-2017
When /hotpatch is used in a compilation, the compiler ensures that first instruction of each function is at least two bytes, which is required for hot patching.
這是一個很有意思的選項,其實編譯器提供這個機制也是舉手之勞吧,雖然簡單,但它確實為程序員HOOK運行中的函數(shù)提供了很大的方便。
/hotpatch的實質(zhì)其實就是在函數(shù)的開頭和結(jié)尾填充了一些無關(guān)緊要的指令,方便HOOK來用自己的jmp指令覆蓋這個無關(guān)緊要的指令。比如下面是一個函數(shù)的開頭:
0x90代表一個nop指令,即 “什么也不做”的意思,如此一來,程序員便非常容易將類似下面的指令插入到函數(shù)開頭了:
無需任何額外的指令保存動作。
既然微軟的編譯器有這個功能可用,GCC有沒有呢?看了GCC的manual,發(fā)現(xiàn)了一個-mhotpatch=x,y的選項,但是在x86平臺不能用,還是比較不爽的。
后來發(fā)現(xiàn)了在編寫函數(shù)的時候,可以加上下面的屬性,然后編譯器就可以將其編譯成帶有填充的指令了:
那么,簡單來用一下,看看效果咯。
由于時間并不是很多,我也沒有那么多精力去應(yīng)對不斷的panic,所以這次準備在用戶態(tài)搞。
由于用戶態(tài)可以直接使用mprotect函數(shù)更改內(nèi)存的使用權(quán)限,所以就不需要那個stub函數(shù)了。今天的這個例子,原理圖如下:
加上ms_hook_prologue屬性修飾的函數(shù),編譯好了之后,你會在函數(shù)最開頭兩行找到下面的廢指令:
隨意替換之就好。所以對于這個例子,上面圖示里的n的值就是5.
此外,上圖中,我們的一個指令buffer不再是一個stub函數(shù),而真的就是一塊分配的內(nèi)存,所以我們需要給它加上EXEC權(quán)限,不然會segment fault。這個在內(nèi)核模塊中是不能直接做的,因為分配帶有EXEC權(quán)限的module_alloc并沒有導(dǎo)出,所以如若想用它,則必須通過kallsyms_lookup_name的內(nèi)省方式來做。
再一個需要注意的是,由于指令buffer是在堆上分配的,在64位系統(tǒng)上,它的位置和函數(shù)代碼的位置之差會超過4個字節(jié)界定的相對偏移,所以就不能用0xe9+4字節(jié)相對偏移來jmp了,而要通過64位絕對地址來跳轉(zhuǎn)了,指令如下:
好了,說了這么多,該上代碼了:
結(jié)果當然是先調(diào)用自己的hook函數(shù),然后再調(diào)用原始函數(shù)咯:
為什么不用kprobe機制呢?kprobe的原理是 為了靈活性,使用int 3指令替換被hook的指令。 這樣就可以任意編寫pre/post回調(diào)函數(shù)了,但是我們也能看出來,通過int方式來hook,對效率的影響是不能忽略,特別是對于那些頻繁被調(diào)用的函數(shù),kprobe更加不可行。
kprobe非常適合做問題排查,熱點分析,但不好在生產(chǎn)環(huán)境跑的。
其實,本文所描述的hotpatch原理還可以做的更好些,達到 在任意點插入自己的邏輯的目的,包括在函數(shù)的內(nèi)部。 這樣可以達到和kprobe相同的效果。當然,這需要對運行中的二進制指令序列做相對周密詳細的分析。
這里還有一篇關(guān)于hotpatch的文章,比我這篇好,可以看一下:
Hotpatching a C Function on x86 :https://nullprogram.com/blog/2016/03/31/
補充說明一下,朋友圈有高手最新評論,讓我又知道了些新東西:
nop指令的實現(xiàn)方式有很多種,比如mov edi edi??赡芎芏嗥脚_并沒有類似獨立的0x90指令吧。不過既然有,那還是0x90純粹些。
kprobe也不全部一來int 3,只有return hook的場景才依賴int 3,其它的也可以做jmp hook。
線程安全,原子化操作也是生產(chǎn)環(huán)境必須考慮的,不然就是玩具。
-
函數(shù)
+關(guān)注
關(guān)注
3文章
4305瀏覽量
62430 -
編譯器
+關(guān)注
關(guān)注
1文章
1618瀏覽量
49049
原文標題:x86_64運行時動態(tài)替換函數(shù)的hotpatch機制
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論