CVE-2017-5123 Analysis - Linux Kernel Vulnerability(Privilege Escalation)
커널은 사용자 메모리에 접근해서 사용자 요청에 따라 read/write을 해야한다. 따라서 2가지 방법이 있는데,
copy_from_user 함수를 사용해서 사용자 메모리에서 데이터를 복사하거나,
일시적으로 SMAP를 비활성화(user_access_begin() 호출)해서 커널이 사용자 메모리에 직접 접근해서 오버헤드를 방지하고, 대량의 데이터에 대한 읽기/쓰기 작업 속도를 높인다.
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 를 활성화한다.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_user
와 unsafe_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이 그곳에 유저가 요청하는거 쓰는 것을 가능하도록 한다.
#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())); \
})
__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
에서 infop
가 unsafe_put_user
함수의 ptr로 들어가므로, 이 infop를 제어할 수 있으면 원하는 곳에 원하는 값을 쓸 수 있다.
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
[1day Analysis] CVE-2022-37958 취약점 분석 (0) | 2023.07.25 |
---|---|
CVE-2021-3156 : Privilege Escalation by Sudo Vulnerability (0) | 2021.09.11 |
CVE-2020-15257 : Docker host escape vulnerability using host networking (0) | 2021.06.30 |
CVE-2021-3493: Kernel Vulnerability in Overlayfs (0) | 2021.05.30 |