首页
论坛
课程
招聘
寻找so中符号的地址
2021-10-28 12:29 8237

寻找so中符号的地址

2021-10-28 12:29
8237

寻找so中符号的地址

总述

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

通过程序头获得符号地址

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的段,这部分可以写一个循环来找到,去记录下其中的字符串表和符号表就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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_paddr
         break;
     }
 
 }
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的物理偏移,所以我们最后.再减去之前记录的首段偏移即可

1
2
3
4
5
6
7
8
9
10
11
  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中就没有这个东西,所以我们只能通过绝对路径找到它,手动解析文件

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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;
 
       }
 
 
   }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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项,所以这种方法只能找到导出表当中的函数或者变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
size_t gnu_nbucket_ = 0;
    // skip symndx
    uint32_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 symndx
                    gnu_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的查询方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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一起学习呀


[2022夏季班]《安卓高级研修班(网课)》月薪两万班招生中~

收藏
点赞1
打赏
分享
最新回复 (1)
雪    币: 234
活跃值: 活跃值 (675)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hjhjl222 活跃值 2021-11-10 11:56
2
0
抽空细研
游客
登录 | 注册 方可回帖
返回