安卓中的 ASLR 实现与关闭
什么是 ASLR
由 Linux 内核在 2001 年引入的一项内核补丁技术,旨在为防止堆栈指针泄漏引发的重定向攻击。通过 ASLR(Address Space Layout Randomization)随机化内存布局,使得指针偏移难以预测,从而防范攻击。
在最新的 Android 版本中,ASLR 已经作为系统安全的一部分被强制开启。并且不可关闭。各版本 ASLR 对比:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
默认开启 |
|
|
|
|
|
|
|
|
|
|
|
强制 PIE |
|
|
|
|
|
ASLR 的实现
ASLR 的实现在安卓中共分为两部分,分别是内核态与用户态。
内核态
ASLR 的开启与关闭由进程的 personality 属性或全局变量 randomize_va_space 控制。当一个进程 execve(init) 启动时,在内核中经由以下链条:
sys_execve -> do_execve -> do_execveat_common -> bprm_execve -> exec_binpr -> search_binary_handler -> load_binary -> load_elf_binary
在 load_elf_binary 装载 ELF 的过程中会设置 PF_RANDOMIZE 标志。
static int load_elf_binary(struct linux_binprm *bprm){ // ...... const int snapshot_randomize_va_space = READ_ONCE(randomize_va_space); if (!(current->personality & ADDR_NO_RANDOMIZE) && snapshot_randomize_va_space) current->flags |= PF_RANDOMIZE; setup_new_exec(bprm); retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack); // ......}
一旦 PF_RANDOMIZE 标志设置成功,后续的二进制程序、共享库、堆、栈内存都将开启随机化。对已经建立完成的内存布局,后续再修改标志也不会重排现有布局。
void setup_new_exec(struct linux_binprm * bprm){ /* Setup things that can depend upon the personality */struct task_struct *me = current; arch_pick_mmap_layout(me->mm, &bprm->rlim_stack); // ......}void arch_pick_mmap_layout(struct mm_struct *mm, const struct rlimit *rlim_stack){ unsigned long random_factor = 0UL; if (current->flags & PF_RANDOMIZE) random_factor = arch_mmap_rnd(); // ......}unsigned long arch_mmap_rnd(void){ return (get_random_u32() & MMAP_RND_MASK) << PAGE_SHIFT;}int setup_arg_pages(struct linux_binprm *bprm, unsigned long stack_top, int executable_stack){ // ...... if (current->flags & PF_RANDOMIZE) stack_base += (STACK_RND_MASK << PAGE_SHIFT); /* Make sure we didn't let the argument array grow too large. */ if (vma->vm_end - vma->vm_start > stack_base) return -ENOMEM; stack_base = PAGE_ALIGN(stack_top - stack_base); stack_shift = vma->vm_start - stack_base; mm->arg_start = bprm->p - stack_shift; bprm->p = vma->vm_end - stack_shift; // ......}
用户态
用户态的 ASLR 是在 Linker 中强制开启的,这是安卓独有的特性。当共享库(so)被 Linker 加载时,在 find_libraries 阶段会将自身以及依赖的库加载顺序随机打乱。
bool find_libraries(android_namespace_t* ns, soinfo* start_with, const char* const library_names[], size_t library_names_count, soinfo* soinfos[], std::vector<soinfo*>* ld_preloads, size_t ld_preloads_count, int rtld_flags, const android_dlextinfo* extinfo, bool add_as_children, bool search_linked_namespaces){ // ...... // Step 2: Load libraries in random order (see b/24047022) LoadTaskList load_list; for (auto&& task : load_tasks) { soinfo* si = task->get_soinfo(); auto pred = [&](const LoadTask* t) { return t->get_soinfo() == si; }; if (!si->is_linked() && std::find_if(load_list.begin(), load_list.end(), pred) == load_list.end() ) { load_list.push_back(task); } } shuffle(&load_list); // ...... return linked;}static void shuffle(std::vector<LoadTask*>* v){ for (size_t i = 0, size = v->size(); i < size; ++i) { size_t n = size - i; size_t r = arc4random_uniform(n); std::swap((*v)[n-1], (*v)[r]); }}
同样在 ReserveAddressSpace 阶段也加入了 ASLR。
bool ElfReader::ReserveAddressSpace(address_space_params* address_space){ // ...... if (load_size_ > address_space->reserved_size) { if (address_space->must_use_address) { DL_ERR("reserved address space %zd smaller than %zd bytes needed for \"%s\"", load_size_ - address_space->reserved_size, load_size_, name_.c_str()); return false; } start = ReserveAligned(load_size_, kLibraryAlignment); } load_start_ = start; load_bias_ = reinterpret_cast<uint8_t*>(start) - addr; return true;}//申请一块大内存,随机选择任意区间做为起始点。static void* ReserveAligned(size_t size, size_t align){ // ...... size_t mmap_size = align_up(size, align) + align - PAGE_SIZE; uint8_t* mmap_ptr = reinterpret_cast<uint8_t*>(mmap(nullptr, mmap_size, PROT_NONE, mmap_flags, -1, 0)); if (mmap_ptr == MAP_FAILED) { return nullptr; } uint8_t* first = align_up(mmap_ptr, align); uint8_t* last = align_down(mmap_ptr + mmap_size, align) - size; size_t n = is_first_stage_init() ? 0 : arc4random_uniform((last - first) / PAGE_SIZE + 1); uint8_t* start = first + n * PAGE_SIZE; munmap(mmap_ptr, start - mmap_ptr); munmap(start + size, mmap_ptr + mmap_size - (start + size)); return start;}
关闭 ASLR
在安卓中要做到完全关闭 ASLR,需要将内核态与用户态两层限制全部绕过。
内核态
在常规的系统中我们可以直接将 /proc/sys/kernel/randomize_va_space 设置为 0 即可关闭内核态的 ASLR。但是因为安卓 APP 的启动都是由 Zygote 进程孵化而来, 而 Zygote 进程是由 Init 在系统启动较早期启动。因此,这个方法在安卓中失效了。
通过上面的内核实现分析,我们已经得知进程的 PF_RANDOMIZE 标志一旦被设置,ASLR 将不可关闭。因此,我们只能在 Zygote 启动之前修改,才能实现内核态 ASLR 的关闭。
如此,我们可以借助 Apatch 提供的 post-fs-data 时机来运行脚本,完成对 randomize_va_space 的修改。
#!/system/bin/shsysctl -w kernel.randomize_va_space=0 2>/dev/nullresetprop -n ro.kernel.randomize_va_space 0echo 0 > /proc/sys/kernel/randomize_va_space
不过实测之后,这段脚本并没有生效。经过一番排查,在 Init 进程的启动过程中会将 randomize_va_space 重置为 2。

因此,我们必须在 Init 进程启动的过程中将 randomize_va_space 改回来。
通过上面的内核分析,我们知道 PF_RANDOMIZE 标志是在 load_elf_binary 函数中设置的。
这样就为我们提供了一个完美的 HOOK 时机,我们可以在 load_elf_binary 函数进入之前将 randomize_va_space 改为 0 。
KPM_NAME("kpm-aslr");KPM_VERSION("1.0.0");KPM_LICENSE("GPL v2");KPM_AUTHOR("lidongyooo");KPM_DESCRIPTION("KernelPatch Module ASLR Hook Example");static unsigned long load_elf_binary_addr;static int load_elf_binary_hooked;static int *randomize_va_space_addr;static void before_load_elf_binary(hook_fargs1_t *args, void *udata){ if (!args) return; if (randomize_va_space_addr != NULL) { *randomize_va_space_addr = 0; } logke("load_elf_binary enter current=%s bprm=%llx randomize_va_space=%d", get_task_comm(current), (unsigned long long)args->arg0, *randomize_va_space_addr);}static void after_load_elf_binary(hook_fargs1_t *args, void *udata){}static long inline_hook_demo_init(const char *args, const char *event, void *__user reserved){ logke("kpm aslr init\n"); randomize_va_space_addr = (int*)kallsyms_lookup_name("randomize_va_space"); load_elf_binary_addr = kallsyms_lookup_name("load_elf_binary"); if (!load_elf_binary_addr) { logke("load_elf_binary not found\n"); return -ENOENT; } hook_err_t err = hook_wrap1((void *)load_elf_binary_addr, before_load_elf_binary, after_load_elf_binary, 0); logke("hook err: %d\n", err); if (err != HOOK_NO_ERR) return err; load_elf_binary_hooked = 1; return 0;}static long inline_hook_control0(const char *args, char *__user out_msg, int outlen){ pr_info("kpm control, args: %s\n", args); return 0;}static long inline_hook_demo_exit(void *__user reserved){ if (load_elf_binary_hooked) { hook_unwrap((void *)load_elf_binary_addr, before_load_elf_binary, after_load_elf_binary); load_elf_binary_hooked = 0; } logke("kpm aslr exit\n"); return 0;}KPM_INIT(inline_hook_demo_init);KPM_CTL0(inline_hook_control0);KPM_EXIT(inline_hook_demo_exit);
将这段编译为 KPM 模块,刷进 boot.img。再次启动时将会发现内存段的高位地址已经被固定。
redfin:/ # ldd /bin/sh linux-vdso.so.1 => [vdso] (0x7ff7eae000) libc.so => /apex/com.android.runtime/lib64/bionic/libc.so (0x7ff504a000) libdl.so => /apex/com.android.runtime/lib64/bionic/libdl.so (0x7ff6b0b000)redfin:/ # ldd /bin/sh linux-vdso.so.1 => [vdso] (0x7ff7eae000) libc.so => /apex/com.android.runtime/lib64/bionic/libc.so (0x7ff56a0000) libdl.so => /apex/com.android.runtime/lib64/bionic/libdl.so (0x7ff6b29000)
用户态
在上面的 Linker 源码分析中,我们知道 find_libraries、ReserveAddressSpace 都使用了 arc4random_uniform 作为随机因子。
因此,我们可以 HOOK arc4random_uniform 让其恒为 0,即可关闭 ASLR。
let linker = Process.findModuleByName('linker64')let __dl_arc4random_uniform_addr = linker.findSymbolByName('__dl_arc4random_uniform')Interceptor.replace(__dl_arc4random_uniform_addr, new NativeCallback(function (range) { return 0}, 'int', ['int']))let modules = Process.enumerateModules()for (const module of modules) { console.log(JSON.stringify(module))}
测试
//第一次启动{"name":"base.odex","version":null,"base":"0x7bec200000","size":329334784,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/oat/arm64/base.odex"}{"name":"split_df_camera.odex","version":null,"base":"0x7cf9680000","size":24576,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/oat/arm64/split_df_camera.odex"}{"name":"split_df_im_bootfinish.odex","version":null,"base":"0x7c92e00000","size":1454080,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/oat/arm64/split_df_im_bootfinish.odex"}{"name":"split_df_kids_mode.odex","version":null,"base":"0x7cf9640000","size":24576,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/oat/arm64/split_df_kids_mode.odex"}{"name":"split_df_location.odex","version":null,"base":"0x7cf9600000","size":114688,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/oat/arm64/split_df_location.odex"}{"name":"libnpth_unwind.so","version":null,"base":"0x7c8d2c0000","size":270336,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/lib/arm64/libnpth_unwind.so"}{"name":"libnpth_dl.so","version":null,"base":"0x7cf94c0000","size":61440,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/lib/arm64/libnpth_dl.so"}//第二次启动{"name":"base.odex","version":null,"base":"0x7bec200000","size":329334784,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/oat/arm64/base.odex"}{"name":"split_df_camera.odex","version":null,"base":"0x7cf9680000","size":24576,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/oat/arm64/split_df_camera.odex"}{"name":"split_df_im_bootfinish.odex","version":null,"base":"0x7c92e00000","size":1454080,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/oat/arm64/split_df_im_bootfinish.odex"}{"name":"split_df_kids_mode.odex","version":null,"base":"0x7cf9640000","size":24576,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/oat/arm64/split_df_kids_mode.odex"}{"name":"split_df_location.odex","version":null,"base":"0x7cf9600000","size":114688,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/oat/arm64/split_df_location.odex"}{"name":"libnpth_unwind.so","version":null,"base":"0x7c8d2c0000","size":270336,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/lib/arm64/libnpth_unwind.so"}{"name":"libnpth_dl.so","version":null,"base":"0x7cf94c0000","size":61440,"path":"/data/app/~~_l2RJrouzXlrlwu9L_rZbQ==/com.example.app-3IT61D6SNh6GsSUiKRg5SA==/lib/arm64/libnpth_dl.so"}
可以发现,两次启动加载的类库地址完全一致。
夜雨聆风