当前位置:首页 > 公众号精选 > Linux阅码场
[导读]本文介绍了动态添加系统调用,即在不重新编译内核的前提下,添加系统调用。

添加新的系统调用 ,这是一个老掉牙的话题。前段时间折腾Rootkit的时候,我有意避开涉及HOOK劫持系统调用的话题,我主要是想来点新鲜的东西,毕竟关于劫持系统调用这种话题,网上的资料可谓汗牛充栋。

本文的主题依然不是劫持系统调用,而是添加系统调用,并且是动态添加系统调用,即在不重新编译内核的前提下添加系统调用,毕竟如果可以重新编译内核的话,那实在是没有意思。

但文中所述动态新增系统调用的方式依然是老掉牙的方式,甚至和2011年的文章有所雷同,但是 这篇文章介绍的方式足够清爽!

我们从一个问题开始。我的问题是:

  • Linux系统中如何获取以及修改当前进程的名字??

你去搜一下这个topic,一堆冗余繁杂的方案,大多数都是借助procfs来完成这个需求,但没有直接的让人感到清爽的方法,比如调用一个getname接口即可获取当前进程的名字,调用一个modname接口就能修改自己的名字,没有这样的方法。

所以,干嘛不增加两个系统调用呢:

  • sys_getname: 获取当前进程名。

  • 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. // 使用内核现成的poke text接口,而不是自己去修改页表权限。
  13. // 当然,也可以修改CR0,不过这显然没有直接用text_poke清爽。
  14. // 这是可行的,不然呢?内核自己的ftrace或者live kpatch怎么办?!
  15. void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
  16. %}
  17. %{
  18. // 2011年文章里的例子,打印一句话而已,我修改了函数名字,称作“皮鞋”
  19. asmlinkage long sys_skinshoe(int i)
  20. {
  21. printk("new call----:%d\n", i);
  22. return 0;
  23. }
  24. %}

  25. function syscall_table_poke()
  26. %{
  27. unsigned short nr_new = 0;
  28. unsigned int off_new = 0;
  29. unsigned char *syscall;
  30. unsigned long new_addr;
  31. int i;

  32. new_addr = (unsigned long)sys_skinshoe;
  33. syscall = (void *)kallsyms_lookup_name("system_call");
  34. old_tbl = (void *)kallsyms_lookup_name("sys_call_table");
  35. _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");

  36. // 拷贝原始的系统调用表,3200个字节有点多了,但绝对不会少。
  37. memcpy(&new_tbl[0], old_tbl, 3200);
  38. // 获取新系统调用表的disp32偏移(x86_64带符号扩展)。
  39. off_new = (unsigned int)((unsigned long)&new_tbl[0]);

  40. // 在system_call函数的指令码里进行特征匹配,匹配cmp $0x143 %rax
  41. for (i = 0; i < 0xff; i++) {
  42. if (syscall[i] == 0x48 && syscall[i+1] == 0x3d) {
  43. nr_addr = (unsigned long)&syscall[i+2];
  44. break;
  45. }
  46. }
  47. // 在system_call函数的指令码里进行特征匹配,匹配callq *xxxxx(,%rax,8)
  48. for (i = 0; i < 0xff; i++) {
  49. if (syscall[i] == 0xff && syscall[i+1] == 0x14 && syscall[i+2] == 0xc5) {
  50. call_addr = (unsigned long)&syscall[i+3];
  51. break;
  52. }
  53. }
  54. // 1. 增加一个系统调用数量
  55. // 2. 使能新的系统调用表
  56. off_old = *(unsigned int *)call_addr;
  57. nr_old = *(unsigned short *)nr_addr;
  58. // 设置新的系统调用入口函数
  59. *(unsigned long *)&new_tbl[nr_old*8 + 8] = new_addr;
  60. nr_new = nr_old + 1;
  61. memcpy(&new_tbl[nr_new*8 + 8], &old_tbl[nr_old*8 + 8], 16);
  62. // poke 代码
  63. _text_poke_smp((void *)nr_addr, &nr_new, 2);
  64. _text_poke_smp((void *)call_addr, &off_new, 4);
  65. %}

  66. function syscall_table_clean()
  67. %{
  68. _text_poke_smp((void *)nr_addr, &nr_old, 2);
  69. _text_poke_smp((void *)call_addr, &off_old, 4);
  70. %}

  71. probe begin
  72. {
  73. syscall_table_poke();
  74. }
  75. probe end
  76. {
  77. syscall_table_clean();
  78. }

唯一需要解释的就是两处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. unsigned char *old_tbl;
  9. // 保存新的系统调用表。
  10. unsigned char *new_tbl;
  11. // call系统调用表的位置。
  12. unsigned long call_addr = 0;
  13. // 系统调用数量限制检查的位置。
  14. unsigned long nr_addr = 0;
  15. // 原始的系统调用表disp32偏移。
  16. unsigned int off_old;
  17. // 原始的系统调用数量。
  18. unsigned short nr_old;
  19. void * *(*___vmalloc_node_range)(unsigned long, unsigned long,
  20. unsigned long, unsigned long, gfp_t,
  21. pgprot_t, int, const void *);
  22. void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
  23. %}
  24. %{

  25. // 新系统调用的text被copy到了新的页面,因此最好不要调用内核函数。

  26. // 这是因为内核函数之间的互调使用的是rel32调用,这就需要校准偏移,太麻烦。
  27. // 记住:作为例子,不调用printk,也不调用memcpy/memset...如果想秀花活儿,自己去校准吧。
  28. // 详细的秀法,参见我前面关于rootkit的文章。
  29. long sys_setskinshoe(char *newname, unsigned int len)
  30. {
  31. int i;
  32. if (len > 16 - 1)
  33. return -1;

  34. for (i = 0; i < len; i++) {
  35. current->comm[i] = newname[i];
  36. }
  37. current->comm[i] = 0;
  38. return 0;
  39. }

  40. long sys_getskinshoe(char *name, unsigned int len)
  41. {
  42. int i;

  43. if (len > 16 - 1)
  44. return -1;

  45. for (i = 0; i < len; i++) {
  46. name[i] = current->comm[i];
  47. }
  48. return 0;
  49. }
  50. unsigned char *stub_sys_skinshoe;
  51. %}

  52. function syscall_table_poke()
  53. %{
  54. unsigned short nr_new = 0;
  55. unsigned int off_new = 0;
  56. unsigned char *syscall;
  57. unsigned long new_addr;
  58. int i;

  59. syscall = (void *)kallsyms_lookup_name("system_call");
  60. old_tbl = (void *)kallsyms_lookup_name("sys_call_table");
  61. ___vmalloc_node_range = (void *)kallsyms_lookup_name("__vmalloc_node_range");
  62. _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");

  63. new_tbl = (void *)___vmalloc_node_range(8*500, 1, START, END,
  64. GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
  65. -1, NULL/*__builtin_return_address(0)*/);
  66. stub_sys_skinshoe = (void *)___vmalloc_node_range(0xff, 1, START, END,
  67. GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
  68. -1, NULL);
  69. // 拷贝代码指令
  70. memcpy(&stub_sys_skinshoe[0], sys_setskinshoe, 90);
  71. memcpy(&stub_sys_skinshoe[96], sys_getskinshoe, 64);
  72. // 拷贝系统调用表
  73. memcpy(&new_tbl[0], old_tbl, 3200);
  74. new_addr = (unsigned long)&stub_sys_skinshoe[0];
  75. off_new = (unsigned int)((unsigned long)&new_tbl[0]);
  76. // cmp指令匹配
  77. for (i = 0; i < 0xff; i++) {
  78. if (syscall[i] == 0x48 && syscall[i+1] == 0x3d) {
  79. nr_addr = (unsigned long)&syscall[i+2];
  80. break;
  81. }
  82. }
  83. // call指令匹配
  84. for (i = 0; i < 0xff; i++) {
  85. if (syscall[i] == 0xff && syscall[i+1] == 0x14 && syscall[i+2] == 0xc5) {
  86. call_addr = (unsigned long)&syscall[i+3];
  87. break;
  88. }
  89. }

  90. off_old = *(unsigned int *)call_addr;
  91. nr_old = *(unsigned short *)nr_addr;
  92. // 设置setskinshoe
  93. *(unsigned long *)&new_tbl[nr_old*8 + 8] = new_addr;
  94. new_addr = (unsigned long)&stub_sys_skinshoe[96];
  95. // 设置getskinshoe
  96. *(unsigned long *)&new_tbl[nr_old*8 + 8 + 8] = new_addr;
  97. // 系统调用数量增加2个
  98. nr_new = nr_old + 2;
  99. // 后移tail stub
  100. memcpy(&new_tbl[nr_new*8 + 8], &old_tbl[nr_old*8 + 8], 16);
  101. _text_poke_smp((void *)nr_addr, &nr_new, 2);
  102. _text_poke_smp((void *)call_addr, &off_new, 4);
  103. // 至此,新的系统调用表已经生效,尽情修改吧!
  104. %}
  105. probe begin
  106. {
  107. syscall_table_poke();
  108. exit();
  109. }

顺便,我把恢复原始系统调用表的操作脚本也附带上:


  1. #!/usr/bin/stap -g
  2. // revert.stp
  3. %{
  4. void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
  5. %}
  6. function syscall_table_revert()
  7. %{
  8. unsigned int off_new, off_old;
  9. unsigned char *syscall;
  10. unsigned long nr_addr = 0, call_addr = 0, orig_addr, *new_tbl;
  11. // 0x143这个还是记在脑子里吧.
  12. unsigned short nr_calls = 0x0143, curr_calls;
  13. int i;

  14. syscall = (void *)kallsyms_lookup_name("system_call");
  15. orig_addr = (unsigned long)kallsyms_lookup_name("sys_call_table");
  16. _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");

  17. for (i = 0; i < 0xff; i++) {
  18. if (syscall[i] == 0x48 && syscall[i+1] == 0x3d) {
  19. nr_addr = (unsigned long)&syscall[i+2];
  20. break;
  21. }
  22. }
  23. for (i = 0; i < 0xff; i++) {
  24. if (syscall[i] == 0xff && syscall[i+1] == 0x14 && syscall[i+2] == 0xc5) {
  25. call_addr = (unsigned long)&syscall[i+3];
  26. break;
  27. }
  28. }
  29. curr_calls = *(unsigned short *)nr_addr;
  30. off_new = *(unsigned int *)call_addr;
  31. off_old = (unsigned int)orig_addr;
  32. // decode出自己的系统调用表的地址。
  33. new_tbl = (unsigned long *)(0xffffffff00000000 | off_new);
  34. _text_poke_smp((void *)nr_addr, &nr_calls, 2);
  35. _text_poke_smp((void *)call_addr, &off_old, 4);

  36. vfree((void *)new_tbl[nr_calls + 1]);
  37. /*
  38. // loop free
  39. // 如果你增加的系统调用比较多,且分布在不同的malloc页面,那么就需要循环free
  40. for (i = 0; i < curr_calls - nr_calls; i ++) {
  41. vfree((void *)new_tbl[nr_calls + 1 + i]);
  42. }
  43. */
  44. // 释放自己的系统调用表
  45. vfree((void *)new_tbl);
  46. %}

  47. probe begin
  48. {
  49. syscall_table_revert();
  50. exit();
  51. }

来吧,开始我们的实验!

我不懂编程,所以我只能写最简单的代码展示效果,下面的C代码直接调用新增的两个系统调用,首先它获得并打印自己的名字,然后把名字改掉,最后再次获取并打印自己的名字:


  1. #include
  2. #include
  3. #include

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

下面是实验结果:


  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也堵死哦...

....不过这是下面文章的主题了。

好了,今天就先写到这儿吧。


免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭