February 6, 2012 分类: ASM/C/C++, Linux     作者: hoverlees     留言: 13

前面我们讲了理论知识,本来打算再慢慢写的,不过有网友对这个话题很感兴趣,我就加快写了。

本文地址:http://www.hoverlees.com/blog/?p=1014

现在要做个实例,要实现这个功能,需要按以下步骤做:

1.创建需要加载到其它进程中的外部函数
2.编译这个代码
3.复制代码的机器指令到文件中
4.加密这个文件(可选)
5.其它进程使用VirtualAlloc(windows)或mmap(linux)将程序加载到内存中,如果加密了,需要在这里解密后加载。
6.执行程序。

下面我们进行要加载到内存的代码的编写,假设其它进程需要加载到内存中执行的函数叫get_number_line,我们先写出它的功能(就是获取一个文件的行数)

int get_number_line(const char* filename){
	FILE* fp;
	char* buffer;
	struct stat st;
	int num=0;
	int i;

	fp=fopen(filename,"r");
	if(fp==NULL) return -1;
	stat(filename,&st);
	buffer=malloc(st.st_size);
	if(buffer==NULL) return -1;
	fread(buffer,st.st_size,1,fp);
	for(i=0;i<st.st_size;i++){
		if(buffer[i]=='\n') num++;
	}
	fclose(fp);
	return num;
}

如果直接把get_number_line函数编译后的代码加载到其它进程里,基本上是执行不起来的。为什么呢,按第一篇文章里说的,有相对寻址问题,那么哪些地方有这个问题呢?

1.fp=fopen(filename,”r”); fopen在另一个进程里的地址很可能是不一样的,所以无法确保。还有一个重要的就是”r”,它在编译后会放到进程的数据段里,在加载的进程里并不存在。

2.包括stat在内的其它函数也会出现地址不正确的问题。

要解决这些问题,我们需要给函数加一个参数,这个参数存放那些函数地址信息,并在函数的实现里,使用这个参数访问函数。

经过修改后,代码变成这个样子,由于我的示例代码是同时支持linux和windows的.读者需要通过修改宏定义后在你的自已的平台下编译。

typedef int (* __stat)( const char *path, struct stat *buffer );
typedef FILE* (* __fopen)( const char *filename, const char *mode );
typedef void * (* __malloc)( size_t size );
typedef size_t (* __fread)( void *buffer, size_t size, size_t count, FILE *stream );
typedef int (* __fclose)( FILE *stream );

int get_number_line(const char* filename,void **p){
	FILE* fp;
	char* buffer;
	struct stat st;
	int num=0;
	int i;
	__stat _stat;
	__fread _fread;
	__fopen _fopen;
	__malloc _malloc;
	__fclose _fclose;
	const char* fmode;

	_stat=(__stat)p[0];
	_fopen=(__fopen)p[1];
	_malloc=(__malloc)p[2];
	_fread=(__fread)p[3];
	_fclose=(__fclose)p[4];
	fmode=(const char*)p[5];

	fp=_fopen(filename,fmode);
	if(fp==NULL) return -1;
	_stat(filename,&st);
	buffer=_malloc(st.st_size);
	if(buffer==NULL) return -1;
	_fread(buffer,st.st_size,1,fp);
	for(i=0;i<st.st_size;i++){
		if(buffer[i]=='\n') num++;
	}
	_fclose(fp);
	return num;
}

这个get_number_line函数编译后的二进制代码在同类CPU下的系统中的任何进程的任何地址空间里都能执行进来了。

代码完成了,我们就将它编译,然后要从编译后的可执行程序中取出get_number_line的机器码到一个文件中。所以编译就要看情况了,如果你的编译器开启了debug的编译模式,可能复制出来的代码注入到其它空间不能执行,因为有可能编译器在函数中添加了其它代码,而其它代码用的地址在其它进程中同样不能使用。

所以我使用了linux下的gcc编译,gcc编译出来的汇编代码是最高效的。那么linux下gcc编译后拷出来的代码在windows下能执行吗?对于支持同样指令集的CPU,答案是肯定的。但是有些细节问题,它不属于本文讨论的范围,我会在后面留思考题并解答。

上面的代码编译好后,我们可以用反编译器找到代码的十六进制码,如下图所示:

我们需要用十六进制软件,把这个函数的所有指令复制出来,到一个新文件中。

我在这个例子中,把指令复制到get_number_line.function文件中的。然后,你可以将这个新文件用自己的算法进行加密。我就不做加密示例了。

下面一步工作是载入程序,载入程序要将这个代码加载到内存中去执行,不同的操作系统有不同的实现方式,但原理都是一样的。Windows使用VirtualAlloc在虚拟地址空间中分配一块可执行的内存块,然后再把代码拷到这个块上去执行就可。对于我们的代码,完全不用在意程序复制到哪个虚拟地址空间上去,但特殊的使用可以分配到指定虚拟地址空间上.

我这儿给大家写好了示例代码,代码如下:

/**
 * 将代码文件安装到本进程虚拟地址空间中 windows版
 * @param mem 保存函数指令的内存
 * @param len 内存长度
 * @return 安装好后的函数指针
 */
void* install_function(void* mem,int len){
	void* function;
	function=VirtualAlloc(NULL,len,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
	if(function==NULL) return NULL;
	memcpy(function,mem,len);
	VirtualProtect(function,len,PAGE_EXECUTE_READ,NULL);
	return function;
}

linux使用mmap方式。原理是差不多的。

/**
 * 将代码文件安装到本进程虚拟地址空间中 linux版
 * @param mem 保存函数指令的内存
 * @param len 内存长度
 * @return 安装好后的函数指针
 */
void* install_function(void* mem,int len){
	void* function;
	function=mmap(NULL,len,PROT_EXEC|PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
	if(function==-1) return NULL;
	memcpy(function,mem,len);
	return function;
}

下面是宿主程序示例代码,如果要在linux下编译,请注释掉第一行。

//编译常量,如果想在windows下测试这个代码,请取消#define _IN_WINDOWS 1前面的注释
//注:必须是32位操作系统测试,因为get_line_number.function是在32位系统下生成的
#define _IN_WINDOWS 1

#include <stdio.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>

#ifdef _IN_WINDOWS
#include <windows.h>
#else
#include <sys/mman.h>
#endif

typedef int (*_get_number_line)(const char* filename,void** p);

#ifdef _IN_WINDOWS
/**
 * 将代码文件安装到本进程虚拟地址空间中 windows版
 * @param mem 保存函数指令的内存
 * @param len 内存长度
 * @return 安装好后的函数指针
 */
void* install_function(void* mem,int len){
	void* function;
	function=VirtualAlloc(NULL,len,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
	if(function==NULL) return NULL;
	memcpy(function,mem,len);
	VirtualProtect(function,len,PAGE_EXECUTE_READ,NULL);
	return function;
}
#else
/**
 * 将代码文件安装到本进程虚拟地址空间中 linux版
 * @param mem 保存函数指令的内存
 * @param len 内存长度
 * @return 安装好后的函数指针
 */
void* install_function(void* mem,int len){
	void* function;
	function=mmap(NULL,len,PROT_EXEC|PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
	if(function==-1) return NULL;
	memcpy(function,mem,len);
	return function;
}
#endif

void main(){
	FILE* fp;
	unsigned char fmem[1024];
	int memLen;
	void* p[6];
	_get_number_line get_number_line;
	//初始化本地地址,供载入的函数用
	p[0]=stat;
	p[1]=fopen;
	p[2]=malloc;
	p[3]=fread;
	p[4]=fclose;
	p[5]="rb";
	//从文件读取要执行的指令
	fp=fopen("./get_line_number.function","rb");
	memLen=fread(fmem,1,1024,fp);
	fclose(fp);
	//如果这个文件是加密的,需要在这里解密
	//...
	//注入指令到本进程虚拟地址空间中
	get_number_line=(_get_number_line) install_function(fmem,memLen);
	if(get_number_line==NULL){
		printf("install function fail.");
		return;
	}
	//调用注入的代码
	printf("total line:%d\n",get_number_line("./test.txt",p));
}

试试程序,果然顺利地执行了get_number_line.function里面的代码。而这个文件里的指令,可以是从网络上下载,或者放到应用程序资源里。

但细心的朋友会发现,在windows下如果注入的get_number_line函数计算的是一个比较大的文件的时候,返回的行数就是一个错误的行数。这是怎么回事呢,请读者自己思考。

我给大家一个提示,就是因为使用的编译器,或者开发者的原因,在不同平台下结构体成员变量的排列是不一样的,而在某个函数里,有个地方我故意使用了结构体。具体就不多说了,多想有益。

本文下关代码下载地址:
http://www.hoverlees.com/diy/sources/inject.zip

标签: , ,
我来留个言

您的电子邮箱我一定会保密的哦!

昵称

邮箱

评论内容

快速链接
推荐文章
推荐标签
最新图片