• Keine Ergebnisse gefunden

Die UNIX-Familie

4.4 UNIX-Hacking in der Praxis

4.4.2 Angriff auf den Slab-Allokator von OpenSolaris

4.4.2.1 Erforderliche Grundlagen

Es dürfte keine große Überraschung darstellen, dass vieles von dem, was wir in Kapitel 3 besprochen haben, auch für den Slab-Allokator von OpenSolaris gilt. Eine oder mehr auf-einanderfolgende Seiten bilden einen Slab, der dann in Objekte gleicher Größe aufgeteilt wird. Wenn Sie lieber in C-Begriffen denken, dann sind diese Objekte einfache C-Struk-turen, deren Elemente teilweise von bestimmten Konstruktor- und Destruktorfunktionen des Caches vorinitialisiert sein können. Slabs enthalten immer nur einen Typ von Objekt, und diejenigen mit demselben Objekttyp werden zu einem Cache gruppiert. Gerätetrei-ber und Kernelteilsysteme erstellen Caches, um häufig verwendete Objekte zu verwalten:

static struct kmem_cache *cred_cache;

static size_t crsize = 0;

void

cred_init(void) {

[...]

crsize = sizeof (cred_t);

[...]

cred_cache = kmem_cache_create("cred_cache", crsize, 0, NULL, NULL, NULL, NULL, NULL, 0);

[...]

}

22 Bonwick, J. 1994. »The slab allocator: an object-caching kernel memory allocator«, in: Proceedings of the USENIX Summer 1994 Technical Conference – Volume 1 (Boston, 6.–10. Juni 1994), USENIX Association, Berkeley; Bonwick, J. und Adams, J. 2001. »Magazines and vmem: extending the slab allocator to many CPUs and arbitrary resources«, in: Proceedings of the General Track: 2002 USENIX Annual Technical Confe-rence (25. –30. Juni 2001). Y. Park (Hrsg.). USENIX Association, Berkeley, 15–33.

Dieses Beispiel stammt aus dem Teilsystem für Berechtigungsnachweise. Es ist dafür zu-ständig, die cred_t-Objekte zu erstellen, mit deren Hilfe die einem Prozess zugewiesenen Rechte im Auge behalten werden. Mit dem Befehl kstat können wir Informationen über cred_cache abrufen:

osol-box$ kstat -n cred_cache module: unix instance: 0

name: cred_cache class: kmem_cache align 8

alloc 441597 alloc_fail 0 buf_avail 100 buf_constructed 83 buf_inuse 148 buf_max 248 buf_size 128 buf_total 248 [...]

empty_magazines 3 free 441498 full_magazines 5 slab_alloc 252 slab_create 8 slab_destroy 0 slab_free 21 slab_size 4096

Wie Sie sehen, bietet uns der Befehl kstat eine Menge Informationen, und dabei kann er mit Benutzerrechten ausgeführt werden. Während der Entwicklung eines Exploits ist es wichtig, die Übersicht über den Zustand des Slab-Allokators zu behalten. In den vorste-henden Beispielen wurden für den Cache cred_cache acht Slabs (slab_create) für insge-samt 248 Objekte (buf_toal) erstellt. Die Bedeutung und Wichtigkeit der anderen von kstat exportierten Werte sehen wir uns weiter hinten in diesem Abschnitt noch an.

Slabs werden mithilfe einer kmem_slab_t-Struktur dargestellt, die entweder am Ende des Slabs steht (wenn die Objekte kleiner als 1/8 der Seite sind) oder außerhalb des Slabs, wobei sie über einen Zeiger verknüpft ist. Im ersten Fall (den wir weiter hinten in diesem Abschnitt besprechen und der schon in Kapitel 3 erwähnt wurde) kann diese Steuerstruk-tur als Angriffsweg dienen:

typedef struct kmem_slab {

struct kmem_cache *slab_cache; /* Cachesteuerung */

void *slab_base; /* Basis des zugew. Speichers */

175 4.4 UNIX-Hacking in der Praxis

avl_node_t slab_link; /* Slab-Verknüpfung */

struct kmem_bufctl *slab_head; /* Erster freier Puffer */

long slab_refcnt; /* Ausstehende Zuweisungen */

long slab_chunks; /* Stücke (bufs) in diesem Slab */

uint32_t slab_stuck_offset; /* Nicht verschobener Pufferoffset */

uint16_t slab_later_count; /* siehe KMEM_CBRC_LATER */

uint16_t slab_flags; /* Bits zur Markierung des Slabs */

} kmem_slab_t;

Mit jedem Objekt im Slab werden Tag-Informationen verknüpft. Die Struktur mit diesen Informationen heißt kmem_bufctrl und hat hauptsächlich dann eine Bedeutung, wenn das Objekt frei ist. In diesen Fällen wird sie auch verwendet, um das Objekt mit der Liste der verfügbaren Objekte (Freiliste) zu verknüpfen. In der Praxis enthält jedes freie Objekt die erforderlichen Informationen, um das nächste freie Objekt zu finden, während die Slab-Steuerstruktur kmem_slab_t die Adresse des ersten verfügbaren Objekts im Slab festhält.

Dieses Design wird unmittelbar einsichtig, wenn Sie sich den Code zur Zuweisung eines neuen Slabs ansehen:

typedef struct kmem_bufctl {

struct kmem_bufctl *bc_next; /* Nächste bufctl-Struktur */

void *bc_addr; /* Adresse des Puffers */

struct kmem_slab *bc_slab; /* Slabsteuerung */

} kmem_bufctl_t;

slab = vmem_alloc(vmp, slabsize, kmflag & KM_VMFLAGS);

[...]

sp->slab_head = NULL;

sp->slab_base = buf = slab + color;

[...]

chunks = (slabsize - sizeof (kmem_slab_t) - color) / chunksize;

[...]

while (chunks-- != 0) {

if (cache_flags & KMF_HASH) { [...]

} else {

bcp = KMEM_BUFCTL(cp, buf);

} [...]

bcp->bc_next = sp->slab_head;

sp->slab_head = bcp;

buf += chunksize;

}

In diesem Code ist bcp vom Typ kmem_bufctl_t und sp vom Typ kmem_slab_t. Bei KMEM_

BUFCTL handelt es ich um ein Makro, um die mit einem Puffer verknüpfte kmem_bufctrl_t abzurufen. Wie Sie am Ende des Codes sehen, werden die Objekte in umgekehrter Reihen-folge verknüpft, also von dem Objekt in der Nähe des Slabendes zurück bis zum ersten Objekt im Slab. Am Ende der Schleife zeigt slab_head auf den letzten Puffer im Slab.

Angesichts dieser Voraussetzung sollten wir erwarten, dass die Slabzuweisung einfach wie folgt abläuft:

• Der Zeiger auf das erste freie Objekt wird aus kmem_slab_t->slab_head abgerufen.

• Das Objekt wird aus der Freiliste herausgenommen.

• Die Adresse des nächsten freien Objekts wird in kmem_bufctl_t->bc_next gelesen.

kmem_slab_t->slab_head wird mit der Adresse des nächsten freien Objekts aktualisiert.

Wir sollten auch erwarten, dass die Freigabe eines Objekts auf die gleiche Weise, aber in umgekehrter Reihenfolge stattfindet: Das Objekt wird in der Freiliste platziert, die zu-gehörige Variable kmem_bufctrl_t->bc_next wird mit dem Wert von kmem_slab_t->slab_

head aktualisiert, das wiederum mit der Adresse des neu freigegebenen Objekts aktualisiert wird. Dies würde auch eine LIFO-Vorgehensweise für Zuweisungen bedeuten (das zuletzt freigegebene Objekt wird bei einer anschließenden Zuweisung als Erstes zurückgegeben), was wir schon in Kapitel 3 als typische Eigenschaft von Slab-Allokatoren kennengelernt haben.

Im Prinzip stimmen unsere Annahmen, allerdings ist der Slab-Allokator von Open-Solaris etwas komplizierter. Um seine Skalierbarkeit zu verbessern, werden außerdem Magazine und CPU-weise Caches verwendet. Deren Design wird ausführlich in einem weiteren Aufsatz von Bonwick beschrieben, nämlich »Magazines and Vmem: Extending the Slab Allocator to Many CPUs and Arbitrary Resources«, weshalb wir hier nur kurz auf die Aspekte eingehen, die für einen Exploit von Bedeutung sind. Abb. 4.2, die auf Bonwicks Text basiert, zeigt einen Überblick über den Slab-Allokator.

Um diese Abbildung besser verstehen zu können, müssen wir zunächst einmal wissen, was ein Magazin ist. Es handelt sich dabei einfach um eine Zusammenstellung von Ob-jektzeigern mit einem Zeiger, der festhält, wie viele davon zugewiesen wurden. Bei einer Zuweisung aus dem Magazin wird das erste verfügbare freie Objekt zurückgegeben und sein Slot als leer gekennzeichnet, bei einer Freigabe dagegen wird das freie Objekt im ers-ten leeren Slot platziert. Das Magazin verhält sich also praktisch wie ein Objektstack, was auch wieder zu einer LIFO-Reihenfolge führt.

Wie Sie in Abb. 4.2 sehen, besteht der Slab-Allokator aus mehreren Schichten, die bei ei-ner Objektzuweisung oder -freigabe nacheinander abgearbeitet werden. Die CPU-Schicht dient als lokaler Cache. Wenn möglich, werden Objekte aus den mit den einzelnen CPUs verknüpften Magazinen ein- oder ausgewechselt. Da diese Magazine jeweils privat zu ei-ner CPU gehören, sind keine Sperren und keine Synchronisation erforderlich. Alle Ope-rationen können parallel auf den verschiedenen CPUs ausgeführt werden. Irgendwann aber erreicht der Allokator einen Zustand, in dem die CPU-Schicht die Anforderung eines

177 4.4 UNIX-Hacking in der Praxis

Kernelpfads nicht mehr erfüllen kann. Dann greift er auf die Depotschicht zurück, um entweder ein volles Magazin (bei einer angeforderten Zuweisung) oder ein leeres (für eine Freigabe) zurückzugeben.23

Magazinschicht (konstruiert)Slab-Schicht (nicht konstruiert)CPU layerDepot

cache_cpu (0)

Eine oder mehr Seiten aus der Vmem-Quelle des Caches

Vmem-Arena bufctl bufctl

Slab

Farbe Puffer Puffer Puffer

Abbildung 4.2: Der Slab-Allokator von OpenSolaris

Die Depotschicht stellt eine Reserve mit vollen und leeren Magazinen dar, aber sie ist na-türlich nicht unerschöpflich. Wenn ein neues Objekt zugewiesen werden muss, aber keine vollen Magazine bereitstehen, wird die Zuweisung an die darunter liegende Slab-Schicht weitergeleitet und dort erfüllt. Das Gleiche geschieht auch bei einer Freigabe, wobei hier jedoch nach Möglichkeit ein neues leeres Magazin zugewiesen wird, um das freigegebene Objekt zu speichern. Das ist ein wichtiges Merkmal des Slab-Allokators (das bei einem Exploit unbedingt beachtet werden muss). Volle Magazine werden niemals zugewiesen, sondern treten als Folge des normalen Allokatorverhaltens auf. Sind daher keine vollen Magazine vorhanden, dann wird die Zuweisung durch die Slab-Schicht erfüllt. Abb. 4.3 gibt einen Überblick über diese beiden Algorithmen.

23 Eine Optimierung dieser Vorgehensweise bildet die Verwendung des »vorherigen« Magazins auf der CPU-Schicht. Da es immer entweder komplett voll oder ganz leer ist, wird es dort aufbewahrt und gegen das aktuelle ausgetauscht, wenn die Anforderung dadurch erfüllt werden kann. In der aktuellen OpenSolaris-Implementierung werden drei Magazine auf der CPU-Schicht vorgehalten: ein volles, ein leeres und ein teilweise gefülltes (das aktuelle).

Ist das aktuelle

Abbildung 4.3: Die Algorithmen für Zuweisung und Freigabe

Für jeden Cache im System gibt es einen CPU-, eine Depot- und eine Slab-Schicht. Aber wie viele Caches sind vorhanden? Auch diese Frage kann uns kstat beantworten:

osol-box$ kstat -l -c kmem_cache -s slab_alloc [...]

Wie Sie sehen, gibt es mehrere Caches. Besonders interessant ist das Ende der Ausgabe, da es die Namen der sogenannten Allzweckcaches zeigt. Sie werden immer dann verwen-det, wenn die Front-End-Funktionen kmem_alloc() bzw. kmem_free() aufgerufen werden, und ermöglichen es, willkürliche Mengen an Speicher zuzuweisen. Dieser Speicher wird gewöhnlich entweder als Scratch-Puffer genutzt (z. B. um einen vom Userland kopierten Wert zu speichern) oder für Strukturen, die zu selten genutzt werden, um das Anlegen eines Ad-hoc-Caches zu rechtfertigen. Jedes Mal, wenn die Funktion kmem_alloc() auf-gerufen wird, erhält sie die Größe der Zuweisung als Parameter. Diese Größe wird dann

179 4.4 UNIX-Hacking in der Praxis

auf die nächste passende Cachegröße gerundet, und von da an wird der Vorgang mit der Standardzuweisungsfunktion kmem_cache_alloc() durchgeführt.

void *

kmem_alloc(size_t size, int kmflag) {

size_t index;

kmem_cache_t *cp;

void *buf;

if ((index = ((size - 1) >> KMEM_ALIGN_SHIFT)) < KMEM_ALLOC_TABLE_MAX) { cp = kmem_alloc_table[index];

/* Fällt durch bis kmem_cache_alloc() */

} else if ((index = ((size - 1) >> KMEM_BIG_SHIFT)) <

kmem_big_alloc_table_max) {

cp = kmem_big_alloc_table[index];

/* Fällt durch bis kmem_cache_alloc() */

[...]

buf = kmem_cache_alloc(cp, kmflag);

In einem der Caches in kmem_alloc_table führen wir eine Indizierung nach der Größe durch. Es ist einfacher (oder zumindest übersichtlicher), sich den Inhalt dieses Arrays mithilfe von kmdb anzusehen, anstatt dem Quellcode zu folgen.24

osol-box# mdb -k

Loading modules: [ unix genunix specfs dtrace mac cpu.generic uppc pcplusmp rootnex scsi_vhci zfs sockfs ip hook neti sctp arp usba uhci s1394 fctl md lofs idm fcp fcip cpc random crypto sd logindmux ptm sdbc nsctl ii ufs rdc sppp nsmb sv ipc nfs ]

> kmem_alloc_table,5/nP | ::print -t kmem_cache_t cache_name char [32] cache_name = [ "kmem_alloc_8" ]

char [32] cache_name = [ "kmem_alloc_16" ] char [32] cache_name = [ "kmem_alloc_24" ] char [32] cache_name = [ "kmem_alloc_32" ] char [32] cache_name = [ "kmem_alloc_40" ]

>

Wie Sie sehen, ist kmem_alloc_table ein Array von Zeigern auf kmem_cache_t-Strukturen, nämlich derjenigen, die die in der Ausgabe von kstat genannten Allzweckcaches beschrei-ben. kmem_alloc_table,5/nP gibt die ersten fünf Werte in dem Array aus (P), und zwar einen pro Zeile (n), sodass wir die Ausgabe leicht an ::print übergeben können.

24 Die verschiedenen Allzweckcaches werden in kmem_chache_init() erstellt, die wiederum kmem_alloc_

caches_create() aufruft.

Für einen Exploit sind Allzweckcaches viel interessanter als Sondercaches, da es im Allge-meinen unwahrscheinlich ist, dass ein Überlauf in einem »konstruierten« Objekt auftritt.

Daher tritt die überwiegende Mehrzahl der Slab-Überläufe jeglicher Betriebssysteme ge-wöhnlich bei einem Mussbrauch eines Puffers auf, der von einem der Allzweckcaches25 zugewiesen wurde. Das anfällige Dummy-Modul, das wir angreifen wollen, um Techniken für das Slab-Hacking vorzuführen, bildet dabei keine Ausnahme.