查看原文
其他

实践中学习CVE-2016-5195

LowRebSwrd 看雪学院 2019-09-18

该漏洞是 Linux 内核的内存子系统在处理写时拷贝(Copy-on-Write)时存在条件竞争漏洞,导致可以破坏私有只读内存映射。利用此漏洞获取其他只读内存映射的写权限,进一步获取 root 权限。

 

看了一些blog,这里就不讲其原理了,网上已经很多了哈,好像常见的利用方法就是如此:

1. Patch run-as

2. 打开 WiFi 开关,实现 ned 进程重启,Patch init 进程,覆盖 Sepolicy,获取 root

3. Patch vdso,反弹 Shell

4. Patch zygote 替换,Hook 逻辑分发

 


假设读者已对此漏洞的原理非常熟悉的情况下,我们来看以下在实践过程中遇到的问题。在以上方式均尝试的过程中,发现并不能很好的适配或者很有效的 root,经常会造成死机,所以一边尝试一边在学习,并在利用的过程中加入了自己的一些理解。


Dirtycow 可以修改运行内存的执行文件的任意一块代码,也可以整体修改。这样会比较暴力,进程会成僵尸或者被 kill,所以想法是:Patch 关键性代码,劫持程序流,让逻辑走别的地方,然后 fork 出我自己的进程,拿到 root。

 

Dirtycow 触发的思路是:

 

patch /system/bin/debuggerd 这个进程(如果熟悉其代码的话)

 

如果系统执行某个程序崩溃了,debuggerd就会检测到程序崩溃,就会跳出循环,执行 dump其实是一个 while 循环:


while(1)

{

accept()

Process_error()

}

一般程序都会阻塞在 accept 函数上,这时候如果有别的进程因为异常,就会被 debuggerd 进程接收 socket,这时候会调用 Process_error(我自己随意写的函数名)函数。

 

然后再处理异常的函数中会调用比较长的代码进行 dump 堆栈,分析目标进程崩溃的日志的原因等一系列操作,那么 Dirtycow 可以从这入手拿到稳定的 root 权限,即也不会影响程序的运行。



1

先让自己进程崩溃,崩溃可以是越界拷贝,或者除0错误,让 debuggerd 收到错误信号,这时候会传递到 debuggerd 执行错误逻辑,都会导致执行 Process_error 的函数,其实可以看到触发还是很容易,这个方法我是当时随便写了一个程序,写错程序,发现了这个可以算是可以简单的利用的点吧。


接下来如果我们把一些 Process_error() 函数内部用 Dirtycow 修改函数内部的一些指令,导致执行 shellcode,不就可以轻松拿到 root 权限了!

 

只要放入这样的 shellcode


if(fork()==0)

{

char *argv_su[] = { "/system/bin/su", NULL, NULL };

execve(argv_su[0], argv_su,0);

exit(0);

}

我们知道 su 是不能放入 su 文件中的,这时候我们用 /system/bin/ 下的任意文件替换,用 dirtycow /system/bin/oatdump /data/local/tmp/su 用 su 替换其 oatdump 文件。然后再执行:


if(fork()==0)

{

char *argv_su[] = { "/system/bin/oatdump", NULL, NULL };

execve(argv_su[0], argv_su,0);

exit(0);

}

我们可以看到系统中已经有一个 oatdump 进程以 root 权限运行了,其实是 su 文件。

 

其实在老版本的系统中,或者 selinux 不严格或者关闭的系统中,这个进程已经为所欲为了,但是在有些高版本的系统中,selinux 的严格限制。这个以 debuggerd fork 出来的进程上下文是 u:r:debuggerd:r0。


其实他的权限很小的,它只能 ptrace,它能把 dump 出来的进程堆栈文件写入 /data/anr 目录中,它不能往 sdcard 写,也不能再 /data/local/tmp 目录下写,也不能读安装程序目录下的文件比如 /data/data 子目录下的文件。所以我当时测试了好多次确实是不行的。

 

想要patch init文件,必须获取到 init 文件,但根目录下 init 文件不能取出,普通shell不能取出,debug 这个进程虽然能读出 /init 进程的二进制文件字节码,但是它根本写不到任何目录,没办法获取 init 的 bin 文件,当时 github 上有利用系统进程 /system/bin/netd/ 进程崩溃,去读出 /init 进程。


但需要人们利用共享 WiFi 的方式才能触发 netd 崩溃,去执行 shellcode,我感觉 netd 的进程比 debuggerd 进程有相对更多的权限,比如连接 socket,那我们就可以 su client 端连接以 u:r:netd:s0 的服务端 daemon su,做更多的事情了。


但 debuggerd 方式并不能连接普通权限的 socket 的,看进程名字就可以发现这个了,以 debuggerd 方式去 fork su 的 daemon 当然也不可以,当时我试了几个版本的 su,不行,都被 selinux 拒绝。

 

经过测试,debuggerd 进程也不能访问 /data/ 子目录下的文件,这上面其实浪费了挺长的时间,还写了几类 shellcode 的 client 端和 server 端,尝试用不同的 socket 链接,不管用。


其实可以查询这个 daemon 的 sepolicy 也可以,但发现一些奇怪的点就是在允许的情况下依然不能 bypass 一些规则。尤其到后来的版本,selinux 已经开启了 mls 军事级别,这样不能整体的覆盖sepolicy,必须借用 sepoliyc-inject 一条规则的设置。


如果在上下文相同的情况下,必须某个 App 下的 files 它是 u:object_r:app_data_file:s0,当然可以操作,如果变为 u:object_r:app_data_file:s123 和 u:object_r:app_data_file:s321,当然即使 init 进程上下文的 root 和修改规则也不行,只好用 setcon 修改上下文的动作来实现,比如将u:object_r:app_data_file:s321 改成 u:object_r:app_data_file:s0。总之这里会遇到一些挫折。



2


所以我们获取到 debuggerd 上下文的的 root 进程只能作为一个跳板,进一步去 patch init 进程代码,去拿到更高权限的 root 进程,但是我们首先要获取init进程的bin文件,才能用ida分析他,去 patch 它,但 debuggerd 能读出来,但是无法写入任何目录,所以我当时用的一个笨方法,就是借用 logcat 打印出十六机制字符串,再用工具组合成 init 的 bin 文件,无奈之举,拿出来 init 之后,对照安卓系统的源代码,可以分析出来,当然可以从 boot.img 文件中提取出来。


我这里进入死胡同,非要通过这个进程来提取,init 进程是安卓应用层第一个运行的程序,肯定不会调用任何动态库,它用的所有库都是静态编译到里面,所有的函数名都被 strip 了,其实只要拿到 init 代码,就可以对 init 代码做手脚。


普通的程序是没有权限读取 init 进程,所以无法借用 dirtycow 去 patch,只能借助 root 的进程即 debuggerd 进程,后来我发现有好多这样的像 debuggerd 进程的普通 root 进程,即都是在 /system/bin 下的文件运行起来的进程可以利用。


后来我又尝试通过 /system/bin/installd 拿到 installd 上下文的root进程(当然也可以通过installd进程拿到init bin文件,这样就不用打印日志了),所以有while循环的地方有可能即是我们的利用点,我们回来继续说debuggerd进程,这个进程非常好拿到root权限的进程,只要patch几个字节即可,我发现也很隐蔽。

 


3


通过了解init进程的大部分源代码,它有 for 循环,一直运行这个循环,有需要执行的命令,就去执行,有修改的属性值,它就去修改属性,有些系统进程异常退出,它会把这个进程重启,好了看到这里,先不用着急,先写一个函数,所有的代码都将变成 shellcode,patch 到目标进程,先写伪代码o( ̄︶ ̄)o:


By_pass_all()

{

//**1.bypass selinux****,相当于禁掉selinux****,用自己制作的sepolicy****文件来覆盖系统的sepolicy****文件**

int load_fd = openat(0, "/sys/fs/selinux/load", O_WRONLY);

int new_fd = openat(0, "/data/local/tmp/sepolicy", O_RDONLY);

size_t new_size = lseek(new_fd, 0, SEEK_END);

lseek(new_fd, 0, SEEK_SET);

void *new_contents = mmap(0, new_size, PROT_READ, MAP_PRIVATE, new_fd, 0);

write(load_fd, new_contents, new_size);

close(load_fd);

close(new_fd);

//2.fork su

if(fork()==0)

{

char *argv_su[] = { "/system/bin/oatdump", NULL, NULL };

execve(argv_su[0], argv_su,0);

exit(0);

}

}

我们可以这个函数放在 init 进程不经常调用的位置,直接在以上的 for 循环中直接修改中间无用的指令 BL By_pass_all(),只修改4个字节的指令就可以实现了。


我们发现只有这样才是可行的,1.bypass selinux ;2. fork su ,如果第一步没有,即使拿到init上下文的进程,也由于5.0以上系统的严格的 selinux 限制,也不能为所欲为,由于init进程是非常脆弱的,如果不了解原理,或者 shellcode 写的不好也就重启了,它里面没有任何函数名字,是一个完全的静态的程序。

 

当然还有另外一种比较好的方法就是:我们知道当系统进程比如 debuggerd 进程退出的时候,init进程就会启动 debuggerd 进程,我们找到 init 进程的 start_serice 这个函数里面进程 BL By_pass_all() 函数的调用,就会更加方便快捷。

 

当然更方便的还有,既然我们都绕过 selinux,那我们 fork su 进程的任务其实完全可以交给 debuggerd 或者其他的非 init 进程上下文的进程来做,也更省事了,但是绕过 selinux 的任务必须交给 init 进程来做。

 

以上的方式可以悄无声息的拿到 root,后来我又尝试了 zygote 的进程的fork zygote 上下文的 root 进程,因为 zygote 只要有程序开始运行就会让 zygote 执行一些固定的指令,所以这一步完全可以程序主动让其触发。


随后我又尝试再安装 APK 的时候,比如 pm install *.apk,这时候 installd 这个进程也可以fork出我们想要的 root 进程,我们记住,无论 zygote 或者installd这样的子进程虽然不能有强大的权限,也不能 bypass selinux,但是他具有获取 /data/data 目录下的东西的能力,也就是它能获取我们手机中的任何有价值的数据。


也就是说,我们即使不能运行su,不绕过 selinux,它依然可以做出一个偷别人数据的病毒,由于 5.0 以上不能往 /system/bin 下写文件,这样我们获取的 root 进程具有临时性,但不妨碍我们添加开机添加 boot_completed的广播,每次开机启动都重新获取 rootdirtycow 的利用也相当稳定,好用。

 

有的人会说通过 setprop 属性设置值触发这块 shellcode 执行,调用属性设置的流程代码去 patch,但我试过了,的确在 adb shell 下非常顺利的拿到了6.0的 root ,但是放到 apk 下,setprop 被 selinux 拒绝了,这里就一个鸡生蛋的问题,不得不寻找其他的方法。

 


4


好了以上分析,就是其原理,看懂其原理,我们发现适配遇到的问题。

 

首先我们写4个代码,他们在运用的时候会以 shellcode 运行。


代码1:

Fork_su_by_debugger()

{

if(fork()==0)

{

char *argv_su[] = { "/system/bin/oatdump", NULL, NULL };

execve(argv_su[0], argv_su,0);

exit(0);

}
}


代码2

Call Fork_su_by_debugger()


代码3

Bypass_selinux()

{

int load_fd = openat(0, "/sys/fs/selinux/load", O_WRONLY);

int new_fd = openat(0, "/data/local/tmp/sepolicy", O_RDONLY);
//有的版本限制了读取/data/local/tmp下的文件,需要再利用一次漏洞覆盖其他目录下的文件,app下严格了这一点。

size_t new_size = lseek(new_fd, 0, SEEK_END);

lseek(new_fd, 0, SEEK_SET);

void *new_contents = mmap(0, new_size, PROT_READ, MAP_PRIVATE, new_fd, 0);

write(load_fd, new_contents, new_size);

close(load_fd);

close(new_fd);

}


代码4

Call Bypass_selinux()


我们可以分为4个函数代码:

 

debuggerd进程的自动适配

A. 代码1:Fork_su_by_debugger()这个shellcode 应该放在哪个位置,这个位置应该如何以程序的角度来寻找。

代码2:Call Fork_su_by_debugger() 虽然是一个函数调用的指令只占4个字节的指令,它应该放在程序的那个位置,去替换别的指令,如果这4个字节的指令放在不合适的地方,有可能程序崩溃。

 

Init进程代码的自动适配

代码3和代码4的位置依然存在以上的问题。而且init进程函数的名字被全部去除。

 

谈到适配的问题需要分两步谈适配:


(1)适配debuggerd程序


我们知道 debuggerd 发现别的进程异常退出就会进入处理流程的函数 Process_error() 函数,然后会在这个函数中,patch 修改一些 arm 汇编,填入我们的代码2:call Fork_su_by_debugger(),在填入代码1:Fork_su_by_debugger()相应位置即可,不过不用这么麻烦,我们看proces_error函数内部反汇编的一小段代码:

 

 

我们再把这段代码以文字形式粘贴过来并且做进一步的精简:


if ( !strcmp(&v98, "false")

|| (property_get("ro.debug_level", &v98, "unknown"),

_android_log_print(6, 0, "ro.debug_level = %s", &v98),

strcmp(&v98, "0x4f4c")) )

{

_sprintf_chk(&**v99**, 0, 128, "dumpstate -k -t -z -d -o /data/log/dumpstate_app_native -m %d");

...........................

system(&**v99**);

}

其实看以上的代码正常别的进程崩溃并没有进入if循环,我们只需要把标注颜色的0x4f4c位置改成其他的字符串,这时候程序按照我们的要求就进入if循环内部了,再把另外一个字符串 dumpstate -k -t -z -d -o /data/log/dumpstate_app_native -m %d 换成 /system/bin/su(/system/bin/otadump) 即可,这样这段代码就变成了:


If(1)

{

_sprintf_chk(&**v99**, 0, 128, "/system/bin/oatdump");

...........................

system(&**v99**);

}


其实我们可以再dumpstate -kt -t -z....这段字符串换成很多个指令的组合,比如 Ps ;/system/bin/ls;/system/bin/oatdump;来让system调用,只要长度不超过即可,这样是否达到了通用和简单。


只需要用 Dirtycow 找到这两个字符串进行修改,很轻易的绕过了寻找代码的位置,轻松 fork 出了我们想要的 root 进程,我发现在大部分手机都有这段代码架构而且并没有多大的变化,具备通用型,这一步已实现。


(2)适配init进程:


A. 代码1和代码2已经不用我们考虑,那么代码3和代码4的位置确实一个头痛的问题。

 

假设我们认为 init 进程在每个手机没有做多大的变化,以汇编指令的比较来 patch,其实也是一个好的方法。

 

B. 我们要注意代码3的位置必须不能让额外的代码调用到它,否则会引起未发现的异常,容易定位不出问题在哪里就死机或者重启了,因为 init 进程非常的脆弱。

 

C. 那么代码3 Bypass_selinux() 的函数位置放在哪里,就用程序自动找到它的位置呢,其实我们发现 main 函数的位置可以以程序的角度来进行定位,那么这个函数直接用作替换 main 函数的起始位置呢,发现它是可行的,并且这个位置,不会被其他的代码去调用,如果放在某个so的库函数的位置,那么大量的程序调用它,会造成不可控的局面。


下一步,代码4的位置选择应该如何选择,即 Call Bypass_selinux() 4个字节的指令应该放在哪个位置呢,是不是也存在适配debuggerd进程一样非常方便的方法,发现并没有,如果替换的指令是一个压栈的指令直接导致init进程崩溃重启,而且通用性问题依然得不到解决,即每个手机的每个版本的替换的位置均是那个流程那个函数的具体我们想要的某个位置。

 

D. 我猜想了几种方法,无论采用 init 在大循环中patch还是选择 start_service 函数,其实我个人比较喜欢 patch start_service,比较稳定,如果选择patch start_service 内部,是不是可以采用 inline hook 的方式实现也是很有道理的,即我们在中间找不到具体4个字节的位置,但在函数头部做 inlinehook 还是能很好的定位的。


或者先用符号表找到 /system/lib 下的 so 的函数 shellcode,通过 shellcode 头部比对 init 进程进行函数的识别,再通过函数之间关系进行定位 init 关键代码,也是可以的。

 

E. 如果 init 进程在每个手机代码形式基本保持不变,可以以某些字符串的引用位置做特征,进行函数的定位。

 

当时已实现 root s6edge 5.0、5.16.0系统。代码这里就不放了哈。由于几年前的草率的记录下来,大家请轻喷哈哈~谨以此记录下知识点,大家一起学习交流.

 

后来遗留了几个问题哈:32位5.1下的会很奇怪,过了一个小时不等会重启,但我想内核调试一下,但没有时间来做了。


还有再设置 
sepolicy 的情况下,到了 6.0 以上,必须一条一条的设置规则,如果整体 permissive 也不起作用,还有的直接设置好下一条上一条就失效了。


如果大家做过尝试,欢迎讨论。

 

#参考:

https://www.jianshu.com/p/b6fd45c2df82

https://zhuanlan.zhihu.com/p/25918300

 

利用:

https://github.com/freddierice/trident

https://github.com/matteoserva/dirtycow-arm32

 

vdso方式利用

https://github.com/hyln9/VIKIROOT




- End -



看雪ID:LowRebSwrd  

https://bbs.pediy.com/user-726411.htm  




⚠️ 注意


2019 看雪安全开发者峰会门票正在热售中!

长按识别下方二维码即可享受 2.5折 优惠!





热门文章阅读

1、攻破 Windows AMD 64 平台的 PatchGuard - 清除执行中的 PatchGuard

2、分析强壳的虚拟机原理

3、VMProtect 3.31的OEP之旅




公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com



↙点击下方“阅读原文”,查看更多干货

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存