Linux如何动态添加新的系统调用

来自 《Linux动态为内核添加新的系统调用》

先来个满满的回忆:
https://blog.csdn.net/dog250/article/details/6446192
2011年写这篇文章的时候,我的女儿小小还没有出生。

评价一下这篇文章,总体写得还不错,但排版不行。时间如白驹过隙,快十年过去了,今天我来旧事重提。


添加新的系统调用 ,这是一个老掉牙的话题。前段时间折腾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%{ 4unsigned char *old_tbl; 5// 这里借用本module的地址,分配静态数组new_tbl作为新的系统调用表。 6// 注意:不能调用kmalloc,vmalloc分配,因为在x86_64平台它们的地址无法被内核rel32跳转过来! 7unsigned char new_tbl[8*500] = {0}; 8unsigned long call_addr = 0; 9unsigned long nr_addr = 0; 10unsigned int off_old; 11unsigned short nr_old; 12 13// 使用内核现成的poke text接口,而不是自己去修改页表权限。 14// 当然,也可以修改CR0,不过这显然没有直接用text_poke清爽。 15// 这是可行的,不然呢?内核自己的ftrace或者live kpatch怎么办?! 16void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len); 17%} 18 19%{ 20// 2011年文章里的例子,打印一句话而已,我修改了函数名字,称作“皮鞋” 21asmlinkage long sys_skinshoe(int i) 22{ 23 printk("new call----:%d\n", i); 24 return 0; 25} 26%} 27 28function 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 73function 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 79probe begin 80{ 81 syscall_table_poke(); 82} 83 84probe end 85{ 86 syscall_table_clean(); 87}

唯一需要解释的就是两处poke:

  1. 修改系统调用数量的限制。
  2. 修改系统调用表的位置。

我们从system_call指令码中一看便知:

1crash> dis system_call 20xffffffff81645110 <system_call>: swapgs 3... 4# 0x143需要修改为0x144 50xffffffff81645173 <system_call_fastpath>: cmp $0x143,%rax 60xffffffff81645179 <system_call_fastpath+6>: ja 0xffffffff81645241 <badsys> 70xffffffff8164517f <system_call_fastpath+12>: mov %r10,%rcx 8# -0x7e9b2c40需要被修正为新系统调用表的disp32偏移 90xffffffff81645182 <system_call_fastpath+15>: callq *-0x7e9b2c40(,%rax,8) 100xffffffff81645189 <system_call_fastpath+22>: mov %rax,0x20(%rsp)

如果代码正常,那么直接执行上面的stap脚本的话,新的系统调用应该已经生成,它的系统调用号为324,也就是0x143+1。至于说为什么系统调用号必须是逐渐递增的,请看:

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

上述代码的含义是:

call index * 8 + disp32_offset 

这意味着内核是按照数组下标的方式索引系统调用的,这要求它们必须连续存放。

好了,回到现实,我们上面的行动是否成功了呢?事情到底是不是我们想象的那样的呢?我们写个测试case验证一下:

1// newcall.c 2int 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 3new 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 2new system call: Function not implemented

完全符合预期。


OK,那么现在开始正事,即新增两个系统调用,sys_getname和sys_setname,分别为获取和设置当前进程的名字。

来吧,让我们开始。

其实 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// 保存原始的系统调用表。 9unsigned char *old_tbl; 10// 保存新的系统调用表。 11unsigned char *new_tbl; 12// call系统调用表的位置。 13unsigned long call_addr = 0; 14// 系统调用数量限制检查的位置。 15unsigned long nr_addr = 0; 16// 原始的系统调用表disp32偏移。 17unsigned int off_old; 18// 原始的系统调用数量。 19unsigned short nr_old; 20void * *(*___vmalloc_node_range)(unsigned long, unsigned long, 21 unsigned long, unsigned long, gfp_t, 22 pgprot_t, int, const void *); 23void *(*_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的文章。 31long 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 45long 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 58unsigned char *stub_sys_skinshoe; 59%} 60 61function 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 119probe begin 120{ 121 syscall_table_poke(); 122 exit(); 123}

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

1#!/usr/bin/stap -g 2// revert.stp 3%{ 4void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len); 5%} 6 7function 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 52probe begin 53{ 54 syscall_table_revert(); 55 exit(); 56}

来吧,开始我们的实验!

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

1#include <stdio.h> 2#include <stdlib.h> 3#include <string.h> 4 5int 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 4my name is 5-- Modify name: Function not implemented 6-- get name after: Function not implemented 7my 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 14my name is test_newcall 15-- Modify name: Success 16-- get name after: Success 17my 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 24my name is 25-- Modify name: Function not implemented 26-- get name after: Function not implemented 27my name is 28[root@localhost test]#

足够简单,足够直接,工人们和经理都可以上手一试。

我们如果让新增的系统调用干点坏事,那再简单不过了,得手之后呢?如何防止被经理抓到呢?封堵模块加载的接口即可咯,反正不加载内核模块,谁也别想看到当前系统的内核被hack成了什么样子,哦,对了,把/dev/mem的mmap也堵死哦…

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

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


浙江温州皮鞋湿,下雨进水不会胖。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )