elf 文件格式细节讲解

     ELF文件是Linux系统中发明的很重要的文件,Linux系统中的可执行文件、Object文件、动态库文件都是ELF格式文件,它的地位就相当于Windows中的PE格式文件,不过整体上ELF文件比PE文件要设计地精简。ELF文件中大致分为文件头、段头表、结头表,剩下的就是段和结所指向的数据了。Object文件的ELF文件内容和可执行文件和动态库的ELF文件的内容是有些区别的,大部分时候在研究so加密时,大多碰到的是动态库的ELF文件。

一. elf文件头

/* ELF Header */
typedef struct elfhdr {
	unsigned char	e_ident[EI_NIDENT]; /* ELF Identification */
	Elf32_Half	e_type;		/* object file type */
	Elf32_Half	e_machine;	/* machine */
	Elf32_Word	e_version;	/* object file version */
	Elf32_Addr	e_entry;	/* virtual entry point */
	Elf32_Off	e_phoff;	/* program header table offset */
	Elf32_Off	e_shoff;	/* section header table offset */
	Elf32_Word	e_flags;	/* processor-specific flags */
	Elf32_Half	e_ehsize;	/* ELF header size */
	Elf32_Half	e_phentsize;	/* program header entry size */
	Elf32_Half	e_phnum;	/* number of program header entries */
	Elf32_Half	e_shentsize;	/* section header entry size */
	Elf32_Half	e_shnum;	/* number of section header entries */
	Elf32_Half	e_shstrndx;	/* section header table's "section 
					   header string table" entry offset */
} Elf32_Ehdr;

    文件头里面记录着elf文件类型(可执行文件、动态库文件、*.o 被链接加载的文件)、是否arm、X86、程序头和节头信息。对于可执行文件和动态库文件节表可选,程序表是一定有,*.o 被链接加载的文件是程序头表可选,节头表必须有。在对安卓native函数做处理时用到的都是elf格式的动态库so文件,它里面是程序头表必有,节头表可选,下面讨论的是针对这种情形。

二. elf文件的程序头表

    对于可执行文件和动态库文件,如果把节表信息抹掉,该文件还是有效的,而如果将程序头表信息抹掉,则文件会失效(注意:这里说的抹掉,只是抹掉表的信息,表指向的内容并没有动)。一般elf文件的程序头表中会有以下类型表:

    方框中四种类型的程序头:Interpreter Path、(R_X)Loadable Segment、(RW_)Loadable Segment、Dynamic Segment。

    Interpreter Path段记录的是链接器linker的路径,这个在可执行elf文件文件才有,在so文件中不会有,因为可执行文件中一般都是会调用其他的动态库的,那么这个时候系统就需要在将执行位置转到可执行文件的入口地址前,先将各种动态库加载到内存,这个操作是有linker来完成的,之后才将执行位置转到可执行文件的入口地址,而动态库是直接被加载的,所以so的入口地址是可以不需要的,也不需要linker。

    Loadable Segment指向的位置是需要加载到内存中的,两个Loadable Segment段,分别是可读可执行权限和可读可写权限。这两个段的作用和PE文件中节的作用是一样的,都是会记录内存虚拟地址偏移,文件偏移的,通过虚拟地址计算文件偏移时对所有的 Loadable Segment 中虚拟地址范围计算即可。其他程序头(非 Loadable Segment)中记录的文件偏移是可以抹掉的,因为系统在加载文件之后会通过虚拟地址来计算出文件偏移,而不是直接用程序头中的文件偏移。当然了,如果抹掉 Loadable Segment中文件偏移就会使文件执行失败。(如果想尝试,通过010工具改动对应值,然后尝试即可,010中有自带elf文件格式解析模板,很快就能找到相应位置。)

    Dynamic Segment 是很重要的一个程序头,里面存储着函数名、使用过的动态库名、重定位表、函数代码偏移等重要信息,不过不是直接记录,而是通过一定的方法查询得到,这个查询过程是elf设计中巧妙且关键的核心所在。Dynamic Segment指向的文件内容是以下结构体的对象数组:

/* Dynamic structure */
typedef struct {
	Elf32_Sword	d_tag;		/* controls meaning of d_val */
	union {
		Elf32_Word	d_val;	/* Multiple meanings - see d_tag */
		Elf32_Addr	d_ptr;	/* program virtual address */
	} d_un;
} Elf32_Dyn;

/*d_tag 的取值*/
/* Dynamic Array Tags - d_tag */
#define DT_NULL		0		/* marks end of _DYNAMIC array */
#define DT_NEEDED	1		/* string table offset of needed lib */
#define DT_PLTRELSZ	2		/* size of relocation entries in PLT */
#define DT_PLTGOT	3		/* address PLT/GOT */
#define DT_HASH		4		/* address of symbol hash table */
#define DT_STRTAB	5		/* address of string table */
#define DT_SYMTAB	6		/* address of symbol table */
#define DT_RELA		7		/* address of relocation table */
#define DT_RELASZ	8		/* size of relocation table */
#define DT_RELAENT	9		/* size of relocation entry */
#define DT_STRSZ	10		/* size of string table */
#define DT_SYMENT	11		/* size of symbol table entry */
#define DT_INIT		12		/* address of initialization func. */
#define DT_FINI		13		/* address of termination function */
#define DT_SONAME	14		/* string table offset of shared obj */
#define DT_RPATH	15		/* string table offset of library
					   search path */
#define DT_SYMBOLIC	16		/* start sym search in shared obj. */
#define DT_REL		17		/* address of rel. tbl. w addends */
#define DT_RELSZ	18		/* size of DT_REL relocation table */
#define DT_RELENT	19		/* size of DT_REL relocation entry */
#define DT_PLTREL	20		/* PLT referenced relocation entry */
#define DT_DEBUG	21		/* bugger */
#define DT_TEXTREL	22		/* Allow rel. mod. to unwritable seg */
#define DT_JMPREL	23		/* add. of PLT's relocation entries */
#define DT_BIND_NOW	24		/* Bind now regardless of env setting */
#define DT_NUM		25		/* Number used. */
#define DT_LOPROC	0x70000000	/* reserved range for processor */
#define DT_HIPROC	0x7fffffff	/*  specific dynamic array tags */

    对于以上d_tag,有的记录的是一块区域,对应的d_un取d_ptr的值,有的是记录某一块区域的大小,如:DT_STRSZ,则对应的d_un取d_val的值。

    安卓中对so中native函数加密时,需要通过native函数名知道该native函数名对应的函数代码所在位置以及代码所占字节数,这就得需要通过Dynamic Segment来查找的,通过节表中动态符号表也是可以查询到,但节表是可以抹掉的,所以最好知道怎么通过Dynamic Segment来查找。其实函数名在elf中是一个动态符号,DT_SYMTAB对应的偏移处记录着所有的动态符号岁对应的信息,是一个同结构体的数组,想要查找的函数名信息就在这个数组中,现在需要知道的是想要查找的函数名的信息所对应的数组下标,步骤如下:1. 通过Dynamic Segment找到DT_STRTAB、DT_SYMTAB、DT_HASH对应的表信息,分别记录着符号名称字符串、符号表信息(符号对应的代码偏移和字节数就在这个表中)、哈希表;2. 通过函数

unsigned int elf_hash(const char *_name)
{
	const unsigned char *name = (const unsigned char *)_name;
	unsigned h = 0, g;

	while (*name) {
		h = (h << 4) + *name++;
		g = h & 0xf0000000;
		h ^= g;
		h ^= g >> 24;
	}
	return h;
}

计算出函数名对应的哈希值;3. 通过上一步中的哈希值在哈希表中查找对应符号在符号表中下标,方法为: 给定一个符号名字,返回一个哈希值 x,然后由 bucket[x%nbucket] 得到一个符号表索引 y,如果索引 y 对应的符号表项不是想要的符号(通过符号表项对应符号名和给定符号名比对就行),则由 chain[y] 得到下一个符号表索引 z,如果仍不是想要的符号,继续 chain[z]…,直到匹配到,或者最后得出下标是0,则说明该符号不存在。bucket、nbucket、chain参考哈希表结构:

    可参考以下代码,另外解析elf的工程也会给出下载链接。

int ElfFile32::getTargetFuncInfo(const char *funcName, funcInfo32 *info) {
	char flag = -1, *dynstr;
	int i;
	Elf32_Off dyn_vaddr;
	Elf32_Word dyn_size, dyn_strsz;
	Elf32_Dyn *dyn;
	Elf32_Addr dyn_symtab, dyn_strtab, dyn_hash;
	Elf32_Sym *funSym;
	unsigned funHash, nbucket;
	unsigned *bucket, *chain;
	int mod;
	Elf32_Phdr* phdr = pProgmHdr;
	//    __android_log_print(ANDROID_LOG_INFO, "JNITag", "phdr =  0x%p, size = 0x%x\n", phdr, ehdr->e_phnum);
	for (i = 0; i < m_dwPhNum; ++i) {
		//		__android_log_print(ANDROID_LOG_INFO, "JNITag", "phdr =  0x%p\n", phdr);
		if (phdr->p_type == PT_DYNAMIC) {
			flag = 0;
			printf("Find .dynamic segment");
			break;
		}
		phdr++;
	}
	if (flag)
		return -1;
	int nOffset = phdr->p_offset;
	nOffset = Rva2Fa(phdr->p_vaddr);
	dyn_vaddr = (Elf32_Addr)(nOffset + baseAddr);
	dyn_size = phdr->p_filesz;
	flag = 0;
	for (dyn = (Elf32_Dyn*)dyn_vaddr; (Elf32_Addr)dyn < dyn_vaddr + dyn_size; dyn++){
		if (dyn->d_tag == DT_HASH){
			flag += 1;
			dyn_hash = dyn->d_un.d_ptr;
		}
		else if (dyn->d_tag == DT_STRTAB){
			flag += 2;
			dyn_strtab = dyn->d_un.d_ptr;
		}
		else if (dyn->d_tag == DT_SYMTAB){
			flag += 4;
			dyn_symtab = dyn->d_un.d_ptr;
		}
		else if (dyn->d_tag == DT_STRSZ) {
			flag += 8;
			dyn_strsz = dyn->d_un.d_val;
		}
	}
	if (flag & 0x0f != 0x0f){
		printf("Find needed .section failed\n");
		return -1;
	}
	dyn_hash = Rva2Fa(dyn_hash) + (Elf32_Addr)baseAddr;
	dyn_strtab = Rva2Fa(dyn_strtab) + (Elf32_Addr)baseAddr;
	dyn_symtab = Rva2Fa(dyn_symtab) + (Elf32_Addr)baseAddr;
	funHash = elf_hash(funcName);
	funSym = (Elf32_Sym *)dyn_symtab;
	dynstr = (char*)dyn_strtab;
	nbucket = *(unsigned*)dyn_hash;
	bucket = (unsigned*)(dyn_hash + 8);
	chain = bucket + nbucket;
	flag = -1;
	mod = (funHash % nbucket);

	for (int i = bucket[mod]; i != 0; i = chain[i]){
		if (strcmp(funSym[i].st_name + dynstr, funcName) == 0) {
			flag = 0;
			break;
		}
	}
	if (flag != 0) {
		return -1;
	}
	info->st_value = funSym[i].st_value;
	info->st_size = funSym[i].st_size;
	return 0;
}

三. elf文件的节头表

    elf格式的可执行程序文件和动态库文件里面节头表是可选的,即抹掉之后也不影响系统对该文件的执行,但是节头表中对应的内容还是存在的,并且在elf的程序头表中指向的内容或者查找的内容在节表中都会有记录,例如:在上一节中通过Dynamic Segment找到DT_STRTAB、DT_SYMTAB、DT_HASH对应的表的内容和节表中.dynstr节、.dynsym节、.hash节所指向的内容是一样的。

    节表中常见的符串的节:.dynstr、.shdrstr,前者是动态符号字符串所在的节,后者是节头表名称所在的节,.shdrstr所在的节在整个节表数组中下标在文件头中是有记录的。

解析elf程序,C++控制台办程序链接:https://download.csdn.net/download/denny_chen_/10887598

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页