最近做项目的时候遇到一个神奇的崩溃。
这个崩溃都集中发生在 Android 10 的机器上。

崩溃堆栈类似下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pid: 2938, tid: 2940, name: crasher64  >>> crasher64 <<<
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x5f2ced24a8
Cause: execute-only (no-read) memory access error; likely due to data in .text.
x0 0000000000000000 x1 0000005f2cecf21f x2 0000000000000078 x3 0000000000000053
x4 0000000000000074 x5 8000000000000000 x6 ff71646772607162 x7 00000020dcf0d16c
x8 0000005f2ced24a8 x9 000000781251c55e x10 0000000000000000 x11 0000000000000000
x12 0000000000000014 x13 ffffffffffffffff x14 0000000000000002 x15 ffffffffffffffff
x16 0000005f2ced52f0 x17 00000078125c0ed8 x18 0000007810e8e000 x19 00000078119fbd50
x20 00000078125d6020 x21 00000078119fbd50 x22 00000b7a00000b7a x23 00000078119fbdd8
x24 00000078119fbd50 x25 00000078119fbd50 x26 00000078119fc018 x27 00000078128ea020
x28 00000078119fc020 x29 00000078119fbcb0
sp 00000078119fba40 lr 0000005f2ced1b94 pc 0000005f2ced1ba4

backtrace:
#00 pc 0000000000003ba4 /system/bin/crasher64 (do_action+2348)
#01 pc 0000000000003234 /system/bin/crasher64 (thread_callback+44)
#02 pc 00000000000e2044 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+36)
#03 pc 0000000000083de0 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)

通过 Bugly 上报的数据发现,这个 SEGV_ACCERR 的崩溃(无访问权限)发生在读取 libc.so 的代码段(.text)内存的时候。
观察其他信息发现,libc.so的代码段权限只剩下 --xp
怪不得我们一访问就崩溃,是因为代码段没有可读权限了!
同样的问题也发生在其他系统库上。

当时的解决方案是,通过读取 /proc/self/maps 避开系统库无可读权限的用户。

直到我不小心看到了这个文档……

只执行内存违规(仅限 Android 10)

仅对于 Android 10 中的 arm64,二进制文件和库的可执行部分会映射到只执行(不可读取)内存,作为防范代码重用攻击的一种安全强化技术。此缓解方法与其他缓解方法产生了不良互动,后已被移除。

将代码设为不可读会导致故意或意外读入已标记为只执行的内存段抛出 SIGSEGV,并且代码为 SEGV_ACCERR。这可能是因为错误、漏洞、混合了代码的数据(例如文字池)或故意进行的内存自省导致的。

编译器会假设代码和数据不是混合在一起的,但是手写程序集会导致出现问题。在许多情况下,只需将常量移动到 .data 部分,即可解决这些问题。如果可执行代码段绝对有必要进行代码内省,应首先调用 mprotect(2) 以将该代码标记为可读,然后在操作完成后重新将该代码标记为不可读。

寻思这不是Android系统你在搞事情啊!
Android 官方提供的解决方案是访问之前先 mprotect 一下需要读的内存,给他们加上可读权限。

提供一个伪代码:

1
2
3
4
5
6
auto lib = dlopen('libc.so');
auto fn_open = dlsym(lib, 'open');
auto page_start = (fn_open >> 12) << 12;
mprotect(page_start, 0x1000, PROT_READ | PROT_EXEC);
int *code = fn_open;
printf("code: 0x%X", *code);

亲测该方案是有效的。
不过为了保守,我们用完了记得把权限设置回去只可执行,以避免一些未知的意外崩溃。

除非注明,麦麦小家文章均为原创,转载请以链接形式标明本文地址。

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

本文地址:https://blog.micblo.com/2022/04/26/Android-10-%E7%B3%BB%E7%BB%9F%E5%BA%93%E4%B8%8D%E5%8F%AF%E8%AF%BB%E7%9A%84%E5%9D%91%E7%82%B9/