
PoC CVE-2017-5123 - LPE - Bypass di SMEP/SMAP. Senza KASLR
PoC CVE-2017-5123 - LPE - Bypass di SMEP/SMAP. No KASLR
In questo piccolo writeup analizzerò una vulnerabilità del kernel che ci consente di ottenere il privilegio root.
Questo file è diviso in quattro parti:
Voglio sottolineare che ci sono molti modi migliori per sfruttare questa CVE (in effetti, questo è solo un PoC per imparare il kernel, non può essere usato in the wild) ma penso che questa metodologia possa essere utile come introduzione allo sfruttamento del kernel.
Questa vulnerabilità è stata introdotta in 4c48abe91be0, quindi dobbiamo compilare quella versione del kernel.
Questo può essere un po' complicato perché è una vecchia versione e il codice dovrebbe essere patchato.
Ho creato un repository con un codice del kernel già patchato e un file .config così puoi clonare e compilare.
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
Nota: questo kernel verrà compilato con driver virtio, così puoi usare virtio disk per condividere file da/verso la VM.
Ora creeremo la rootfs iniziale:
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
Poi dovremmo installare una distribuzione Linux di base, ad esempio usando pacstrap o debootstrap.
sudo pacstrap /tmp/mount1 base base-devel vim
Infine, possiamo modificare il sistema:
# 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
Se tutto è in ordine, possiamo ora provare il nostro sistema di test con 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
La descrizione della CVE dice che c'è un'operazione di scrittura non limitata durante la system call waitid.
Apriamo kernel/exit.c e osserviamo il codice:
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;
}
Questa funzione è abbastanza semplice: dopo alcuni controlli, ci sono varie chiamate a unsafe_put_user(...) e la funzione ritorna.
La parte principale di questa funzione è composta dalla funzione unsafe_put_user(...), quindi spostiamoci lì (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)
C'è un grande avvertimento nel commento: se vuoi usare unsafe_put/get_user, dovresti prima chiamare access_ok() e circondarli con user_access_begin/end().
Se guardiamo il codice precedente (waitid), possiamo vedere che access_ok() non viene mai chiamato, quindi la system call viola questo avvertimento.
Ma cosa sono queste macro?
SMAP e SMEP sono due funzionalità di sicurezza introdotte nel kernel per rendere più difficile scrivere exploit. Da notare che queste funzionalità sono applicate dalla CPU.
SMEP impedisce di eseguire codice nello spazio utente mentre la CPU è in modalità supervisore; SMAP, invece, blocca l'accesso di lettura/scrittura alla memoria utente.
Il kernel deve scrivere/leggere dati dalla/alla memoria utente e questo può essere fatto in due modi:
copy_from_user) che permettono di copiare la memoria nello spazio del kernel;Come possiamo vedere nella definizione di unsafe_put_user, questa funzione copia solo il valore di x nella memoria puntata da ptr (e salta a err_label se c'è un errore). Abbiamo appena detto che il kernel non può accedere allo spazio utente a causa di SMAP, ed è per questo che queste funzioni dovrebbero essere racchiuse tra user_access_begin/end().
#define __uaccess_begin() stac()
#define __uaccess_end() clac()
Come possiamo vedere, user_access_begin/end sono semplicemente le istruzioni ASM stac e clac.