Linux內核如何替換內核函數並調用原始函數
已經多久沒有編程了?很久了吧…其實我本來就不怎麼會寫代碼,時不時的也就是為了驗證一個系統特性,寫點玩具而已,工程化的代碼,對於我而言,實在是吃力。
最近遇到一些問題,需要特定的解法,也就有機會手寫點代碼了。其實這個話題記得上一次遇到是在8年前,時間過得好快。
替換一個已經在內存中的函數,使得執行流流入我們自己的邏輯,然後再調用原始的函數,這是一個很古老的話題了。比如有個函數叫做funcion,而你希望統計一下調用function的次數,最直接的方法就是 如果有誰調用function的時候,調到下面這個就好了 :
void new_function()
{
count++;
return function();
}
1
2
3
4
5
網上很多文章給出了實現這個思路的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 OPTSIZE 5
// 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
");
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
");
// 僅僅列印一行信息後,調用原始函數。
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)
{
get_online_cpus();
ptr_poke_smp(ptr_ipv4_conntrack_in, saved_op, OPTSIZE);
ptr_poke_smp(stub_ipv4_conntrack_in, stub_op, OPTSIZE);
barrier();
put_online_cpus();
}
module_exit(hook_conn_exit);
MODULE_DESCRIPTION("hook test");
MODULE_LICENSE("GPL");
MODULE_VERSION("1.1");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
測試是OK的。
在上面的代碼中,saved_op中為什麼沒有old inst呢?直接就是一個jmp y,這豈不是將原始函數中的頭幾個位元組的指令給遺漏了嗎?
其實說到這裡,還真有個不好玩的Trick,起初我真的就是老老實實保存了前5個自己的指令,然後當需要調用原始ipv4_conntrack_in時,就先執行那5個保存的指令,也是OK的。隨後我objdump這個函數發現了下面的代碼:
0000000000000380 <ipv4_conntrack_in>:
380: e8 00 00 00 00 callq 385 <ipv4_conntrack_in+0x5>
385: 55 push %rbp
386: 49 8b 40 18 mov 0x18(%r8),%rax
38a: 48 89 f1 mov %rsi,%rcx
38d: 8b 57 2c mov 0x2c(%rdi),%edx
390: be 02 00 00 00 mov $0x2,%esi
395: 48 89 e5 mov %rsp,%rbp
398: 48 8b b8 e8 03 00 00 mov 0x3e8(%rax),%rdi
39f: e8 00 00 00 00 callq 3a4 <ipv4_conntrack_in+0x24>
3a4: 5d pop %rbp
3a5: c3 retq
3a6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
3ad: 00 00 00
1
2
3
4
5
6
7
8
9
10
11
12
13
14
注意前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;
...
1
2
3
4
5
6
7
8
但是以上的只是玩具。
有個非常現實的問題。在我保存原始函數的頭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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
這是正確的做法。
杭州的秋冬陰冷而潮濕,這對於我而言給讓我獲得最佳的體感,非常舒適。我比較喜歡陰暗潮濕的環境,不太喜歡陽光和明亮,曾經很多人說我比較消極,不自信…我差點就信了。
和我同住的室友非常喜歡中國傳統文化,他倒是說了點我覺得有點意思的,因為我陽氣旺嘛,很簡單,陽氣再加陽光,那是相抵觸的,任何時候都講陰陽調和才最舒適。我非常贊同!就幾點可以肯定:
越是下雨天,陰暗潮濕的環境,我就越自信,工作效率就越高;
不知為什麼,我不覺得墳地陰森,體感一般,總之只要到黑暗陰冷的環境,就興奮;
現在是11月18號了,杭州氣溫10度左右,我依然是短袖短褲;
連續低溫下短袖短褲,但不會感冒。我爸60多歲也如此,我家小小也一樣…


※interrupt方法的使用
※網路模型中Inception的作用與結構全解析
TAG:程序員小新人學習 |