android逆向奇技淫巧三十:so加壳&加固原理简述

  1、古人云:没有规矩,不成方圆!任何组织要想正常运作,肯定需要有一系列的规章制度来约束参与其中的每个个体,否则每个个体都是各干各的,从全盘来看就是一群做着布朗运动的散沙!IT技术也一样,最典型的就是计算机网络了:为了确保整个网络能正常收发数据包,早在几十年前就制定了一整套计算机网络的通信协议,这就是大家耳熟能详的TCP/IP协议!无论是源端发送数据包的、中途转发数据包的,还是目的端接受数据包的,大家都按照统一的协议格式生成、转发和解析数据包(本质就是一串字符串),确保了目的端能正确收到和解析源端的数据,由此带来了计算机网络的飞速发展!

  基于计算机网络协议扩展一下:其实windwos下的PE格式、linux/android下的ELF格式本质上讲也是一种介于操作系统和编译器之间的协议!开发人员写好了代码,为了能在操作系统上顺利运行,编译器必须把高级语言的代码转换成操作系统能识别的文件格式+cpu能识别的机器码,只有这样,操作系统才能正确加载可执行文件到内存,然后运行!加壳最关键的原理就在这了:为了防止被静态分析,很多时候要想尽各种办法隐藏真实的代码,等执行的时候才还原!而且为了防止IDA等逆向软件的分析,文件格式可能还被故意破坏,不是标准的PE或ELF格式,导致操作系统无法正确地识别和加载,只能加壳的程序自己加载和执行,相当于“自立门户”了!整个过程大致如下:

     

  加壳工具、loader、被保护SO。

  • SO: 即被保护的目标 SO。
  • loader: 自身也是一个 SO,系统加载时首先加载 loader,loader 首先还原出经过加密、压缩、变换的 SO,再将 SO 加载到内存,并完成链接过程,使 SO 可以正常被其他模块使用(本质是把自定义格式的so修复成操作系统能执行的格式)
  • 加壳工具: 将被保护的 SO 加密、压缩、变换,并将结果作为数据与 loader 整合为 packed SO。

   从上述的描述看,loader本身就是个脱壳工具(只不过是加壳厂家官方的工具),抓住了loader,不就等于找到了脱壳的方法了?为了更好的学习loader,这里有必要研究一下加载到内存、修复链接等核心工作的原理和关键点!

  2、(1)加载到内存!不知道大家平时在写代码调用其他so中函数的时候有没有仔细想过:为啥要用dlopen、dlsym函数,而不直接简单粗暴地用open、fopen等函数打开so了?dlopen和普通的open函数有啥本质区别了?open函数都很熟悉,本质是通过系统调用找到文件在磁盘的位置,然后生成fd,后续通过fd操控文件!相比之下,dlopen要复杂多了:不但要加载到内存,还要解析文件格式,然后修复链接。先来看看第一步加载和解析文件格式都是怎么做的(这里以8.0.1版本为例,源码链接在文章末尾参考处有)!

  • 在/bionic/linker/linker.cpp中有do_dlopen函数(这里多说几句,linux内核有很多do开头的函数,都是内核内部的执行函数),核心代码如下:
      ProtectedDataGuard guard;
2013    soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);//查找so是否已经被加载
2014    loading_trace.End();
2015  
2016    if (si != nullptr) {
2017      void* handle = si->to_handle();
2018      LD_LOG(kLogDlopen,
2019             "... dlopen calling constructors: realpath=\"%s\", soname=\"%s\", handle=%p",
2020             si->get_realpath(), si->get_soname(), handle);
2021      si->call_constructors();//初始化so
2022      failure_guard.Disable();
2023      LD_LOG(kLogDlopen,
2024             "... dlopen successful: realpath=\"%s\", soname=\"%s\", handle=%p",
2025             si->get_realpath(), si->get_soname(), handle);
2026      return handle;

  会先在soInfo链表中挨个遍历,看看目标so是否已经加载。如果没有,就继续调用CallConstructor继续初始化so!其中 find_library_internal 函数核心代码如下:

      soinfo* candidate;
1438  
1439    if (find_loaded_library_by_soname(ns, task->get_name(), search_linked_namespaces, &candidate)) {  //根据so的名称查找是否已经加载
1440      task->set_soinfo(candidate);
1441      return true;
1442    }
1443  
1444    // Library might still be loaded, the accurate detection
1445    // of this fact is done by load_library.
1446    TRACE("[ \"%s\" find_loaded_library_by_soname failed (*candidate=%s@%p). Trying harder...]",
1447        task->get_name(), candidate == nullptr ? "n/a" : candidate->get_realpath(), candidate);
1448  
1449    if (load_library(ns, task, zip_archive_cache, load_tasks, rtld_flags, search_linked_namespaces)) { //正式加载so
1450      return true;
1451    }

  先根据so的名称查找在soInfo的链表中查找是否已经存在。如果不存在,说明还没加载,继续调用load_library 加载so。代码非常多(在这里:http://aospxref.com/android-8.1.0_r81/xref/bionic/linker/linker.cpp#1178),而且分散在不同的函数,我这里选重点说明整个流程:

// Open the file. 先打开文件
1341    int fd = open_library(ns, zip_archive_cache, name, needed_by, &file_offset, &realpath);
1342    if (fd == -1) {
1343      DL_ERR("library \"%s\" not found", name);
1344      return false;
1345    }

//生成soInfo结构体,后续用于记录so的属性
soinfo* si = soinfo_alloc(ns, realpath.c_str(), &file_stat, file_offset, rtld_flags);
1277    if (si == nullptr) {
1278      return false;
1279    }
1280  
1281    task->set_soinfo(si);
1282  
1283    // Read the ELF header and some of the segments.读取so的elf头,用于解析so
1284    if (!task->read(realpath.c_str(), file_stat.st_size)) {
1285      soinfo_free(si);
1286      task->set_soinfo(nullptr);
1287      return false;
1288    }
1289  
1290    // find and set DT_RUNPATH and dt_soname
1291    // Note that these field values are temporary and are
1292    // going to be overwritten on soinfo::prelink_image
1293    // with values from PT_LOAD segments.
1294    const ElfReader& elf_reader = task->get_elf_reader();
1295    for (const ElfW(Dyn)* d = elf_reader.dynamic(); d->d_tag != DT_NULL; ++d) {
1296      if (d->d_tag == DT_RUNPATH) {
1297        si->set_dt_runpath(elf_reader.get_string(d->d_un.d_val));
1298      }
1299      if (d->d_tag == DT_SONAME) {
1300        si->set_soname(elf_reader.get_string(d->d_un.d_val));
1301      }
1302    }
1303  
1304    for_each_dt_needed(task->get_elf_reader(), [&](const char* name) {
1305      load_tasks->push_back(LoadTask::create(name, si, ns, task->get_readers_map()));
1306    });
1307  

 //装载so,并给soInfo结构体赋值
641      ElfReader& elf_reader = get_elf_reader();
642      if (!elf_reader.Load(extinfo_)) {
643        return false;
644      }
645  
646      si_->base = elf_reader.load_start();
647      si_->size = elf_reader.load_size();
648      si_->set_mapped_by_caller(elf_reader.is_mapped_by_caller());
649      si_->load_bias = elf_reader.load_bias();
650      si_->phnum = elf_reader.phdr_count();
651      si_->phdr = elf_reader.loaded_phdr();
652  
653      return true;
654    }

   整个流程简单清晰:先打开文件,然后解析elf文件头,最后新生成so结构体记录加载的so文件属性!打开和加载的过程中,/bionic/linker/linker_phdr.cpp 中的这两个函数里面包括了整个加载过程的精髓:

 bool ElfReader::Read(const char* name, int fd, off64_t file_offset, off64_t file_size) {
150    CHECK(!did_read_);
151    CHECK(!did_load_);
152    name_ = name;
153    fd_ = fd;
154    file_offset_ = file_offset;
155    file_size_ = file_size;
156  
157    if (ReadElfHeader() &&  //读elf头
158        VerifyElfHeader() && //校验elf头
159        ReadProgramHeaders() && //读程序头
160        ReadSectionHeaders() &&  //读接头
161        ReadDynamicSection()) { //读动态节头
162      did_read_ = true;
163    }
164  
165    return did_read_;
166  }
167  
168  bool ElfReader::Load(const android_dlextinfo* extinfo) {
169    CHECK(did_read_);
170    CHECK(!did_load_);
171    if (ReserveAddressSpace(extinfo) && //为段开辟内存空间
172        LoadSegments() && //加载段,这里是很好的脱壳点
173        FindPhdr()) { //设置程序的加载地址
174      did_load_ = true;
175    }
176  
177    return did_load_;
178  }

  自己写壳的加载程序时,完全可以仿照这8个步骤一步一步做(事实上,很多加壳和脱壳loader程序也都是这么干的!)!至此,这个so的代码是不是就能执行了?很明显还不行,原因很简单:链接还没修复了!所谓的链接,本质就是调用其他函数时需要正确的地址

(2)修复链接

    整个过程最核心的函数就是prelink_image和link_image了,作用分别是:

  •   解析linker文件中dynamic段的各项,例如重定位表,符号表
  •        对全局变量,外部函数等地址进行重定位

   核心代码如下:prelink_image  大都是这种switch case结构,根据不同的tag做不同的解析;

for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {
2857      DEBUG("d = %p, d[0](tag) = %p d[1](val) = %p",
2858            d, reinterpret_cast<void*>(d->d_tag), reinterpret_cast<void*>(d->d_un.d_val));
2859      switch (d->d_tag) {
2860        case DT_SONAME:
2861          // this is parsed after we have strtab initialized (see below).
2862          break;
2863  
2864        case DT_HASH: //导出函数定位
2865          nbucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[0];
2866          nchain_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[1];
2867          bucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr + 8);
2868          chain_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr + 8 + nbucket_ * 4);
2869          break;
2870  
2871        case DT_GNU_HASH:  //导出函数定位
2872          gnu_nbucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[0];
2873          // skip symndx
2874          gnu_maskwords_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[2];
2875          gnu_shift2_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[3];
2876  
2877          gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr)*>(load_bias + d->d_un.d_ptr + 16);
2878          gnu_bucket_ = reinterpret_cast<uint32_t*>(gnu_bloom_filter_ + gnu_maskwords_);
2879          // amend chain for symndx = header[1]
2880          gnu_chain_ = gnu_bucket_ + gnu_nbucket_ -
2881              reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[1];
2882  
2883          if (!powerof2(gnu_maskwords_)) {
2884            DL_ERR("invalid maskwords for gnu_hash = 0x%x, in \"%s\" expecting power to two",
2885                gnu_maskwords_, get_realpath());
2886            return false;
2887          }
2888          --gnu_maskwords_;
2889  
2890          flags_ |= FLAG_GNU_HASH;
2891          break;
2892  
2893        case DT_STRTAB:
2894          strtab_ = reinterpret_cast<const char*>(load_bias + d->d_un.d_ptr);
2895          break;
2896  
2897        case DT_STRSZ:
2898          strtab_size_ = d->d_un.d_val;
2899          break;
2900  
2901        case DT_SYMTAB:
2902          symtab_ = reinterpret_cast<ElfW(Sym)*>(load_bias + d->d_un.d_ptr);
2903          break;
2904  
2905        case DT_SYMENT:
2906          if (d->d_un.d_val != sizeof(ElfW(Sym))) {
2907            DL_ERR("invalid DT_SYMENT: %zd in \"%s\"",
2908                static_cast<size_t>(d->d_un.d_val), get_realpath());
2909            return false;
2910          }
2911          break;

  解析完后就是重定位了,根据不同的条件给relocate函数传入不同的参数!

if (android_relocs_ != nullptr) {
3300      // check signature
3301      if (android_relocs_size_ > 3 &&
3302          android_relocs_[0] == 'A' &&
3303          android_relocs_[1] == 'P' &&
3304          android_relocs_[2] == 'S' &&
3305          android_relocs_[3] == '2') {
3306        DEBUG("[ android relocating %s ]", get_realpath());
3307  
3308        bool relocated = false;
3309        const uint8_t* packed_relocs = android_relocs_ + 4;
3310        const size_t packed_relocs_size = android_relocs_size_ - 4;
3311  
3312        relocated = relocate(
3313            version_tracker,
3314            packed_reloc_iterator<sleb128_decoder>(
3315              sleb128_decoder(packed_relocs, packed_relocs_size)),
3316            global_group, local_group);
3317  
3318        if (!relocated) {
3319          return false;
3320        }
3321      } else {
3322        DL_ERR("bad android relocation header.");
3323        return false;
3324      }
3325    }
3326  
3327  #if defined(USE_RELA)
3328    if (rela_ != nullptr) {
3329      DEBUG("[ relocating %s ]", get_realpath());
3330      if (!relocate(version_tracker,
3331              plain_reloc_iterator(rela_, rela_count_), global_group, local_group)) {
3332        return false;
3333      }
3334    }
3335    if (plt_rela_ != nullptr) {
3336      DEBUG("[ relocating %s plt ]", get_realpath());
3337      if (!relocate(version_tracker,
3338              plain_reloc_iterator(plt_rela_, plt_rela_count_), global_group, local_group)) {
3339        return false;
3340      }
3341    }
3342  #else
3343    if (rel_ != nullptr) {
3344      DEBUG("[ relocating %s ]", get_realpath());
3345      if (!relocate(version_tracker,
3346              plain_reloc_iterator(rel_, rel_count_), global_group, local_group)) {
3347        return false;
3348      }
3349    }
3350    if (plt_rel_ != nullptr) {
3351      DEBUG("[ relocating %s plt ]", get_realpath());
3352      if (!relocate(version_tracker,
3353              plain_reloc_iterator(plt_rel_, plt_rel_count_), global_group, local_group)) {
3354        return false;
3355      }
3356    }
3357  #endif

  relocate函数内部同样也是switch case结构,根据不同的type修改reloc表、符号表等,还有修改系统函数、全局/静态变量的绝对地址!

switch (type) {
2579        case R_GENERIC_JUMP_SLOT:
2580          count_relocation(kRelocAbsolute);
2581          MARK(rel->r_offset);
2582          TRACE_TYPE(RELO, "RELO JMP_SLOT %16p <- %16p %s\n",
2583                     reinterpret_cast<void*>(reloc),
2584                     reinterpret_cast<void*>(sym_addr + addend), sym_name);
2585  
2586          *reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
2587          break;
2588        case R_GENERIC_GLOB_DAT:
2589          count_relocation(kRelocAbsolute);
2590          MARK(rel->r_offset);
2591          TRACE_TYPE(RELO, "RELO GLOB_DAT %16p <- %16p %s\n",
2592                     reinterpret_cast<void*>(reloc),
2593                     reinterpret_cast<void*>(sym_addr + addend), sym_name);
2594          *reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
2595          break;
2596        case R_GENERIC_RELATIVE:
2597          count_relocation(kRelocRelative);
2598          MARK(rel->r_offset);
2599          TRACE_TYPE(RELO, "RELO RELATIVE %16p <- %16p\n",
2600                     reinterpret_cast<void*>(reloc),
2601                     reinterpret_cast<void*>(load_bias + addend));
2602          *reinterpret_cast<ElfW(Addr)*>(reloc) = (load_bias + addend);
2603          break;
2604        case R_GENERIC_IRELATIVE:
2605          count_relocation(kRelocRelative);
2606          MARK(rel->r_offset);
2607          TRACE_TYPE(RELO, "RELO IRELATIVE %16p <- %16p\n",
2608                      reinterpret_cast<void*>(reloc),
2609                      reinterpret_cast<void*>(load_bias + addend));
2610          {
2611  #if !defined(__LP64__)
2612            // When relocating dso with text_relocation .text segment is
2613            // not executable. We need to restore elf flags for this
2614            // particular call.
2615            if (has_text_relocations) {
2616              if (phdr_table_protect_segments(phdr, phnum, load_bias) < 0) {
2617                DL_ERR("can't protect segments for \"%s\": %s",
2618                       get_realpath(), strerror(errno));
2619                return false;
2620              }
2621            }
2622  #endif
2623            ElfW(Addr) ifunc_addr = call_ifunc_resolver(load_bias + addend);

  总结:所谓的重定位,本质就是把函数或变量在内存中的地址改正,方便其他代码找到和使用!想想x86架构下jmp指令为啥是相对偏移,而不是绝对地址了?就是因为代码加载到内存的地址不固定,但是代码之间的偏移是固定的,所以jmp指令只能用偏移而不是绝对地址如果要用绝对地址,加载的时候就要把函数或变量的在内存的绝对地址标注清楚

(3)加入soInfo结构体

   此时只有loader这个so是被操作系统linker加载的,所以也只有loader的so能和art交互(猜猜为什么?)!为了让自己关键代码的so也能和art交互,需要替换掉loader的so的属性,把其改成关键代码so的属性; PS:soInfo记录了内存中so结构体的核心属性,完全可以用来检索so,核心属性列举如下:

装载链接期间主要使用的成员:

装载信息
const ElfW(Phdr)* phdr;
size_t phnum;
ElfW(Addr) base;
size_t size;
符号信息
const char* strtab;
ElfW(Sym)* symtab;

下一个soInfo节点
 soinfo* next;

重定位信息
ElfW(Rel)* plt_rel;
size_t plt_rel_count;
ElfW(Rel)* rel;
size_t rel_count;
init 函数和 finit 函数
Linker_function_t* init_array;
size_t init_array_count;
Linker_function_t* fini_array;
size_t fini_array_count;
Linker_function_t init_func;
Linker_function_t fini_func;
运行期间主要使用的成员:

导出符号查找(dlsym):
const char* strtab;
ElfW(Sym)* symtab;
size_t nbucket;
size_t nchain;
unsigned* bucket;
unsigned* chain;
ElfW(Addr) load_bias;
异常处理:
unsigned* ARM_exidx;
size_t ARM_exidx_count;
load_library 在为 SO 分配 soinfo 后,会将装载结果更新到 soinfo 中,后面的链接过程就可以直接使用soinfo的相关字段去访问 SO 中的信息。

  操作系统为了便于管理,还会把soInfo组织成链表形式,所以在soinfo_alloc函数中就会把新生成的soInfo结构体加入链表,开发人员只要遍历链表就能查找到所有已经加载的so了!这里扩展一下:windows下做安全防护时,经常会把进程或线程的链表断开,让逆向破解人员找不到自己的进程或线程!  修改soInfo结构体的时候建议通过如下基址+偏移的形式修改!每个偏移代表什么含义可通过AOSP源码查询得到!

          

   3、最后总结一下一般加壳的原理流程:

  •   关键文件可以不是elf格式,比如自定义的文件格式;还可以对关键代码加密
  •        执行时先装载loader,由于loader本身是标准的so,所以直接用操作系统的linker加载即可(就是调用操作系统提供现成的dlopen等API)!
  •        loader被加载后,会调用callConstractor函数,该函数内部先后依次执行init、initArray、jni_onload等函数,所以加载自己关键文件、解密代码、还原成elf结构、修复链接/重定位、加入soInfo链表(“解壳”三部曲:加载、链接、替换soInfo结构体等工作都可以在这三个方法中做。具体代码完全可以参考android源码,结合自己文件实际格式即可!

   这里借(抄)鉴(袭)某位大佬整理的ELF格式的解析图凑数:

  

  附上看雪r0ysue大佬的加壳demo 链接: 提取码:kjvm    思路就是自己定义的loader完成了操作系统linker的功能!

 

脱壳点:

  1、mprotect函数:以前关注这个函数是在反调试阶段,比如把内存设置为不可执行,等自己代码快要执行到目标内存区域时再调用mprotect把内存设置为可执行。如果检测到被ida调试,就不改内存属性为可执行,让ida调试时跳转到这里时就会报signal异常!除了反调试,在加壳领域mprotect也能用上。比如在脱壳时,肯定需要一段内存存放代码(以任何形式申请的内存默认都没有执行权限),并且同样需要调用mprotect函数把内存设置为可执行!所以逆向时可以通过hook mprotect函数,看看第三个参数是不是设置内存为可执行。同时如果第二个参数len也不小,那么这个mprotect很有可能就是在脱壳用了!参考的hook代码如下:

            

   2、soInfo结构体:因为每个加载的so都会生成soInfo结构体,然后加入链表中,所以soInfo链表保存了所有已加载so的soInfo,从这个链表就能找到自己想要so的信息,然后借此脱壳;poc代码如下: 从maps文件查找linker64的地址,然后继续找solist的地址,最后遍历soInfo!

void initlinker(){
    char line[1024];
    int *end;
    long* base;
    int n = 1;
    FILE *fp = fopen("/proc/self/maps", "r");
    while (fgets(line, sizeof(line), fp)) {
        if (strstr(line, "linker64")) {
            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++;
        }
    }
    int soheaderoff = findsym("/system/bin/linker64", "__dl__ZL6solist");
    soheader = reinterpret_cast<long *>((char *) start + soheaderoff);
}
void *findsobase(const char *soname) {

    long* base;
    long* soinfo;
    int  n=0;
    for (_QWORD * result = reinterpret_cast<uint64 *>(*(_QWORD *) soheader); result; result = (_QWORD *)result[5] ){
        if(*(_QWORD *) ((__int64) result + 408)!= 0&&strcmp(reinterpret_cast<const char *>(*(_QWORD *) ((__int64) result + 408)), soname)==0) {
            base= reinterpret_cast<long *>(*(_QWORD *) ((char *) result + 16));
            long* size= reinterpret_cast<long *>(*(_QWORD *) ((char *) result + 24));
            long* load_bias= reinterpret_cast<long *>(*(_QWORD *) ((char *) result + 256));
            const char* name= reinterpret_cast<const char *>(*(_QWORD *) ((__int64) result + 408));
            soinfo= reinterpret_cast<long *>(result);
            break;
        }
    }
    return soinfo;
}

   3、mmap:从磁盘读取大文件后为了节约内存,用mmap是最合适不过的了!大文件很有可能是加壳的so!

   4、so加载加壳总结:

        

   为了增加内存中so被识别和dump的难度,可以把elf header或section header删除,或者用异或的方式加密混淆,比如下面这样的:

   

    标红部分是改过的,再用ida打开就不行了!总的来说,建议用异或而不是直接删除,能让逆向人员dump还原so的时候怀疑人生: 我是不是dump的位置选错了?代码上就类似这种:

bool LoadSegments() {
        for (size_t i = 0; i < phdr_num_; ++i) {
            const ElfW(Phdr) *phdr = &phdr_table_[i];
            if (phdr->p_type != PT_LOAD) {
                continue;
            }
            // Segment addresses in memory.
            ElfW(Addr) seg_start = phdr->p_vaddr^0x5a + load_bias_;
            ElfW(Addr) seg_end = seg_start + phdr->p_memsz^0x5a;
            ElfW(Addr) seg_page_start = PAGE_START(seg_start);
            ElfW(Addr) seg_page_end = PAGE_END(seg_end);
            ElfW(Addr) seg_file_end = seg_start + phdr->p_filesz^0x5a;
            // File offsets.
            ElfW(Addr) file_start = phdr->p_offset^0a5x;
            ElfW(Addr) file_end = file_start^0x5a + phdr->p_filesz;

            ElfW(Addr) file_page_start = PAGE_START(file_start);
            ElfW(Addr) file_length = file_end^0x5a - file_page_start;
            long* pp= reinterpret_cast<long *>(seg_page_start);

  选一些自认为比较重要的字段在完全加载后再异或0x5a(当然其他数字也行),达到迷惑逆向人员的目的!

 

 

 

 

 

 

参考:

1、https://cloud.tencent.com/developer/article/1071358  android linker与so加壳技术

2、https://www.cnblogs.com/r0ysue/p/15398936.html 基于linker实现so加壳 上篇

   https://zhuanlan.zhihu.com/p/413630468   下篇

3、http://aospxref.com/android-8.1.0_r81/   AOSP源码查询

4、https://wwm0609.github.io/2020/06/12/android-linker/  android dynamic linking简介

posted @ 2022-06-12 22:17  第七子007  阅读(7243)  评论(2编辑  收藏  举报