android crc检测及绕过

[复制链接]
查看1683 | 回复0 | 2025-3-3 16:38:47 | 显示全部楼层 |阅读模式

立即注册,突破游客限制,与技术大牛交流切磋,畅享海量资源,让你的技术之路不再孤单!

您需要 登录 才可以下载或查看,没有账号?立即注册

×
一、前言
在开始之前先说下什么是crc检测,通俗点讲就是,把本地文件中的数据和内存中的数据进行crc计算得到的结果进行比较,来校验结果是否一致,不一致则判定数据被篡改。
举个例子:以libc.so为目标so,当我们第一次用frida以spwan的方式注入hook 时,未对libc.so的函数进行hook的话,app未退出,一旦我们对libc.so中的函数或指令进行了修改注入(不考虑inline hoook的因素影响),app便直接崩溃退出,这种情况基本就是检测到了数据被篡改,也就是crc检测。
基本的介绍到这里,下面开始对crc相关途径的检测进行分析,以及如何去绕过。
本次分析以libc.so为目标,以下的绕过都是用frida去处理。
二、分析环境
app是我总结的一部分crc检测,会把链接放置在结尾。
截图202503031627353577.png
frida版本: 16.5.9
目标app: LinkerDemo
分析so: libc.so
ELF工具:010Editor
arm平台: arm64
三、分析及绕过(一) 检测种类
目前crc的检测大的方向分两种:
1.本地文件与所属app的/proc/{id}/maps文件中so的内存范围作比较
2.本地文件与linker中获取到的so的内存范围作比较
(二)本地文件与maps内存的校验
先说一下此校验方法的相关逻辑
描述:提取本地文件/apex/com.android.runtime/lib64/bionic/libc.so的可执行段数据和app在/proc/{id}/maps下映射的libc.so可执行段内存进行crc校验
1.用010Editor打开libc.so
截图202503031627581154.png
获取可执行段表中的 p_offset(在文件中的偏移)和 p_filesz(在文件中的大小)。后续都是以这个为参照物与内存进行crc校验
2.获取maps内存中libc.so的可执行段
截图202503031628152649.png
正常来说只有一行r-xp段,因为我使用了frida,所以会出现这种内存布局)
提取出里面带有x的内存段数据。
3.校验
最后通过相关算法计算出两种途径获取到的内存结果进行比较。
算法一般都是crc32,当然个例可能会使用其他的算法,比如md5,aes等等,很少见的。
4.hook现象
打开app,点击libc maps crc,可以在控制台看到如下输出:
截图202503031628374389.png
此时我们的环境是正常的。接着我们用frida注入,对libc.so中的pthread_create方法进行hook,得到以下输出:
截图202503031628528553.png
可以很明显的看出内存中可执行段的crc值与文件中可执行的不一致,并且检测出了环境是hook的。
5.绕过
针对上述的检测,我们可以在maps中模拟一段可执行段数据,并把libc.so原本的可执行段名称给抹去,变为匿名内存。最后app获取到的maps内存范围就是我们模拟的一段数据
  1. function hiddenSoExecSegmentInMaps(so_path) {
  2.     //      /apex/com.android.runtime/lib64/bionic/libc.so
  3.     let mmap_addr = Module.findExportByName("libc.so", "mmap");
  4.     let mmapFunc = new NativeFunction(mmap_addr, 'pointer', ['pointer', 'int', 'int', 'int', 'int', 'int'])
  5.     let munmap_addr = Module.findExportByName("libc.so", "munmap");
  6.     let munmapFunc = new NativeFunction(munmap_addr, 'int', ['pointer', 'int'])
  7.     let mremap_addr = Module.findExportByName("libc.so", "mremap");
  8.     let mremapFunc = new NativeFunction(mremap_addr, 'pointer', ['pointer', 'int64', 'int64', 'int64', 'pointer'])
  9.     let open_addr = Module.findExportByName("libc.so", "open");
  10.     var openFunc = new NativeFunction(open_addr, 'int', ['pointer', 'int']);
  11.     let memset_addr = Module.findExportByName("libc.so", "memset");
  12.     var memsetFunc = new NativeFunction(memset_addr, 'pointer', ['pointer', 'int', 'int'])
  13.     let close_addr = Module.findExportByName("libc.so", "close");
  14.     var closeFunc = new NativeFunction(close_addr, 'int', ['int'])
  15.     const parts = so_path.split('/');
  16.     const so_name = parts.pop();
  17.     let soExecSegmentRangeFromMaps = findSoExecSegmentRangeFromMaps(so_name);
  18.     let startAddress = soExecSegmentRangeFromMaps.base;
  19.     let size = soExecSegmentRangeFromMaps.size;
  20.     if (startAddress === 0 || size === 0) {
  21.         console.log("可执行段未找到:", startAddress, size)
  22.         return;
  23.     }
  24.     let soExecSegmentFromFile = findSoExecSegmentFromFile(so_path);
  25.     //创建匿名内存,临时存储so可执行段内存
  26.     let new_addr = mmapFunc(ptr(-1), size, 7, 0x20 | 2, -1, 0);//0x20:匿名内存标识符(MAP_ANONYMOUS), 2:私有(MAP_PRIVATE)
  27.     console.log("创建的可执行段匿名内存起始地址:" + new_addr);
  28.     //把so可执行段内存复制到创建的匿名内存中去
  29.     Memory.copy(new_addr, startAddress, size);
  30.     console.log("复制完毕")
  31.     //调整so,使传入的so可执行段内存变成匿名内存
  32.     let ret = mremapFunc(new_addr, size, size, 1 | 2, startAddress);
  33.     if (ret === -1) {
  34.         console.log("mremap  调整失败")
  35.         return;
  36.     }
  37.     console.log("匿名目标so可执行段完成 ret:" + ret)
  38.     // 打开需要模拟的文件路径,用于后续在maps中生成指定名称的内存区域
  39.     let moniter_path = so_path;
  40.     let moniter_path_addr = Memory.allocUtf8String(moniter_path);
  41.     var fd = openFunc(moniter_path_addr, 0);
  42.     if (fd === -1) {
  43.         console.log("open " + moniter_path + " is error")
  44.         return -1;
  45.     }
  46.     //在maps中创建传入so路径名称的内存区域
  47.     let target_addr = mmapFunc(ptr(-1), size, 7, 2, fd, 0);
  48.     console.log("模拟的可执行段内存起始地址:" + target_addr);
  49.     closeFunc(fd)
  50.     //给创建的so内存区域全部置0
  51.     memsetFunc(target_addr, 0, size);
  52.     //把so文件中获取的可执行段内存复制到创建的so名称的内存区域中
  53.     Memory.copy(target_addr, soExecSegmentFromFile.start, soExecSegmentFromFile.size)
  54.     Memory.protect(target_addr, size, "r-x");
  55.     //卸载映射的匿名内存
  56.     // munmapFunc(new_addr, size);
  57.     console.log("maps中隐藏可执行段完成")
  58. }
复制代码
截图202503031630234957.png
注入上述代码后再次点击按钮看控制台输出:
截图202503031630344191.png
可以看到两种方式获取到的值一致了,环境也是安全的了。
这里面主要使用到了mmap在maps中映射一段名称为libc.so的数据,用mremap把原本的可执行段数据给设置为匿名内存
截图202503031630562315.png
这里就可以看到maps中libc.so的可执行段已设置为匿名内存,并且map中也有我们模拟的可执行段内存。
(三)本地文件与Linker获取的内存校验
本地文件获取的方式不再赘述了,直接看如何从Linker中获取内存。
1.获取libc.so soinfo结构体
linker作为so加载器,里面存放了所有已经加载的so,并把这些已经加载的so会依次存放进solist变量中,solist存储了所有so的soinfo结构体,它是一个soinfo结构体数组,我们可以从solist中获取到自己想要的so。
那么如何获取到libc.so的结构体呢?
带着这个疑问我们先了解下soinfo的相关结构组成。
  1. struct soinfo {
  2. #if defined(__work_around_b_24465209__)
  3.   private:
  4.    char old_name_[SOINFO_NAME_LEN];
  5. #endif
  6.   public:
  7.    const ElfW(Phdr)* phdr; //0
  8.    size_t phnum;   //1
  9. #if defined(__work_around_b_24465209__)
  10.    ElfW(Addr) unused0; // DO NOT USE, maintained for compatibility.
  11. #endif
  12.    ElfW(Addr) base;    //2
  13.    size_t size;    //3
  14.   
  15. #if defined(__work_around_b_24465209__)
  16.    uint32_t unused1;  // DO NOT USE, maintained for compatibility.
  17. #endif
  18.   
  19.    ElfW(Dyn)* dynamic; //4
  20.   
  21. #if defined(__work_around_b_24465209__)
  22.    uint32_t unused2; // DO NOT USE, maintained for compatibility
  23.    uint32_t unused3; // DO NOT USE, maintained for compatibility
  24. #endif
  25.   
  26.    soinfo* next;   //5
  27.   private:
  28.    uint32_t flags_;    //6
  29.   
  30.    const char* strtab_;    //7 .dynstr
  31.    ElfW(Sym)* symtab_; //8     .dynsym
  32.   
  33.    size_t nbucket_;
  34.    size_t nchain_;
  35.    uint32_t* bucket_; //11
  36.    uint32_t* chain_; //12
  37.   
  38. #if defined(__mips__) || !defined(__LP64__)
  39.    // This is only used by mips and mips64, but needs to be here for
  40.    // all 32-bit architectures to preserve binary compatibility.
  41.    ElfW(Addr)** plt_got_;  //arm64未被使用
  42. #endif
  43.   
  44. #if defined(USE_RELA)
  45.    ElfW(Rela)* plt_rela_; //.real.plt  13
  46.    size_t plt_rela_count_;
  47.   
  48.    ElfW(Rela)* rela_;  //15 //real.dyn
  49.    size_t rela_count_;             //16
  50. #else
  51.    ElfW(Rel)* plt_rel_;   
  52.    size_t plt_rel_count_;
  53.   
  54.    ElfW(Rel)* rel_;      
  55.    size_t rel_count_;     
  56. #endif
  57.   
  58.    linker_ctor_function_t* preinit_array_;//空 17
  59.    size_t preinit_array_count_;//空 18
  60.   
  61.    linker_ctor_function_t* init_array_;
  62.    size_t init_array_count_;
  63.    linker_dtor_function_t* fini_array_;
  64.    size_t fini_array_count_;
  65.   
  66.    linker_ctor_function_t init_func_; //空 23
  67.    linker_dtor_function_t fini_func_;//    空 24
  68.   
  69. #if defined(__arm__)  //arm64不进入
  70.   public:
  71.    // ARM EABI section used for stack unwinding.
  72.    uint32_t* ARM_exidx;
  73.    size_t ARM_exidx_count;
  74.   private:
  75. #elif defined(__mips__)//arm64不进入
  76.    uint32_t mips_symtabno_;
  77.    uint32_t mips_local_gotno_;
  78.    uint32_t mips_gotsym_;
  79.    bool mips_relocate_got(const VersionTracker& version_tracker,
  80.                           const soinfo_list_t& global_group,
  81.                           const soinfo_list_t& local_group);
  82. #if !defined(__LP64__)//arm64不进入
  83.    bool mips_check_and_adjust_fp_modes();
  84. #endif
  85. #endif
  86.    size_t ref_count_;  //25
  87.   public:
  88.    link_map link_map_head; //26 27 28 29 30
  89.      // struct link_map {
  90.            //     ElfW(Addr) l_addr;
  91.            //     char* l_name;
  92.            //     ElfW(Dyn)* l_ld;
  93.            //     struct link_map* l_next;
  94.            //     struct link_map* l_prev;
  95.            // };
  96.    bool constructors_called;
  97.   
  98.    // When you read a virtual address from the ELF file, add this
  99.    // value to get the corresponding address in the process' address space.
  100.    ElfW(Addr) load_bias; //32
  101.   
  102. #if !defined(__LP64__)
  103.    bool has_text_relocations;
  104. #endif
  105.    bool has_DT_SYMBOLIC;
  106.   
  107.   public:
  108.    soinfo(android_namespace_t* ns, const char* name, const struct stat* file_stat,
  109.           off64_t file_offset, int rtld_flags);
  110.    ~soinfo();
  111.   
  112.    void call_constructors();
  113.    void call_destructors();
  114.    void call_pre_init_constructors();
  115.    bool prelink_image();
  116.    bool link_image(const soinfo_list_t& global_group, const soinfo_list_t& local_group,
  117.                    const android_dlextinfo* extinfo, size_t* relro_fd_offset);
  118.    bool protect_relro();
  119.   
  120.    void add_child(soinfo* child);
  121.    void remove_all_links();
  122.   
  123.    ino_t get_st_ino() const;
  124.    dev_t get_st_dev() const;
  125.    off64_t get_file_offset() const;
  126.   
  127.    uint32_t get_rtld_flags() const;
  128.    uint32_t get_dt_flags_1() const;
  129.    void set_dt_flags_1(uint32_t dt_flags_1);
  130.   
  131.    soinfo_list_t& get_children();
  132.    const soinfo_list_t& get_children() const;
  133.   
  134.    soinfo_list_t& get_parents();
  135.   
  136.    bool find_symbol_by_name(SymbolName& symbol_name,
  137.                             const version_info* vi,
  138.                             const ElfW(Sym)** symbol) const;
  139.   
  140.    ElfW(Sym)* find_symbol_by_address(const void* addr);
  141.    ElfW(Addr) resolve_symbol_address(const ElfW(Sym)* s) const;
  142.   
  143.    const char* get_string(ElfW(Word) index) const;
  144.    bool can_unload() const;
  145.    bool is_gnu_hash() const;
  146.   
  147.    bool inline has_min_version(uint32_t min_version __unused) const {
  148. #if defined(__work_around_b_24465209__)
  149.      return (flags_ & FLAG_NEW_SOINFO) != 0 && version_ >= min_version;
  150. #else
  151.      return true;
  152. #endif
  153.    }
  154.   
  155.    bool is_linked() const;
  156.    bool is_linker() const;
  157.    bool is_main_executable() const;
  158.   
  159.    void set_linked();
  160.    void set_linker_flag();
  161.    void set_main_executable();
  162.    void set_nodelete();
  163.   
  164.    size_t increment_ref_count();
  165.    size_t decrement_ref_count();
  166.    size_t get_ref_count() const;
  167.   
  168.    soinfo* get_local_group_root() const;
  169.   
  170.    void set_soname(const char* soname);
  171.    const char* get_soname() const;
  172.    const char* get_realpath() const;
  173.    const ElfW(Versym)* get_versym(size_t n) const;
  174.    ElfW(Addr) get_verneed_ptr() const;
  175.    size_t get_verneed_cnt() const;
  176.    ElfW(Addr) get_verdef_ptr() const;
  177.    size_t get_verdef_cnt() const;
  178.   
  179.    int get_target_sdk_version() const;
  180.   
  181.    void set_dt_runpath(const char *);
  182.    const std::vector<std::string>& get_dt_runpath() const;
  183.    android_namespace_t* get_primary_namespace();
  184.    void add_secondary_namespace(android_namespace_t* secondary_ns);
  185.    android_namespace_list_t& get_secondary_namespaces();
  186.   
  187.    soinfo_tls* get_tls() const;
  188.   
  189.    void set_mapped_by_caller(bool reserved_map);
  190.    bool is_mapped_by_caller() const;
  191.   
  192.    uintptr_t get_handle() const;
  193.    void generate_handle();
  194.    void* to_handle();
  195.   
  196.   private:
  197.    bool is_image_linked() const;
  198.    void set_image_linked();
  199.   
  200.    bool elf_lookup(SymbolName& symbol_name, const version_info* vi, uint32_t* symbol_index) const;
  201.    ElfW(Sym)* elf_addr_lookup(const void* addr);
  202.    bool gnu_lookup(SymbolName& symbol_name, const version_info* vi, uint32_t* symbol_index) const;
  203.    ElfW(Sym)* gnu_addr_lookup(const void* addr);
  204.   
  205.    bool lookup_version_info(const VersionTracker& version_tracker, ElfW(Word) sym,
  206.                             const char* sym_name, const version_info** vi);
  207.   
  208.    template<typename ElfRelIteratorT>
  209.    bool relocate(const VersionTracker& version_tracker, ElfRelIteratorT&& rel_iterator,
  210.                  const soinfo_list_t& global_group, const soinfo_list_t& local_group);
  211.    bool relocate_relr();
  212.    void apply_relr_reloc(ElfW(Addr) offset);
  213.   
  214.   private:
  215.    // This part of the structure is only available
  216.    // when FLAG_NEW_SOINFO is set in this->flags.
  217.    uint32_t version_;
  218.   
  219.    // version >= 0
  220.    dev_t st_dev_;
  221.    ino_t st_ino_;
  222.   
  223.    // dependency graph
  224.    soinfo_list_t children_;
  225.    soinfo_list_t parents_;
  226.   
  227.    // version >= 1
  228.    off64_t file_offset_;
  229.    uint32_t rtld_flags_;
  230.    uint32_t dt_flags_1_;
  231.    size_t strtab_size_;
  232.   
  233.    // version >= 2
  234.   
  235.    size_t gnu_nbucket_;
  236.    uint32_t* gnu_bucket_;
  237.    uint32_t* gnu_chain_;
  238.    uint32_t gnu_maskwords_;
  239.    uint32_t gnu_shift2_;
  240.    ElfW(Addr)* gnu_bloom_filter_;
  241.   
  242.    soinfo* local_group_root_;
  243.   
  244.    uint8_t* android_relocs_;
  245.    size_t android_relocs_size_;
  246.   
  247.    const char* soname_;
  248.    std::string realpath_;
  249.   
  250.    const ElfW(Versym)* versym_;
  251.   
  252.    ElfW(Addr) verdef_ptr_;
  253.    size_t verdef_cnt_;
  254.   
  255.    ElfW(Addr) verneed_ptr_;
  256.    size_t verneed_cnt_;
  257.   
  258.    int target_sdk_version_;
  259.   
  260.    // version >= 3
  261.    std::vector<std::string> dt_runpath_;
  262.    android_namespace_t* primary_namespace_;
  263.    android_namespace_list_t secondary_namespaces_;
  264.    uintptr_t handle_;
  265.   
  266.    friend soinfo* get_libdl_info(const char* linker_path, const soinfo& linker_si);
  267.   
  268.    // version >= 4
  269.    ElfW(Relr)* relr_;
  270.    size_t relr_count_;
  271.   
  272.    // version >= 5
  273.    std::unique_ptr<soinfo_tls> tls_;
  274.    std::vector<TlsDynamicResolverArg> tlsdesc_args_;
  275. }
复制代码
这里我已经标注了相关变量在内存中的指针索引。里面描述了so的基址和一些节表和大量的方法。当然这些不是我们这里的关注重点,我们只需要多关注以下的变量:


  1. phdr:程序头表(段表)
  2. base:   so基址
  3. size:   so大小
  4. dynamic: .dynamic节
  5. next: 下一个soinfo结构体
  6. strtab_: .dynstr节
  7. symtab_: .dynsym节
  8. plt_rela_: .real.plt节
  9. rela_:  .real.dyn节
  10. link_map_head: 存储有so的基址和名字(dl_iterate_phdr方法可以获取)
  11. load_bias: so基址
复制代码
2.hook现象
检测逻辑:
lib base mem crc:获取本地文件的可执行段的偏移地址,计算soinfo结构体中的base与可执行段偏移地址的和,得到内存中的可执行段地址,再取内存中可执行段数据和本地文件可执行段数据作比较。
lib func mem crc: 通过dl_iterate_phdr方法获取到linker map,取linker map中的dlpi_addr获取到so的基址,获取本地文件的可执行段的偏移地址,计算基址和偏移的和,通过再取内存中可执行段数据和本地文件可执行段数据作比较。
分别点击libc base mem crc按钮和libc func mem crc按钮,控制台输出如下
截图202503031633096110.png
3.绕过
针对上述的检测,我们可以把soinfo结构体中的base,和link_map_head指针指向我们在maps中映射的地址,达到绕过检测的目的。
  1. function hiddenSobaseInMem(so_path) {
  2.     //  /apex/com.android.runtime/lib64/bionic/libc.so
  3.     let mmap_addr = Module.findExportByName("libc.so", "mmap");
  4.     let mmapFunc = new NativeFunction(mmap_addr, 'pointer', ['pointer', 'int', 'int', 'int', 'int', 'int'])
  5.     const parts = so_path.split('/');
  6.     const so_name = parts.pop();
  7.     let soExecSegmentFromFile = findSoExecSegmentFromFile(so_path);
  8.     //获取maps中so的内存
  9.     let soRangeFromMaps = findSoRangeFromMaps(so_name);
  10.     let startAddress = soRangeFromMaps.base;
  11.     let size = soRangeFromMaps.size;
  12.     console.log(startAddress,size)
  13.     //创建匿名内存,存储so内存区域的内存
  14.     let new_addr = mmapFunc(ptr(-1), size, 7, 0x20 | 2, -1, 0);
  15.     console.log("创建的匿名内存起始地址:" + new_addr);
  16.     //把maps中so内存区域的内存复制到创建的匿名内存中去
  17.     Memory.copy(new_addr, startAddress, size);
  18.     console.log("复制完毕")
  19.     //把文件中的可执行段复制到匿名内存中去
  20.     Memory.copy(ptr(new_addr).add(soExecSegmentFromFile.p_offset), ptr(soExecSegmentFromFile.start), soExecSegmentFromFile.size);
  21.     console.log("真实节区复制成功")
  22.     //从linker中获取到soinfo结构链表
  23.     let solist = getSolist();
  24.     let num = 0;
  25.     let soinfo_next;
  26.     let realpath;
  27.     console.log("开始遍历")
  28.     do {
  29.         realpath = getRealpath(solist);//获取soinfo所属的名字
  30.         console.log(num + "-->" + realpath);
  31.         if (realpath.indexOf(so_name) !== -1) {
  32.             Memory.protect(ptr(solist).add(Process.pointerSize * 2), 4, "rw");
  33.             ptr(solist).add(Process.pointerSize * 2).writePointer(new_addr);//so base
  34.             Memory.protect(ptr(solist).add(Process.pointerSize * 26), 4, "rw");
  35.             ptr(solist).add(Process.pointerSize * 26).writePointer(new_addr);//linker map
  36.             //load_bias 没法直接修改 否则会因为找不到符号崩溃
  37.             break;
  38.         }
  39.         soinfo_next = ptr(solist).add(Process.pointerSize * 5).readPointer();//soinfo结构体
  40.         num++;
  41.         solist = soinfo_next;
  42.     } while (soinfo_next.toUInt32() !== 0);
  43.     console.log("遍历完成:")
  44.     console.log("内存中隐藏so首地址完成");
  45. }
复制代码
截图202503031634385040.png
注入上述代码后,再次点击这两个按钮查看控制台输出:
截图202503031635035344.png
此时环境也正常了
注:对base和load_bias进行修改,会有app崩溃的风险
4.libc section mem crc的绕过
这个我就简单说下检测及绕过思路:
检测:
获取到soinfo结构体中的节表地址(这里以strtab_变量作检测),再与本地文件或取到的节表偏移相见得到so的基地址,计算基地址与可执行段的偏移得到可执行段的地址,最后提取内存中可执行段数据和本地文件可执行段数据作比较。
绕过:
把soinfo结构体中的节表指针指向我们在maps中映射的地址,达到绕过检测的目的。
hook现象:
截图202503031635214211.png
截图202503031635345199.png
注入代码后再次点击按钮:
截图202503031636026508.png
总结
此次分享主要是提供个思路,仅供参考,包括libart.so也可以这样去弄,总之crc的大方向就是上述的两个,其次小方向的检测手段就是有很多细节去相互嵌套了。

frida绕过脚本和校验 demo apk.zip (2.6 MB, 下载次数: 7, 售价: 5 金钱)
这个人还没有签名
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

图文推荐
大神求
大神求
6 天前
热门排行