初探 android crc 检测及绕过

admin 2025年6月7日09:58:49评论2 views字数 12537阅读41分47秒阅读模式

1

前言

在开始之前先说下什么是crc检测,通俗点讲就是,把本地文件中的数据和内存中的数据进行crc计算得到的结果进行比较,来校验结果是否一致,不一致则判定数据被篡改。

举个例子:以libc.so为目标so,当我们第一次用frida以spwan的方式注入hook 时,未对libc.so的函数进行hook的话,app未退出,一旦我们对libc.so中的函数或指令进行了修改注入(不考虑inline hoook的因素影响),app便直接崩溃退出,这种情况基本就是检测到了数据被篡改,也就是crc检测。

基本的介绍到这里,下面开始对crc相关途径的检测进行分析,以及如何去绕过。

本次分析以libc.so为目标,以下的绕过都是用frida去处理。

2

分析环境

app是我总结的一部分crc检测,会把链接放置在结尾。

初探 android crc 检测及绕过

frida版本: 16.5.9
目标app: LinkerDemo
分析so: libc.so
ELF工具:010Editor
arm平台: arm64

3

分析及绕过

(一) 检测种类

目前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

初探 android crc 检测及绕过

获取可执行段表中的 p_offset(在文件中的偏移)和 p_filesz(在文件中的大小)。后续都是以这个为参照物与内存进行crc校验。

2.获取maps内存中libc.so的可执行段

初探 android crc 检测及绕过

(正常来说只有一行r-xp段,因为我使用了frida,所以会出现这种内存布局)

提取出里面带有x的内存段数据。

3.校验

最后通过相关算法计算出两种途径获取到的内存结果进行比较。

算法一般都是crc32,当然个例可能会使用其他的算法,比如md5,aes等等,很少见的。

4.hook现象

打开app,点击libc maps crc,可以在控制台看到如下输出:

初探 android crc 检测及绕过

此时我们的环境是正常的。接着我们用frida注入,对libc.so中的pthread_create方法进行hook,得到以下输出:

初探 android crc 检测及绕过

可以很明显的看出内存中可执行段的crc值与文件中可执行的不一致,并且检测出了环境是hook的。

5.绕过

针对上述的检测,我们可以在maps中模拟一段可执行段数据,并把libc.so原本的可执行段名称给抹去,变为匿名内存。最后app获取到的maps内存范围就是我们模拟的一段数据。

function hiddenSoExecSegmentInMaps(so_path) {
// /apex/com.android.runtime/lib64/bionic/libc.so
let mmap_addr = Module.findExportByName("libc.so", "mmap");
let mmapFunc = new NativeFunction(mmap_addr, 'pointer', ['pointer', 'int', 'int', 'int', 'int', 'int'])

let munmap_addr = Module.findExportByName("libc.so", "munmap");
let munmapFunc = new NativeFunction(munmap_addr, 'int', ['pointer', 'int'])

let mremap_addr = Module.findExportByName("libc.so", "mremap");
let mremapFunc = new NativeFunction(mremap_addr, 'pointer', ['pointer', 'int64', 'int64', 'int64', 'pointer'])

let open_addr = Module.findExportByName("libc.so", "open");
var openFunc = new NativeFunction(open_addr, 'int', ['pointer', 'int']);

let memset_addr = Module.findExportByName("libc.so", "memset");
var memsetFunc = new NativeFunction(memset_addr, 'pointer', ['pointer', 'int', 'int'])

let close_addr = Module.findExportByName("libc.so", "close");
var closeFunc = new NativeFunction(close_addr, 'int', ['int'])

const parts = so_path.split('/');
const so_name = parts.pop();
let soExecSegmentRangeFromMaps = findSoExecSegmentRangeFromMaps(so_name);
let startAddress = soExecSegmentRangeFromMaps.base;
let size = soExecSegmentRangeFromMaps.size;

if (startAddress === 0 || size === 0) {
console.log("可执行段未找到:", startAddress, size)
return;
}

let soExecSegmentFromFile = findSoExecSegmentFromFile(so_path);

//创建匿名内存,临时存储so可执行段内存
let new_addr = mmapFunc(ptr(-1), size, 7, 0x20 | 2, -1, 0);//0x20:匿名内存标识符(MAP_ANONYMOUS), 2:私有(MAP_PRIVATE)
console.log("创建的可执行段匿名内存起始地址:" + new_addr);

//把so可执行段内存复制到创建的匿名内存中去
Memory.copy(new_addr, startAddress, size);
console.log("复制完毕")

//调整so,使传入的so可执行段内存变成匿名内存
let ret = mremapFunc(new_addr, size, size, 1 | 2, startAddress);
if (ret === -1) {
console.log("mremap 调整失败")
return;
}
console.log("匿名目标so可执行段完成 ret:" + ret)

// 打开需要模拟的文件路径,用于后续在maps中生成指定名称的内存区域
let moniter_path = so_path;
let moniter_path_addr = Memory.allocUtf8String(moniter_path);
var fd = openFunc(moniter_path_addr, 0);
if (fd === -1) {
console.log("open " + moniter_path + " is error")
return -1;
}

//在maps中创建传入so路径名称的内存区域
let target_addr = mmapFunc(ptr(-1), size, 7, 2, fd, 0);
console.log("模拟的可执行段内存起始地址:" + target_addr);

closeFunc(fd)

//给创建的so内存区域全部置0
memsetFunc(target_addr, 0, size);

//把so文件中获取的可执行段内存复制到创建的so名称的内存区域中
Memory.copy(target_addr, soExecSegmentFromFile.start, soExecSegmentFromFile.size)
Memory.protect(target_addr, size, "r-x");

//卸载映射的匿名内存
// munmapFunc(new_addr, size);
console.log("maps中隐藏可执行段完成")

}

初探 android crc 检测及绕过

注入上述代码后再次点击按钮看控制台输出:

初探 android crc 检测及绕过

可以看到两种方式获取到的值一致了,环境也是安全的了。

这里面主要使用到了mmap在maps中映射一段名称为libc.so的数据,用mremap把原本的可执行段数据给设置为匿名内存。

初探 android crc 检测及绕过
初探 android crc 检测及绕过

这里就可以看到maps中libc.so的可执行段已设置为匿名内存,并且map中也有我们模拟的可执行段内存。

(三)本地文件与Linker获取的内存校验

本地文件获取的方式不再赘述了,直接看如何从Linker中获取内存。

1.获取libc.so soinfo结构体

linker作为so加载器,里面存放了所有已经加载的so,并把这些已经加载的so会依次存放进solist变量中,solist存储了所有so的soinfo结构体,它是一个soinfo结构体数组,我们可以从solist中获取到自己想要的so。

那么如何获取到libc.so的结构体呢?

带着这个疑问我们先了解下soinfo的相关结构组成。

 struct soinfo {
#if defined(__work_around_b_24465209__)
private:
char old_name_[SOINFO_NAME_LEN];
#endif
public:
const ElfW(Phdr)* phdr; //0
size_t phnum;//1
#if defined(__work_around_b_24465209__)
ElfW(Addr) unused0; // DO NOT USE, maintained for compatibility.
#endif
ElfW(Addr) base;//2
size_t size;//3

#if defined(__work_around_b_24465209__)
uint32_t unused1; // DO NOT USE, maintained for compatibility.
#endif

ElfW(Dyn)* dynamic;//4

#if defined(__work_around_b_24465209__)
uint32_t unused2; // DO NOT USE, maintained for compatibility
uint32_t unused3; // DO NOT USE, maintained for compatibility
#endif

soinfo* next;//5
private:
uint32_t flags_;//6

const char* strtab_;//7 .dynstr
ElfW(Sym)* symtab_;//8.dynsym

size_t nbucket_;
size_t nchain_;
uint32_t* bucket_; //11
uint32_t* chain_; //12

#if defined(__mips__) || !defined(__LP64__)
// This is only used by mips and mips64, but needs to be here for
// all 32-bit architectures to preserve binary compatibility.
ElfW(Addr)** plt_got_;//arm64未被使用
#endif

#if defined(USE_RELA)
ElfW(Rela)* plt_rela_; //.real.plt13
size_t plt_rela_count_;

ElfW(Rela)* rela_;//15 //real.dyn
size_t rela_count_; //16
#else
ElfW(Rel)* plt_rel_;
size_t plt_rel_count_;

ElfW(Rel)* rel_;
size_t rel_count_;
#endif

linker_ctor_function_t* preinit_array_;//空 17
size_t preinit_array_count_;//空18

linker_ctor_function_t* init_array_;
size_t init_array_count_;
linker_dtor_function_t* fini_array_;
size_t fini_array_count_;

linker_ctor_function_t init_func_; //空 23
linker_dtor_function_t fini_func_;//空 24

#if defined(__arm__)//arm64不进入
public:
// ARM EABI section used for stack unwinding.
uint32_t* ARM_exidx;
size_t ARM_exidx_count;
private:
#elif defined(__mips__)//arm64不进入
uint32_t mips_symtabno_;
uint32_t mips_local_gotno_;
uint32_t mips_gotsym_;
bool mips_relocate_got(const VersionTracker& version_tracker,
const soinfo_list_t& global_group,
const soinfo_list_t& local_group)
;
#if !defined(__LP64__)//arm64不进入
bool mips_check_and_adjust_fp_modes();
#endif
#endif
size_t ref_count_;//25
public:
link_map link_map_head;//26 27 28 29 30
// struct link_map {
// ElfW(Addr) l_addr;
// char* l_name;
// ElfW(Dyn)* l_ld;
// struct link_map* l_next;
// struct link_map* l_prev;
// };
bool constructors_called;

// When you read a virtual address from the ELF file, add this
// value to get the corresponding address in the process' address space.
ElfW(Addr) load_bias; //32

#if !defined(__LP64__)
bool has_text_relocations;
#endif
bool has_DT_SYMBOLIC;

public:
soinfo(android_namespace_t* ns, const char* name, const struct stat* file_stat,
off64_t file_offset, int rtld_flags);
~soinfo();

void call_constructors();
void call_destructors();
void call_pre_init_constructors();
bool prelink_image();
bool link_image(const soinfo_list_t& global_group, const soinfo_list_t& local_group,
const android_dlextinfo* extinfo, size_t* relro_fd_offset)
;
bool protect_relro();

void add_child(soinfo* child);
void remove_all_links();

ino_t get_st_ino() const;
dev_t get_st_dev() const;
off64_t get_file_offset() const;

uint32_t get_rtld_flags() const;
uint32_t get_dt_flags_1() const;
void set_dt_flags_1(uint32_t dt_flags_1);

soinfo_list_t& get_children();
const soinfo_list_t& get_children() const;

soinfo_list_t& get_parents();

bool find_symbol_by_name(SymbolName& symbol_name,
const version_info* vi,
const ElfW(Sym)** symbol)
const;

ElfW(Sym)* find_symbol_by_address(const void* addr);
ElfW(Addr) resolve_symbol_address(const ElfW(Sym)* s) const;

const char* get_string(ElfW(Word) index) const;
bool can_unload() const;
bool is_gnu_hash() const;

bool inline has_min_version(uint32_t min_version __unused) const {
#if defined(__work_around_b_24465209__)
return (flags_ & FLAG_NEW_SOINFO) != 0 && version_ >= min_version;
#else
return true;
#endif
}

bool is_linked() const;
bool is_linker() const;
bool is_main_executable() const;

void set_linked();
void set_linker_flag();
void set_main_executable();
void set_nodelete();

size_t increment_ref_count();
size_t decrement_ref_count();
size_t get_ref_count() const;

soinfo* get_local_group_root() const;

void set_soname(const char* soname);
const char* get_soname() const;
const char* get_realpath() const;
const ElfW(Versym)* get_versym(size_t n) const;
ElfW(Addr) get_verneed_ptr() const;
size_t get_verneed_cnt() const;
ElfW(Addr) get_verdef_ptr() const;
size_t get_verdef_cnt() const;

int get_target_sdk_version() const;

void set_dt_runpath(const char *);
const std::vector<std::string>& get_dt_runpath() const;
android_namespace_t* get_primary_namespace();
void add_secondary_namespace(android_namespace_t* secondary_ns);
android_namespace_list_t& get_secondary_namespaces();

soinfo_tls* get_tls() const;

void set_mapped_by_caller(bool reserved_map);
bool is_mapped_by_caller() const;

uintptr_t get_handle() const;
void generate_handle();
void* to_handle();

private:
bool is_image_linked() const;
void set_image_linked();

bool elf_lookup(SymbolName& symbol_name, const version_info* vi, uint32_t* symbol_index) const;
ElfW(Sym)* elf_addr_lookup(const void* addr);
bool gnu_lookup(SymbolName& symbol_name, const version_info* vi, uint32_t* symbol_index) const;
ElfW(Sym)* gnu_addr_lookup(const void* addr);

bool lookup_version_info(const VersionTracker& version_tracker, ElfW(Word) sym,
const char* sym_name, const version_info** vi)
;

template<typename ElfRelIteratorT>
bool relocate(const VersionTracker& version_tracker, ElfRelIteratorT&& rel_iterator,
const soinfo_list_t& global_group, const soinfo_list_t& local_group)
;
bool relocate_relr();
void apply_relr_reloc(ElfW(Addr) offset);

private:
// This part of the structure is only available
// when FLAG_NEW_SOINFO is set in this->flags.
uint32_t version_;

// version >= 0
dev_t st_dev_;
ino_t st_ino_;

// dependency graph
soinfo_list_t children_;
soinfo_list_t parents_;

// version >= 1
off64_t file_offset_;
uint32_t rtld_flags_;
uint32_t dt_flags_1_;
size_t strtab_size_;

// version >= 2

size_t gnu_nbucket_;
uint32_t* gnu_bucket_;
uint32_t* gnu_chain_;
uint32_t gnu_maskwords_;
uint32_t gnu_shift2_;
ElfW(Addr)* gnu_bloom_filter_;

soinfo* local_group_root_;

uint8_t* android_relocs_;
size_t android_relocs_size_;

const char* soname_;
std::string realpath_;

const ElfW(Versym)* versym_;

ElfW(Addr) verdef_ptr_;
size_t verdef_cnt_;

ElfW(Addr) verneed_ptr_;
size_t verneed_cnt_;

int target_sdk_version_;

// version >= 3
std::vector<std::string> dt_runpath_;
android_namespace_t* primary_namespace_;
android_namespace_list_t secondary_namespaces_;
uintptr_t handle_;

friend soinfo* get_libdl_info(const char* linker_path, const soinfo& linker_si);

// version >= 4
ElfW(Relr)* relr_;
size_t relr_count_;

// version >= 5
std::unique_ptr<soinfo_tls> tls_;
std::vector<TlsDynamicResolverArg> tlsdesc_args_;
}

这里我已经标注了相关变量在内存中的指针索引。里面描述了so的基址和一些节表和大量的方法。当然这些不是我们这里的关注重点,我们只需要多关注以下的变量:

phdr:程序头表(段表)
base:so基址
size:so大小
dynamic: .dynamic节
next:下一个soinfo结构体
strtab_:.dynstr节
symtab_: .dynsym节
plt_rela_: .real.plt节
rela_:.real.dyn节
link_map_head: 存储有so的基址和名字(dl_iterate_phdr方法可以获取)
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按钮,控制台输出如下。

初探 android crc 检测及绕过

3.绕过

针对上述的检测,我们可以把soinfo结构体中的base,和link_map_head指针指向我们在maps中映射的地址,达到绕过检测的目的。

function hiddenSobaseInMem(so_path) {
// /apex/com.android.runtime/lib64/bionic/libc.so
let mmap_addr = Module.findExportByName("libc.so", "mmap");
let mmapFunc = new NativeFunction(mmap_addr, 'pointer', ['pointer', 'int', 'int', 'int', 'int', 'int'])

const parts = so_path.split('/');
const so_name = parts.pop();

let soExecSegmentFromFile = findSoExecSegmentFromFile(so_path);

//获取maps中so的内存
let soRangeFromMaps = findSoRangeFromMaps(so_name);
let startAddress = soRangeFromMaps.base;
let size = soRangeFromMaps.size;
console.log(startAddress,size)
//创建匿名内存,存储so内存区域的内存
let new_addr = mmapFunc(ptr(-1), size, 7, 0x20 | 2, -1, 0);
console.log("创建的匿名内存起始地址:" + new_addr);

//把maps中so内存区域的内存复制到创建的匿名内存中去
Memory.copy(new_addr, startAddress, size);
console.log("复制完毕")

//把文件中的可执行段复制到匿名内存中去
Memory.copy(ptr(new_addr).add(soExecSegmentFromFile.p_offset), ptr(soExecSegmentFromFile.start), soExecSegmentFromFile.size);
console.log("真实节区复制成功")

//从linker中获取到soinfo结构链表
let solist = getSolist();

let num = 0;
let soinfo_next;
let realpath;

console.log("开始遍历")
do {
realpath = getRealpath(solist);//获取soinfo所属的名字
console.log(num + "-->" + realpath);

if (realpath.indexOf(so_name) !== -1) {

Memory.protect(ptr(solist).add(Process.pointerSize * 2), 4, "rw");
ptr(solist).add(Process.pointerSize * 2).writePointer(new_addr);//so base

Memory.protect(ptr(solist).add(Process.pointerSize * 26), 4, "rw");
ptr(solist).add(Process.pointerSize * 26).writePointer(new_addr);//linker map

//load_bias 没法直接修改 否则会因为找不到符号崩溃
break;
}
soinfo_next = ptr(solist).add(Process.pointerSize * 5).readPointer();//soinfo结构体
num++;
solist = soinfo_next;
} while (soinfo_next.toUInt32() !== 0);
console.log("遍历完成:")
console.log("内存中隐藏so首地址完成");
}

初探 android crc 检测及绕过

注入上述代码后,再次点击这两个按钮查看控制台输出:

初探 android crc 检测及绕过

此时环境也正常了

注:对base和load_bias进行修改,会有app崩溃的风险

4.libc section mem crc的绕过

这个我就简单说下检测及绕过思路。

检测:

获取到soinfo结构体中的节表地址(这里以strtab_变量作检测),再与本地文件或取到的节表偏移相见得到so的基地址,计算基地址与可执行段的偏移得到可执行段的地址,最后提取内存中可执行段数据和本地文件可执行段数据作比较。

绕过:

把soinfo结构体中的节表指针指向我们在maps中映射的地址,达到绕过检测的目的。

hook现象:

初探 android crc 检测及绕过
初探 android crc 检测及绕过

注入代码后再次点击按钮:

初探 android crc 检测及绕过

此次分享主要是提供个思路仅供参考,包括libart.so也可以这样去弄,总之crc的大方向就是上述的两个,其次小方向的检测手段就是有很多细节去相互嵌套了。

初探 android crc 检测及绕过

看雪ID:九天666

https://bbs.kanxue.com/user-home-947335.htm

*本文为看雪论坛优秀文章,由 九天666 原创,转载请注明来自看雪社区

原文始发于微信公众号(看雪学苑):初探 android crc 检测及绕过

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月7日09:58:49
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   初探 android crc 检测及绕过https://cn-sec.com/archives/3806621.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息