首页
社区
课程
招聘
《基于linker实现so加壳技术基础》下篇
发表于: 2021-9-24 18:43 8044

《基于linker实现so加壳技术基础》下篇

2021-9-24 18:43
8044

《基于linker实现so加壳技术基础》下篇

获得linker维护的本so的soinfo

但是问题又来了如何获得当前so的soinfo指针的基址呢?翻阅网上的资料说可以dlopen打开self,我看了一下那是安卓7之前的方法安卓8.1不支持了(555这不是坑人嘛咋搞),于是我阅读安卓源码发现了获得soinfo的方法,这是一套组合拳,可以先dlopen自己然后再用soinfo_from_handle函数来把handle转换成soinfo,正当我性高彩烈的打开ida查看它的symble的时候,发现没有这个函数,他不是导出函数(sblinker 5555),坑人呢呀,那么就只能照着ida一点一点的翻译它的代码了,找一个调用它的稍微短一点的函数,我找到的是do_dlclose函数,那么中间那一大坨就是soinfo_from_handle的实现了,返回值就是soinfo_unload,的参数,接着我傻眼了,f5之后这玩意没参数(逆天f5),只能看汇编了,还好不长,就是这个x12+0x18中的地址值,切过去一看就是v7[3]那么就对了,我就可以写一个属于自己的handle转soinfo

1
2
void* dlopen(const char* filename, int flag);
static soinfo* soinfo_from_handle(void* handle)



就是如下的这个函数,有些东西不好处理,比如它搞了好多全局变量,所以我们要从maps里面扫描linker的基址,剩下的直接抄就好了

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
66
_QWORD * getsoinfo(unsigned __int64 a1,void* base){
    unsigned int v2; // w19
    unsigned __int64 v3; // x11
    __int64 v4; // x9
    __int64 v5; // x10
    _QWORD *v6; // x12
    uint64 *bas1e= reinterpret_cast<uint64 *>((char *) base + 0xFD468);
    uint64 *bas2= reinterpret_cast<uint64 *>((char *) base + 0xFD460);
    _QWORD qword_FD468=*bas1e;
    _QWORD _dl_g_soinfo_handles_map=*bas2;
    unsigned __int64 v7; // x13
    __int64 v8; // x20
    __int64 v9; // x0
    __int64 v11; // [xsp+0h] [xbp-20h] BYREF
    char v12[8]; // [xsp+8h] [xbp-18h] BYREF
    if ( (a1 & 1) != 0 )
    {
        if ( qword_FD468 )
        {
            v3 = a1 - a1 / qword_FD468 * qword_FD468;
            v4 = qword_FD468 - 1;
            v5 = (qword_FD468 - 1) & qword_FD468;
            if ( qword_FD468 > a1 )
                v3 = a1;
            if ( !v5 )
                v3 = v4 & a1;
            v6 = *(_QWORD **)(_dl_g_soinfo_handles_map + 8 * v3);
            if ( v6 )
            {
                while ( 1 )
                {
                    v6 = (_QWORD *)*v6;
                    if ( !v6 )
                        break;
                    v7 = v6[1];
                    if ( v7 == a1 )
                    {
                        if ( v6[2] == a1 )
                        {
                            if ( v6[3] )
 
                                break;
                        }
                    }
                    else
                    {
                        if ( v5 )
                        {
                            if ( v7 >= qword_FD468 )
                                v7 -= v7 / qword_FD468 * qword_FD468;
                        }
                        else
                        {
                            v7 &= v4;
                        }
                        if ( v7 != v3 )
                            break;
                    }
                }
            }
        }
    }
    _QWORD * st= reinterpret_cast<uint64 *>((char *) (v6[3]) );
    return st;
 
}
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
void* ax=dlopen("libnative-lib.so",RTLD_NOW);
  __android_log_print(6,"r0ysue","%s",strerror(errno));
  char line[1024];
  int *startr;
  int *end;
  int n=1;
  FILE *fp=fopen("/proc/self/maps","r");
  while (fgets(line, sizeof(line), fp)) {
      if (strstr(line, "linker64") ) {
          __android_log_print(6,"r0ysue","%s", line);
          if(n==1){
              startr = 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++;
 
 
      }
 
 
  }
 
   void** old_soinfo= reinterpret_cast<void **>(getsoinfo((unsigned __int64) ax, startr));

链接&soinfo的修正

这里修正soinfo直接用了结构体的->,由于我没有实现soinfo类所以这篇文章就到这里了。。。。。。。那是不可能的肉丝老师教我们永远不放弃,没有条件要创造条件也要解决这个问题,既然没实现soinfo我就用笨方法来实现就是c的偏移,而一个一个数soinfo当中的变量大小太过于麻烦,因为它的变量实在是太多了(555),于是我想到可以使用ida来辅助查看它的偏移,先直接查看LoadTask对象的Load函数

那么其实就是这里,只需要一一对应即可,也就是说

1
2
3
4
5
si_->base = *(si+16)
si_->size = *(si+24)
si_->load_bias =* (si+256)
si_->phnum = *(si+8)
si_->phdr = *(si)

那么修正代码就是

1
2
3
4
5
6
7
8
memcpy(&secstr,(char*)(start)+bb.sh_offset,bb.sh_size);
 mprotect((void*)PAGE_START((ElfW(Addr))((char *)start)),a.load_size_,PROT_WRITE|PROT_READ|PROT_EXEC);//申请读写执行权限因为我们要执行插件so的代码所以要执行权限
 __android_log_print(6,"r0ysue","size %s",strerror(errno));
 *reinterpret_cast<uint64 *>((char *) old_soinfo + 16) = reinterpret_cast<uint64>(a.load_start_);
 *(int*)((char*)(old_soinfo)+24)= a.load_size_;
 *reinterpret_cast<uint64 *>((char *) old_soinfo + 256) = reinterpret_cast<uint64>(start);
 *(int*)((char*)(old_soinfo)+8) = a.phdr_num_;
 *reinterpret_cast<uint64 *>((char *) old_soinfo )= (uint64) a.loaded_phdr_;

接下来就是链接过程,要将函数的绝对地址填上去,并且将引用的其他so的函数地址也填上去,这里安卓源码实现的函数是prelink_image,非常的长仔细读一下就知道,它其实是可以抄的,这里我们主要修正的是导入表、导出表、重定向表、符号表、字符串表、重定位表、异常处理,但是其实可以照着安卓源码和ida全部把它抄上,这里我从elf头开始获得了程序头然后再程序头中寻找Dynamic段,因为这些表都在动态段中,至于起始地址直接用mmap将上面load得到的loadbias映射过来即可

1
2
3
4
5
6
7
8
9
10
11
12
13
Elf64_Ehdr aa;
void* start= mmap(reinterpret_cast<void *>(a.load_bias_), sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
memcpy(&aa,start,sizeof(Elf64_Ehdr));//elf头解析,其实直接用a里面的也行我这里忘了
int secoff= aa.e_shoff;
int secsnum=aa.e_shnum;
Elf64_Shdr bb;
Elf64_Phdr cc;
memcpy (&cc,((char*)(start)+aa.e_phoff),sizeof(Elf64_Phdr));//将程序头表存入cc里面
for(int y=0;y<aa.e_phnum;y++){//做遍历
    memcpy(&cc, (char *) (start) +aa.e_phoff+sizeof(Elf64_Phdr) * y, sizeof(Elf64_Phdr));
    if(cc.p_type==2){
      //当p_type为0x2是就代表是Dynamic段
    }

接下来就开始漫长的修正过程了,可以对照着ida都抄源码,主要对照着上面的段都要修复成功。主要就是要将相对地址转化为绝对地址,内容部分使用Elf64_Dyn这个结构体对他进行解析就好,也就是d_tag等于0x6ffffef5时的导出表(so一定要导出给art使用),等于5时的字符串表,等于6时的符号表等等这些都要修正,最终我只取了几个我的so中有的段类型进行修正

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
66
67
68
if(dd.d_tag==0x6ffffef5 ){//对导出表进行修正这个很重要导出失败则无法运行
                size_t   gnu_nbucket_ = reinterpret_cast<uint32_t*>((char*)start + dd.d_un.d_ptr)[0];
                // skip symndx
                uint32_t     gnu_maskwords_ = reinterpret_cast<uint32_t*>((char*)start + dd.d_un.d_ptr)[2];
                uint32_t  gnu_shift2_ = reinterpret_cast<uint32_t*>((char*)start + dd.d_un.d_ptr)[3];
 
                ElfW(Addr)*  gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr)*>((char*)start + dd.d_un.d_ptr + 16);
                uint32_t*  gnu_bucket_ = reinterpret_cast<uint32_t*>(gnu_bloom_filter_ + gnu_maskwords_);
                // amend chain for symndx = header[1]
                uint32_t*  gnu_chain_ = reinterpret_cast<uint32_t *>( gnu_bucket_ +
                                                                     gnu_nbucket_-reinterpret_cast<uint32_t *>(
                                                                             (char *) start +
                                                                             dd.d_un.d_ptr)[1]);
                --gnu_maskwords_;
                uint32_t  flags_ = FLAG_GNU_HASH|flags_;
                *reinterpret_cast<size_t *>((char *) old_soinfo + 344) = gnu_nbucket_;
                *reinterpret_cast<uint32_t *>((char *) old_soinfo + 368) = gnu_maskwords_;
                *reinterpret_cast<uint32_t *>((char *) old_soinfo + 372) = gnu_shift2_;
                *reinterpret_cast<  ElfW(Addr)* *>((char *) old_soinfo +  376) = gnu_bloom_filter_;
                *reinterpret_cast<uint32_t **>((char *) old_soinfo + 352) = gnu_bucket_;
                *reinterpret_cast<uint32_t **>((char *) old_soinfo + 360) = gnu_chain_;
                *reinterpret_cast<uint32_t *>((char *) old_soinfo + 48) = *reinterpret_cast<uint32_t *>((char *) old_soinfo + 48) |FLAG_GNU_HASH;
 
            }
             if(dd.d_tag==2 ){
                *reinterpret_cast<uint64 *>((char *) old_soinfo + 48)=dd.d_un.d_val / sizeof(ElfW(Rela));
             }
             if(dd.d_tag==0x17 ){//导入表修正
                 *reinterpret_cast<uint64 *>((char *) old_soinfo + 104)= reinterpret_cast<uint64>(
                         (char *) start + dd.d_un.d_ptr);
             }
             if(dd.d_tag==7){//重定位修正
                 *reinterpret_cast<uint64 *>((char *) old_soinfo + 120)= reinterpret_cast<uint64>(
                         (char *) start + dd.d_un.d_ptr);
             }
             if(dd.d_tag==5){//对字符串表进行修正
                 *reinterpret_cast<char **>((char *) old_soinfo + 56) = reinterpret_cast< char*>((char *) start+dd.d_un.d_ptr);
             }
             if(dd.d_tag==6){//对符号表进行修正
                 *reinterpret_cast<uint64 *>((char *) old_soinfo + 64) = reinterpret_cast<uint64>(
                         (char *) start + dd.d_un.d_ptr);
             }
             if(dd.d_tag==10){
                 *reinterpret_cast<uint64 *>((char *) old_soinfo + 336) = reinterpret_cast<uint64>(
                         (char *) start + dd.d_un.d_ptr);
             }
             if(dd.d_tag==8){
                 *reinterpret_cast<uint64 *>((char *) old_soinfo + 336) =  dd.d_un.d_val / sizeof(ElfW(Rela));
             }
 
             if(dd.d_tag==0x6ffffff0){
                 *reinterpret_cast<uint64 *>((char *) old_soinfo + 440) =  reinterpret_cast<uint64 >((char*)start + dd.d_un.d_ptr);
             }
             if(dd.d_tag==0x6fffffff){
                 *reinterpret_cast<uint64 *>((char *) old_soinfo + 472) =  dd.d_un.d_val;
             }
 
             if(dd.d_tag==0x6ffffffe){
                 *reinterpret_cast<uint64 *>((char *) old_soinfo + 464) = reinterpret_cast<uint64>(
                         (char *) start + dd.d_un.d_ptr);
             }
 
 
              if(dd.d_tag==1){
                 mynedd[needed]=dd.d_un.d_val;
                 needed++;
 
             }

这样其实如果我们被加固的so如果没有引用外部函数就可以正常使用了(哪个so可能没有外部函数呀),因为我们已经修复了导出表,但是为了追求完整性还需要补依赖,比如我要是在被加壳的so中引用了printf或者__android_log_print就会报错

修正依赖函数地址

由于我上面未实现neededso的装载与链接为了方便所以我下面对于依赖so的加载都采用dlopen和dlsym这种方式。这里可以看安卓源码中的link_image函数他调用了relocate来修复JMPREL Relocation Table表,所以我们跟进去看一下,其实这里就很清楚了,用迭代的方法获得so中引用的地址并且根据类型瑱回去我们的so当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool soinfo::relocate(const VersionTracker& version_tracker, ElfRelIteratorT&& rel_iterator,
                    const soinfo_list_t& global_group, const soinfo_list_t& local_group) {
....
    ElfW(Word) type = ELFW(R_TYPE)(rel->r_info);
   ElfW(Word) sym = ELFW(R_SYM)(rel->r_info);
....
  if (!soinfo_do_lookup(this, sym_name, vi, &lsi, global_group, local_group, &s)) {
      return false;
      }
....
 switch (type) {
 
   ...
 }
 
 
                    }

由于我没有实现soinfo所以只能另辟蹊径,从原理出发用dlopen和dlsym另写一套方案。首先把上面的符号表和字符串表用起来,然后照着源码实现一个遍历的类(不实现用循环也可以,但是直接ctrl+cv就好了还不用动脑仁何乐而不为呢),而且要用到上面的导入库表,当然不知道安卓源码咋抽风了,就是没有R_SYM和R_TYPE这两个类型的定义我只能自己导入了,其实这两个就是对info的解析十分的简单

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
class plain_reloc_iterator {
 
public:
    plain_reloc_iterator(rel_t* rel_array, size_t count)
            : begin_(rel_array), end_(begin_ + count), current_(begin_) {}
 
    bool has_next() {
        return current_ < end_;
    }
 
    rel_t* next() {
        return current_++;
    }
public:
    rel_t* const begin_;
    rel_t* const end_;
    rel_t* current_;
 
 
};
 
#define ELFW(what) ELF64_ ## what
 
#define R_TYPE(sym) ((((Elf64_Xword)sym) << 32)
#define R_SYM(type) ((type) & 0xffffffff))
 
 
    char* strtab_= *reinterpret_cast<char **>((char *) old_soinfo + 56) ;//字符串表基址
    Elf64_Sym* symtab_= *reinterpret_cast<Elf64_Sym **>((char *) old_soinfo + 64);//符号表基址
    plain_reloc_iterator myit(
            reinterpret_cast<rel_t *>(*reinterpret_cast<uint64 *>(
                    (char *) old_soinfo + 104)), *reinterpret_cast<size_t *>((char *) old_soinfo + 48));
    __android_log_print(6,"r0ysue","finish xxx%x",*reinterpret_cast<size_t *>((char *) old_soinfo + 48));

最后写一个循环回填就好了

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
    for (size_t idx = 0; myit.has_next(); ++idx) {
        const auto rel = myit.next();
 
 
        ElfW(Word) type = ELFW(R_TYPE)(rel->r_info);
        ElfW(Word) sym = ELFW(R_SYM)(rel->r_info);
 
 
        ElfW(Addr) sym_addr = 0;
        const char *sym_name = nullptr;
        const Elf64_Sym *s = nullptr;
        if (type == 0) {//不处理类型为0的部分
            continue;
        }
        sym_name = reinterpret_cast<const char *>(strtab_+symtab_[sym].st_name);//根据get_string函数改编
 
 
        for(int s=0;s<needed;s++) {//遍历所有的导入库表用dlopen和dlsym查找是否有我们需要的符号
            void* handle=dlopen(strtab_ + mynedd[s],RTLD_NOW);
            sym_addr= reinterpret_cast<Elf64_Addr>(dlsym(handle, sym_name));
            if(sym_addr==0)
                continue;
            else
//            __android_log_print(6, "r0ysue", "finish xxwwwwwwwwwwwwwwwx%p %s", sym_addr,sym_name);
break;
        }
 
        switch (type) {
            case 1026://我只有0x402类型的部分所以就简化处理了
                *reinterpret_cast<uint64 *>((char *) start+ rel->r_offset)  = (sym_addr );
                break;
 
        }
 
    }

跟到这里其实就完成了,下面看一下结果

1
2
3
4
5
6
7
8
//插件so当中的代码
extern "C"
JNIEXPORT jint JNICALL
Java_com_roysue_elfso_MainActivity_add(JNIEnv *env, jobject thiz, jint a, jint b) {
    printf("cxzcxzcxz");
    __android_log_print(6,"r0ysue","i am from 1.so %p",a);
   return a+b;
}

最后日志,这样就完成和art的交互,后面还有执行init_arry函数和Jni_Onload也是十分的简单我就不实现了

总结

本篇文章只是一个基础用于对新手的so加壳入门,我粗略的实现了一个简单的so壳,算是我踩到的许多坑,其中导出表的修复就花费了好久的时间最终才成功,感谢大家观看

 

附件加壳demo
链接:https://pan.baidu.com/s/1MZSjotH8cs7wrOIAiZM5NQ
提取码:kjvm

 


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (1)
雪    币: 5
活跃值: (506)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
void* start= mmap(reinterpret_cast<void *>(a.load_bias_), sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
之前不是映射过一次了吗?为什么又要再次映射一次
2024-6-16 20:09
0
游客
登录 | 注册 方可回帖
返回
//