[Android] adb setuid提权漏洞的分析

原来我看到一篇关于分析RageAgainstTheCage源码的很不错的文章,后来发现原连接失效了,只能从网页的缓存副本中找寻蛛丝马迹。下面就是我找到的内容,保存到本身的博客中,来保证之后能找到。html

去年的Android adb setuid提权漏洞被用于各种root刷机,漏洞发现人Sebastian Krahmer公布的利用工具RageAgainstTheCage(rageagainstthecage-arm5.bin)被用于z4root等提权工具、Trojan.Android.Rootcager等恶意代码之中。下面咱们来分析这一漏洞的产生缘由。android

The Android Exploid Crew小组在后来发布了一份PoC代码:rageagainstthecage.c。从这份代码开始着手。程序员

在main(:72)函数中,首先获取了RLIMIT_NPROC的值(:83),这个值是Linux内核中定义的每一个用户能够运行的最大进程数。shell

而后,调用find_adb()函数(:94)来搜索Android系统中adb进程的PID,具体而言,该函数读取每一个进程对应的文件的/proc/<pid>/cmdline,根据其是否等于”/sbin/adb”来判断是否adb进程。缓存

接下来,fork了一个新的进程(:109),父进程退出,而子进程继续。接下来,在113行建立一个管道。安全

if (fork() > 0)
		exit(0);

	setsid();
	pipe(pepe);

重头戏发生在下面的122到138行,代码以下:app

if (fork() == 0) {
		close(pepe[0]);
		for (;;) {
			if ((p = fork()) == 0) {
				exit(0);
			} else if (p < 0) {
				if (new_pids) {
					printf("\n[+] Forked %d childs.\n", pids);
					new_pids = 0;
					write(pepe[1], &c, 1);
					close(pepe[1]);
				}
			} else {
				++pids;
			}
		}
	}

新建一个进程后,在子进程之中,exploit代码不断地fork()(:125),而新的子进程不断退出,从而产生大量的僵尸进程(占据shell用户的进程数)。最终,进程数达到上限,fork()返回小于0,因而打印当前已经建立多少子进程,并向管道输入一个字符(:131)。ionic

在这里,管道的做用是和(:122)fork出来的父进程同步,该进程在141行read这一管道,于是阻塞直至僵尸进程已经达到上限(:131)。函数

进一步的,exploit杀掉adb进程,并在系统检测到这一现象并重启一个adb以前,再一次fork(),将前一个adb留下的进程空位占据。最后,在152行,exploit调用wait_for_root_adb(),等待系统重启一个adb,这个新建的adb就会具备root权限。工具

为何在shell用户的进程数达到上限RLIMIT_NPROC之后,新建的adb会具备root权限?咱们来看adb的源码。

在<android_src>/system/core/adb/adb.c的第918行,咱们能够看到以下代码:

/* then switch user and group to "shell" */
        if (setgid(AID_SHELL) != 0) {
            exit(1);
        }
        if (setuid(AID_SHELL) != 0) {
            exit(1);
        }

这已是漏洞修补之后的代码。在漏洞最初被发现时,代码以下:

/* then switch user and group to "shell" */
        setgid(AID_SHELL);
        setuid(AID_SHELL);

简而言之,原来没有检查setuid()函数的返回值。事实上,在此以前,adb.c中的代码都是以root权限运行,以完成部分初始化工做。在这一行,经过调用setuid()将用户从root切换回shell,但setuid()在shell用户进程数达到上限RLIMIT_NPROC时,会失败,所以adb.c继续以root身份运行,而没有报错。

咱们来看setuid()的man手册(man 2 setuid),其中有以下说明:

RETURN VALUE
       On  success,  zero is returned.  On error, -1 is returned, and errno is
       set appropriately.

ERRORS
       EAGAIN The uid does not match the current uid and  uid  brings  process
              over its RLIMIT_NPROC resource limit.

能够看到,setuid是可能发生错误的,而且在uid的进程数超过RLIMIT_NPROC极限时,发生EAGAIN错误。

在android的源码中,setuid()定义于<android_src>/bionic/libc/unistd/setuid.c,实际上引用了一个外部符号__setuid,这个符号在<android_src>/bionic/libc/arch_xxx/syscalls/__setuid.S中定义,最终是一个%eax=$__NR_setuid32,%ebx=uid的int 0×80中断。

由于只是要分析原理,咱们再也不鏖战于Android,转而看向Linux内核。

在最新的kernel2.6中,setuid()位于kernel/sys.c的682行,其中,在697行,一切正常的状况下,它会调用set_user()来完成用户切换。

set_user()实现于同一文件的587行,其中一部分代码以下:

if (atomic_read(&new_user->processes) >= rlimit(RLIMIT_NPROC) &&
			new_user != INIT_USER) {
		free_uid(new_user);
		return -EAGAIN;
	}

含义很明显,当目标用户的进程数达到上限,那系统就不能再将一个进程分配给它,于是返回-EAGEIN。而后再setuid()中,直接跳事后面的代码,而返回错误。

至此,整个漏洞的原理已经分析完毕。整理以下:

一、在Android的shell用户下,制造大量的僵尸进程,直至达到shell用户的进程数上限RLIMIT_NPROC;

二、kill当前系统中的adb进程,并再次占据其进程位置以保持达到上限;

三、系统会在一段时间后重启一个adb进程,该进程最初是root用户,在完成少量初始化工做后,调用setuid()切换至shell用户;

四、此时shell用户的进程数已经达到上限,因此setuid()失败,返回-1,而且用户更换没有完成,adb仍是root权限;

五、adb没有检查setuid()的返回值,继续后续的工做,所以产生了一个具备root权限的adb进程,能够被用于与用户的下一步交互。

实际上,setuid在目标用户进程数达到RLIMIT_NPROC极限时返回错误,这一问题可能产生的安全隐患最先能够追溯到2000年。而在2006年,出现了真正利用这一编码问题的漏洞(CVE-2006-2607)。

所以,这并非一个全新的漏洞。咱们能够得出几点结论:

一、函数返回值一直是忽略的对象,由于getuid()永远不会失败,程序员可能会认为setuid()也不会失败——至少没有遇到过,所以忽略了对返回值的检查。检查一个系统函数是否调用失败是一个常识,但又是很麻烦的事,若是为了省事而忽略,问题就可能产生了。

二、Android下的安全问题,不少并不是全新的,并且我的判断未来还会有大量漏洞、恶意代码产生于传统思路,而做用于新的平台。面对这一新的平台,咱们是否能抢先于攻击者作好防范准备,是一个须要咱们思考和实践的问题。

相关文章
相关标签/搜索