Linux核心如何替換核心函式並呼叫原始函式
版權宣告:本文為博主原創,無版權,未經博主允許可以隨意轉載,無需註明出處,隨意修改或保持可作為原創! https://blog.csdn.net/dog250/article/details/84201114
浙江溫州皮鞋溼,下雨進水不會胖。週六的雨夜,期待明天的雨會更大更冷。
已經多久沒有程式設計了?很久了吧…其實我本來就不怎麼會寫程式碼,時不時的也就是為了驗證一個系統特性,寫點玩具而已,工程化的程式碼,對於我而言,實在是吃力。
最近遇到一些問題,需要特定的解法,也就有機會手寫點程式碼了。其實這個話題記得上一次遇到是在8年前,時間過得好快。
替換一個已經在記憶體中的函式,使得執行流流入我們自己的邏輯,然後再呼叫原始的函式,這是一個很古老的話題了。比如有個函式叫做funcion,而你希望統計一下呼叫function的次數,最直接的方法就是 如果有誰呼叫function的時候,調到下面這個就好了 :
void new_function() { count++; return function(); }
網上很多文章給出了實現這個思路的Trick,而且一直以來計算機病毒也都採用了這種偷樑換柱的伎倆來實現自己的目的。然而,當你親自去測試時,發現事情並不那麼簡單。
網上給出的許多方法均不再適用了,原因是在早期,這樣做的人比較少,處理器和作業系統大可不必理會一些不符合常規的做法,但是隨著這類Trick開始做壞事影響到正常的業務邏輯時,處理器廠商以及作業系統廠商或者社群便不得不在底層增加一些限制性機制,以防止這類Trick繼續起作用。
常見的措施有兩點:
- 可執行程式碼段不可寫
這個措施便封堵住了你想通過簡單memcpy的方式替換函式指令的方案。 - 記憶體buffer不可執行
這個措施便封堵住了你想把執行流jmp到你的一個儲存指令的buffer的方案。 - stack不可執行
別看這些措施都比較low,一看誰都懂,它們卻避免了大量的緩衝區溢位帶來的危害。
那麼如果我們想用替換函式的Trick做正常的事情,怎麼辦?
我來簡單談一下我的方法。首先我不會去HOOK使用者態的程序的函式,因為這樣意義不大,改一下重啟服務會好很多。所以說,本文特指HOOK核心函式的做法。畢竟核心重新編譯,重啟裝置代價非常大。
我們知道,我們目前所使用的幾乎所有計算機都是馮諾伊曼式的統一儲存式計算機,即指令和資料是存在一起的,這就意味著我們必然可以在作業系統層面隨意解釋記憶體空間的含義。
我們在做正當的事情,所以我假設我們已經拿到了系統的root許可權並且可以編譯和插入核心模組。那麼接下來的事情似乎就是一個流程了。
是的,修改頁表項即可,即便無法簡單地通過memcpy來替換函式指令,我們還是可以用以下的步驟來進行指令替換:
- 重新將函式地址對應的實體記憶體對映成可寫;
- 用自己的jmp指令替換函式指令;
- 解除可寫對映。
非常幸運,核心已經有了現成的 text_poke/text_poke_smp 函式來完成上面的事情。
同樣的,針對一個堆上或者棧上分配的buffer不可執行,我們依然有辦法。辦法如下:
- 編寫一個stub函式,實現隨意,其程式碼指令和buffer相當;
- 用上面重對映函式地址為可寫的方法用buffer重寫stub函式;
- 將stub函式儲存為要呼叫的函式指標。
是不是有點意思呢?下面是一個步驟示意圖:

下面是一個程式碼,我稍後會針對這個程式碼,說幾個細節方面的東西:
#include <linux/kernel.h> #include <linux/kprobes.h> #include <linux/cpu.h> #include <linux/module.h> #include <net/tcp.h> #define OPTSIZE5 // saved_op儲存跳轉到原始函式的指令 char saved_op[OPTSIZE] = {0}; // jump_op儲存跳轉到hook函式的指令 char jump_op[OPTSIZE] = {0}; static unsigned int (*ptr_orig_conntrack_in)(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state); static unsigned int (*ptr_ipv4_conntrack_in)(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state); // stub函式,最終將會被儲存指令的buffer覆蓋掉 static unsigned int stub_ipv4_conntrack_in(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state) { printk("hook stub conntrack\n"); return 0; } // 這是我們的hook函式,當核心在呼叫ipv4_conntrack_in的時候,將會到達這個函式。 static unsigned int hook_ipv4_conntrack_in(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state) { printk("hook conntrack\n"); // 僅僅列印一行資訊後,呼叫原始函式。 return ptr_orig_conntrack_in(ops, skb, in, out, state); } static void *(*ptr_poke_smp)(void *addr, const void *opcode, size_t len); static __init int hook_conn_init(void) { s32 hook_offset, orig_offset; // 這個poke函式完成的就是重對映,寫text段的事 ptr_poke_smp = kallsyms_lookup_name("text_poke_smp"); if (!ptr_poke_smp) { printk("err"); return -1; } // 嗯,我們就是要hook住ipv4_conntrack_in,所以要先找到它! ptr_ipv4_conntrack_in = kallsyms_lookup_name("ipv4_conntrack_in"); if (!ptr_ipv4_conntrack_in) { printk("err"); return -1; } // 第一個位元組當然是jump jump_op[0] = 0xe9; // 計算目標hook函式到當前位置的相對偏移 hook_offset = (s32)((long)hook_ipv4_conntrack_in - (long)ptr_ipv4_conntrack_in - OPTSIZE); // 後面4個位元組為一個相對偏移 (*(s32*)(&jump_op[1])) = hook_offset; // 事實上,我們並沒有儲存原始ipv4_conntrack_in函式的頭幾條指令, // 而是直接jmp到了5條指令後的指令,對應上圖,應該是指令buffer裡沒 // 有old inst,直接就是jmp y了,為什麼呢?後面細說。 saved_op[0] = 0xe9; // 計算目標原始函式將要執行的位置到當前位置的偏移 orig_offset = (s32)((long)ptr_ipv4_conntrack_in + OPTSIZE - ((long)stub_ipv4_conntrack_in + OPTSIZE)); (*(s32*)(&saved_op[1])) = orig_offset; get_online_cpus(); // 替換操作! ptr_poke_smp(stub_ipv4_conntrack_in, saved_op, OPTSIZE); ptr_orig_conntrack_in = stub_ipv4_conntrack_in; barrier(); ptr_poke_smp(ptr_ipv4_conntrack_in, jump_op, OPTSIZE); put_online_cpus(); return 0; } module_init(hook_conn_init); static __exit void hook_conn_exit(void) { } MODULE_DESCRIPTION("hook test"); MODULE_LICENSE("GPL"); MODULE_VERSION("1.1");
測試是OK的。
在上面的程式碼中,saved_op中為什麼沒有old inst呢?直接就是一個jmp y,這豈不是將原始函式中的頭幾個位元組的指令給遺漏了嗎?
其實說到這裡,還真有個不好玩的Trick,起初我真的就是老老實實儲存了前5個自己的指令,然後當需要呼叫原始ipv4_conntrack_in時,就先執行那5個儲存的指令,也是OK的。隨後我objdump這個函式發現了下面的程式碼:
0000000000000380 <ipv4_conntrack_in>: 380:e8 00 00 00 00callq385 <ipv4_conntrack_in+0x5> 385:55push%rbp 386:49 8b 40 18mov0x18(%r8),%rax 38a:48 89 f1mov%rsi,%rcx 38d:8b 57 2cmov0x2c(%rdi),%edx 390:be 02 00 00 00mov$0x2,%esi 395:48 89 e5mov%rsp,%rbp 398:48 8b b8 e8 03 00 00mov0x3e8(%rax),%rdi 39f:e8 00 00 00 00callq3a4 <ipv4_conntrack_in+0x24> 3a4:5dpop%rbp 3a5:c3retq 3a6:66 2e 0f 1f 84 00 00nopw%cs:0x0(%rax,%rax,1) 3ad:00 00 00
注意前5個指令: e8 00 00 00 00 callq 385 <ipv4_conntrack_in+0x5>
可以看到,這個是可以忽略的。因為不管怎麼說都是緊接著執行下面的指令。所以說,我就省去了inst的儲存。
如果按照我的圖示中常規的方法的話,程式碼稍微改一下即可:
char saved_op[OPTSIZE+OPTSIZE] = {0}; ... // 增加一個指令拷貝的操作 memcpy(saved_op, (unsigned char *)ptr_ipv4_conntrack_in, OPTSIZE); saved_op[OPTSIZE] = 0xe9; orig_offset = (s32)((long)ptr_ipv4_conntrack_in + OPTSIZE - ((long)stub_ipv4_conntrack_in + OPTSIZE + OPTSIZE)); (*(s32*)(&saved_op[OPTSIZE+1])) = orig_offset; ...
但是以上的只是玩具。
有個非常現實的問題。在我儲存原始函式的頭n條指令的時候,n到底是多少呢?在本例中,顯然n是5,符合如今Linux核心函式第一條指令幾乎都是callq xxx的慣例。
然而,如果一個函式的第一條指令是下面的樣子:
op d1 d2 d3 d4 d5
即一個操作碼需要5個運算元,我要是隻儲存5個位元組,最後在stub中的指令將會是下面的樣子:
op d1 d2 d3 d4 0xe9 off1 off2 off3 off4
這顯然是錯誤的,op操作碼會將jmp指令0xe9解釋成運算元。
解藥呢?當然有咯。
我們不能魯莽地備份固定長度的指令,而是應該這樣做:
curr = 0 if orig[0] 為單位元組操作碼 saved_op[curr] = orig[curr]; curr++; else if orig[0] 攜帶1個1位元組運算元 memcpy(saved_op, orig, 2); curr += 2; else if orig[0] 攜帶2位元組運算元 memcpy(saved_op, orig, 3); curr += 3; ... saved_op[curr] = 0xe9; // jmp offset = ... (*(s32*)(&saved_op[curr+1])) = offset;
這是正確的做法。
杭州的秋冬陰冷而潮溼,這對於我而言給讓我獲得最佳的體感,非常舒適。我比較喜歡陰暗潮溼的環境,不太喜歡陽光和明亮,曾經很多人說我比較消極,不自信…我差點就信了。
和我同住的室友非常喜歡中國傳統文化,他倒是說了點我覺得有點意思的,因為我陽氣旺嘛,很簡單,陽氣再加陽光,那是相牴觸的,任何時候都講陰陽調和才最舒適。我非常贊同!就幾點可以肯定:
- 越是下雨天,陰暗潮溼的環境,我就越自信,工作效率就越高;
- 不知為什麼,我不覺得墳地陰森,體感一般,總之只要到黑暗陰冷的環境,就興奮;
- 現在是11月18號了,杭州氣溫10度左右,我依然是短袖短褲;
- 連續低溫下短袖短褲,但不會感冒。我爸60多歲也如此,我家小小也一樣…
不過明天要穿長褲了,不然心理壓力太大了,也確實太冷了,走在街上別人看來跟個傻逼一樣…今天我外出打聽小小轉學插班問題,冒著冷冷的冰雨,短袖短褲,真的感覺有點冷了…
浙江溫州皮鞋溼,下雨進水不會胖。
zhejiang wenzhou skinshoe wet, rain flooding water will not fat.