在开始之前先说下什么是crc检测,通俗点讲就是,把本地文件中的数据和内存中的数据进行crc计算得到的结果进行比较,来校验结果是否一致,不一致则判定数据被篡改。
举个例子:以libc.so为目标so,当我们第一次用frida以spwan的方式注入hook 时,未对libc.so的函数进行hook的话,app未退出,一旦我们对libc.so中的函数或指令进行了修改注入(不考虑inline hoook的因素影响),app便直接崩溃退出,这种情况基本就是检测到了数据被篡改,也就是crc检测。
基本的介绍到这里,下面开始对crc相关途径的检测进行分析,以及如何去绕过。
本次分析以libc.so为目标,以下的绕过都是用frida去处理。
app是我总结的一部分crc检测,会把链接放置在结尾。
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
获取可执行段表中的 p_offset(在文件中的偏移)和 p_filesz(在文件中的大小)。后续都是以这个为参照物与内存进行crc校验。
2.获取maps内存中libc.so的可执行段
(正常来说只有一行r-xp段,因为我使用了frida,所以会出现这种内存布局)
提取出里面带有x的内存段数据。
3.校验
最后通过相关算法计算出两种途径获取到的内存结果进行比较。
算法一般都是crc32,当然个例可能会使用其他的算法,比如md5,aes等等,很少见的。
4.hook现象
打开app,点击libc maps crc,可以在控制台看到如下输出:
此时我们的环境是正常的。接着我们用frida注入,对libc.so中的pthread_create方法进行hook,得到以下输出:
可以很明显的看出内存中可执行段的crc值与文件中可执行的不一致,并且检测出了环境是hook的。
5.绕过
针对上述的检测,我们可以在maps中模拟一段可执行段数据,并把libc.so原本的可执行段名称给抹去,变为匿名内存。最后app获取到的maps内存范围就是我们模拟的一段数据。
function hiddenSoExecSegmentInMaps(so_path) {
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);
let new_addr = mmapFunc(ptr(-1), size, 7, 0x20 | 2, -1, 0);
console.log("创建的可执行段匿名内存起始地址:" + new_addr);
Memory.copy(new_addr, startAddress, size);
console.log("复制完毕")
let ret = mremapFunc(new_addr, size, size, 1 | 2, startAddress);
if (ret === -1) {
console.log("mremap 调整失败")
return;
}
console.log("匿名目标so可执行段完成 ret:" + ret)
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;
}
let target_addr = mmapFunc(ptr(-1), size, 7, 2, fd, 0);
console.log("模拟的可执行段内存起始地址:" + target_addr);
closeFunc(fd)
memsetFunc(target_addr, 0, size);
Memory.copy(target_addr, soExecSegmentFromFile.start, soExecSegmentFromFile.size)
Memory.protect(target_addr, size, "r-x");
console.log("maps中隐藏可执行段完成")
}
注入上述代码后再次点击按钮看控制台输出:
可以看到两种方式获取到的值一致了,环境也是安全的了。
这里面主要使用到了mmap在maps中映射一段名称为libc.so的数据,用mremap把原本的可执行段数据给设置为匿名内存。
这里就可以看到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;
size_t phnum;
#if defined(__work_around_b_24465209__)
ElfW(Addr) unused0;
#endif
ElfW(Addr) base;
size_t size;
#if defined(__work_around_b_24465209__)
uint32_t unused1;
#endif
ElfW(Dyn)* dynamic;
#if defined(__work_around_b_24465209__)
uint32_t unused2;
uint32_t unused3;
#endif
soinfo* next;
private:
uint32_t flags_;
const char* strtab_;
ElfW(Sym)* symtab_;
size_t nbucket_;
size_t nchain_;
uint32_t* bucket_;
uint32_t* chain_;
#if defined(__mips__) || !defined(__LP64__)
ElfW(Addr)** plt_got_;
#endif
#if defined(USE_RELA)
ElfW(Rela)* plt_rela_;
size_t plt_rela_count_;
ElfW(Rela)* rela_;
size_t rela_count_;
#else
ElfW(Rel)* plt_rel_;
size_t plt_rel_count_;
ElfW(Rel)* rel_;
size_t rel_count_;
#endif
linker_ctor_function_t* preinit_array_;
size_t preinit_array_count_;
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_;
linker_dtor_function_t fini_func_;
#if defined(__arm__)
public:
uint32_t* ARM_exidx;
size_t ARM_exidx_count;
private:
#elif defined(__mips__)
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__)
bool mips_check_and_adjust_fp_modes();
#endif
#endif
size_t ref_count_;
public:
link_map link_map_head;
bool constructors_called;
ElfW(Addr) load_bias;
#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<: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:
uint32_t version_;
dev_t st_dev_;
ino_t st_ino_;
soinfo_list_t children_;
soinfo_list_t parents_;
off64_t file_offset_;
uint32_t rtld_flags_;
uint32_t dt_flags_1_;
size_t strtab_size_;
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_;
std::vector<: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);
ElfW(Relr)* relr_;
size_t relr_count_;
std::unique_ptr tls_;
std::vector 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按钮,控制台输出如下。
3.绕过
针对上述的检测,我们可以把soinfo结构体中的base,和link_map_head指针指向我们在maps中映射的地址,达到绕过检测的目的。
function hiddenSobaseInMem(so_path) {
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);
let soRangeFromMaps = findSoRangeFromMaps(so_name);
let startAddress = soRangeFromMaps.base;
let size = soRangeFromMaps.size;
console.log(startAddress,size)
let new_addr = mmapFunc(ptr(-1), size, 7, 0x20 | 2, -1, 0);
console.log("创建的匿名内存起始地址:" + new_addr);
Memory.copy(new_addr, startAddress, size);
console.log("复制完毕")
Memory.copy(ptr(new_addr).add(soExecSegmentFromFile.p_offset), ptr(soExecSegmentFromFile.start), soExecSegmentFromFile.size);
console.log("真实节区复制成功")
let solist = getSolist();
let num = 0;
let soinfo_next;
let realpath;
console.log("开始遍历")
do {
realpath = getRealpath(solist);
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);
Memory.protect(ptr(solist).add(Process.pointerSize * 26), 4, "rw");
ptr(solist).add(Process.pointerSize * 26).writePointer(new_addr);
break;
}
soinfo_next = ptr(solist).add(Process.pointerSize * 5).readPointer();
num++;
solist = soinfo_next;
} while (soinfo_next.toUInt32() !== 0);
console.log("遍历完成:")
console.log("内存中隐藏so首地址完成");
}
注入上述代码后,再次点击这两个按钮查看控制台输出:
此时环境也正常了
注:对base和load_bias进行修改,会有app崩溃的风险
4.libc section mem crc的绕过
这个我就简单说下检测及绕过思路。
检测:
获取到soinfo结构体中的节表地址(这里以strtab_变量作检测),再与本地文件或取到的节表偏移相见得到so的基地址,计算基地址与可执行段的偏移得到可执行段的地址,最后提取内存中可执行段数据和本地文件可执行段数据作比较。
绕过:
把soinfo结构体中的节表指针指向我们在maps中映射的地址,达到绕过检测的目的。
hook现象: