通过进程2加载shell进程,详解execve

一直以来都对execve到底做了什么,总是犯迷糊,原来看Linux内核设计的艺术,这部分讲解的非常不细致,这次结合赵博士的书,重新理解了这部分代码。

首先列出代码,如下:

	if (!(pid=fork())) {//进程1创建进程2
		close(0);
		if (open("/etc/rc",O_RDONLY,0))
			_exit(1);
		execve("/bin/sh",argv_rc,envp_rc);
		_exit(2);
	}

进程1创建进程2,进程2的页目录表及页表如图1,页目录表项是第32位,由于页目录表从内核0x0的位置,所以进程2的页目录项的位置为32,由于每个页目录项所占的字节数为4,所以内存地址为128。

图 1

此时用命令行,查看内核地址为128的数据。0xffa027,就是进程2页表的首地址 。

图 2

0xffa000开始存放的进程2的页表项,如下图:

图 3

下面看真正的execve,代码如下:

int do_execve(unsigned long * eip,long tmp,char * filename,
	char ** argv, char ** envp)
{
	struct m_inode * inode;
	struct buffer_head * bh;
	struct exec ex;
	unsigned long page[MAX_ARG_PAGES];//MAX_ARG_PAGES为32
	int i,argc,envc;
	int e_uid, e_gid;
	int retval;
	int sh_bang = 0;
	unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;//4096*32-4=131068=1FFFC

	if ((0xffff & eip[1]) != 0x000f)
		panic("execve called from supervisor mode");
	for (i=0 ; i<MAX_ARG_PAGES ; i++)	/* clear page-table */
		page[i]=0;
	if (!(inode=namei(filename)))		//找到/bin/sh的i节点
		return -ENOENT;
	argc = count(argv);//参数的数量
	envc = count(envp);//环境变量的数量

restart_interp:
	.....
	if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {//读取第一块的数据
		retval = -EACCES;
		goto exec_error2;
	}
	ex = *((struct exec *) bh->b_data);	//赋值给文件头
	.....
	if (!sh_bang) {
		p = copy_strings(envc,envp,page,p,0);
		p = copy_strings(argc,argv,page,p,0);//最后返回的p是131068减去参数和环境变量的字节数,堆栈的指针。目前page数组中,page[31]已经是一个新申请页面的地址了。
		if (!p) {
			retval = -ENOMEM;
			goto exec_error2;
		}
	}
/* OK, This is the point of no return */
	if (current->executable)
		iput(current->executable);
	current->executable = inode;//刚刚获取的/bin/sh节点
	for (i=0 ; i<32 ; i++)
		current->sigaction[i].sa_handler = NULL;//信号处理函数为NULL
	for (i=0 ; i<NR_OPEN ; i++)//close_on_exec为0
		if ((current->close_on_exec>>i)&1)//不会执行
			sys_close(i);
	current->close_on_exec = 0;
	free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));//get_base(current->ldt[1])为128MB,get_limit(0x0f)为640KB,页目录项的第32项清零,它所指向的页表也都清0了。
	free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
	if (last_task_used_math == current)
		last_task_used_math = NULL;
	current->used_math = 0;
	p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;//p最后为PAGE_SIZE*MAX_ARG_PAGES-4 - 参数和环境变量的字节数 + 64MB - MAX_ARG_PAGES*PAGE_SIZE,最后就是64MB-4-
参数和环境变量的字节数,也就是换成了以64MB为界限的堆栈值了。
	p = (unsigned long) create_tables((char *)p,argc,envc);
	current->brk = ex.a_bss +(current->end_data = ex.a_data +(current->end_code = ex.a_text));//代码段,数据段,bss段
	current->start_stack = p & 0xfffff000;
	current->euid = e_uid;
	current->egid = e_gid;
	......
	eip[0] = ex.a_entry;		//内核栈要返回给用户空间的eip
	eip[3] = p;			//内核栈用返回给用户空间的esp
	return 0;
	....
	return(retval);
}

1、inode=namei(filename),找到/bin/sh的i节点。

2、argc = count(argv); envc = count(envp),计算参数及环境变量的数量。

3、bh = bread(inode->i_dev,inode->i_zone[0]);ex = *((struct exec *) bh->b_data),找到/bin/sh的文件头。

下面是copy_strings(envc,envp,page,p,0)。代码如下:

static unsigned long copy_strings(int argc,char ** argv,unsigned long *page,
		unsigned long p, int from_kmem)
{
	char *tmp, *pag=NULL;
	int len, offset = 0;
	unsigned long old_fs, new_fs;

	if (!p)
		return 0;	/* bullet-proofing */
	new_fs = get_ds();
	old_fs = get_fs();
	.....
	while (argc-- > 0) {//参数的个数
		.....
		if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))//第一个参数的指针
			panic("argc is wrong");
		.....
		len=0;		/* remember zero-padding */
		do {
			len++;
		} while (get_fs_byte(tmp++));//获取第一个参数的长度
		if (p-len < 0) {	/* this shouldn‘t happen - 128kB */
			set_fs(old_fs);
			return 0;
		}
		while (len) {
			--p; --tmp; --len;
			if (--offset < 0) {//第一次进入offset为-1
				offset = p % PAGE_SIZE;//offset为4092
				......
				if (!(pag = (char *) page[p/PAGE_SIZE]) && //p/PAGE_SIZE为31
				    !(pag = (char *) page[p/PAGE_SIZE] =
				      (unsigned long *) get_free_page())) //page[31]存的是新申请页面的地址
					return 0;
				.....

			}
			*(pag + offset) = get_fs_byte(tmp);//伴随循环,参数被写到新申请的页面(从4092依次向低地址4091,4090.....)
		}
	}
        ......
	return p;//最后返回的131068-参数的字节,堆栈的指针。
}

由于from_kmem为0,我们去掉from_kmem为其他值的情况。

参数的含义如下:p为131068,argc位参数的个数,agrv为参数的指针数组,page是page[MAX_ARG_PAGES]的首地址。

我们假设只申请了一个页面就足够存参数和环境变量了,最后返回的p是131068减去参数和环境变量的字节数,堆栈的指针。目前page数组中,page[31]已经是一个新申请页面的地址了。

下面分析,free_page_tables代码,如下:

int free_page_tables(unsigned long from,unsigned long size)//from为128MB,size为640KB
{
	unsigned long *pg_table;
	unsigned long * dir, nr;

	if (from & 0x3fffff)
		panic("free_page_tables called with wrong alignment");
	if (!from)
		panic("Trying to free up swapper memory space");
	size = (size + 0x3fffff) >> 22;//size为1
	dir = (unsigned long *) ((from>>20) & 0xffc); //dir为128
	for ( ; size-->0 ; dir++) {
		if (!(1 & *dir))
			continue;
		pg_table = (unsigned long *) (0xfffff000 & *dir);//pg_table为页目录项第32项所指向的内存地址
		for (nr=0 ; nr<1024 ; nr++) {
			if (1 & *pg_table)
				free_page(0xfffff000 & *pg_table);//mem_map对应的位减1,也许会清0,可以重新被用于分配
			*pg_table = 0;//对应的页表项都清零
			pg_table++;
		}
		free_page(0xfffff000 & *dir);//由于地址小于1MB,所以直接返回
		*dir = 0;//第32项页目录项页清零
	}
	invalidate();
	return 0;
}

free_page,代码如下:

void free_page(unsigned long addr)
{
	if (addr < LOW_MEM) return;//1MB以上
	if (addr >= HIGH_MEMORY)
		panic("trying to free nonexistent page");
	addr -= LOW_MEM;
	addr >>= 12;
	if (mem_map[addr]--) return;//mem_map对应的位减1
	mem_map[addr]=0;
	panic("trying to free free page");
}

free_page_tables,页目录项的第32项清零,它所指向的页表也都清0了。

图 4

下面来看change_ldt,代码如下:

static unsigned long change_ldt(unsigned long text_size,unsigned long * page)
{
	unsigned long code_limit,data_limit,code_base,data_base;
	int i;

	code_limit = text_size+PAGE_SIZE -1;//text_size为shell代码段的长度
	code_limit &= 0xFFFFF000;//代码段的界限就是shell代码段的长度
	data_limit = 0x4000000;//数据段的界限是64MB
	code_base = get_base(current->ldt[1]);//代码段基地址为128MB
	data_base = code_base;//数据段基地址为128
	set_base(current->ldt[1],code_base);//修改代码段基地址为128MB
	set_limit(current->ldt[1],code_limit);//修改代码段界限为shell代码段的长度
	set_base(current->ldt[2],data_base);//修改数据段基地址为128MB
	set_limit(current->ldt[2],data_limit);//修改数据段界限为64MB
/* make sure fs points to the NEW data segment */
	__asm__("pushl $0x17\n\tpop %%fs"::);
	data_base += data_limit;//192MB
	for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) {//MAX_ARG_PAGES为31
		data_base -= PAGE_SIZE;//data_base为192MB-4096B
		if (page[i])//现在只有page[31]有地址,里面存放着参数和环境变量的页面的首地址
			put_page(page[i],data_base);
	}
	return data_limit;//返回64MB
}

change_ldt,修改了代码段和数据段的基地址和段界限,数据段的段界限为64MB,由于一个页目录项可以代表4MB的内存地址,所以需要16个页目录项。也就是从第32个页目录项到第48个页目录项。

下面来看put_page,代码如下:

unsigned long put_page(unsigned long page,unsigned long address)//address为0xBFFF000
{
	unsigned long tmp, *page_table;

/* NOTE !!! This uses the fact that _pg_dir=0 */

	if (page < LOW_MEM || page >= HIGH_MEMORY)
		printk("Trying to put page %p at %p\n",page,address);
	if (mem_map[(page-LOW_MEM)>>12] != 1)
		printk("mem_map disagrees with %p at %p\n",page,address);
	page_table = (unsigned long *) ((address>>20) & 0xffc);//BxBC,为188
	if ((*page_table)&1)//目前为0
		page_table = (unsigned long *) (0xfffff000 & *page_table);
	else {//走到这里
		if (!(tmp=get_free_page()))//获取存放页表项的页表
			return 0;
		*page_table = tmp|7;//页目录表第188项存放刚刚获取的页表地址
		page_table = (unsigned long *) tmp;//把页表地址赋值给page_table
	}
	page_table[(address>>12) & 0x3ff] = page | 7;//页表的最后一项存放的是page(存放的参数和环境变量的页面的首地址)
/* no need for invalidate */
	return page;
}

put_page,address为0xBFFF000,page为page[31],存放的参数和环境变量的页面的首地址。执行完put_page后,内存的图如下:

  

页目录项的第48位,指向页表的首地址。页表的首地址+4092,这个地址存放的内容的就是存放的参数和环境变量的页面的首地址。

p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;//p最后为PAGE_SIZE*MAX_ARG_PAGES-4 - 参数和环境变量的字节数 + 64MB - MAX_ARG_PAGES*PAGE_SIZE,最后就是64MB-4- 参数和环境变量的字节数,也就是换成了以64MB为界限的堆栈值了。

然后调用create_tables,代码如下:

static unsigned long * create_tables(char * p,int argc,int envc)
{
	unsigned long *argv,*envp;
	unsigned long * sp;

	sp = (unsigned long *) (0xfffffffc & (unsigned long) p);
	sp -= envc+1;
	envp = sp;
	sp -= argc+1;
	argv = sp;
	put_fs_long((unsigned long)envp,--sp);
	put_fs_long((unsigned long)argv,--sp);
	put_fs_long((unsigned long)argc,--sp);
	while (argc-->0) {
		put_fs_long((unsigned long) p,argv++);
		while (get_fs_byte(p++)) /* nothing */ ;
	}
	put_fs_long(0,argv);
	while (envc-->0) {
		put_fs_long((unsigned long) p,envp++);
		while (get_fs_byte(p++)) /* nothing */ ;
	}
	put_fs_long(0,envp);
	return sp;
}

最后形成如下图:

最后的点睛之作,

	eip[0] = ex.a_entry;		//内核栈要返回给用户空间的eip
	eip[3] = p;			//内核栈用返回给用户空间的esp

eip为0,esp为64MB-4-参数和环境变量的字节数,也就是说在用户空间,访问ss:eip,就是访问128MB+64MB-4-参数和环境变量的字节数,在经过分页机制,最后能够访问最终存放参数和环境变量的页面的指定位置。

时间: 2024-08-29 09:04:38

通过进程2加载shell进程,详解execve的相关文章

【转】web.xml 中的listener、 filter、servlet 加载顺序及其详解

web.xml 中的listener. filter.servlet 加载顺序及其详解 原文链接 http://www.cnblogs.com/JesseV/archive/2009/11/17/1605015.html 在项目中总会遇到一些关于加载的优先级问题,近期也同样遇到过类似的,所以自己查找资料总结了下,下面有些是转载其他人的,毕竟人家写的不错,自己也就不重复造轮子了,只是略加点了自己的修饰. 首先可以肯定的是,加载顺序与它们在 web.xml 文件中的先后顺序无关.即不会因为 filt

web.xml 中的listener、 filter、servlet 加载顺序及其详解

转自:http://zhxing.iteye.com/blog/399668 在项目中总会遇到一些关于加载的优先级问题,近期也同样遇到过类似的,所以自己查找资料总结了下,下面有些是转载其他人的,毕竟人家写的不错,自己也就不重复造轮子了,只是略加点了自己的修饰. 首先可以肯定的是,加载顺序与它们在 web.xml 文件中的先后顺序无关.即不会因为 filter 写在 listener 的前面而会先加载 filter.最终得出的结论是:listener -> filter -> servlet 同

(转载)web.xml 中的listener、 filter、servlet 加载顺序及其详解

首先可以肯定的是,加载顺序与它们在 web.xml 文件中的先后顺序无关.  但不会因为 filter 写在 listener 的前面而会先加载 filter.  最终得出的结论是:listener -> filter -> servlet 同时还存在着这样一种配置节:context-param,它用于向 ServletContext 提供键值对,即应用程序上下文信息.我们的 listener, filter 等在初始化时会用到这些上下文中的信息,那么 context-param 配置节是不是

(转)web.xml 中的listener、 filter、servlet 加载顺序及其详解

在项目中总会遇到一些关于加载的优先级问题,近期也同样遇到过类似的,所以自己查找资料总结了下,下面有些是转载其他人的,毕竟人家写的不错,自己也就不重复造轮子了,只是略加点了自己的修饰. 首先可以肯定的是,加载顺序与它们在 web.xml 文件中的先后顺序无关.即不会因为 filter 写在 listener 的前面而会先加载 filter.最终得出的结论是:listener -> filter -> servlet 同时还存在着这样一种配置节:context-param,它用于向 Servlet

转:web.xml 中的listener、 filter、servlet 加载顺序及其详解

在项目中总会遇到一些关于加载的优先级问题,刚刚就遇到了一个问题,由于项目中使用了quartz任务调度,quartz在web.xml中是使用listener进行监听的,使得在tomcat启动的时候能马上检查数据库查看那些任务未被按时执行,而数据库的配置信息在是在web.xml中使用servlet配置的,导致tomcat启动后在执行quartz任务时报空指针,原因就是servlet中的数据库连接信息未被加载.网上查询了下web.xml中配置的加载优先级: 首先可以肯定的是,加载顺序与它们在 web.

web.xml 中的listener、 filter、servlet 加载顺序及其详解【转】

在项目中总会遇到一些关于加载的优先级问题,刚刚就遇到了一个问题,由于项目中使用了quartz任务调度,quartz在web.xml中是使用listener进行监听的,使得在tomcat启动的时候能马上检查数据库查看那些任务未被按时执行,而数据库的配置信息在是在web.xml中使用servlet配置的,导致tomcat启动后在执行quartz任务时报空指针,原因就是servlet中的数据库连接信息未被加载.网上查询了下web.xml中配置的加载优先级: 首先可以肯定的是,加载顺序与它们在 web.

web.xml 中元素加载顺序及其详解

一.概述 1.启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取<listener>和<context-param>两个结点. 2.紧接着,容器创建一个ServletContext(servlet上下文),这个web项目的所有部分都将共享这个上下文. 3.容器将<context-param>转换为键值对,并交给servletContext. 4.容器创建<listener>中的类实例,创建监听器 二 . load-on-startu

使用CSS、JavaScript及Ajax实现图片预加载的方法详解 

预加载图片是提高用户体验的一个很好方法.图片预先加载到浏览器中,访问者便可顺利地在你的网站上冲浪,并享受到极快的加载速度.这对图片画廊及图片占据很大比例的网站来说十分有利,它保证了图片快速.无缝地发布,也可帮助用户在浏览你网站内容时获得更好的用户体验.本文将分享使用CSS.JavaScript及Ajax实现图片预加载的三个不同技术,来增强网站的性能与可用性.一起来看看吧,希望对大家学习web前端有所帮助. 方法一:用CSS和JavaScript实现预加载 实现预加载图片有很多方法,包括使用CSS

构建自己的PHP框架之自动加载类中详解spl_autoload_register()函数

在了解这个函数之前先来看另一个函数:__autoload. 一.__autoload 这是一个自动加载函数,在PHP5中,当我们实例化一个未定义的类时,就会触发此函数.看下面例子: printit.class.php <?php class PRINTIT { function doPrint() { echo 'hello world'; } } ?> index.php <? function __autoload( $class ) { $file = $class . '.cla