酷勤网 – 程序员的那点事!

当前位置:首页 > 编程 > 移动开发 > 正文

Android应用自杀之谜

浏览次数: ink_CSDN专栏 2015年09月14日 字号:

GCC编译器有一种名为FORTIFY_SOURCE的机制,用于检查缓冲区溢出方面的安全漏洞,Android的Jelly Bean版本将这一机制引入系统,旨在增强它的安全防卫能力。这样的防卫系统一旦检查到风险,就会强令应用程序“自杀”。本文将通过一个真实的案例介绍FORTIFY_SOURCE机制在Android系统上的工作过程和有关的调试方法。

强制终止

有同行遇到一个问题,他的Android应用程序在执行某个操作时戛然而止。在ADB Shell中执行并努力重现问题,可以看到程序死亡前,留下这样一行消息:

FORTIFY_SOURCE: strcpy: prevented write past end of buffer. Calling abort().

书中暗表,这行消息被称为Abort Message(终止信息),简称AM,对调试程序意外终止很重要。

审视这行宝贵的终止信息,从最后的Calling abort两个单词来看,程序是主动调用abort函数终止的,可能是“自杀”。顺便介绍下“古老”的abort函数。它让我回忆起初学编程的日子。在用C语言写Hello World类型的小程序时,有时会用到abort函数,它的作用是立刻终止,放弃执行,退回到控制台。它的特点是简单易用(既没有参数,也没有返回值),效果明显。后来学习图形界面编程,便不大使用了。多年之后,在Android这样的时尚平台中看到它,让我顿生感叹。看来时尚外衣之下,还保留着一些祖先遗传下来的印痕。

继续看消息的中间部分,又是一个古老的C函数:strcpy。它比abort著名得多。因为历史上它曾无限荣光,身影遍布几乎所有软件,但突然有一天,它被打入冷宫,成为软件安全的大敌。因为它与著名的缓冲区溢出漏洞关系密切,是黑客努力搜索的目标,几乎所有讨论安全的教科书都会谈到这个不安全的函数。

再看strcpy后面的话:“prevented write past end of buffer. ”可以翻译为:“阻止了超出缓冲区末尾的写操作”。有意思,它报告成功阻止了溢出,要知道,溢出可能意味着黑客得手,恶意逻辑即将肆虐。从这个角度来看,发现溢出风险而且将其阻止,其功劳不啻于“挽大厦于将倾”啊。

在strcpy前面还有一个冒号,冒号前是FORTIFY_SOURCE,它是什么意思呢?

FORTIFY_SOURCE

2004年9月,RedHat的几位软件工程师提交了一个针对GCC和GLibc的新补丁。其作用是为内存和字符串函数提供一种轻量级的缓冲区溢出保护机制。它可以通过定义_FORTIFY_SOURCE标志来配置,因此常被称为FORTIFY_SOURCE。Fortify有构筑防御工事的意思,FORTIFY_SOURCE的意思是在源代码这一根源上筑城设防,与中文的“固本”类似。今天,我们还可以在GCC的官方网站,找到当初的邮件,详细介绍了FORTIFY_SOURCE机制的目标、原理、使用方法和详细代码,很值得仔细读一读(https://gcc.gnu.org/ml/gcc-patches/2004-09/msg02055.html)。

在使用FORTIFY_SOURCE时,可以把FORTIFY_SOURCE标志定义为0、1和2三个值。0代表禁用,1和2都是启用,上面提到的公开邮件解释了1和2的区别。

以下面这段代码为例:

 struct S { struct T { char buf[5]; int x; } t; char buf[20]; } var; 
strcpy (&var.t.buf[1], "abcdefg");

如果-D_FORTIFY_SOURCE=1,那么其中的strcpy语句就不会被认为有溢出风险,因为编译器根据var变量整体的大小来估计目标缓冲区的空间。而当-D_FORTIFY_SOURCE=2时,就会被认为有溢出风险。

墓碑(Tombstone)

上面详细分析了应用程序“自杀”时留下的“终止消息”,大体知道了程序自杀的原因,是FORTIFY_SOURCE机制检测到了strcpy语句有溢出风险,果断“出手”,把程序了断了。但简单的终止信息也就只告诉我们这么多。而我还有很多疑问,例如是何处调用的strcpy?其参数到底为何?目标缓冲区和源字符串都是什么内容?是真的要溢出,还是FORTIFY_SOURCE执法时,判断有误“冤枉好人”?

如何寻找答案呢?如果是Windows系统,那么二话不说就上调试器了,在调试器里埋伏断点,然后再重现问题。但现在是Android系统,虽然也有调试器方案,但是颇为麻烦,要先找合适版本的gdbserver,安装到目标机,然后再在主机端安装GDB前端(Client)…… 如此考虑,我们还是先尝试一下“非调试器方案”,看用其他调试机制能否解决问题。

首先尝试源于Linux的DMESG机制,在模拟终端里执行dmesg命令,信息很多,浏览一番,一无所获。顺便说一下,因为dmesg是Linux内核的调试消息机制,对于触发CPU异常的应用程序崩溃,使用dmesg可得到一些有价值信息(如segment fault)。而对眼下应用程序主动调用abort“自杀”,内核不管这个闲事,因此dmesg里没有留下任何记录。

接下来尝试Android系统特有的LogCat机制。执行logcat命令,也是大量信息涌出来。安静之后,仔细浏览,果然有重大发现。首先看到前面分析的Abort消息,之后还跟了很多行,齐刷刷地排列在那里,黑色的背景,白色的字符,看起来还真有点像“墓碑”(如图1)。的确,这组调试信息的名字就叫墓碑(Tombstone),不知是哪位天才同行取了这样一个名字——恰当而让人警醒。

 

 

图1   宝贵的栈回溯信息

 

 

纵观图1中的“墓碑”信息,其结构与Linux中的Panic信息类似。严格说来,全是星号的第3行才是信息的开始,图1故意多包含了两行libc输出的终止信息和发出致命的Abort信号时的信息。

每一行都是标准的LogCat结构,第一个字符表示信息的严重程度,F代表Fatal,I代表Information,W代表Warnning,E代表Error。斜杠后是信息的来源,图1中的信息有三个来源,libc、DEBUG(debuggerd进程)和BootReceiver。括号中是进程ID,4224是出问题的App进程,2158是Android系统的debuggerd服务进程。冒号后面便是每一行信息的正文。

可以把整个墓碑文字大体分为四个部分:概要信息、寄存器值、栈回溯和附加信息。下面结合Android系统4.4版本的源代码分别解释,如果本地没有Android的代码,可以看这个网页:

http://code.metager.de/source/xref/android/4.4/system/core/debuggerd/tombstone.c

产生“墓碑”信息的主函数名叫dump_crash,图2显示了它的前半部分。

 

 

图2   产生墓碑信息的dump_crash函数(部分)

 

 

代码对应的刚好是产生概要信息的部分,先输出Build信息,再输出版本和线程的概要(图1中显示此操作失败),而后是终止消息。

在输出概要信息后,dump_crash函数会先调用load_ptrace_context()获取线程的寄存器上下文结构,然后调用dump_thread函数。在dump_thread中,我们可以看到如下两行代码:

 dump_registers(context, log, tid, at_fault); 
dump_backtrace_and_stack(context, log, tid, at_fault);

图中寄存器和栈回溯信息就是它们分别输出的。

执行过程

有了上面的基础,再回到我们的问题。我们迫切想知道谁调用了strcpy,对此最有价值的信息莫过于栈回溯了。为了便于观察,我们把图1中栈回溯部分单独拿出来观察(如图3)。

 

 

图3   宝贵的栈回溯信息

 

 

图3中的栈回溯信息格式与WinDBG使用的格式很类似,最左边是栈帧序号,第二列的PC代表程序指针(Program Counter),第三列描述的是PC值,对于栈帧0,它就是寄存器上下文中的PC寄存器值(x86中即EIP),对于其它栈帧,它代表的是子函数的返回地址。值得说明的是,这里输出的PC值都是经过转换的,是相对于模块基地址的内部偏移。这样做的好处是,当我们没有模块基地址信息时(例如现在),还可以在map文件中寻找这个地址对应的函数名。

接下来看函数的调用过程,从下向上看,最下面的三个栈帧是libc中的线程启动函数,__bionic_clone(bionic是Android系统的C库函数实现),接下来的#10 - #07是同行写的应用(以下简称App)中的函数。#10中的XXXLoop是线程的工作函数,经过一番调用后,App中的代码调用了libc中的__strcpy_chk函数(#6栈帧)。接下来被调用是__fortify_chk_fail,名字的前半部分很容易让我们想起前面介绍的Fortify检查,后面的fail说明检查没有通过。接下来几个栈帧的函数名,一个比一个吓人,可以说是一步步走向死亡,__libc_fatal(致命错误),abort(自杀终止),raise(发起死亡信号),pthread_kill(pthread杀),tgkill(线程组杀)。

综合起来看,可以把图3中的执行过程分为四个部分:线程启动(#13-#11),执行App逻辑并调用(#10 - #07),调用__strcpy_chk和Fortify检查失败程序自杀。而其中的调用__strcpy_chk是道分水岭,调用前程序很可能是正常执行的,调用后便走上了不归路。因此,下一个目标自然是__strcpy_chk函数。

__strcpy_chk

仔细阅读前面提到的公开邮件,其中多次提到__strcpy_chk函数。特别是有下面这样的定义:

 +#undef strcpy 
+#define strcpy(dst, src)  
+ __builtin___strcpy_chk (dst, src, os (dst))

其中的os是object size的缩写。查看Android源代码中的string.h文件,我们看到类似的定义:

 __BIONIC_FORTIFY_INLINE  char* strcpy(char* __restrict dest, const char* __restrict src) { 
return __builtin___strcpy_chk(dest, src, __bos(dest)); 
}

其中的__bos是builtin_object_size的缩写,如此看来,两种定义没有大的差异,都是把strcpy“重定向”到__builtin___strcpy_chk,只不过一个用的是宏定义,另一个用的是强制INLINE。

解释一下,这里的__builtin___strcpy_chk代表的是编译器内建的“标志”,编译器内部具有特殊的支持,其工作逻辑就是执行前面提到的Fortify检查。前面提到的公开邮件介绍了它的工作原理,简单的说,编译器会分析strcpy的参数,将其确定为如下四种情况之一:

1. 已知正确,例如strcpy (buf, "abcd"); (buf的定义为:char buf[5];);

2. 未知正确与否,可以在运行期检查,例如strcpy(buf, bar);

3. 已知不正确,比如strcpy (buf, "abcde");

4. 未知正确与否,也无法在运行期检查,例如strcpy(p, q);。

对于情况1,编译器产生代码,让用户代码继续直接调用strcpy。对于情况2,编译器便会产生代码调用运行期的检查函数__strcpy_chk,这正是前面我们看到的情况。图4给出了__strcpy_chk函数的代码,注意它的注释部分,第一句的说明便是“__builtin___strcpy_chk的运行实现”。

 

 

图4   __builtin___strcpy_chk的运行期实现

 

 

阅读代码可以看到,如果源字符串的长度+1大于目标缓冲区的长度,那么便调用__fortify_chk_fail函数报告错误,调用时的第一个参数正是我们开头介绍的终止消息的中间部分。继续看__fortify_chk_fail函数,就可以找到终止信息的源头了:

 __libc_fatal("FORTIFY_SOURCE: %s. Calling abort().", msg);

App的代码

搞清楚了调用Abort的过程后,是时候看一下应用程序自己的代码了,根据图3中#7栈帧中提示的函数名,很容易找到了对应的源程序,再花一点时间就定位到了调用strcpy的源代码,即:

 strcpy((char *)event->ProcessInfo.szName, module->name);

进一步追查,发现ProcessInfo结构体是这样定义的:

 typedef struct _tagPROCESS_INFO64 
{ 
//无关字段 
unsigned short szName[1]; 
} PROCESS_INFO64;

虽然szName的长度很短,但代码中对此已有所考虑,在分配event区域时就特意多分配了空间给szName用。有些同行看到这里,立刻明白了很多,因为这样的“少定义,动态分”方法曾经是一种比较常用的编程技巧。但这个技术确实与Fortify机制有了冲突。分析到这里,水落石出,暂时禁止Fortify机制(_D_FORTIFY_SOURCE=0),程序就没有问题了。

无觅相关文章插件,快速提升流量