前言:初学逆向 请多多指教

为了加深ELF文件格式,自己用C来实现ELF文件结构的解析

自己写的代码大家可以参考一下,地址:

https://github.com/adezz/ParseElf

ELF Header

typedef struct elf32_hdr{  unsigned char    e_ident[EI_NIDENT];  Elf32_Half   e_type;  Elf32_Half   e_machine;  Elf32_Word   e_version;  Elf32_Addr   e_entry;  /* Entry point */  Elf32_Off    e_phoff;  Elf32_Off    e_shoff;  Elf32_Word   e_flags;  Elf32_Half   e_ehsize;  Elf32_Half   e_phentsize;  Elf32_Half   e_phnum;  Elf32_Half   e_shentsize;  Elf32_Half   e_shnum;  Elf32_Half   e_shstrndx;} Elf32_Ehdr;

通过如下代码将硬盘上的so文件加载进内存:

// 文件读取到内存void MyReadFile(PVOID* pSoFile){  DWORD dwSoBufferSize = 0;  fstream fs(soFileName, ios::in | ios::binary);  if(fs.fail())    return;  fs.seekg(0, ios::end);  int filesize = fs.tellg();  fs.seekg(0, ios::beg);  *pSoFile = new byte[filesize];  fs.read((char*)*pSoFile, filesize);  fs.close();}

有了上面关于ELF header头的结构体,通过结构体指针就很方便进行打印了

void getElfHeader(PVOID pSoFile){  elf32_hdr* pElf32_hdr = NULL;  pElf32_hdr = (elf32_hdr*)pSoFile;  printf("Magic: ");  for (DWORD i = 0;i<16;i++){    printf("%02X ", *(PBYTE)&pElf32_hdr->e_ident[i]);  }  printf("\n");  printf("e_type: %x\n", pElf32_hdr->e_type);  printf("e_machine: %x\n", pElf32_hdr->e_machine);  printf("e_version: %x\n", pElf32_hdr->e_version);  printf("e_entry: %x\n", pElf32_hdr->e_entry);  printf("e_phoff: %x\n", pElf32_hdr->e_phoff);  printf("e_shoff: %x\n", pElf32_hdr->e_shoff);  printf("e_flags: %x\n", pElf32_hdr->e_flags);  printf("e_ehsize: %x\n", pElf32_hdr->e_ehsize);  printf("e_phentsize: %x\n", pElf32_hdr->e_phentsize);  printf("e_phnum: %x\n", pElf32_hdr->e_phnum);  printf("e_shentsize: %x\n", pElf32_hdr->e_shentsize);  printf("e_shnum: %x\n", pElf32_hdr->e_shnum);  printf("e_shstrndx: %x\n", pElf32_hdr->e_shstrndx);}

e7655649cede42eab0911b4c62c4926b.png

Section Header 符号表

.symtab中还保存了可执行文件的本地符号,如全局变量,或者代码中定义的本地函数 等。因此, .symtab保存了所有的符号,而.dynsym只保存动态/全局符号。 .dynsym符号表对于动态链接可执行文件的执行来说是必需的,而.symtab符号表只是用来进行调试和链接的,有时候为了节省空间,会将.symtab符号表从生产二进制文件中删掉。
readelf.exe -S libhello-jni.so

发现也只能看到.dynsym符号表

e08d000ea5b4e79c31e97134285f3353.png

如下命令,打印的就是.dynsym符号表中的内容

readelf.exe -s libhello-jni.so

8c4a47bd3d0749fb9e6033511a5a9aa7.png

先解析符号表的位置,先知道Section Headers中有许多个section header的结构体,而符号表就是其中的一个,那么就需要先知道section headers的首地址,然后通过我之前发的几篇文章可以知道,每个section header的第一个成员sh_name则是说明相关节的名称,然后遍历每个section header来进行判断哪个是符号表就可以了

这里还需要注意一个问题,section header的sh_name是一个结构体,其中有个偏移值是指向的是.shstrtab表中的地址,该地址有对应的section header的名称,所以这里也需要先找到.shstrtab节的地址

那么先找到Section Headers的首地址,通过Elf Header中找到相关的偏移地址,也就是e_phoff成员,通过上面的代码可以看到如下

688c381014577f11fc9b9cd73bd628f0.png

Section Header的结构体如下:

typedef struct elf32_shdr {  Elf32_Word   sh_name; // 节区名称,如果是SHN_UNDEF标记未定义的  Elf32_Word   sh_type; // 节区的内容和语义进行分类  Elf32_Word   sh_flags; // 节区支持 1 位形式的标志,这些标志描述了多种属性。  Elf32_Addr   sh_addr; // 如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。  Elf32_Off    sh_offset; // 偏移地址,也就是基地址+偏移地址指向的地址就是实际当前节区  Elf32_Word   sh_size; // 当前实际节区的长度(字节数)  Elf32_Word   sh_link; // 节区头部表索引链接  Elf32_Word   sh_info; // 附加信息  Elf32_Word   sh_addralign; // 对齐约束  Elf32_Word   sh_entsize; // 当前节区中每个成员的固定大小字节,如果没用固定则默认为0} Elf32_Shdr;

获取每个节的sh_name的代码:

  elf32_hdr* pElf32_hdr = NULL;  elf32_shdr* pElf32_shdr = NULL;  elf32_shdr* pElf32_shdr_shstrtab = NULL;  elf32_shdr* pElf32_shdr_dynsym = NULL;  elf32_sym* pElf32_dynsym = NULL;  DWORD dwCount;  pElf32_hdr = (elf32_hdr*)pSoFile;  pElf32_shdr = (elf32_shdr*)((DWORD)pSoFile + (DWORD)pElf32_hdr->e_shoff);  pElf32_shdr_shstrtab = (elf32_shdr*)&pElf32_shdr[pElf32_hdr->e_shnum - 1];  for (DWORD i = 0; ie_shnum; i++){        printf("%s \n", (char*)((DWORD)pSoFile + (DWORD)pElf32_shdr_shstrtab->sh_offset + pElf32_shdr[i].sh_name));  }

到这里之后就开始解析符号表的内容了,关于符号表的结构体如下

typedef struct elf32_sym{     Elf32_Word st_name;  //符号表项名称。如果该值非0,则表示符号名的字符串表索引(offset),否则符号表项没有名称。     Elf32_Addr st_value;  //符号的取值。依赖于具体的上下文,可能是一个绝对值、一个地址等等。     Elf32_Word st_size;  //符号的尺寸大小。例如一个数据对象的大小是对象中包含的字节数。     unsigned char st_info;  //符号的类型和绑定属性。     unsigned char st_other;  //未定义。     Elf32_Half st_shndx;  //每个符号表项都以和其他节区的关系的方式给出定义。此成员给出相关的节区头部表索引。} Elf32_Sym;

可以发现,这样的结构体不止是只有一个,每个结构体都是如上的结构,并且

第一个成员st_name中是符号表项的名称,如果想要得到相关符号表的名称的话就需要先拿到第一个成员st_name的name off的偏移,然后再去.dynstr这个表中的首地址加偏移值获得相关符号表的名称

4785de0b5facb070ac0ffc7d4caf83eb.png

那也就是如果想要完全解析符号表,.dynstr和.dynsym这两个表缺一不可

fd26f584032ed9834c3abfa5fa0c6a63.png

解析符号表索引找到对应的字符串的实现代码:

void getSectionHeaders_dynsym(PVOID pSoFile){  elf32_hdr* pElf32_hdr = NULL;  elf32_shdr* pElf32_shdr = NULL;  elf32_shdr* pElf32_shdr_shstrtab = NULL;  elf32_shdr* pElf32_shdr_dynsym = NULL;  elf32_sym* pElf32_dynsym = NULL;  PVOID pElf32_dymstr = NULL;  DWORD dwIndexDynstr = 0;  DWORD dwCount = 0;  pElf32_hdr = (elf32_hdr*)pSoFile;  pElf32_shdr = (elf32_shdr*)((DWORD)pSoFile + (DWORD)pElf32_hdr->e_shoff);  pElf32_shdr_shstrtab = (elf32_shdr*)&pElf32_shdr[pElf32_hdr->e_shnum - 1];  for (DWORD i = 0; ie_shnum; i++){        // printf("%s \n", (char*)((DWORD)pSoFile + (DWORD)pElf32_shdr_shstrtab->sh_offset + pElf32_shdr[i].sh_name));    if (strcmp((char*)((DWORD)pSoFile + (DWORD)pElf32_shdr_shstrtab->sh_offset + pElf32_shdr[i].sh_name), ".dynsym") == 0){            dwCount = pElf32_shdr[i].sh_size / pElf32_shdr[i].sh_entsize; //dwCount表示符号表dynsym中的项目数量      printf("Symbol table '.dynsym' contains %d entries \n", dwCount);      //还得获取符号表字符串.synstr的表的首地址      for (DWORD m = 0; me_shnum; m++){        if (strcmp((char*)((DWORD)pSoFile + (DWORD)pElf32_shdr_shstrtab->sh_offset + pElf32_shdr[m].sh_name), ".dynstr") == 0){          dwIndexDynstr = m;          break;        }      }      pElf32_dymstr = (PVOID)((DWORD)pSoFile + (DWORD)pElf32_shdr[dwIndexDynstr].sh_offset); //确认符号表字符串获取成功            for (DWORD j = 0; j < dwCount; j++){        pElf32_dynsym = (elf32_sym*)((DWORD)pSoFile + pElf32_shdr[i].sh_offset);        //printf("%x\n", pElf32_dynsym[j].st_name);        printf("%s \n", (PCHAR)((DWORD)pElf32_dymstr + (DWORD)pElf32_dynsym[j].st_name));      }      break;    }  }  }

ef690f9fea50c62de00ee76387b6303d.png

到这里,其实Section Headers都应该了解了,其他的section header解析跟这个都一样的,并且比这个还简单,因为其他表的解析都是单一的!

Program Headers

接下来继续看Program Headers

如何拿到Program Headers的首地址,这个跟上面Section Headers一样的,都是从Elf Header中的成员获取的,这里是e_phoff成员

typedef struct {    Elf32_Word p_type;  // 描述的当前段的类型    Elf32_Off p_offset; // 当前段区的偏移地址    Elf32_Addr p_vaddr; // 虚拟地址    Elf32_Addr p_paddr; // 物理地址    Elf32_Word p_filesz;// 在文件映像中所占的字节数    Elf32_Word p_memsz; // 在内存映像中占用的字节数    Elf32_Word p_flags; // 段相关的标志    Elf32_Word p_align; // 文件对齐相关约定} Elf32_phdr;

p_type的类型有许多,这里如果想要进一步识别的,可以通过switch来进行分支判断,自己这里就不实现了,单纯的解析下字段就好了

代码实现:

void getProgramHeader(PVOID pSoFile){  elf32_hdr* pElf32_hdr = NULL;  elf32_phdr* pElf32_phdr = NULL;  pElf32_hdr = (elf32_hdr*)pSoFile;  pElf32_phdr = (elf32_phdr*)((DWORD)pSoFile + (DWORD)pElf32_hdr->e_phoff);  for (DWORD i = 0; i < pElf32_hdr->e_phnum; i++){    printf("p_type: 0x%x\n", pElf32_phdr[i].p_type);    printf("p_offset: 0x%x\n", pElf32_phdr[i].p_offset);    printf("p_vaddr: 0x%x\n", pElf32_phdr[i].p_vaddr);    printf("p_paddr: 0x%x\n", pElf32_phdr[i].p_paddr);    printf("p_filesz: 0x%x\n", pElf32_phdr[i].p_filesz);    printf("p_memsz: 0x%x\n", pElf32_phdr[i].p_memsz);    printf("p_flags: 0x%x\n", pElf32_phdr[i].p_flags);    printf("p_filesz: 0x%x\n", pElf32_phdr[i].p_filesz);    printf("\n");  }}

8237b7a5baf2c5bce51ccd963be2dae1.png

最后实现的代码中还可以参考一下实现的新增节的玩法!

9c93913fe79cc3ecedb98ca9be36c8a4.png

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐