吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

搜索
查看: 2427|回复: 10
上一主题 下一主题
收起左侧

[Android 原创] 基于linker实现so加壳技术下

[复制链接]
跳转到指定楼层
楼主
roysue 发表于 2021-9-24 17:28 回帖奖励
本帖最后由 roysue 于 2021-9-24 17:35 编辑

《基于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

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



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

_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;

}
  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函数


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

    si_->base = *(si+16)
    si_->size = *(si+24)
    si_->load_bias =* (si+256)
    si_->phnum = *(si+8)
    si_->phdr = *(si)

那么修正代码就是

   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得到的load_bias_映射过来即可

    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中有的段类型进行修正

   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当中。

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的解析十分的简单

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));

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


    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;

        }

    }

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

//插件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

免费评分

参与人数 10威望 +2 吾爱币 +109 热心值 +9 收起 理由
红烧排骨 + 1 热心回复!
gaosld + 1 + 1 谢谢@Thanks!
yixi + 1 + 1 谢谢@Thanks!
fengbolee + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
独行风云 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
hjw01 + 1 我很赞同!
Stoneone + 1 + 1 谢谢@Thanks!
qtfreet00 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
笙若 + 1 + 1 谢谢@Thanks!
风绕柳絮轻敲雪 + 2 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
咔c君 发表于 2021-9-24 21:13
学习了不错
3#
aonima 发表于 2021-9-25 20:08
4#
gunanyi 发表于 2021-9-25 20:12
5#
anzer 发表于 2021-9-26 07:42
这个有点看不懂
6#
J527 发表于 2021-9-29 20:16
多谢大佬分享!!!
7#
hjw01 发表于 2021-9-30 23:31
本来也想写so加密加壳贴, 那就先学习下
8#
13697i 发表于 2021-10-4 19:11
感谢楼主分享!
9#
ongp1347 发表于 2021-10-7 13:18
很强大 给楼主三连
10#
jncsw 发表于 2021-10-8 08:09
谢谢楼主分享,支持支持
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2021-10-28 07:02

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表