C语言内存中执行外部代码详解(二)

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

本文地址: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

Join the Conversation

13 Comments

Leave a Reply to kk

Your email address will not be published. Required fields are marked *

  1. 请检查一下
    1. 有没有正确加载到代码文件
    2. 示例中的代码文件好像是64位系统的
    3. 你自己提取的代码是否正确。

  2. 我不知道,您会不会知道我说的是哪一个篇blog.我继续说明下是这一篇
    《C语言内存中执行外部代码详解(二)》

  3. 您好,首先很感谢您提供的技术资料,我研究了很就都没有解决掉“Segmentation fault (core dumped)”这个问题。我就是直接运行您给的压缩包,也没能成功。我的环境是,linux16.04 虚拟机是12 cpu i5。希望您给我解答。
    补充一句:很喜欢你页面上的兔子,哈哈

  4. 理论上可行,但实际上很难保证,因为操作系统一般对内存的读、写、执行权限是分开管理的,只有VirtualMalloc或mmap可以保证内存一定有可执行权限;另外,malloc分配到的内存地址是随机的,但VirtualMalloc或mmap可以申请指定地址的内存。

  5. 谢谢,昨晚用src.c生成的执行文件怎么也弄不出get_line_number.function的代码。今天终于明白了。

  6. 我刚刚为linux 64编译并提取了get_line_number.function,更新到上面的压缩包中了,朋友可以自己下载下来看看.get_line_number.function_X64适用于Intel X64,AMD 64架构.

  7. get_line_number.function就是纯二进制文件,所以输出结果是data.
    要复制出执行代码,必须反编译生成文件(objdump -d -M intel src),找到函数定义的二进制代码,再从生成文件中将函数主体的二进制数据(从进入函数到函数返回的所有数据)复制到新文件中,这些数据就可以直接用install_function注入到内存中并执行.

  8. 补充,查询了文件属性,
    file get_line_number.function

    get_line_number.function: data

    到底怎么才可以保存为可执行代码呢?

  9. 你好,按照你的资料和代码
    我在linux下执行了,提示段错误。
    已经编译src.c并提取出get_number_line.function

    请问可能哪里出问题呢?