
PoC CVE-2017-5123 - LPE - SMEP/SMAP 우회. KASLR 없음
PoC CVE-2017-5123 - LPE - SMEP/SMAP 우회. KASLR 없음
이 간단한 분석에서는 root 권한을 얻을 수 있는 커널 취약점을 분석하겠습니다.
이 문서는 네 부분으로 구성됩니다:
이 CVE를 익스플로잇하는 더 좋은 방법이 많다는 점을 지적하고 싶습니다(실제로 이것은 커널 학습을 위한 _PoC_일 뿐이며 _실전_에서는 사용할 수 없습니다). 하지만 이 방법론이 커널 익스플로잇 입문에 유용할 수 있다고 생각합니다.
이 취약점은 _4c48abe91be0_에서 도입되었으므로 해당 버전의 커널을 빌드해야 합니다.
이것은 오래된 버전이고 코드에 패치가 필요하기 때문에 약간 까다로울 수 있습니다.
이미 패치된 커널 코드가 있는 저장소와 .config 파일을 만들었으므로 _클론하고 빌드_할 수 있습니다.
git clone https://github.com/c3r34lk1ll3r/kernel_mirror.git
cd kernel_mirror
git checkout origin/modified_v4.14
wget https://gist.githubusercontent.com/c3r34lk1ll3r/c9c34ae86140cc7a24d0d90141686ee8/raw/52431b577a71e3fe8f89d6ce355ce9c1c54c53b6/.config
make -j 8 --output-sync=recurse
참고: 이 커널은 virtio 드라이버로 빌드되므로 VM과 파일을 공유하기 위해 _virtio 디스크_를 사용할 수 있습니다.
이제 초기 _rootfs_를 생성합니다:
qemu-img create -f raw hda.raw 10G
# Format the disk to ext4
mkfs.ext4 ./hda.raw
# Make a mountpoint for the image
mkdir /tmp/mount1
# Mount the disk
sudo mount -o loop ./hda.raw /tmp/mount1
그런 다음 기본 Linux 배포판을 설치해야 합니다. 예를 들어 pacstrap 또는 debootstrap을 사용합니다.
sudo pacstrap /tmp/mount1 base base-devel vim
마지막으로 시스템을 수정합니다:
# Add a 'test' user
echo 'test:x:1000:1000::/home/test:/bin/bash' | sudo tee -a /tmp/mount1/etc/passwd
# without password
echo 'test::14871::::::' | sudo tee -a /tmp/mount1/etc/shadow
# we can mount a virtio disk in order to share files between host and guest
echo '/transient /home/test/shared 9p trans=virtio,version=9p2000.L,rw,user,exec 0 0' | sudo tee -a /tmp/mount1/etc/fstab
sudo mkdir -p /tmp/mount1/home/test/shared
# It is usefull to have sudo permission
echo '%wheel ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /tmp/mount1/etc/sudoers
echo 'wheel:x:998:test' | sudo tee -a /tmp/mount1/etc/group
sudo chown -R 1000:1000 /tmp/mount1/home/test
sudo umount /tmp/mount1
모든 것이 정상이면 _qemu_로 테스트 시스템을 시험해볼 수 있습니다:
qemu-system-x86_64 \
-kernel ./kernel_mirror/arch/x86_64/boot/bzImage \
-hda ./hda.raw \
-m 4G \
-cpu "Skylake-Client-IBRS,ss=on,vmx=on,hypervisor=on,tsc-adjust=on,clflushopt=on,umip=on,md-clear=on,stibp=on,arch-capabilities=on,ssbd=on,xsaves=on,pdpe1gb=on,ibpb=on,amd-ssbd=on,skip-l1dfl-vmentry=on,hle=off,rtm=off" \
-smp 4 \
-vga virtio \
-enable-kvm \
-nographic \
-machine type=q35,accel=kvm \
-virtfs "fsdriver=local,id=fs.1,path=./trans_fs,security_model=mapped,writeout=immediate,mount_tag=/transient" \
-append "root=/dev/sda rw noquiet nokaslr console=ttyS0 loglevel=5" \
-chardev "vc,id=vc.0,cols=1920,rows=1080" \
-net "user,hostfwd=tcp::10022-:22" \
-net "nic" \
-s
CVE 설명에 따르면 waitid 시스템 호출 중에 제한되지 않은 쓰기 작업이 있습니다.
kernel/exit.c를 열고 코드를 봅시다:
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;
}
이 함수는 매우 간단합니다. 몇 가지 검사 후 unsafe_put_user(...)에 대한 여러 호출이 있고 함수가 반환됩니다.
이 함수의 주요 부분은 unsafe_put_user(...) 함수로 구성되어 있으므로 (arch/x86/include/asm/uaccess.h)로 이동합니다:
/*
* 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)
주석에는 크고 뚱뚱한 경고가 있습니다: unsafe_put/get_user를 사용하려면 먼저 access_ok()를 호출하고 user_access_begin/end()로 감싸야 합니다.
이전 코드(waitid)를 살펴보면 access_ok()가 호출되지 않았으므로 시스템 호출이 이 _경고_를 _위반_합니다.
하지만 그 매크로들은 무엇일까요?
_SMAP_와 _SMEP_는 익스플로잇 작성을 어렵게 하기 위해 커널에 도입된 두 가지 보안 기능입니다. 이 기능들은 CPU에 의해 강제된다는 점에 유의하십시오.
_SMEP_는 CPU가 수퍼바이저 모드에 있을 때 사용자 공간 코드를 실행하는 것을 방지합니다. 반면 _SMAP_는 사용자 메모리에 대한 읽기/쓰기 액세스를 차단합니다.
커널은 사용자 메모리에 데이터를 쓰거나 읽어야 하며, 이는 두 가지 방법으로 수행할 수 있습니다:
copy_from_user)가 있습니다.unsafe_put_user의 정의에서 볼 수 있듯이 이 함수는 ptr이 가리키는 메모리에 x의 값만 복사합니다(오류가 있으면 err_label로 점프). 우리는 SMAP 때문에 커널이 사용자 공간에 액세스할 수 없다고 말했으며, 이것이 바로 이러한 함수들이 user_access_begin/end()로 감싸져야 하는 이유입니다.
#define __uaccess_begin() stac()
#define __uaccess_end() clac()
기본적으로 이 두 매크로는 _SMAP_를 활성화/비활성화합니다.
이전의 "경고"는 access_ok 함수도 언급합니다:
/**
* access_ok: - Checks if a user space pointer is valid
* @type: Type of access: %VERIFY_READ or %VERIFY_WRITE. Note that
* %VERIFY_WRITE is a superset of %VERIFY_READ - if it is safe
* to write to a block, it is always safe to read from it.
* @addr: User space pointer to start of block to check
* @size: Size of block to check
*
* Context: User context only. This function may sleep if pagefaults are
* enabled.
*
* Checks if a pointer to a block of memory in user space is valid.
*
* Returns true (nonzero) if the memory block may be valid, false (zero)
* if it is definitely invalid.
*
* Note that, depending on architecture, this function probably just
* checks that the pointer is in the user space range - after calling
* this function, memory access functions may still return -EFAULT.
*/
#define access_ok(type, addr, size) \
({ \
WARN_ON_IN_IRQ(); \
likely(!__range_not_ok(addr, size, user_addr_max())); \
})
여기서 주석은 자명합니다: 이 매크로는 포인터가 유효한 사용자 공간 포인터인지 확인합니다.
waitid 코드를 다시 살펴보겠습니다:
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();
이미 짐작하셨겠지만, access_ok()의 부재로 인해 infop 포인터가 공격자에 의해 완전히 제어되므로 메모리 어디에나 _임의 쓰기_가 가능합니다.