添加新的系統調用 ,這是一個老掉牙的話題。前段時間折騰Rootkit的時候,我有意避開涉及HOOK劫持系統調用的話題,我主要是想來點新鮮的東西,畢竟關於劫持系統調用這種話題,網上的資料可謂汗牛充棟。
本文的主題依然不是劫持系統調用,而是添加系統調用,並且是動態添加系統調用,即在不重新編譯內核的前提下添加系統調用,畢竟如果可以重新編譯內核的話,那實在是沒有意思。
但文中所述動態新增系統調用的方式依然是老掉牙的方式,甚至和2011年的文章有所雷同,但是 這篇文章介紹的方式足夠清爽!
我們從一個問題開始。我的問題是:
- Linux系統中如何獲取以及修改當前進程的名字??
你去搜一下這個topic,一堆冗餘繁雜的方案,大多數都是藉助procfs來完成這個需求,但沒有直接的讓人感到清爽的方法,比如調用一個getname接口即可獲取當前進程的名字,調用一個modname接口就能修改自己的名字,沒有這樣的方法。
所以,幹嘛不增加兩個系統調用呢:
- sys_getname: 獲取當前進程名。
- sys_setname: 修改當前進程名。
總體上,這是一個 增加兩個系統調用的問題。
下面先演示動態增加一個系統調用的原理。還是使用2011年的老例子,這次我簡單點,用systemtap腳本來實現。
千萬不要質疑systemtap的威力,它的guru模式其實就是一個普通的內核模塊,只是讓編程變得更簡單,所以, 把systemtap當一種方言來看待,而不僅僅作為調試探測工具。 甚至純guru模式的stap腳本根本沒有用到int 3斷點,它簡直可以用於線上生產環境!
演示增加系統調用的stap腳本如下:
<code>#!/usr/bin/stap -g// newsyscall.stap%{unsigned char *old_tbl;// 這裡借用本module的地址,分配靜態數組new_tbl作為新的系統調用表。// 注意:不能調用kmalloc,vmalloc分配,因為在x86_64平臺它們的地址無法被內核rel32跳轉過來!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;// 使用內核現成的poke text接口,而不是自己去修改頁表權限。// 當然,也可以修改CR0,不過這顯然沒有直接用text_poke清爽。// 這是可行的,不然呢?內核自己的ftrace或者live kpatch怎麼辦?!void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);%}%{// 2011年文章裡的例子,打印一句話而已,我修改了函數名字,稱作“皮鞋”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"); // 拷貝原始的系統調用表,3200個字節有點多了,但絕對不會少。 memcpy(&new_tbl[0], old_tbl, 3200); // 獲取新系統調用表的disp32偏移(x86_64帶符號擴展)。 off_new = (unsigned int)((unsigned long)&new_tbl[0]); // 在system_call函數的指令碼裡進行特徵匹配,匹配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函數的指令碼裡進行特徵匹配,匹配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. 增加一個系統調用數量 // 2. 使能新的系統調用表 off_old = *(unsigned int *)call_addr; nr_old = *(unsigned short *)nr_addr; // 設置新的系統調用入口函數 *(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();}/<code>
唯一需要解釋的就是兩處poke:
- 修改系統調用數量的限制。
- 修改系統調用表的位置。
我們從system_call指令碼中一看便知:
<code>crash> dis system_call0xffffffff81645110 <system>: swapgs...# 0x143需要修改為0x1440xffffffff81645173 <system>: cmp $0x143,%rax0xffffffff81645179 <system>: ja 0xffffffff81645241 <badsys>0xffffffff8164517f <system>: mov %r10,%rcx# -0x7e9b2c40需要被修正為新系統調用表的disp32偏移0xffffffff81645182 <system>: callq *-0x7e9b2c40(,%rax,8)0xffffffff81645189 <system>: mov %rax,0x20(%rsp)/<system>/<system>/<system>/<badsys>/<system>/<system>/<system>/<code>
如果代碼正常,那麼直接執行上面的stap腳本的話,新的系統調用應該已經生成,它的系統調用號為324,也就是0x143+1。至於說為什麼系統調用號必須是逐漸遞增的,請看:
<code>callq *-0x7e9b2c40(,%rax,8)/<code>
上述代碼的含義是:
<code>call index * 8 + disp32_offset /<code>
這意味著內核是按照數組下標的方式索引系統調用的,這要求它們必須連續存放。
好了,回到現實,我們上面的行動是否成功了呢?事情到底是不是我們想象的那樣的呢?我們寫個測試case驗證一下:
<code>// newcall.cint main(int argc, char *argv[]){ syscall(324, 1234); perror("new system call");}/<code>
執行之,看結果:
<code>[root@localhost test]# gcc newcall.c[root@localhost test]# ./a.outnew 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/<code>
OK,成功!此時我們Ctrl-C掉我們的stap腳本,再次執行a.out:
<code>[root@localhost test]# ./a.outnew system call: Function not implemented/<code>
完全符合預期。
OK,那麼現在開始正事,即新增兩個系統調用,sysgetname和syssetname,分別為獲取和設置當前進程的名字。
來吧,讓我們開始。
其實 newsyscall.stap 已經足夠了,稍微改一下即可,但是這裡的 稍微改 體現了品質和優雅:
- 改為oneshot模式,畢竟我不希望有個模塊在系統裡。
oneshot模式需要動態分配內存,保證在stap模塊退出後這塊內存不會隨著模塊的卸載而自動釋放。而這個,我已經玩膩了。
直接上代碼:
<code>#!/usr/bin/stap -g// poke.stp%{// 為了rel32偏移的可達性,借用模塊映射空間的範圍來分配內存。#define START _AC(0xffffffffa0000000, UL)#define END _AC(0xffffffffff000000, UL)// 保存原始的系統調用表。unsigned char *old_tbl;// 保存新的系統調用表。unsigned char *new_tbl;// call系統調用表的位置。unsigned long call_addr = 0;// 系統調用數量限制檢查的位置。unsigned long nr_addr = 0;// 原始的系統調用表disp32偏移。unsigned int off_old;// 原始的系統調用數量。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);%}%{// 新系統調用的text被copy到了新的頁面,因此最好不要調用內核函數。// 這是因為內核函數之間的互調使用的是rel32調用,這就需要校準偏移,太麻煩。// 記住:作為例子,不調用printk,也不調用memcpy/memset...如果想秀花活兒,自己去校準吧。// 詳細的秀法,參見我前面關於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); // 拷貝系統調用表 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; // 設置setskinshoe *(unsigned long *)&new_tbl[nr_old*8 + 8] = new_addr; new_addr = (unsigned long)&stub_sys_skinshoe[96]; // 設置getskinshoe *(unsigned long *)&new_tbl[nr_old*8 + 8 + 8] = new_addr; // 系統調用數量增加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); // 至此,新的系統調用表已經生效,盡情修改吧!%}probe begin{ syscall_table_poke(); exit();}/<code>
順便,我把恢復原始系統調用表的操作腳本也附帶上:
<code>#!/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出自己的系統調用表的地址。 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 // 如果你增加的系統調用比較多,且分佈在不同的malloc頁面,那麼就需要循環free for (i = 0; i < curr_calls - nr_calls; i ++) { vfree((void *)new_tbl[nr_calls + 1 + i]); } */ // 釋放自己的系統調用表 vfree((void *)new_tbl);%}probe begin{ syscall_table_revert(); exit();}/<code>
來吧,開始我們的實驗!
我不懂編程,所以我只能寫最簡單的代碼展示效果,下面的C代碼直接調用新增的兩個系統調用,首先它獲得並打印自己的名字,然後把名字改掉,最後再次獲取並打印自己的名字:
<code>#include <stdio.h>#include <stdlib.h>#include <string.h>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;}/<string.h>/<stdlib.h>/<stdio.h>/<code>
下面是實驗結果:
<code># 未poke時的結果[root@localhost test]# ./test_newcall skinshoe-- get name before: Function not implementedmy name is-- Modify name: Function not implemented-- get name after: Function not implementedmy name is[root@localhost test]#[root@localhost test]# ./poke.stp [root@localhost test]## poke之後的結果,此時lsmod,你將看不到任何和這個poke相關的內核模塊,這就是oneshot的效果。[root@localhost test]# ./test_newcall skinshoe-- get name before: Successmy name is test_newcall-- Modify name: Success-- get name after: Successmy name is skinshoe[root@localhost test]#[root@localhost test]# ./revert.stp[root@localhost test]## revert之後的結果[root@localhost test]# ./test_newcall skinshoe-- get name before: Function not implementedmy name is-- Modify name: Function not implemented-- get name after: Function not implementedmy name is[root@localhost test]#/<code>
足夠簡單,足夠直接,工人們和經理都可以上手一試。
我們如果讓新增的系統調用乾點壞事,那再簡單不過了,得手之後呢?如何防止被經理抓到呢?封堵模塊加載的接口即可咯,反正不加載內核模塊,誰也別想看到當前系統的內核被hack成了什麼樣子,哦,對了,把/dev/mem的mmap也堵死哦...
....不過這是下面文章的主題了。
好了,今天就先寫到這兒吧。
閱讀更多 linux內核 的文章