vivo内存安全检测实践
2023-11-8 10:50:47 Author: mp.weixin.qq.com(查看原文) 阅读量:22 收藏

可以看到,MTE相对其他内存安全检测技术,无论是在检测效率,还是性能开销上,都有极大提高。因此自从arm推出MTE后,Android生态上下游厂商做了很多努力将其在实际中应用起来:

  • Google在Android中加入了MTE的支持代码,允许应用开启MTE开关以利用硬件能力;同时,将一些系统组件(主要是对性能稍有下降不敏感的组件),比如netd、bluetooth、NFC HALs、statd、system_server、zygote64等默认打开MTE[6]

  • MTK在其SOC系统中实现了MTE硬件能力。

  • 终端厂商,如vivo在x100,x90等设备支持MTE。

  • 应用厂商,如快手[7],在使用MTE来提高应用的稳定性方面已经取得很好的效果,还有更多的厂商也在使用中。

vivo在使用MTE后发现内存安全问题时,遇到了两个主要痛点:

① 测试效率低

测试版本1->发现一个内存安全问题->等待开发的修正->测试版本2->又发现一个内存问题->等待开发修正的循环,即使这些内存错误相互之间可能毫无关联,也不得不因为串行的工作流导致测试合格流程超时。

三方代码问题阻塞测试流程

测试时因为三方代码的内存安全/兼容性问题导致应用崩溃,如果这部分代码无法绕过时,会导致无法测试到自身代码的问题。另外,三方代码修改困难、修改周期长问题也让这个问题更为严重。

二、系统如何处理内存错误

为了解决上述痛点,我们需要分析CPU触发内存错误后,系统的处理流程是什么。只有弄清楚整个流程后,才能找到合适的点来解决问题。

首先忽略那些耗时繁琐的分析过程,先上一张总结图:

图1:进程启动以及信号处理简图

可能导致应用退出的有3个关键点(红色块,native和APP的自定义信号处理是一个类型的点):

① APP自定义信号处理方法,一般是各种崩溃处理SDK,可能会在特定场景下杀掉自身进程;

② 系统的默认信号处理函数debugger_signal_handler中,最后将信号重新发送,而此时不会有任何handler来处理,系统将杀死进程;

③ crash_dump进程在处理过程中,会根据信号是否严重,发送通知给AMS,AMS则根据情况杀掉对应进程。

搞清楚可能的点后,我们就可以针对性的改动,避免代码运行到上述3点就能够让应用不崩溃。

下面我们分别对这3点进行详细分析。

三、定制系统信号处理逻辑

3.1 不让APP自定义信号处理方法杀死自身

主要有两个思路:

① 修改杀掉进程的方法,让自定义信号处理方法调用时不生效。但是这需要在杀进程方法中识别调用来自哪里,以免误伤正常的杀进程场景,这通常是一个很难实现的目标。

② 从信号处理流程入手,看看在流程中哪个点修改后可以让APP自定义信号处理方法不执行。

我们来分析思路②如何实现。

Android信号处理是一个比较复杂的系统,限于篇幅,我们这里不做详细介绍,仅简要提下我们需要重点关注的内容。

Android信号处理机制在linux信号处理机制[8]基础上,增加了一个libsigchain的中间层。这个libsigchain hook了linux信号处理相关的函数,比如sigaction。APP调用sigaction来注册信号处理方法时,实际上调用的是libsigchain的__sigaction。这个__sigchain会根据需要,实际调用sigaction注册信号处理函数,或者仅仅记录到内部数据结构chain[signo].action_中[9]。最终达到的效果是:

对于Android关心的所有信号对应的处理函数,比如SIGSEGV、SIGILL、SIGBUS等等,都是libsigchain的SignalChain::Handler函数;当内核触发对应信号时,调用的都是SignalChain::Handler这个入口函数。SignalChain::Handler内部则根据需要,调用ART的信号处理方法、APP的信号处理方法、linker[10]注册的系统默认处理方法

② 其他信号的处理函数则根据实际注册的处理函数来处理。

用一个协作图简要表示如下:

图2:Android信号处理系统简要协作图

从上图我们可以看出,要想让APP的信号处理方法不执行,可以有两个办法:

① 让APP注册过程不生效

注意到APP是调用libsigchain的sigaction来注册的,那么我们只要在sigaction里面,不让注册生效即可,对应代码修改可以像这样:

//art/sigchainlib/sigchain.ccstatic bool no_userhandler_on_sigsegv() {#ifdef ART_TARGET_ANDROID  return GetBoolProperty("debug.mte.sigsegv.no.userhandler", false);#else  return false;#endif}
static int __sigaction(int signal, const SigactionType* new_action, SigactionType* old_action, int (*linked)(int, const SigactionType*, SigactionType*)) { if (chains[signal].IsClaimed()) { SigactionType saved_action = chains[signal].GetAction<SigactionType>(); if (new_action != nullptr) { //判断满足我们自定义条件时,才让APP注册new_action生效 if((!no_userhandler_on_sigsegv()) && (signo == SIGSEGV)){ log("SetAction for signal %d, because no_userhandler_on_sigsegv false", signal); chains[signal].SetAction(new_action); } }}

由于action_里面根本没有保存APP的处理方法,里面仍然是linker最初注册的debuggerd_signal_handler,这样保证了信号signo发生时,SignalChain::Handler调用chain[signo].action_时调用的不是APP的信号处理方法。

② 不调用APP注册的信号处理方法

这个办法不修改sigchain,APP注册信号处理方法仍然是成功的,只是在发生信号signo时,SignalChain::Handler需要判断chain[signo].action_保存的是否是 debuggerd_signal_handler,如果是就调用debuggerd_signal_handler。代码修改可以像这样:

//art/sigchainlib/sigchain.ccvoid SignalChain::Handler(int signo, siginfo_t* siginfo, void* ucontext_raw) {  if ((handler_flags & SA_SIGINFO)) {    //当满足条件时,只调用系统默认信号处理方法;其他情况按原生逻辑   if (no_userhandler_on_sigsegv()  && (signo == SIGSEGV) && chains[signo].action_.sa_sigaction == debuggerd_signal_handler)      debuggerd_signal_handler(signo, siginfo, ucontext_raw);   else      chains[signo].action_.sa_sigaction(signo, siginfo, ucontext_raw);  } else {    auto handler = chains[signo].action_.sa_handler;    if (handler == SIG_IGN) {      return;    } else if (handler == SIG_DFL) {      fatal("exiting due to SIG_DFL handler for signal %d, ucontext %p", signo, ucontext); } else {    } else {      //当满足条件时,只调用系统默认信号处理方法;其他情况按原生逻辑      if (no_userhandler_on_sigsegv() && (handler == debuggerd_signal_handler) && (signo == SIGSEGV))        debuggerd_signal_handler (signo);      else        handler(signo);    }  }}

这两种办法都可以达到我们的目的,使用任意一种即可。

3.2 不让debugger_handler重新发送信号

从图1我们可以了解到,debuggerd_signal_handler处理信号时,会先拉起crash_dump进程以获取本进程堆栈并输出tombstone文件,然后将信号处理函数设置为默认信号处理函数SIG_DFL(实际就是nullptr),重新通过内核将信号发送给进程。

// system/core/debuggerd/handler/debuggerd_handler.cppstatic void resend_signal(siginfo_t* info) {  if (info->si_signo != BIONIC_SIGNAL_DEBUGGER) {    signal(info->si_signo, SIG_DFL);    int rc = syscall(SYS_rt_tgsigqueueinfo, __getpid(), __gettid(), info->si_signo, info);    if (rc != 0) {      fatal_errno("failed to resend signal during crash");    }  }}

此后,内核向进程发送信号后,发现没有对应的信号处理函数,进程就被杀死。

因此,为了让进程不退出,我们只要在debuggerd_signal_handler里面,不重新向内核发送信号即可。 当然,我们不能无脑的在所有情况下都不发送,必须满足一定条件才能不发送。这个条件可以根据业务的需求来设置,比如:

① MTE 触发的信号不重新发送

这种处理方式就是Android 14引入的permissive模式,关闭线程MTE检测后,不重新发送信号。

② MTE开启时,SIGSEGV信号不重新发送

这种情况可以跳过在MTE开启时进程触发的SI_CODE不等SEGV_MTESEER/SEGV_MTEAERR的SIGSEGV错误。

③ MTE开启时,任意信号都不重新发送

这种情况可以跳过MTE开启时,进程触发的SIG_ILL, SIG_BUS等错误。

3.3 不让crash_dump通知AMS

crash_dump在运行过程中,会根据当前信号的严重程度,判断是否要通知AMS。AMS则会在收到native信号时,根据信号的严重程度,采取杀掉进程的动作。毫无疑问,内存问题的SIGSEGV信号是严重的,AMS将杀死进程。不让crsh_dump通知AMS的方法也很简单,就是在通知前加个开关控制一下:

// system/core/debuggerd/crash_dump.cppint main(int argc, char** argv) {  DefuseSignalHandlers();  InstallSigPipeHandler();  if (fatal_signal) {    // Don't try to notify ActivityManager if it just crashed, or we might hang until timeout.    if (thread_info[target_process].thread_name != "system_server") {      //满足我们自定义条件时,才通知AMS      if(need_notify_am(&siginfo)) {                activity_manager_notify(target_process, signo, amfd_data);        LOG(INFO) << "after activity_manager_notify";      }}  …
四、充分利用MTE能力

在3.3节中我们通过修改系统系统信号处理函数让APP进程不在本次信号上崩溃。那么接下来该如何做呢?

有三个思路:

① 忽略当前指令,直接执行下一条指令

虽然当前触发问题的指令被跳过,但是后续指令如果依赖当前指令的结果,那么就会带来不可预料的错误,比如死循环、进程莫名被杀掉等问题。因此这个办法是不可取的。

② 关闭MTE检测,再继续执行当前指令

这实际上就是Android 14引入的permissive模式,遇到MTE触发的SIGSEGV后,关闭本线程的MTE检测。虽然此方案可以让线程继续正常运行,但是也失去了继续利用MTE检测内存安全问题的能力。

③ 关闭MTE检测,一段时间后再开启MTE检测

此方案既能够让当前指令正常执行,又能够在一段时间后继续利用MTE的能力检测内存安全问题。正常情况下,应用在很短时间内连续产生内存安全问题的几率很小,因此我们因为短暂停止MTE检测而遗漏的内存安全问题应该是可以接受的。

下面我们看思路三如何具体实现。

一个很自然的想法可能就是直接在debuggerd_signal_handler函数内,识别到是因为MTE产生的SIGSEGV后,调用prctl关闭当前线程MTE检测,然后sleep一段时间,再打开MTE检测。但很可惜,这种办法无法达到目的,只会导致进程一直执行有问题的指令,然后持续的关闭-开启MTE检测,陷入死循环。

为什么呢?原因是debuggerd_signal_handler函数与问题指令是在同一个线程内运行的,而问题指令抛出的信号被处理后,系统仍然会再执行一遍问题指令。这样就产生了MTE开启—>问题指令—>MTE关闭(sleep一段时间)->MTE开启—>问题指令的死循环。

正确的跳出循环的方式是,先关闭问题线程的MTE,退出debuggerd_signal_handler函数,执行问题指令;同时拉起专门的通知线程,让它在一段时间后触发问题线程再开启MTE。而触发问题线程开启MTE则可以通过向其发送自定义的信号来完成。

整个过程可以简要命名为stop_delay_start_mte,具体过程如下:

图3:stop_delay_start_mte流程简图

通过上述设计实现,我们已经基本解决了开启MTE后,可能产生多个内存错误时导致的测试-修改工作流的串行化问题,大大提高了测试效率。

下面我们来看如何跳过三方代码的问题。

五、跳过三方代码问题

大型的native应用,都会使用很多三方native库,比如libffmpeg.so,libil2cpp.so、libunity.so等等。有些三方库可能存在内存安全问题,也有可能因为使用了与MTE不兼容的指令,导致开启MTE后会报内存错误,这些错误产生的信息(如tombstone文件)会污染测试过程的统计数据,需要额外的处理。三方库的问题通常不是应用开发商能够解决的,因此希望能够跳过指定三方库的错误,以专注在自身代码产生的错误,获得更聚焦的错误信息,提高测试效率。

为了实现此需求,我们的思路是设置相应的白名单,在debuggerd_signal_handler函数内,对发生内存安全错误时的堆栈进行回溯匹配。一旦匹配上白名单中的内容,则表明当前的内存错误是可以被忽略的,无需后续流程,比如调用crash_dump流程来生成tombstone文件。

思路有了,实现起来也相对简单,参考样例代码如下:

// 实现堆栈白名单检测的函数static bool crash_in_ignored_frames(void* context){  get_ignore_frames(); //从属性读取白名单配置,代码略  if (g_target_frame_num == 0) {    return false;  }  ThreadInfo info;  info.pid = __getpid();  info.tid = __gettid();  info.registers.reset(unwindstack::Regs::CreateFromUcontext(unwindstack::Regs::CurrentArch(), context));  auto process_memory = unwindstack::Memory::CreateProcessMemoryCached(__getpid());  unwindstack::UnwinderFromPid unwinder(kMaxFrames, __getpid(), process_memory);  unwinder.SetRegs(info.registers.get());  unwinder.Unwind();  if (unwinder.NumFrames() == 0) {    async_safe_format_log(ANDROID_LOG_INFO, "libc", "crash_in_ignored_frames unwinder failed for thread %d",info.tid);    return false;  }  for (const auto& frame : unwinder.frames()) {    std::string formated_frame = unwinder.FormatFrame(frame);    const char *frame_name = formated_frame.c_str();    for(int i = 0; i < g_target_frame_num; i++) {      if(strstr(frame_name, g_target_frame[i]) != nullptr) {        async_safe_format_log(ANDROID_LOG_INFO, "libc", "target %s in %s", g_target_frame[i], frame_name);        return true;      }    }  }  async_safe_format_log(ANDROID_LOG_INFO, "libc", "no target frames in stack");  return false;

需要注意的是,需要在debuggerd_signal_handler的合适地方调用crash_in_ignored_frames,尽可能降低对系统原生逻辑的影响,同时又能够达到我们减少tombstone文件生成的目的。我们选择在log_signal_summary之后:

// system/core/debuggerd/handler/debuggerd_handler.cppstatic void debuggerd_signal_handler(int signal_number, siginfo_t* info, void* context) {  log_signal_summary(info);//在log_signal_summary之后,拉起crash_dump进程之前判断堆栈,这样不会产生tombstone文件  if(crash_in_ignored_frames(context)) {     stop_delay_start_mte("crash_in_ignored_frames");//流程参考第四章,细节此处略     pthread_mutex_unlock(&crash_mutex);       return;  } }

用类似的方法,我们还可以实现历史堆栈对比来跳过相同崩溃点的功能。在收到信号时,对比历史堆栈,如果发现堆栈一样,那么就忽略此次信号。

六、总结

上面我们详细描述了vivo内存安全检测定制框架的设计与实现,总结如下:

① 发生MTE识别的SIGSEGV时,立即关闭MTE,一段时间后打开MTE,继续检测;

② 可设置白名单,崩溃堆栈包含白名单关键字时,跳过此次崩溃,系统不做处理;

③ 崩溃堆栈与上一次崩溃堆栈一致时,跳过此次崩溃,系统不做处理;

④ 拦截发送给AMS的信号,避免AMS杀进程;

⑤ 旁路APP自定义崩溃处理流程,避免处理流程中杀死进程,APP无需修改自身崩溃SDK的行为。

同样的,我们用一张图表示如下,可以直观的看到在不同的开关组合下,内存安全检测框架的效果:

vivo内部,已经基于定制内存安全检测框架对内部的重点应用进行内存安全检测。

同时,我们也在和Android生态中的上下游厂商紧密合作,以更好的提升Android应用内存安全水平。在近期举行的VDC上,我们发布了千镜内存安全检测平台(平台链接:https://developers.vivo.com/product/d/memSec),集成了定制框架,并提供简单易用的自动化检测手段,以解决以往内存安全检测中检测开销大、安全问题定位低效与检测效率低等不足的问题,助力开发者主动发现并预防问题,降低安全风险,欢迎使用。

千镜内存安全检测平台二维码

END

参考资料:

[1] Android 内存bug分析统计 https://source.Android.com/docs/security/test/memory-safety#affects-security [2] Konstantin Serebryany AddressSanitizer: A Fast Address Sanity Checker USENIX ATC 2012 https://www.usenix.org/system/files/conference/atc12/atc12-final39.pdf [3] Hardware-assisted AddressSanitizer Design Documentation https://clang.llvm.org/docs/HardwareAssistedAddressSanitizerDesign.html [4] GWP-ASan https://llvm.org/docs/GwpAsan.html [5] arm MTE手册 https://developer.arm.com/documentation/108035/0100 [6] android默认启用MTE的模块 https://source.Android.com/docs/security/test/memory-safety/arm-mte#mte-enabled-components [7] 快手MTE探索与实践 https://mp.weixin.qq.com/s/fJ4yWpyhHHFQy65OZEh-iw [8] linux信号机制分析 https://juejin.cn/post/7081189234245107742 [9] Android应用中SIGSEGV信号处理流程 https://juejin.cn/post/6966169836703449119 [10] Android linker介绍 https://www.cnblogs.com/ntiger/p/12122308.html


文章来源: https://mp.weixin.qq.com/s?__biz=MzI0Njg4NzE3MQ==&mid=2247491309&idx=1&sn=87fef5abc0379d975cfef25f9078a90b&chksm=e9b93881deceb1972a9d1139c2e0d7036ef2aa92b41a7b4e74c433fc581130e2c310f8a68689&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh