寻找so符号地址

  • 寻找so中符号的地址
    • 总述
    • 通过程序头获得符号地址
    • 通过节头获得符号地址
    • 模仿安卓通过hash寻找符号
  • 总结

寻找so中符号的地址

总述

我们在使用so中的函数的时候可以使用dlopen和dlsym配合来寻找该函数的起始地址,但是在安卓高版本中android不允许打开白名单之外的so,这就让我们很头疼,比如我们hook libart.so中的函数都没有办法来找到函数的具体位置,所以有了此文,这里介绍3种方法来获得符号的地址,网上方案挺多的我这里主要介绍原理

通过程序头获得符号地址

首先是如何找到so的首地址,这个android系统中提供了maps文件来记录so的内存分步,所以我们可以遍历maps文件来寻找so的首地址,如下

   char line[1024];int *start;int *end;int n=1;FILE *fp=fopen("/proc/self/maps","r");while (fgets(line, sizeof(line), fp)) {if (strstr(line, "libart.so") ) {__android_log_print(6,"r0ysue","");if(n==1){start = reinterpret_cast<int *>(strtoul(strtok(line, "-"), NULL, 16));end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "), NULL, 16));}else{strtok(line, "-");end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "), NULL, 16));}n++;}}

通过elf头结构我们可以找到程序头的地址,ndk中自带了elf.h就很友好,就是e_phoff是相对于我们上面扫到的so首地址的偏移,e_phnum是我们的程序头表中结构体的总个数,程序头中存着elf装载信息,如下图

这里有一个问题就是上面的地址是so的起始地址,不是load_bias,所以我们在计算物理偏移的时候要减去一个首段的物理偏移,这里需要遍历程序头,得到第一个e_type为1的段记录下它的p_vaddr。其中对我们索引符号地址有用的就是Dynamic Segment,也就是type为2的段,这部分可以写一个循环来找到,去记录下其中的字符串表和符号表就可以了

    Elf64_Ehdr header;memcpy(&header, startr, sizeof(Elf64_Ehdr));memcpy(&cc, ((char *) (startr) + header.e_phoff), sizeof(Elf64_Phdr));for (int y = 0; y < header.e_phnum; y++) {//寻找首段偏移memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,sizeof(Elf64_Phdr));if (cc.p_type == 1) {phof =cc.p_paddrbreak;}}for (int y = 0; y < header.e_phnum; y++) {memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,sizeof(Elf64_Phdr));if (cc.p_type == 2) {Elf64_Dyn dd;for (y = 0; y == 0 || dd.d_tag != 0; y++) {memcpy(&dd, (char *) (startr) + cc.p_offset + y * sizeof(Elf64_Dyn) + 0x1000,sizeof(Elf64_Dyn));if (dd.d_tag == 5) {//符号表strtab_ = reinterpret_cast< char *>((char *) startr + dd.d_un.d_ptr - phof);}if (dd.d_tag == 6) {//字符串表symtab_ = reinterpret_cast<Elf64_Sym *>(((char *) startr + dd.d_un.d_ptr - phof));}if (dd.d_tag == 10) {//字符串表大小strsz = dd.d_un.d_val;}}}}

接下来遍历符号表就可以了,这里有一个问题就是如何确定符号表的大小,这里观察一下ida反编译的结果,发现符号表后面接的就是字符串表,那么用字符串表的首地址减去符号表的首地址就是符号表的大小,之后再用Elf64_Sym结构体解析,st_value就是该函数相对于load_bias的物理偏移,所以我们最后.再减去之前记录的首段偏移即可

   char strtab[strsz];memcpy(&strtab, strtab_, strsz);Elf64_Sym mytmpsym;for (n = 0; n < (long) strtab_ - (long) symtab_; n = n + sizeof(Elf64_Sym)) {//遍历符号表memcpy(&mytmpsym,(char*)symtab_+n,sizeof(Elf64_Sym));if(strstr(strtab_+mytmpsym.st_name,"artFindNativeMethod")){    __android_log_print(6,"r0ysue","%p %s",mytmpsym.st_value,strtab_+mytmpsym.st_name);break;}}return (char*)start+mytmpsym.st_value-phof;

通过节头获得符号地址

通过elf头结构我们也可以找到节头的地址,也就是e_shoff,节头表相对于程序头表就友好许多,它的项非常多,唯一不好的一点就是它不会加载到内存中,所以Execution View中就没有这个东西,所以我们只能通过绝对路径找到它,手动解析文件

    int fd;void *start;struct stat sb;fd = open(lib, O_RDONLY);fstat(fd, &sb);start = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

在这种解析方式中我们在elf头中需要的值是e_shoff、e_shentsize、e_shnum、e_shstrndx,就是节头表偏移,节大小,节个数,节头表字符串,不过我们最终的目标仍然是拿到符号表和字符串表,也就是下面的symtab和strtab中的sh_offset

 Elf64_Ehdr header;memcpy(&header, start, sizeof(Elf64_Ehdr));int secoff = header.e_shoff;int secsize = header.e_shentsize;int secnum = header.e_shnum;int secstr = header.e_shstrndx;Elf64_Shdr strtab;memcpy(&strtab, (char *) start + secoff + secstr * secsize, sizeof(Elf64_Shdr));int strtaboff = strtab.sh_offset;char strtabchar[strtab.sh_size];memcpy(&strtabchar, (char *) start + strtaboff, strtab.sh_size);Elf64_Shdr enumsec;int symoff = 0;int symsize = 0;int strtabsize = 0;int stroff = 0;for (int n = 0; n < secnum; n++) {memcpy(&enumsec, (char *) start + secoff + n * secsize, sizeof(Elf64_Shdr));if (strcmp(&strtabchar[enumsec.sh_name], ".symtab") == 0) {symoff = enumsec.sh_offset;symsize = enumsec.sh_size;}if (strcmp(&strtabchar[enumsec.sh_name], ".strtab") == 0) {stroff = enumsec.sh_offset;strtabsize = enumsec.sh_size;}}

最后和上面一样遍历符号表即可可得到物理偏移

    int realoff=0;char relstr[strtabsize];Elf64_Sym tmp;memcpy(&relstr, (char *) start + stroff, strtabsize);for (int n = 0; n < symsize; n = n + sizeof(Elf64_Sym)) {memcpy(&tmp, (char *)start + symoff+n, sizeof(Elf64_Sym));if(tmp.st_name!=0&&strstr(relstr+tmp.st_name,sym)){realoff=tmp.st_value;break;}}return realoff;

这种方式能够找到非导出符号的地址,还是有一定作用的,比如我在寻找soinfo地址的时候就用到了寻找soinfo_map在linker中的相对地址

模仿安卓通过hash寻找符号

这种方式就是dlsym的官方写法,由于libart.so这种so自动就会加载到内存种所以就不需要dlopen了,我们只需要在map里面找到它的首地址就可以了,代码和上面一样就不贴了,这里我们主要看看官方如何实现的,一路追踪do_dlopen最终找到了函数soinfo::gnu_lookup,这里面是他的主要实现逻辑,我们只需要实现它即可,这里多了4个项我们之前没有提到,就是它的导出表4项,所以这种方法只能找到导出表当中的函数或者变量

size_t gnu_nbucket_ = 0;// skip symndxuint32_t gnu_maskwords_ = 0;uint32_t gnu_shift2_ = 0;ElfW(Addr) *gnu_bloom_filter_ = nullptr;uint32_t *gnu_bucket_ = nullptr;uint32_t *gnu_chain_ = nullptr;int phof = 0;Elf64_Ehdr header;memcpy(&header, startr, sizeof(Elf64_Ehdr));uint64 rel = 0;size_t size = 0;long *plt = nullptr;char *strtab_ = nullptr;Elf64_Sym *symtab_ = nullptr;Elf64_Phdr cc;memcpy(&cc, ((char *) (startr) + header.e_phoff), sizeof(Elf64_Phdr));for (int y = 0; y < header.e_phnum; y++) {memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,sizeof(Elf64_Phdr));if (cc.p_type == 6) {phof = cc.p_paddr - cc.p_offset;//改用程序头的偏移获得首段偏移用之前的方法也行}}for (int y = 0; y < header.e_phnum; y++) {memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,sizeof(Elf64_Phdr));if (cc.p_type == 2) {Elf64_Dyn dd;for (y = 0; y == 0 || dd.d_tag != 0; y++) {memcpy(&dd, (char *) (startr) + cc.p_offset + y * sizeof(Elf64_Dyn) + 0x1000,sizeof(Elf64_Dyn));if (dd.d_tag == 0x6ffffef5) {//0x6ffffef5为导出表项gnu_nbucket_ = reinterpret_cast<uint32_t *>((char *) startr + dd.d_un.d_ptr -phof)[0];// skip symndxgnu_maskwords_ = reinterpret_cast<uint32_t *>((char *) startr + dd.d_un.d_ptr -phof)[2];gnu_shift2_ = reinterpret_cast<uint32_t *>((char *) startr + dd.d_un.d_ptr -phof)[3];gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr) *>((char *) startr +dd.d_un.d_ptr + 16 - phof);gnu_bucket_ = reinterpret_cast<uint32_t *>(gnu_bloom_filter_ + gnu_maskwords_);// amend chain for symndx = header[1]gnu_chain_ = reinterpret_cast<uint32_t *>( gnu_bucket_ +gnu_nbucket_ -reinterpret_cast<uint32_t *>((char *) startr +dd.d_un.d_ptr - phof)[1]);}if (dd.d_tag == 5) {strtab_ = reinterpret_cast< char *>((char *) startr + dd.d_un.d_ptr - phof);}if (dd.d_tag == 6) {symtab_ = reinterpret_cast<Elf64_Sym *>(((char *) startr + dd.d_un.d_ptr - phof));}}}}

之后模仿gnu_lookup函数即可,hashmap的查询方法

 char* name_=symname;//直接抄的安卓源码uint32_t h = 5381;const uint8_t* name = reinterpret_cast<const uint8_t*>(name_);while (*name != 0) {h += (h << 5) + *name++; // h*33 + c = h + h * 32 + c = h + h << 5 + c}int index=0;uint32_t h2 = h >> gnu_shift2_;uint32_t bloom_mask_bits = sizeof(ElfW(Addr))*8;uint32_t word_num = (h / bloom_mask_bits) & gnu_maskwords_;ElfW(Addr) bloom_word = gnu_bloom_filter_[word_num];n = gnu_bucket_[h % gnu_nbucket_];do {Elf64_Sym * s = symtab_ + n;char * sb=strtab_+ s->st_name;if (strcmp(sb ,reinterpret_cast<const char *>(name_)) == 0 ) {break;}} while ((gnu_chain_[n++] & 1) == 0);Elf64_Sym * mysymf=symtab_+n;long* finaladdr= reinterpret_cast<long*>(sb->st_value + (char *) start-phof);return finaladdr;

总结

这里介绍了三种得到符号地址的方法,都比较简单,只是我们写hook或者主动调用框架的一个基础,只有深刻的了解了elf格式才能完成我们的目标
有兴趣可以加微信:roysu3一起学习呀


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部