原創 dog250 Linux閱碼場 4月29日node
先來個滿滿的回憶:https://blog.csdn.net/dog250/article/details/64461922011年寫這篇文章的時候,個人女兒小小尚未出生。編程
評價一下這篇文章,整體寫得還不錯,但排版不行。時間如白駒過隙,快十年過去了,今天我來舊事重提。數組
添加新的系統調用 ,這是一個老掉牙的話題。前段時間折騰Rootkit的時候,我有意避開涉及HOOK劫持系統調用的話題,我主要是想來點新鮮的東西,畢竟關於劫持系統調用這種話題,網上的資料可謂汗牛充棟。ide
本文的主題依然不是劫持系統調用,而是添加系統調用,而且是動態添加系統調用,即在不從新編譯內核的前提下添加系統調用,畢竟若是能夠從新編譯內核的話,那實在是沒有意思。函數
但文中所述動態新增系統調用的方式依然是老掉牙的方式,甚至和2011年的文章有所雷同,可是 這篇文章介紹的方式足夠清爽!工具
咱們從一個問題開始。個人問題是:oop
你去搜一下這個topic,一堆冗餘繁雜的方案,大多數都是藉助procfs來完成這個需求,但沒有直接的讓人感到清爽的方法,好比調用一個getname接口便可獲取當前進程的名字,調用一個modname接口就能修改本身的名字,沒有這樣的方法。測試
因此,幹嗎不增長兩個系統調用呢:ui
sys_getname: 獲取當前進程名。.net
整體上,這是一個 增長兩個系統調用的問題。
下面先演示動態增長一個系統調用的原理。仍是使用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:
修改系統調用數量的限制。
咱們從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模式須要動態分配內存,保證在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)