标签:csdn probe lin 调试 断点 memcpy ref def 模块
原创 dog250 Linux阅码场 4月29日先来个满满的回忆:https://blog.csdn.net/dog250/article/details/64461922011年写这篇文章的时候,我的女儿小小还没有出生。
评价一下这篇文章,总体写得还不错,但排版不行。时间如白驹过隙,快十年过去了,今天我来旧事重提。
添加新的系统调用 ,这是一个老掉牙的话题。前段时间折腾Rootkit的时候,我有意避开涉及HOOK劫持系统调用的话题,我主要是想来点新鲜的东西,毕竟关于劫持系统调用这种话题,网上的资料可谓汗牛充栋。
本文的主题依然不是劫持系统调用,而是添加系统调用,并且是动态添加系统调用,即在不重新编译内核的前提下添加系统调用,毕竟如果可以重新编译内核的话,那实在是没有意思。
但文中所述动态新增系统调用的方式依然是老掉牙的方式,甚至和2011年的文章有所雷同,但是 这篇文章介绍的方式足够清爽!
我们从一个问题开始。我的问题是:
你去搜一下这个topic,一堆冗余繁杂的方案,大多数都是借助procfs来完成这个需求,但没有直接的让人感到清爽的方法,比如调用一个getname接口即可获取当前进程的名字,调用一个modname接口就能修改自己的名字,没有这样的方法。
所以,干嘛不增加两个系统调用呢:
sys_getname: 获取当前进程名。
总体上,这是一个 增加两个系统调用的问题。
下面先演示动态增加一个系统调用的原理。还是使用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)
标签:csdn probe lin 调试 断点 memcpy ref def 模块
原文地址:https://blog.51cto.com/15015138/2555272