상세 컨텐츠

본문 제목

CVE-2017-5123 Analysis - Linux Kernel Vulnerability(Privilege Escalation)

ANALYSIS/Vulnerability Analysis

by koharin 2021. 5. 9. 02:15

본문

728x90
반응형

Description

  • upstream 커널의 waitid 구현 시 access_ok 함수를 이용한 체크의 부재로 타겟 목적지에 정보를 복사하는 것을 제한하지 않아서, 로컬의 사용자는 임의의 커널 메모리에 쓸 수 있고, 이는 권한 상승(privilege escalation)으로 이어질 수 있다.

 

Background

SMEP (Supervisor Mode Execution Prevention)

  • User Mode의 실행 코드를 Ring-0 권한으로 제한하여 신뢰할 수 없는 애플리케이션의 메모리 실행을 방지하는 커널 보호 기법
  • EoP exploit 실행하지 못하도록 하여 EoP(Escalation of Privilege) 공격 방지
  • Supervisor Mode에서 명령 패치 및 코드 실행 제어
  • 커널이 user mode 코드를 실행하지 못하도록 한다.

 

SMAP (Supervisor Mode Access Prevention)

  • Supervisor Mode의 data access 방지하도록 하는 커널 보호 기법
  • supervisor mode에서 코드 실행 시 사용자의 read/write/execute 접근 방지

 

커널의 Read/Write

커널은 사용자 메모리에 접근해서 사용자 요청에 따라 read/write을 해야한다. 따라서 2가지 방법이 있는데,

copy_from_user 함수를 사용해서 사용자 메모리에서 데이터를 복사하거나,

일시적으로 SMAP를 비활성화(user_access_begin() 호출)해서 커널이 사용자 메모리에 직접 접근해서 오버헤드를 방지하고, 대량의 데이터에 대한 읽기/쓰기 작업 속도를 높인다.

  • 시스템콜 처리 동안, 커널은 (시스템콜을 발생시킨) 프로세스의 메모리로부터 읽거나 메모리에 쓸 수 있어야 한다. 이것을 위해 커널에는 copy_from_user, put_user 등의 함수가 있다.
    • copy_from_user() : copy data from userland (read)
    • put_user() : copy data to userland (write)
put_user(x, void __user *ptr)
    if (access_ok(VERIFY_WRITE, ptr, sizeof(*ptr)))
        return -EFAULT
    user_access_begin()
    *ptr = x
    user_access_end()
  • access_ok() : ptr이 커널 메모리가 아닌 userland에 있는지 확인한다.
  • user_access_begin() : SMAP 비활성화
  • user_access_end() : SMAP 활성화
  • access_ok() 체크를 통과하면, 커널이 사용자 메모리에 지정한 값을 쓸 수 있게 하기 위해 user_access_begin()을 호출해서 SMAP를 비활성화하는데, 이는 커널이 userland에 접근할 수 있도록 하는 것이다. 이것으로 커널은 ptr이 가리키는 메모리에 x 값을 쓴 후(*ptr = x) user_access_end() 호출로 SMAP 를 활성화한다.
  • 커널의 메모리 read/write 동안은 user access function은 page fault를 처리 중이므로 unmapped 메모리에 접근할 때 crash를 발생시키지 않는다.

 

Vulnerability

SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *, 
                infop, int, options, struct rusage __user *, ru)
{
    struct rusage r;
    struct waitid_info info = {.status = 0};
    long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
    int signo = 0;

    if (err > 0) {
        signo = SIGCHLD;
        err = 0;
        if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
            return -EFAULT;
        }
        if (!infop)
            return err;

        user_access_begin();
        unsafe_put_user(signo, &infop->si_signo, Efault);
        unsafe_put_user(0, &infop->si_errno, Efault);
        unsafe_put_user(info.cause, &infop->si_code, Efault);
        unsafe_put_user(info.pid, &infop->si_pid, Efault);
        unsafe_put_user(info.uid, &infop->si_uid, Efault);
        unsafe_put_user(info.status, &infop->si_status, Efault);
        user_access_end();
        return err;
Efault:
        user_access_end();
        return -EFAULT;
}

특정 시스템콜은 커널과 userland 사이에서 데이터를 복사(copy)하기 위해 put_user 또는 get_user 함수를 호출이 필요하다.

/*
 * The "unsafe" user accesses aren't really "unsafe", but the naming
 * is a big fat warning: you have to not only do the access_ok()
 * checking before using them, but you have to surround them with the
 * user_access_begin/end() pair.
 */
#define user_access_begin()    __uaccess_begin()
#define user_access_end()    __uaccess_end()

#define unsafe_put_user(x, ptr, err_label)                    \
do {                                        \
    int __pu_err;                                \
    __typeof__(*(ptr)) __pu_val = (x);                    \
    __put_user_size(__pu_val, (ptr), sizeof(*(ptr)), __pu_err, -EFAULT);    \
    if (unlikely(__pu_err)) goto err_label;                    \
} while (0)

#define unsafe_get_user(x, ptr, err_label)                    \
do {                                        \
    int __gu_err;                                \
    __inttype(*(ptr)) __gu_val;                        \
    __get_user_size(__gu_val, (ptr), sizeof(*(ptr)), __gu_err, -EFAULT);    \
    (x) = (__force __typeof__(*(ptr)))__gu_val;                \
    if (unlikely(__gu_err)) goto err_label;                    \
} while (0)

반복적인 체크와 SMAP 활성화/비활성화에서의 추가 오버헤드(Background의 put_user 함수 확인)를 피하기 위해, 커널 개발자는 unsafe 버전인 __put_userunsafe_put_user 함수를 추가했다. 새롭게 추가된 함수들에는 시스템 콜에서 user의 ptr이 유효한 user space를 가리키는지에 대한 체크가 없다.

또한 기존의 put_user() 함수에는 함수 내부에 다음의 코드가 있다.

user_access_begin()
*ptr = x
user_access_end()

user_access_begin()으로 SMAP을 비활성화하고 커널이 ptr이 가리키는 곳에 쓰고(write), 작업이 끝나면 바로 user_access_end()로 SMAP를 활성화한다.

하지만 unsafe_write_user나 unsafe_get_user 함수 내부에는 SMAP 키고 끄는거 없이 그냥 write, read 한다.

따라서 커널이 waitid() 시스템콜로 write 요청받을 때 한번 user_access_begin()으로 SMAP 비활성화(supervisor mode의 데이터 접근 방지)하고 계속 unsafe_put_user 함수 호출하는데, 이 unsafe_put_user에는 또 access_ok가 없어서 user ptr가 kernel space 가리키는지 체크하지 않아 ptr로 kernel space 가리키면서 kernel이 그곳에 유저가 요청하는거 쓰는 것을 가능하도록 한다.

access_ok()

#define user_addr_max() (current->thread.addr_limit.seg)

...

/*
 * Test whether a block of memory is a valid user space address.
 * Returns 0 if the range is valid, nonzero otherwise.
 */
static inline bool __chk_range_not_ok(unsigned long addr,  
                                unsigned long size, unsigned long limit)
{
    /*
     * If we have used "sizeof()" for the size,
     * we know it won't overflow the limit (but
     * it might overflow the 'addr', so it's
     * important to subtract the size from the
     * limit, not add it to the address).
     */
    if (__builtin_constant_p(size))
        return unlikely(addr > limit - size);

    /* Arbitrary sizes? Be careful about overflow */
    addr += size;
    if (unlikely(addr < size))
        return true;
    return unlikely(addr > limit);
}

#define __range_not_ok(addr, size, limit)                \
({                                    \
    __chk_user_ptr(addr);                        \
    __chk_range_not_ok((unsigned long __force)(addr), size, limit); \
})

...

#define access_ok(type, addr, size)                    \
({                                    \
    WARN_ON_IN_IRQ();                        \
    likely(!__range_not_ok(addr, size, user_addr_max()));        \
})
  • ptr(user specified pointer)이 kernel space가 아닌 user space를 가리키는지 체크한다.
  • 따라서 권한이 없는 사용자는 임의의 커널 메모리에 쓸 수 없도록 한다.
  • __range_not_ok 함수를 통해 address limit을 체크하여 user의 ptr이 kernel space가 아닌 접근가능한 user space를 가리키고 있는지 체크한다.

kernel 4.12 버전에서, waitid 시스템콜은 unsafe_put_user 함수를 사용하는 것으로 업데이트되었고, unsafe_put_user 함수는 체크를 하지 않기 때문에 access_ok() 함수 호출을 하지 않는다. 따라서 이 부분이 취약한 원인이다.

(kernel 4.12와 4.13 버전이 영향을 받는다.)

따라서 해당 취약점은 권한없는(unprivileged) 사용자는 waitid() 호출해서 infop를 사용해서 특정 커널 주소를 가리킬 수 있고, 커널은 거기에 write한다.

waitid에서 infopunsafe_put_user 함수의 ptr로 들어가므로, 이 infop를 제어할 수 있으면 원하는 곳에 원하는 값을 쓸 수 있다.

 

Exploitation

  • waitid 시스템콜 내 unsafe_put_user() 함수에서 infop를 조작해서 프로세스의 UID를 0으로 만드는 것으로 privilege escalation을 할 수 있다.

 

Reference

kernel/git/torvalds/linux.git - Linux kernel source tree
Exploiting CVE-2017-5123 with full protections. SMEP, SMAP, and the Chrome Sandbox!
Exploiting CVE-2017-5123
Kernel exploitation - CVE-2017-5123 PoC e Writeup

728x90
반응형

관련글 더보기