dirtycow (CVE-2016-5195) 와 Android 6.0
2016년 10월 즈음, 2007년 이후 출시된 리눅스 커널 전체에서 사용이 가능한 취약점인 dirtycow (CVE-2016-5195) 가 발견됩니다.
리눅스 커널을 사용하는 안드로이드 OS 기반의 스마트폰에서도 물론 사용이 가능합니다.
많은 사용자들이 이 취약점 발견으로 인해 루팅이 쉬워지거나, 루팅이 되지 않던 기종에서 루팅이 되기를 기대하고 있을겁니다.
하지만, 안드로이드와 SELinux 는 호락호락하지 않았답니다.
구글이 안드로이드 4.대부터 SELinux 를 탑재하기 시작했고, 현재 대부분의 제조사들이 채택하고 있는 Android 6.0.1 은 Enforcing 모드일 경우 매우 강하게 권한 제한을 겁니다.
바이너리가 아무리 uid 0, gid 0 인 root 권한을 가지고 있더라도 SELinux Context 가 제한하는 범위 내에서만 활동이 가능하죠.
dirtycow 는 read 권한이 있는 바이너리만 갈아치울 수 있습니다.
갈아치울 수 있고, 특수한 권한을 가진 바이너리 목록을 보려면 /system/bin/ 내에서 ( ls -lZ 2>/dev/null ) | grep -v system_file 를 쳐보시면 됩니다.
제 기기는 매우 강한 SELinux policy를 가지고 있군요, 삼성이나 LG 기기보다도요.
shell@ef71s:/system/bin $ ( ls -lZ 2>/dev/null ) | grep -v system_file
-rwxr-xr-x root shell u:object_r:zygote_exec:s0 app_process32
-rwxr-xr-x root shell u:object_r:cpmgrt_exec:s0 cpmgrt
-rwxr-xr-x root shell u:object_r:dex2oat_exec:s0 dex2oat
-rwxr-xr-x root shell u:object_r:qlogd_exec:s0 diag_mdlog
-rwxr-xr-x root shell u:object_r:logcat_exec:s0 logcat
-rwxr-xr-x root shell u:object_r:ssa_test_exec:s0 oem_ssa_test
-rwxr-xr-x root shell u:object_r:dex2oat_exec:s0 patchoat
-rwxr-x--- root shell u:object_r:runas_exec:s0 run-as
-rwxr-xr-x root shell u:object_r:shell_exec:s0 sh
-rwxr-xr-x root shell u:object_r:toolbox_exec:s0 toolbox
-rwxr-xr-x root shell u:object_r:toolbox_exec:s0 toybox
일례로 /system/bin/run-as를 hijack 해 루트쉘을 띄울 수 있습니다.
이건 현재 발견된 dirtycow 취약점으로 매우 쉽게 가능합니다. 게다가 run-as 는 SetUID 권한까지 가지고 있는, SELinux 만 없었다면 무적인 바이너리입니다.
하지만 Android 6.0의 run-as 에는 u:r:run-as_exec:s0 이라는 context 가 붙습니다.
run-as 를 hijack 해서 root shell 을 띄운다고 해도, 활용 가능한 범위은 u:r:run-as_exec:s0 가 제한하는 만큼입니다.
ls /data/ 를 하는 순간 open dir failed, permission denied 를 내뱉습니다.
이렇게 안드로이드 6.0의 SELinux 정책은 매우 강력한데, 이를 뚫어보기 위해 제가 진행한 몇가지 삽질을 공개해볼까 합니다.
아래에서 소개하는 context 는 모두 root shell 상태일 때 기준입니다. (uid 0, gid 0)
대부분의 context 가 가진 권한은 (제조사의 커스터마이징 제외) androidSRC/external/sepolicy, androidSRC/device/qcom/common/sepolicy 등 에서 확인하실 수 있습니다.
0. u:r:shell:s0
run-as 를 hijack 하여
ret = (*setcon_p)("u:r:shell:s0");
코드만 넣어주면 손쉽게 얻을 수 있는 context 입니다.
활용도가 극히 제한되며, /system remount 는 상상도 할 수 없습니다.
1. u:r:system_server:s0
app_process (zygote) 를 hijack 하여 nc reverse shell 을 열어 접근하는게 편합니다.
작업이 진행되는 동안, android OS 는 zygote 가 갈아치워졌으므로 무한 부트애니메이션이나 시스템이 응답하지 않는 상태가 초래됩니다.
이 권한도 활용도가 상당히 제한됩니다.
/system remount 나 block device 에 바로 접근은 불가합니다.
그나마 제일 쓸모있는 권한입니다.
setprop ctl.restart
setprop ctl.start
등을 활용해 서비스를 실행시키거나 재실행이 가능합니다.
이는 서비스 바이너리를 dirtycow 로 갈아치운 후 실행시킴으로써 그 서비스가 갖는 고유 context 로 원하는 바이너리 실행이 가능해집니다.
일례로 Android OS 는 recovery 파티션이 손상되거나 변조되면 강제로 제조사가 지정한 파일로 덮어씌우는 서비스가 있습니다. [flash_recovery]
/system/bin/applypatch 를 원하는 내용으로 갈아치운 후(dirtycow), setprop ctl.start flash_recovery 로 서비스를 실행시킨다면
applypatch 는 flash_recovery 의 권한으로 실행됩니다.
이 때 applypatch 에서는 boot 파티션 read와 recovery 파티션 read/write 가 가능합니다.
bootloader lock 이 걸려있지 않은 기기는, 이것을 활용해 permissive boot 를 제작해 recovery 에 플래슁 한뒤 리커버리로 부팅할 수 있습니다.
관련된 재미있는 소스입니다.
https://github.com/jcadduono/android_external_dirtycow
이 후에 얻는 대부분의 context 는 system_server 의 서비스 실행을 통해 얻어집니다.
2. u:r:adbd:s0
얻긴 쉬운데 짜증나는 권한입니다.
얻으려면 /sbin/adbd 를 hijack 해야합니다.
이 뜻은, dirtycow 를 하는 순간 PC 와 스마트폰간의 ADB 컨넥션이 끊어진단걸 의미합니다.
이를 대비하기 위해, 포트가 다른 2가지 nc reverse shell 바이너리를 제작합니다.
제작한 바이너리를 /system/bin/applypatch 나 /system/xbin/dexdump 같이 만만한 바이너리에 dirtycow 해놓습니다.
그리고 /system/bin/app_process 를 /data/local/tmp/app_process 같은 곳에 백업해둡니다.
* adb shell 상태에서는 /data/local/tmp/ 에 접근이 가능하지만,
* u:r:system_server:s0 은 /data/local/tmp/나 /sdcard 에 접근 권한이 없습니다.
app_process 를 hijack 해 루트쉘을 땁니다.
nohup를 적극 활용합니다.
nohup applypatch &
nohup dexdump &
해놓고 백그라운드로 reverse shell 바이너리들을 띄워둡니다. [이제 저 reverse shell 바이너리는 u:r:system_server:s0 의 권한을 갖게됩니다.]
원래 reverse shell 을 PC 에서 nc 를 활용하여 접근하기 위해서는
adb forward tcp:11112 tcp:11112
nc -v localhost 11112
이런식으로 ADB 를 거쳐 포트를 따왔습니다.
우리는 adbd 를 갈아치울것이기 때문에, 저렇게 ADB 를 통해서는 안됩니다.
그래서 네트워크를 활용합니다.
하지만, 현재 app_process 를 hijack 한 상태이므로 휴대폰의 OS 는 응답이 없습니다.
또한 이전에 wifi 네트워크에 연결이 되어있었더라도 시스템이 죽으면서 접근이 불가해집니다. [wpa_supplicant 가 동반자살하기 때문이죠]
그래서 우리는 저렇게 nohup 로 reverse shell server 을 백그라운드로 열어두고, 시스템을 정상화 시킨 뒤, wifi 네트워크를 잡은 후에 PC에서 네트워크로 접근할겁니다.
백그라운드로 reverse shell 을 열었으면 이제 app_process 를 정상화시킵니다.
adb shell로 백업해둔 바이너리를 활용해 정상으로 만듭니다. [./dirtycow /system/bin/app_process ./app_process]
그리고 열려있는 nc reverse shell 을 exit 해주면 자동으로 app_process 를 시스템이 재 실행시키면서 원본 app_process 가 로드되고, 시스템이 정상부팅 됩니다.
시스템 부팅이 끝나면 WIFI 를 잡습니다.
네트워크 연결이 되면 USB 포트를 뽑고
nc -v 192.168.0.39 11113 - 1
nc -v 192.168.0.39 11113 - 2
두개의 터미널을 열고, 하나는 로깅용, 하나는 명령용으로 사용하시면 됩니다.
/sbin/adbd dirtycow 방법은 설명 안해도 아시리라 믿습니다. ^^;
하지만 진행해본 결과 u:r:adbd:s0 을 얻어도, u:r:init:s0 을 얻을 수는 없더군요
02-12 01:33:43.311 29210 29210 I exploit : STARTING
02-12 01:33:43.311 29210 29210 I exploit : uid 0
02-12 01:33:43.311 29210 29210 I exploit : uid 0
02-12 01:33:43.313 29210 29210 I exploit : 0 u:r:adbd:s0
02-12 01:33:43.295 29210 29210 W adbd : type=1400 audit(0.0:1663):
avc: denied { setcurrent } for scontext=u:r:adbd:s0 tcontext=u:r:adbd:s0
tclass=process permissive=0
02-12 01:33:43.305 29210 29210 W adbd : type=1400 audit(0.0:1664):
avc: denied { net_raw } for capability=13 scontext=u:r:adbd:s0
tcontext=u:r:adbd:s0 tclass=capability permissive=0
02-12 01:33:43.314 29210 29210 I exploit : context 0 u:r:adbd:s0
이는 adbd.te 내에서 빌드 형식에 따라 권한을 제한시키기 때문입니다.
userdebug_or_eng(`
allow adbd self:process setcurrent;
allow adbd su:process dyntransition;
')
3. init 갈아치위 새 sepolicy 로딩하기
계속 SELinux context 때문에 사람들이 고생하자, 이를 보다 못한 matteoserva 는 유저는 sepolicy 를 reload 할 수 있는 /init 바이너리를 분석하고 리버싱하기 시작합니다.
그렇게 안드로이드 OS 에서 주기적으로 실행하는 /init 바이너리 내의 bootchart_sample() 라는 함수에서 착안, /init 를 리버싱해서 bootchart_sample() 함수 내의 내용을
/data/local/tmp 에 있는 sepolicy로 reload 하는 방법을 고안해냅니다.
매우 좋은 시도이고, 많은 기종들에게 방법이 열릴 수 있었지만, 제 기기에는 아니였습니다.
방법대로 /init 와 sepolicy 를 수정에는 성공하였지만, 망할 제 sepolicy 는 /init 를 read 할 권한을 주지 않았습니다.
즉, 갈아치울 바이너리는 만들어놨는데 권한 부족으로 갈아치울수가 없단것이죠.
안드로이드가 정말 독종인게 u:r:system_server:s0 에게도 /init read 권한이 허용되지 않았습니다.
https://github.com/matteoserva/dirtycow-arm32
관련 소스입니다.