该漏洞是Linux内核的内存子系统在处理写时拷贝(Copy-on-Write)时存在条件竞争漏洞,导致可以破坏私有只读内存映射。,利用此漏洞获取其他只读内存映射的写权限,进一步获取root权限。
看了一些blog,这里就不讲其原理了,网上已经很多了哈,好像常见的利用方法就是如此:
patch run-as
打开wifi开关,实现ned进程重启,patch init进程,覆盖sepolicy,获取root
Patch vdso,反弹shell
Patch zygote 替换,hook逻辑分发。
…………………
假设读者已对此漏洞的原理非常熟悉的情况下,我们来看以下在实践过程中遇到的问题。在以上方式均尝试的过程中,发现并不能很好的适配或者很有效的root,经常会造成死机,所以一边尝试一边在学习,并在利用的过程中加入了自己的一些理解。
1. 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的广播,每次开机启动都重新获取root,dirtycow的利用也相当稳定,好用。
有的人会说通过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个函数代码,以下我们以其
1.debuggerd进程的自动适配
A. 代码1:Fork_su_by_debugger()这个shellcode应该放在哪个位置,这个位置应该如何以程序的角度来寻找,代码2:Call Fork_su_by_debugger()虽然是一个函数调用的指令只占4个字节的指令,它应该放在程序的那个位置,去替换别的指令,如果这4个字节的指令放在不合适的地方,有可能程序崩溃。
2.Init进程代码的自动适配
代码3和代码4的位置依然存在以上的问题。而且init进程函数的名字被全部去除。
谈到适配的问题需要分两步谈适配,
- 适配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进程,我发现在大部分手机都有这段代码架构而且并没有多大的变化,具备通用型,这一步已实现。
- 适配init进程:
1.代码1和代码2已经不用我们考虑,那么代码3和代码4的位置确实一个头痛的问题。
假设我们认为init进程在每个手机没有做多大的变化,以汇编指令的比较来patch,其实也是一个好的方法。
2.我们要注意代码3的位置必须不能让额外的代码调用到它,否则会引起未发现的异常,容易定位不出问题在哪里就死机或者重启了,因为init进程非常的脆弱。
3.那么代码3 Bypass_selinux()的函数位置放在哪里,就用程序自动找到它的位置呢,其实我们发现main函数的位置可以以程序的角度来进行定位,那么这个函数直接用作替换main函数的起始位置呢,发现它是可行的,并且这个位置,不会被其他的代码去调用,如果放在某个so的库函数的位置,那么大量的程序调用它,会造成不可控的局面。下一步,代码4的位置选择应该如何选择,即Call Bypass_selinux() 4个字节的指令应该放在哪个位置呢,是不是也存在适配debuggerd进程一样非常方便的方法,发现并没有,如果替换的指令是一个压栈的指令直接导致init进程崩溃重启,而且通用性问题依然得不到解决,即每个手机的每个版本的替换的位置均是那个流程那个函数的具体我们想要的某个位置。
4.我猜想了几种方法,无论采用init 在大循环中patch还是选择start_service函数,其实我个人比较喜欢patch start_service,比较稳定,如果选择patch start_service内部,是不是可以采用inline hook的方式实现也是很有道理的,即我们在中间找不到具体4个字节的位置,但在函数头部做inlinehook 还是能很好的定位的。或者先用符号表找到/system/lib下的so的函数shellcode,通过shellcode头部比对init进程进行函数的识别,再通过函数之间关系进行定位init关键代码,也是可以的。
5.如果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
··