Linux動態(tài)為內(nèi)核添加新的系統(tǒng)調(diào)用
添加新的系統(tǒng)調(diào)用 ,這是一個老掉牙的話題。前段時間折騰Rootkit的時候,我有意避開涉及HOOK劫持系統(tǒng)調(diào)用的話題,我主要是想來點新鮮的東西,畢竟關(guān)于劫持系統(tǒng)調(diào)用這種話題,網(wǎng)上的資料可謂汗牛充棟。
本文的主題依然不是劫持系統(tǒng)調(diào)用,而是添加系統(tǒng)調(diào)用,并且是動態(tài)添加系統(tǒng)調(diào)用,即在不重新編譯內(nèi)核的前提下添加系統(tǒng)調(diào)用,畢竟如果可以重新編譯內(nèi)核的話,那實在是沒有意思。
但文中所述動態(tài)新增系統(tǒng)調(diào)用的方式依然是老掉牙的方式,甚至和2011年的文章有所雷同,但是 這篇文章介紹的方式足夠清爽!
我們從一個問題開始。我的問題是:
-
Linux系統(tǒng)中如何獲取以及修改當(dāng)前進程的名字??
你去搜一下這個topic,一堆冗余繁雜的方案,大多數(shù)都是借助procfs來完成這個需求,但沒有直接的讓人感到清爽的方法,比如調(diào)用一個getname接口即可獲取當(dāng)前進程的名字,調(diào)用一個modname接口就能修改自己的名字,沒有這樣的方法。
所以,干嘛不增加兩個系統(tǒng)調(diào)用呢:
-
sys_getname: 獲取當(dāng)前進程名。
-
sys_setname: 修改當(dāng)前進程名。
總體上,這是一個 增加兩個系統(tǒng)調(diào)用的問題。
下面先演示動態(tài)增加一個系統(tǒng)調(diào)用的原理。還是使用2011年的老例子,這次我簡單點,用systemtap腳本來實現(xiàn)。
千萬不要質(zhì)疑systemtap的威力,它的guru模式其實就是一個普通的內(nèi)核模塊,只是讓編程變得更簡單,所以, 把systemtap當(dāng)一種方言來看待,而不僅僅作為調(diào)試探測工具。 甚至純guru模式的stap腳本根本沒有用到int 3斷點,它簡直可以用于線上生產(chǎn)環(huán)境!
演示增加系統(tǒng)調(diào)用的stap腳本如下:
- #!/usr/bin/stap -g
- // newsyscall.stap
- %{
- unsigned char *old_tbl;
- // 這里借用本module的地址,分配靜態(tài)數(shù)組new_tbl作為新的系統(tǒng)調(diào)用表。
- // 注意:不能調(diào)用kmalloc,vmalloc分配,因為在x86_64平臺它們的地址無法被內(nèi)核rel32跳轉(zhuǎn)過來!
- unsigned char new_tbl[8*500] = {0};
- unsigned long call_addr = 0;
- unsigned long nr_addr = 0;
- unsigned int off_old;
- unsigned short nr_old;
-
- // 使用內(nèi)核現(xiàn)成的poke text接口,而不是自己去修改頁表權(quán)限。
- // 當(dāng)然,也可以修改CR0,不過這顯然沒有直接用text_poke清爽。
- // 這是可行的,不然呢?內(nèi)核自己的ftrace或者live kpatch怎么辦?!
- void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
- %}
-
- %{
- // 2011年文章里的例子,打印一句話而已,我修改了函數(shù)名字,稱作“皮鞋”
- asmlinkage long sys_skinshoe(int i)
- {
- printk("new call----:%d\n", i);
- return 0;
- }
- %}
- function syscall_table_poke()
- %{
- unsigned short nr_new = 0;
- unsigned int off_new = 0;
- unsigned char *syscall;
- unsigned long new_addr;
- int i;
- new_addr = (unsigned long)sys_skinshoe;
- syscall = (void *)kallsyms_lookup_name("system_call");
- old_tbl = (void *)kallsyms_lookup_name("sys_call_table");
- _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
- // 拷貝原始的系統(tǒng)調(diào)用表,3200個字節(jié)有點多了,但絕對不會少。
- memcpy(&new_tbl[0], old_tbl, 3200);
- // 獲取新系統(tǒng)調(diào)用表的disp32偏移(x86_64帶符號擴展)。
- off_new = (unsigned int)((unsigned long)&new_tbl[0]);
- // 在system_call函數(shù)的指令碼里進行特征匹配,匹配cmp $0x143 %rax
- for (i = 0; i < 0xff; i++) {
- if (syscall[i] == 0x48 && syscall[i+1] == 0x3d) {
- nr_addr = (unsigned long)&syscall[i+2];
- break;
- }
- }
- // 在system_call函數(shù)的指令碼里進行特征匹配,匹配callq *xxxxx(,%rax,8)
- for (i = 0; i < 0xff; i++) {
- if (syscall[i] == 0xff && syscall[i+1] == 0x14 && syscall[i+2] == 0xc5) {
- call_addr = (unsigned long)&syscall[i+3];
- break;
- }
- }
- // 1. 增加一個系統(tǒng)調(diào)用數(shù)量
- // 2. 使能新的系統(tǒng)調(diào)用表
- off_old = *(unsigned int *)call_addr;
- nr_old = *(unsigned short *)nr_addr;
- // 設(shè)置新的系統(tǒng)調(diào)用入口函數(shù)
- *(unsigned long *)&new_tbl[nr_old*8 + 8] = new_addr;
- nr_new = nr_old + 1;
- memcpy(&new_tbl[nr_new*8 + 8], &old_tbl[nr_old*8 + 8], 16);
- // poke 代碼
- _text_poke_smp((void *)nr_addr, &nr_new, 2);
- _text_poke_smp((void *)call_addr, &off_new, 4);
- %}
- function syscall_table_clean()
- %{
- _text_poke_smp((void *)nr_addr, &nr_old, 2);
- _text_poke_smp((void *)call_addr, &off_old, 4);
- %}
- probe begin
- {
- syscall_table_poke();
- }
-
- probe end
- {
- syscall_table_clean();
- }
唯一需要解釋的就是兩處poke:
-
修改系統(tǒng)調(diào)用數(shù)量的限制。
-
修改系統(tǒng)調(diào)用表的位置。
我們從system_call指令碼中一看便知:
- crash> dis system_call
- 0xffffffff81645110 <system_call>: swapgs
- ...
- # 0x143需要修改為0x144
- 0xffffffff81645173 <system_call_fastpath>: cmp $0x143,%rax
- 0xffffffff81645179 <system_call_fastpath+6>: ja 0xffffffff81645241 <badsys>
- 0xffffffff8164517f <system_call_fastpath+12>: mov %r10,%rcx
- # -0x7e9b2c40需要被修正為新系統(tǒng)調(diào)用表的disp32偏移
- 0xffffffff81645182 <system_call_fastpath+15>: callq *-0x7e9b2c40(,%rax,8)
- 0xffffffff81645189 <system_call_fastpath+22>: mov %rax,0x20(%rsp)
如果代碼正常,那么直接執(zhí)行上面的stap腳本的話,新的系統(tǒng)調(diào)用應(yīng)該已經(jīng)生成,它的系統(tǒng)調(diào)用號為324,也就是0x143+1。至于說為什么系統(tǒng)調(diào)用號必須是逐漸遞增的,請看:
- callq *-0x7e9b2c40(,%rax,8)
上述代碼的含義是:
- call index * 8 + disp32_offset
這意味著內(nèi)核是按照數(shù)組下標(biāo)的方式索引系統(tǒng)調(diào)用的,這要求它們必須連續(xù)存放。
好了,回到現(xiàn)實,我們上面的行動是否成功了呢?事情到底是不是我們想象的那樣的呢?我們寫個測試case驗證一下:
- // newcall.c
- int main(int argc, char *argv[])
- {
- syscall(324, 1234);
- perror("new system call");
- }
執(zhí)行之,看結(jié)果:
- [root@localhost test]# gcc newcall.c
- [root@localhost test]# ./a.out
- new system call: Success
- [root@localhost test]# dmesg
- [ 1547.387847] stap_6874ae02ddb22b6650aee5cd2e080b49_2209: systemtap: 3.3/0.176, base: ffffffffa03b6000, memory: 106data/24text/0ctx/2063net/9alloc kb, probes: 2
- [ 1549.119316] new call----:1234
OK,成功!此時我們Ctrl-C掉我們的stap腳本,再次執(zhí)行a.out:
- [root@localhost test]# ./a.out
- new system call: Function not implemented
完全符合預(yù)期。
OK,那么現(xiàn)在開始正事,即新增兩個系統(tǒng)調(diào)用,sysgetname和syssetname,分別為獲取和設(shè)置當(dāng)前進程的名字。
來吧,讓我們開始。
其實 newsyscall.stap 已經(jīng)足夠了,稍微改一下即可,但是這里的 稍微改 體現(xiàn)了品質(zhì)和優(yōu)雅:
-
改為oneshot模式,畢竟我不希望有個模塊在系統(tǒng)里。
oneshot模式需要動態(tài)分配內(nèi)存,保證在stap模塊退出后這塊內(nèi)存不會隨著模塊的卸載而自動釋放。而這個,我已經(jīng)玩膩了。
直接上代碼:
- #!/usr/bin/stap -g
- // poke.stp
- %{
- // 為了rel32偏移的可達性,借用模塊映射空間的范圍來分配內(nèi)存。
- #define START _AC(0xffffffffa0000000, UL)
- #define END _AC(0xffffffffff000000, UL)
- // 保存原始的系統(tǒng)調(diào)用表。
- unsigned char *old_tbl;
- // 保存新的系統(tǒng)調(diào)用表。
- unsigned char *new_tbl;
- // call系統(tǒng)調(diào)用表的位置。
- unsigned long call_addr = 0;
- // 系統(tǒng)調(diào)用數(shù)量限制檢查的位置。
- unsigned long nr_addr = 0;
- // 原始的系統(tǒng)調(diào)用表disp32偏移。
- unsigned int off_old;
- // 原始的系統(tǒng)調(diào)用數(shù)量。
- unsigned short nr_old;
- void * *(*___vmalloc_node_range)(unsigned long, unsigned long,
- unsigned long, unsigned long, gfp_t,
- pgprot_t, int, const void *);
- void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
- %}
-
%{
// 新系統(tǒng)調(diào)用的text被copy到了新的頁面,因此最好不要調(diào)用內(nèi)核函數(shù)。
- // 這是因為內(nèi)核函數(shù)之間的互調(diào)使用的是rel32調(diào)用,這就需要校準(zhǔn)偏移,太麻煩。
- // 記?。鹤鳛槔樱徽{(diào)用printk,也不調(diào)用memcpy/memset...如果想秀花活兒,自己去校準(zhǔn)吧。
- // 詳細的秀法,參見我前面關(guān)于rootkit的文章。
- long sys_setskinshoe(char *newname, unsigned int len)
- {
- int i;
-
- if (len > 16 - 1)
- return -1;
- for (i = 0; i < len; i++) {
- current->comm[i] = newname[i];
- }
- current->comm[i] = 0;
- return 0;
- }
- long sys_getskinshoe(char *name, unsigned int len)
- {
- int i;
- if (len > 16 - 1)
- return -1;
- for (i = 0; i < len; i++) {
- name[i] = current->comm[i];
- }
- return 0;
- }
-
- unsigned char *stub_sys_skinshoe;
- %}
- function syscall_table_poke()
- %{
- unsigned short nr_new = 0;
- unsigned int off_new = 0;
- unsigned char *syscall;
- unsigned long new_addr;
- int i;
- syscall = (void *)kallsyms_lookup_name("system_call");
- old_tbl = (void *)kallsyms_lookup_name("sys_call_table");
- ___vmalloc_node_range = (void *)kallsyms_lookup_name("__vmalloc_node_range");
- _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
- new_tbl = (void *)___vmalloc_node_range(8*500, 1, START, END,
- GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
- -1, NULL/*__builtin_return_address(0)*/);
- stub_sys_skinshoe = (void *)___vmalloc_node_range(0xff, 1, START, END,
- GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
- -1, NULL);
- // 拷貝代碼指令
- memcpy(&stub_sys_skinshoe[0], sys_setskinshoe, 90);
- memcpy(&stub_sys_skinshoe[96], sys_getskinshoe, 64);
- // 拷貝系統(tǒng)調(diào)用表
- memcpy(&new_tbl[0], old_tbl, 3200);
- new_addr = (unsigned long)&stub_sys_skinshoe[0];
-
- off_new = (unsigned int)((unsigned long)&new_tbl[0]);
- // cmp指令匹配
- for (i = 0; i < 0xff; i++) {
- if (syscall[i] == 0x48 && syscall[i+1] == 0x3d) {
- nr_addr = (unsigned long)&syscall[i+2];
- break;
- }
- }
- // call指令匹配
- for (i = 0; i < 0xff; i++) {
- if (syscall[i] == 0xff && syscall[i+1] == 0x14 && syscall[i+2] == 0xc5) {
- call_addr = (unsigned long)&syscall[i+3];
- break;
- }
- }
- off_old = *(unsigned int *)call_addr;
- nr_old = *(unsigned short *)nr_addr;
- // 設(shè)置setskinshoe
- *(unsigned long *)&new_tbl[nr_old*8 + 8] = new_addr;
- new_addr = (unsigned long)&stub_sys_skinshoe[96];
- // 設(shè)置getskinshoe
- *(unsigned long *)&new_tbl[nr_old*8 + 8 + 8] = new_addr;
- // 系統(tǒng)調(diào)用數(shù)量增加2個
- nr_new = nr_old + 2;
- // 后移tail stub
- memcpy(&new_tbl[nr_new*8 + 8], &old_tbl[nr_old*8 + 8], 16);
- _text_poke_smp((void *)nr_addr, &nr_new, 2);
- _text_poke_smp((void *)call_addr, &off_new, 4);
- // 至此,新的系統(tǒng)調(diào)用表已經(jīng)生效,盡情修改吧!
- %}
-
- probe begin
- {
- syscall_table_poke();
- exit();
- }
順便,我把恢復(fù)原始系統(tǒng)調(diào)用表的操作腳本也附帶上:
- #!/usr/bin/stap -g
- // revert.stp
- %{
- void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
- %}
-
- function syscall_table_revert()
- %{
- unsigned int off_new, off_old;
- unsigned char *syscall;
- unsigned long nr_addr = 0, call_addr = 0, orig_addr, *new_tbl;
- // 0x143這個還是記在腦子里吧.
- unsigned short nr_calls = 0x0143, curr_calls;
- int i;
- syscall = (void *)kallsyms_lookup_name("system_call");
- orig_addr = (unsigned long)kallsyms_lookup_name("sys_call_table");
- _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
- for (i = 0; i < 0xff; i++) {
- if (syscall[i] == 0x48 && syscall[i+1] == 0x3d) {
- nr_addr = (unsigned long)&syscall[i+2];
- break;
- }
- }
- for (i = 0; i < 0xff; i++) {
- if (syscall[i] == 0xff && syscall[i+1] == 0x14 && syscall[i+2] == 0xc5) {
- call_addr = (unsigned long)&syscall[i+3];
- break;
- }
- }
- curr_calls = *(unsigned short *)nr_addr;
- off_new = *(unsigned int *)call_addr;
- off_old = (unsigned int)orig_addr;
- // decode出自己的系統(tǒng)調(diào)用表的地址。
- new_tbl = (unsigned long *)(0xffffffff00000000 | off_new);
- _text_poke_smp((void *)nr_addr, &nr_calls, 2);
- _text_poke_smp((void *)call_addr, &off_old, 4);
- vfree((void *)new_tbl[nr_calls + 1]);
- /*
- // loop free
- // 如果你增加的系統(tǒng)調(diào)用比較多,且分布在不同的malloc頁面,那么就需要循環(huán)free
- for (i = 0; i < curr_calls - nr_calls; i ++) {
- vfree((void *)new_tbl[nr_calls + 1 + i]);
- }
- */
- // 釋放自己的系統(tǒng)調(diào)用表
- vfree((void *)new_tbl);
- %}
- probe begin
- {
- syscall_table_revert();
- exit();
- }
來吧,開始我們的實驗!
我不懂編程,所以我只能寫最簡單的代碼展示效果,下面的C代碼直接調(diào)用新增的兩個系統(tǒng)調(diào)用,首先它獲得并打印自己的名字,然后把名字改掉,最后再次獲取并打印自己的名字:
- #include
- #include
- #include
- int main(int argc, char *argv[])
- {
- char name[16] = {0};
- syscall(325, name, 12);
- perror("-- get name before");
- printf("my name is %s\n", name);
- syscall(324, argv[1], strlen(argv[1]));
- perror("-- Modify name");
- syscall(325, name, 12);
- perror("-- get name after");
- printf("my name is %s\n", name);
- return 0;
- }
下面是實驗結(jié)果:
- # 未poke時的結(jié)果
- [root@localhost test]# ./test_newcall skinshoe
- -- get name before: Function not implemented
- my name is
- -- Modify name: Function not implemented
- -- get name after: Function not implemented
- my name is
- [root@localhost test]#
- [root@localhost test]# ./poke.stp
- [root@localhost test]#
- # poke之后的結(jié)果,此時lsmod,你將看不到任何和這個poke相關(guān)的內(nèi)核模塊,這就是oneshot的效果。
- [root@localhost test]# ./test_newcall skinshoe
- -- get name before: Success
- my name is test_newcall
- -- Modify name: Success
- -- get name after: Success
- my name is skinshoe
- [root@localhost test]#
- [root@localhost test]# ./revert.stp
- [root@localhost test]#
- # revert之后的結(jié)果
- [root@localhost test]# ./test_newcall skinshoe
- -- get name before: Function not implemented
- my name is
- -- Modify name: Function not implemented
- -- get name after: Function not implemented
- my name is
- [root@localhost test]#
足夠簡單,足夠直接,工人們和經(jīng)理都可以上手一試。
我們?nèi)绻屝略龅南到y(tǒng)調(diào)用干點壞事,那再簡單不過了,得手之后呢?如何防止被經(jīng)理抓到呢?封堵模塊加載的接口即可咯,反正不加載內(nèi)核模塊,誰也別想看到當(dāng)前系統(tǒng)的內(nèi)核被hack成了什么樣子,哦,對了,把/dev/mem的mmap也堵死哦...
....不過這是下面文章的主題了。
好了,今天就先寫到這兒吧。
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!