
PoC CVE-2017-5123 - LPE - Contornando SMEP/SMAP. Sem KASLR
Prova de Conceito CVE-2017-5123 - EPL - Contornando SMEP/SMAP. Sem KASLR
Neste pequeno writeup, analisarei uma vulnerabilidade do kernel que nos permite obter privilégio root.
Este arquivo está dividido em quatro partes:
Quero destacar que existem muitas maneiras melhores de explorar este CVE (de fato, isso é apenas um PoC para aprender o kernel, não pode ser usado in the wild), mas acredito que esta metodologia pode ser útil como uma introdução à exploração do kernel.
Esta vulnerabilidade foi introduzida em 4c48abe91be0, então precisamos compilar essa versão do kernel.
Isso pode ser um pouco complicado porque é uma versão antiga e o código deve ser corrigido.
Criei um repositório com um código de kernel já corrigido e um arquivo .config para que você possa clonar e compilar.
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
Observe que este kernel será compilado com drivers virtio para que você possa usar virtio disk para compartilhar arquivos de/para a VM.
Agora, criaremos o rootfs inicial:
qemu-img create -f raw hda.raw 10G
# Formata o disco para ext4
mkfs.ext4 ./hda.raw
# Cria um ponto de montagem para a imagem
mkdir /tmp/mount1
# Monta o disco
sudo mount -o loop ./hda.raw /tmp/mount1
Em seguida, devemos instalar uma distribuição Linux básica, por exemplo usando pacstrap ou debootstrap.
sudo pacstrap /tmp/mount1 base base-devel vim
Finalmente, podemos modificar o sistema:
# Adiciona um usuário 'test'
echo 'test:x:1000:1000::/home/test:/bin/bash' | sudo tee -a /tmp/mount1/etc/passwd
# Sem senha
echo 'test::14871::::::' | sudo tee -a /tmp/mount1/etc/shadow
# Podemos montar um disco virtio para compartilhar arquivos entre host e convidado
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
# É útil ter permissão sudo
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 tudo estiver em ordem, podemos testar nosso sistema com 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
A descrição do CVE diz que há uma operação de escrita não restrita durante a chamada de sistema waitid.
Vamos abrir kernel/exit.c e olhar o código:
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;
}
Esta função é bastante direta: após algumas verificações, há várias chamadas para unsafe_put_user(...) e a função retorna.
A parte principal desta função é composta pela função unsafe_put_user(...), então vamos para 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)
Há um grande aviso no comentário: se você quiser usar unsafe_put/get_user, deve primeiro chamar access_ok() e envolvê-los com user_access_begin/end().
Se observarmos o código anterior (waitid), podemos ver que access_ok() nunca é chamado, então a chamada de sistema viola este aviso.
Mas o que são essas macros?
SMAP e SMEP são dois recursos de segurança introduzidos no kernel para dificultar a escrita de exploits. Vale notar que esses recursos são aplicados pela CPU.
SMEP impede a execução de código do espaço do usuário enquanto a CPU está em modo supervisor; SMAP, por outro lado, bloqueia o acesso de leitura/escrita à memória do usuário.
O kernel precisa escrever/ler dados de/para a memória do usuário e isso pode ser feito de duas maneiras:
copy_from_user) que permitem copiar a memória no espaço do kernel;Como podemos ver na definição de unsafe_put_user, esta função irá apenas copiar o valor de x para a memória apontada por ptr (e pular para err_label se houver erro). Acabamos de dizer que o kernel não pode acessar o espaço do usuário por causa do SMAP, e é por isso que essas funções devem ser envolvidas por user_access_begin/end().
#define __uaccess_begin() stac()
#define __uaccess_end() clac()
Como podemos ver, user_access_begin/end são simplesmente as instruções ASM stac e clac.