Linux動態爲內核添加新的系統調用

原創 dog250 Linux閱碼場 4月29日node

先來個滿滿的回憶:https://blog.csdn.net/dog250/article/details/64461922011年寫這篇文章的時候,個人女兒小小尚未出生編程

評價一下這篇文章,整體寫得還不錯,但排版不行。時間如白駒過隙,快十年過去了,今天我來舊事重提。數組


添加新的系統調用 ,這是一個老掉牙的話題。前段時間折騰Rootkit的時候,我有意避開涉及HOOK劫持系統調用的話題,我主要是想來點新鮮的東西,畢竟關於劫持系統調用這種話題,網上的資料可謂汗牛充棟。ide

本文的主題依然不是劫持系統調用,而是添加系統調用,而且是動態添加系統調用,即在不從新編譯內核的前提下添加系統調用,畢竟若是能夠從新編譯內核的話,那實在是沒有意思。函數

但文中所述動態新增系統調用的方式依然是老掉牙的方式,甚至和2011年的文章有所雷同,可是 這篇文章介紹的方式足夠清爽!工具

咱們從一個問題開始。個人問題是:oop

  • Linux系統中如何獲取以及修改當前進程的名字??

你去搜一下這個topic,一堆冗餘繁雜的方案,大多數都是藉助procfs來完成這個需求,但沒有直接的讓人感到清爽的方法,好比調用一個getname接口便可獲取當前進程的名字,調用一個modname接口就能修改本身的名字,沒有這樣的方法。測試

因此,幹嗎不增長兩個系統調用呢:ui

  • sys_getname: 獲取當前進程名。.net

  • sys_setname: 修改當前進程名。

整體上,這是一個 增長兩個系統調用的問題。

下面先演示動態增長一個系統調用的原理。仍是使用2011年的老例子,此次我簡單點,用systemtap腳原本實現。

千萬不要質疑systemtap的威力,它的guru模式其實就是一個普通的內核模塊,只是讓編程變得更簡單,因此, 把systemtap當一種方言來看待,而不只僅做爲調試探測工具。 甚至純guru模式的stap腳本根本沒有用到int 3斷點,它簡直能夠用於線上生產環境!

演示增長系統調用的stap腳本以下:

1.
    #!/usr/bin/stap -g
2.
    // newsyscall.stap
3.
    %{
4.
    unsigned char *old_tbl;
5.
    // 這裏借用本module的地址,分配靜態數組new_tbl做爲新的系統調用表。
6.
    // 注意:不能調用kmalloc,vmalloc分配,由於在x86_64平臺它們的地址沒法被內核rel32跳轉過來!
7.
    unsigned char new_tbl[8*500] = {0};
8.
    unsigned long call_addr = 0;
9.
    unsigned long nr_addr = 0;
10.
    unsigned int off_old;
11.
    unsigned short nr_old;
12.
13.
    // 使用內核現成的poke text接口,而不是本身去修改頁表權限。
14.
    // 固然,也能夠修改CR0,不過這顯然沒有直接用text_poke清爽。
15.
    // 這是可行的,否則呢?內核本身的ftrace或者live kpatch怎麼辦?!
16.
    void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
17.
    %}
18.
19.
    %{
20.
    // 2011年文章裏的例子,打印一句話而已,我修改了函數名字,稱做「皮鞋」
21.
    asmlinkage long sys_skinshoe(int i)
22.
    {
23.
        printk("new call----:%d\n", i);
24.
        return 0;
25.
    }
26.
    %}
27.
28.
    function syscall_table_poke()
29.
    %{
30.
        unsigned short nr_new = 0;
31.
        unsigned int off_new = 0;
32.
        unsigned char *syscall;
33.
        unsigned long new_addr;
34.
        int i;
35.
36.
        new_addr = (unsigned long)sys_skinshoe;
37.
        syscall = (void *)kallsyms_lookup_name("system_call");
38.
        old_tbl = (void*)kallsyms_lookup_name("sys_call_table");
39.
        _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
40.
41.
        // 拷貝原始的系統調用表,3200個字節有點多了,但絕對不會少。
42.
        memcpy(&new_tbl[0], old_tbl, 3200);
43.
        // 獲取新系統調用表的disp32偏移(x86_64帶符號擴展)。
44.
        off_new = (unsigned int)((unsigned long)&new_tbl[0]);
45.
46.
        // 在system_call函數的指令碼裏進行特徵匹配,匹配cmp $0x143 %rax
47.
       for (i = 0; i < 0xff; i++) {
48.
            if (syscall[i] == 0x48 && syscall[i+1] == 0x3d) {
49.
                nr_addr = (unsigned long)&syscall[i+2];
50.
                break;
51.
            }
52.
        }
53.
        // 在system_call函數的指令碼裏進行特徵匹配,匹配callq  *xxxxx(,%rax,8)
54.
        for (i = 0; i < 0xff; i++) {
55.
            if (syscall[i] == 0xff && syscall[i+1] == 0x14 && syscall[i+2] == 0xc5) {
56.
                call_addr = (unsigned long)&syscall[i+3];
57.
                break;
58.
            }
59.
        }
60.
        // 1. 增長一個系統調用數量
61.
        // 2. 使能新的系統調用表
62.
        off_old = *(unsigned int *)call_addr;
63.
        nr_old = *(unsigned short *)nr_addr;
64.
        // 設置新的系統調用入口函數
65.
        *(unsigned long *)&new_tbl[nr_old*8 + 8] = new_addr;
66.
        nr_new = nr_old + 1;
67.
        memcpy(&new_tbl[nr_new*8 + 8], &old_tbl[nr_old*8 + 8], 16);
68.
        // poke 代碼
69.
        _text_poke_smp((void *)nr_addr, &nr_new, 2);
70.
        _text_poke_smp((void *)call_addr, &off_new, 4);
71.
    %}
72.
73.
    function syscall_table_clean()
74.
    %{
75.
        _text_poke_smp((void *)nr_addr, &nr_old, 2);
76.
        _text_poke_smp((void *)call_addr, &off_old, 4);
77.
     %}
78.
79.
    probe begin
80.
    {
81.
        syscall_table_poke();
82.
    }
83.
84.
    probe end
85.
    {
86.
        syscall_table_clean();
87.
    }

惟一須要解釋的就是兩處poke:

  1. 修改系統調用數量的限制。

  2. 修改系統調用表的位置。

咱們從system_call指令碼中一看便知:

1.
    crash> dis system_call
2.
    0xffffffff81645110 <system_call>:       swapgs
3.
    ...
4.
    # 0x143須要修改成0x144
5.
    0xffffffff81645173 <system_call_fastpath>:      cmp    $0x143,%rax
6.
    0xffffffff81645179 <system_call_fastpath+6>:    ja     0xffffffff81645241 <badsys>
7.
    0xffffffff8164517f <system_call_fastpath+12>:   mov    %r10,%rcx
8.
    # -0x7e9b2c40須要被修正爲新系統調用表的disp32偏移
9.
    0xffffffff81645182 <system_call_fastpath+15>:   callq  *-0x7e9b2c40(,%rax,8)
10.
    0xffffffff81645189 <system_call_fastpath+22>:   mov    %rax,0x20(%rsp)

若是代碼正常,那麼直接執行上面的stap腳本的話,新的系統調用應該已經生成,它的系統調用號爲324,也就是0x143+1。至於說爲何系統調用號必須是逐漸遞增的,請看:

1.
    callq  *-0x7e9b2c40(,%rax,8)

上述代碼的含義是:

1.
    call index * 8 + disp32_offset

這意味着內核是按照數組下標的方式索引系統調用的,這要求它們必須連續存放。

好了,回到現實,咱們上面的行動是否成功了呢?事情究竟是不是咱們想象的那樣的呢?咱們寫個測試case驗證一下:

1.
    // newcall.c
2.
    int main(int argc, char *argv[])
3.
    {
4.
        syscall(324, 1234);
5.
        perror("new system call");
6.
    }

執行之,看結果:

1.
    [root@localhost test]# gcc newcall.c
2.
    [root@localhost test]# ./a.out
3.
    new system call: Success
4.
    [root@localhost test]# dmesg
5.
    [ 1547.387847] stap_6874ae02ddb22b6650aee5cd2e080b49_2209: systemtap: 3.3/0.176, base: ffffffffa03b6000, memory: 106data/24text/0ctx/2063net/9alloc kb, probes: 2
6.
    [ 1549.119316] new call----:1234

OK,成功!此時咱們Ctrl-C掉咱們的stap腳本,再次執行a.out:

1.
    [root@localhost test]# ./a.out
2.
    new system call: Function not implemented

徹底符合預期。


OK,那麼如今開始正事,即新增兩個系統調用,sysgetname和syssetname,分別爲獲取和設置當前進程的名字。

來吧,讓咱們開始。

其實 newsyscall.stap 已經足夠了,稍微改一下便可,可是這裏的 稍微改 體現了品質和優雅:

  • 改成oneshot模式,畢竟我不但願有個模塊在系統裏。

oneshot模式須要動態分配內存,保證在stap模塊退出後這塊內存不會隨着模塊的卸載而自動釋放。而這個,我已經玩膩了。

直接上代碼:

1.

    #!/usr/bin/stap -g
2.
    // poke.stp
3.
    %{
4.
    // 爲了rel32偏移的可達性,借用模塊映射空間的範圍來分配內存。
5.
    #define START   _AC(0xffffffffa0000000, UL)
6.
    #define END     _AC(0xffffffffff000000, UL)
7.
8.
    // 保存原始的系統調用表。
9.
    unsigned char *old_tbl;
10.
    // 保存新的系統調用表。
11.
    unsigned char *new_tbl;
12.
    // call系統調用表的位置。
13.
    unsigned long call_addr = 0;
14.
    // 系統調用數量限制檢查的位置。
15.
    unsigned long nr_addr = 0;
16.
    // 原始的系統調用表disp32偏移。
17.
    unsigned int off_old;
18.
    // 原始的系統調用數量。
19.
    unsigned short nr_old;
20.
    void * *(*___vmalloc_node_range)(unsigned long, unsigned long,
21.
                unsigned long, unsigned long, gfp_t,
22.
                pgprot_t, int, const void *);
23.
    void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
24.
    %}
25.
26.
    %{
27.
    // 新系統調用的text被copy到了新的頁面,所以最好不要調用內核函數。
28.
    // 這是由於內核函數之間的互調使用的是rel32調用,這就須要校準偏移,太麻煩。
29.
    // 記住:做爲例子,不調用printk,也不調用memcpy/memset...若是想秀花活兒,本身去校準吧。
30.
    // 詳細的秀法,參見我前面關於rootkit的文章。
31.
    long sys_setskinshoe(char *newname, unsigned int len)
32.
    {
33.
        int i;
34.
35.
        if (len > 16 - 1)
36.
            return -1;
37.
38.
        for (i = 0; i < len; i++) {
39.
            current->comm[i] = newname[i];
40.
        }
41.
        current->comm[i] = 0;
42.
        return 0;
43.
    }
44.
45.
    long sys_getskinshoe(char *name, unsigned int len)
46.
    {
47.
        int i;
48.
49.
        if (len > 16 - 1)
50.
            return -1;
51.
52.
        for (i = 0; i < len; i++) {
53.
            name[i] = current->comm[i];
54.
        }
55.
        return 0;
56.
    }
57.
58.
    unsigned char *stub_sys_skinshoe;
59.
    %}
60.
61.
    function syscall_table_poke()
62.
    %{
63.
        unsigned short nr_new = 0;
64.
        unsigned int off_new = 0;
65.
        unsigned char *syscall;
66.
        unsigned long new_addr;
67.
        int i;
68.
69.
        syscall = (void *)kallsyms_lookup_name("system_call");
70.
        old_tbl = (void *)kallsyms_lookup_name("sys_call_table");
71.
        ___vmalloc_node_range = (void *)kallsyms_lookup_name("__vmalloc_node_range");
72.
        _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
73.
74.
        new_tbl = (void *)___vmalloc_node_range(8*500, 1, START, END,
75.
                                    GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
76.
                                    -1, NULL/*__builtin_return_address(0)*/);
77.
        stub_sys_skinshoe = (void *)___vmalloc_node_range(0xff, 1, START, END,
78.
                                    GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
79.
                                    -1, NULL);
80.
        // 拷貝代碼指令
81.
        memcpy(&stub_sys_skinshoe[0], sys_setskinshoe, 90);
82.
        memcpy(&stub_sys_skinshoe[96], sys_getskinshoe, 64);
83.
        // 拷貝系統調用表
84.
        memcpy(&new_tbl[0], old_tbl, 3200);
85.
        new_addr = (unsigned long)&stub_sys_skinshoe[0];
86.
87.
        off_new = (unsigned int)((unsigned long)&new_tbl[0]);
88.
        // cmp指令匹配
89.
        for (i = 0; i < 0xff; i++) {
90.
            if (syscall[i] == 0x48 && syscall[i+1] == 0x3d) {
91.
                nr_addr = (unsigned long)&syscall[i+2];
92.
                break;
93.
            }
94.
        }
95.
        // call指令匹配
96.
        for (i = 0; i < 0xff; i++) {
97.
            if (syscall[i] == 0xff && syscall[i+1] == 0x14 && syscall[i+2] == 0xc5) {
98.
                call_addr = (unsigned long)&syscall[i+3];
99.
                break;
100.
            }
101.
        }
102.
103.
        off_old = *(unsigned int *)call_addr;
104.
        nr_old = *(unsigned short *)nr_addr;
105.
        // 設置setskinshoe
106.
        *(unsigned long *)&new_tbl[nr_old*8 + 8] = new_addr;
107.
        new_addr = (unsigned long)&stub_sys_skinshoe[96];
108.
        // 設置getskinshoe
109.
        *(unsigned long *)&new_tbl[nr_old*8 + 8 + 8] = new_addr;
110.
        // 系統調用數量增長2個
111.
        nr_new = nr_old + 2;
112.
        // 後移tail stub
113.
        memcpy(&new_tbl[nr_new*8 + 8], &old_tbl[nr_old*8 + 8], 16);
114.
        _text_poke_smp((void *)nr_addr, &nr_new, 2);
115.
        _text_poke_smp((void *)call_addr, &off_new, 4);
116.
        // 至此,新的系統調用表已經生效,盡情修改吧!
117.
    %}
118.
119.
    probe begin
120.
    {
121.
        syscall_table_poke();
122.
        exit();
123.
    }

順便,我把恢復原始系統調用表的操做腳本也附帶上:

1.
    #!/usr/bin/stap -g
2.
    // revert.stp
3.
    %{
4.
    void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
5.
    %}
6.
7.
    function syscall_table_revert()
8.
    %{
9.
        unsigned int off_new, off_old;
10.
        unsigned char *syscall;
11.
        unsigned long nr_addr = 0, call_addr = 0, orig_addr, *new_tbl;
12.
        // 0x143這個仍是記在腦子裏吧.
13.
        unsigned short nr_calls = 0x0143, curr_calls;
14.
        int i;
15.
16.
        syscall = (void *)kallsyms_lookup_name("system_call");
17.
        orig_addr = (unsigned long)kallsyms_lookup_name("sys_call_table");
18.
        _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
19.
20.
        for (i = 0; i < 0xff; i++) {
21.
            if (syscall[i] == 0x48 && syscall[i+1] == 0x3d) {
22.
                nr_addr = (unsigned long)&syscall[i+2];
23.
                break;
24.
            }
25.
        }
26.
        for (i = 0; i < 0xff; i++) {
27.
            if (syscall[i] == 0xff && syscall[i+1] == 0x14 && syscall[i+2] == 0xc5) {
28.
                call_addr = (unsigned long)&syscall[i+3];
29.
                break;
30.
            }
31.
        }
32.
        curr_calls = *(unsigned short *)nr_addr;
33.
        off_new = *(unsigned int *)call_addr;
34.
        off_old = (unsigned int)orig_addr;
35.
        // decode出本身的系統調用表的地址。
36.
        new_tbl = (unsigned long *)(0xffffffff00000000 | off_new);
37.
        _text_poke_smp((void *)nr_addr, &nr_calls, 2);
38.
        _text_poke_smp((void *)call_addr, &off_old, 4);
39.
40.
        vfree((void *)new_tbl[nr_calls + 1]);
41.
        /*
42.
        // loop free
43.
        // 若是你增長的系統調用比較多,且分佈在不一樣的malloc頁面,那麼就須要循環free
44.
        for (i = 0; i < curr_calls - nr_calls; i ++) {
45.
            vfree((void *)new_tbl[nr_calls + 1 + i]);
46.
        }
47.
        */
48.
        // 釋放本身的系統調用表
49.
        vfree((void *)new_tbl);
50.
    %}
51.
52.
    probe begin
53.
    {
54.
        syscall_table_revert();
55.
        exit();
56.
    }

來吧,開始咱們的實驗!

我不懂編程,因此我只能寫最簡單的代碼展現效果,下面的C代碼直接調用新增的兩個系統調用,首先它得到並打印本身的名字,而後把名字改掉,最後再次獲取並打印本身的名字:

1.
    #include <stdio.h>
2.
    #include <stdlib.h>
3.
    #include <string.h>
4.
5.
    int main(int argc, char *argv[])
6.
    {
7.
        char name[16] = {0};
8.
        syscall(325, name, 12);
9.
        perror("-- get name before");
10.
        printf("my name is %s\n", name);
11.
        syscall(324, argv[1], strlen(argv[1]));
12.
        perror("-- Modify name");
13.
        syscall(325, name, 12);
14.
        perror("-- get name after");
15.
        printf("my name is %s\n", name);
16.
        return 0;
17.
    }

下面是實驗結果:

1.
    # 未poke時的結果
2.
    [root@localhost test]# ./test_newcall skinshoe
3.
    -- get name before: Function not implemented
4.
    my name is
5.
    -- Modify name: Function not implemented
6.
    -- get name after: Function not implemented
7.
    my name is
8.
    [root@localhost test]#
9.
    [root@localhost test]# ./poke.stp 
10.
    [root@localhost test]#
11.
    # poke以後的結果,此時lsmod,你將看不到任何和這個poke相關的內核模塊,這就是oneshot的效果。
12.
    [root@localhost test]# ./test_newcall skinshoe
13.
    -- get name before: Success
14.
    my name is test_newcall
15.
    -- Modify name: Success
16.
    -- get name after: Success
17.
    my name is skinshoe
18.
    [root@localhost test]#
19.
    [root@localhost test]# ./revert.stp
20
    [root@localhost test]#
21.
    # revert以後的結果
22.
    [root@localhost test]# ./test_newcall skinshoe
23.
    -- get name before: Function not implemented
24.
    my name is
25.
    -- Modify name: Function not implemented
26.
    -- get name after: Function not implemented
27.
    my name is
28.
    [root@localhost test]#

足夠簡單,足夠直接,工人們和經理均可以上手一試。

咱們若是讓新增的系統調用乾點壞事,那再簡單不過了,得手以後呢?如何防止被經理抓到呢?封堵模塊加載的接口便可咯,反正不加載內核模塊,誰也別想看到當前系統的內核被hack成了什麼樣子,哦,對了,把/dev/mem的mmap也堵死哦...

....不過這是下面文章的主題了。

好了,今天就先寫到這兒吧。


浙江溫州皮鞋溼,下雨進水不會胖。(END)

相關文章
相關標籤/搜索