Android加壳过程中mprotect调用失败的原因及解决方案
Posted yunshouhu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android加壳过程中mprotect调用失败的原因及解决方案相关的知识,希望对你有一定的参考价值。
问题原由
函数抽取壳是当前最为流行的DEX加壳方式之一,这种加壳方式的主要流程包含两个步骤:一、将DEX中需要保护的函数指令置空(即抽取函数体);二、在应用启动的过程中,HOOK 类的加载过程,比如ClassLinker::LoadMethod函数,然后及时回填指令。
笔者在实现抽取壳的过程中遇到了一个问题,即在步骤二回填指令之前,需要先调用mprotect将目标内存设置为“可写”,但在初次尝试过程中一直调用失败,于是有了今天这篇文章。
本文探讨的主要内容是mprotect调用失败的根本原因,以及在加壳实现中的解决方案,通过本文的阐述,一方面能够帮助遇到同类问题的小伙伴解决心中的疑惑,另一方面能够给大家提供可落地的实现方案。
调用mprotect修改内存失败的现象
以下代码块截取自自定义LoadMethod函数,其目标是将目标函数指令所在内存页的属性修改为可写——通过mprotect函数的参数“PROT_WRITE”指定,实际结果是mprotect调用失败了,返回”-1“,errno为”13“。
1 2 3 4 5 6 7 |
|
”13“号errno的符号为EACCES,查看linux手册可知是权限问题。手册中给出一个可能的场景,即如果使用mmap映射一个以”只读“模式打开的文件,然后使用mprotect尝试修改内存属性为可写,就会返回EACCES错误。
1 2 3 4 |
|
接下来我们将沿着这个可能的场景,首先验证DEX文件是否以只读模式打开,然后再进行下一步分析。
mprotect调用失败的原因分析
使用strace跟踪应用的系统调用,验证了DEX文件的打开模式为只读模式——"O_RDONLY",然后通过mmap2将DEX文件映射进内存,内存属性为只读的私有映射。
1 2 |
|
为了进一步证实并彻底理清背后的逻辑,我研究了下mprotect的设计文档[1]。mprotect是用户空间PAX的一部分,它的核心目标是缓解可利用内存漏洞被利用的情况,所以我理解mprotect实际上就是“memory protect”,它的主要目的是从安全的角度保护内存:
The goal of MPROTECT is to help prevent the introduction of new executable
code into the task's address space. This is accomplished by restricting the
mmap() and mprotect() interfaces.
mprotect通过内存属性控制内存的访问权限,其中安全状态良好的属性组合包括如下几种:
VM_WRITE
VM_MAYWRITE
VM_WRITE | VM_MAYWRITE
VM_EXEC
VM_MAYEXEC
VM_EXEC | VM_MAYEXEC
即内存要么是“可写”的,要么是“可执行”的,“可写”与“可执行”必须互斥,这样才能阻断“写入并执行”的内存攻击。
理解了mprotect的设计理念之后,我们再回到本文所遇到的问题本身:为什么以只读方式打开的DEX文件映射到内存之后,无法使用mprotect修改为“可写”内存?
根据mprotect设计文档的阐述,mprotect主要通过VM_MAYWRITE控制内存是否可被设置为“可写”,该属性的设置时机在mmap调用之时:
VM_WRITE | VM_MAYWRITE or VM_MAYWRITE if PROT_WRITE was requested at
mmap() time
mmap首先将所有可能的属性标致置位,然后再进行合法性检查:
kernel/msm/+/refs/heads/android-msm-vega-4.4-oreo-daydream/mm/mmap.c
1 2 3 4 5 6 |
|
如果文件打开时未设置“可写”属性,则清除“VM_MAYWRITE”属性。
kernel/msm/+/refs/heads/android-msm-vega-4.4-oreo-daydream/mm/mmap.c
1 2 3 |
|
最后mprotect会对相关属性进行检查,如果VM_MAYWRITE没有被设置,则不可通过mprotect设置内存的写属性,返回EACCES错误标识:
kernel/msm/+/refs/heads/android-msm-vega-4.4-oreo-daydream/mm/mprotect.c
1 2 3 4 5 |
|
通过strace日志可以证实mmap DEX文件到内存的过程中并没有设置VM_MAYWRITE和VM_WRITE,所以直接使用mprotect设置内存为“可写”的行为会被拒绝。
1 |
|
综上,mprotect修改内存为可写的整个逻辑如下:
系统以只读模式打开DEX文件,所以mmap在映射文件时清除了VM_MAYWRITE标志,导致接下来在调用mprotect修改内存为可写的过程中,mprotect检测目标内存未设置VM_MAYWRITE标志,返回EACCES错误代码。
两种可行的解决方案
在研究清楚原因之后,我们再来聊聊可能的解决方案。我这里给出两种经过验证的思路:
1)hook openat函数,设置文件打开时的属性为可读写——O_RDWR;
1 2 3 4 5 |
|
2)hook mmap函数,或者在mmap之前修改传入mmap的标签,直接将内存属性修改为“可写”。这里我们以后面一种思路为例,HOOK MemMap::MapFileAtAddress函数,在调用mmap映射文件之前修改prot参数:
art/runtime/mem_map.cc
1 2 3 4 5 6 7 8 9 |
|
小结
网络上很多关于抽取壳实现的教程都没有提过mprotect的问题,默认mprotect修改内存是成功的,这可能是因为大多数人都是通过模拟器进行实验。然而,如果我们要做线上的加壳产品,面向生产环境进行开发的话,mprotect调用失败的问题大概率会遇到,希望本文能有所帮助。
参考:
[1].mprotect设计文档:https[:][/][/]pax[.]grsecurity[.]net[/]docs[/]mprotect[.]txt
以上是关于Android加壳过程中mprotect调用失败的原因及解决方案的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向加壳技术简介 ( 动态加载 | 第一代加壳技术 - DEX 整体加固 | 第二代加壳技术 - 函数抽取 | 第三代加壳技术 - VMP / Dex2C | 动态库加壳技术 )
Linux 内核 内存管理内存管理系统调用 ① ( mmap 创建内存映射 | munmap 删除内存映射 | mprotect 设置虚拟内存区域访问权限 )