1. 程式人生 > >elf 檔案格式細節講解

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