• Keine Ergebnisse gefunden

Die UNIX-Familie

4.2 Die Mitglieder der UNIX-Familie

4.2.1.1 Debugging des Linux-Kernels

Bei der Entwicklung eines Exploits müssen Sie früher oder später den laufenden Kernel debuggen. Das sollte nicht weiter überraschend sein, denn da wir einen Bug ausnutzen, ist es wahrscheinlich, dass wir es mit einigen Abstürzen zu tun bekommen, bevor wir alle Puzzlesteine zusammen haben, oder dass wir einige Variablenwerte benötigen, um die Schwachstelle besser verstehen zu können. In diesen Fällen ist es von großem Vorteil, den Zielkernel debuggen zu können.

Der Linux-Kernel wurde lange Zeit ohne einen standardmäßigen internen Debugger 6 gelie-fert, weshalb verschiedene Vorgehensweisen und Kombinationen davon eingesetzt wurden, um ein rudimentäres Debugging zu ermöglichen. Da einige dieser Vorgehensweisen immer noch nützlich sein können (vor allem dann, wenn nur eine kurze Überprüfung erforderlich ist), beginnen wir unsere Erörterung damit.

Die klassische und einfachste Form des Debuggings ist die mithilfe eines Print-Befehls.

Die Linux-Funktion printk() verhält sich ähnlich wie printf() und ermöglicht es Ihnen, eine Anweisung aus dem Kernelland im Userland auszugeben. Als zusätzlicher Vorteil ist printk() interruptsicher und kann daher auch Werte aus dem ungünstigen Interruptkon-text heraus melden.

int printk(const char *fmt, ...)

printk(KERN_NOTICE "log_buf_len: %d\n", log_buf_len);

In dem vorstehenden Code sehen wir den Prototyp der Funktion und ein typisches An-wendungsbeispiel. Der statische Wert KERN_NOTICE definiert die Debug-Ebene. Damit wird festgelegt, ob bestimmte Meldungen ausgegeben werden und wenn ja, wo (lokale Konsole, Systemprotokoll usw.). Linux definiert acht verschiedene Ebenen von KERN_EMERG (höchste Priorität) bis KERN_DEBUG (niedrigste):

#define KERN_EMERG "<0>" /* System nicht nutzbar */

#define KERN_ALERT "<1>" /* Sofortige Maßnahmen erforderlich */

#define KERN_CRIT "<2>" /* Kritische Bedingungen */

#define KERN_ERR "<3>" /* Fehlerbedingungen */

#define KERN_WARNING "<4>" /* Warnbedingungen */

#define KERN_NOTICE "<5>" /* Normal, aber signifikante Bedingung */

#define KERN_INFO "<6>" /* Nur zur Infomration */

#define KERN_DEBUG "<7>" /* Debugging-Meldungen */

Wird nichts anderes angegeben, so wird die Standardebene KERN_WARNING verwendet. Die Verwendung von printk() ist einfach. Sie müssen lediglich den Kernelquelltext ändern, indem Sie an den erforderlichen Stellen printk()-Zeilen einfügen, und ihn dann neu kom-pilieren. Diese Einfachheit ist auch die große Stärke dieser Vorgehensweise. Sie sieht zwar ziemlich rudimentär aus, ist aber erstaunlich wirkungsvoll. (Einige der in diesem Buch beschriebenen Exploits wurden ursprünglich mithilfe von Print-Debugging ausgearbei-tet.) Außerdem lässt sie sich auf jedem Kernel einsetzen, auf dessen Quellcode Sie Zugriff haben (nicht nur auf Linux). Der größte Nachteil besteht darin, dass jedes Mal, wenn Sie eine neue Anweisung hinzufügen und sie ausprobieren möchten, eine Neu kompilierung und ein Neustart erforderlich sind.

6 Sowohl KDB als auch KGDB waren lange Zeit externe Patches.

139 4.2 Die Mitglieder der UNIX-Familie

Es kann zwar akzeptabel (allerdings nicht optimal) sein, während der Exploit-Entwick-lung mehrfache Neustarts durchzuführen, doch für ein ausführlicheres Debugging (und für das Debugging auf einem Remotecomputer) lässt sich dieses Verfahren nicht gut ein-setzen. Um diese Einschränkung zu umgehen, haben Entwickler des Linux-Kernels das Framework kprobes eingeführt. Eine ausführliche Beschreibung, was »Kprobes« (»Ker-nelsonden«) sind, wie sie funktionieren und wie Sie sie einsetzen können, finden Sie in Documentation/kprobes.txt im Kernelquellcodebaum. In diesem Dokument7 heißt es:

Mit KProbes können Sie dynamisch Haltepunkte in jeder Kernelroutine setzen und Debug- und Leistungsinformationen ohne Störungen erfassen. Sie können Traps an fast sämtlichen Kernelcodeadressen einrichten und eine Handlerroutine festlegen, die beim Erreichen des Haltepunkts aufgerufen werden soll.

Es gibt zurzeit drei Arten von Sonden: KProbes, JProbes und KRetProbes (auch Rück-sprungsonden genannt). Eine KProbe lässt sich in praktisch allen Anweisungen im Kernel einfügen. Eine JProbe wird am Eintritt in eine Kernelfunktion eingefügt und bietet komfortablen Zugriff auf deren Argumente. Eine Rücksprungsonde wird ausge-löst, wenn eine gegebene Funktion die Steuerung zurückgibt.

Gewöhnlich wird eine Instrumentierung auf der Grundlage von KProbes als Kernel-modul verpackt. Die Initialisierungsfunktion des Moduls installiert (»registriert«) eine oder mehrere Sonden, und die Beendigungsfunktion hebt deren Registrierung wieder auf. Eine Registrierungsfunktion wie register_kprobe() gibt an, wann die Sonde eingefügt werden soll und welcher Handler beim Erreichen der Sonde aufzu-rufen ist.

Das Grundprinzip besteht darin, dass wir ein Modul und bestimmte Handler (Funktio-nen), die aufgerufen werden, wenn unsere Sonden erreicht werden, schreiben. KProbes bieten sehr viel Flexibilität, da praktisch jede Adresse mit einem Prä- und einem Post-Handler verknüpft werden kann, doch meistens sind wir nur am Zustand beim Eintritt in eine Funktion oder beim Verlassen der Funktion interessiert (wozu wir eine JProbe bzw.

eine Rücksprungsonde verwenden können). Der folgende Code zeigt ein Beispiel für eine JProbe:

#include <linux/kernel.h>

#include <linux/module.h>

#include <linux/sched.h>

#include <linux/kprobes.h>

#include <linux/kallsyms.h>

static struct jprobe setuid_jprobe;

static asmlinkage int

7 Keninston J, Panchamukih PS, Hiramatasu M. »Kernel probes (KProbes)«, http://www.kernel.org/doc/

Documentation/kprobes.txt.

kp_setuid(uid_t uid) [1]

{

printk("process %s [%d] attempted setuid to %d\n", current->comm, current->cred->uid, uid);

jprobe_return();

/* NICHT ERREICHT */

return (0);

} int

init_module(void) {

int ret;

setuid_jprobe.entry = (kprobe_opcode_t *)kp_setuid;

setuid_jprobe.kp.addr = (kprobe_opcode_t *)

kallsyms_lookup_name("sys_setuid"); [2]

if (!setuid_jprobe.kp.addr) {

printk("unable to lookup symbol\n");

return (-1);

}

if ((ret = register_jprobe(&setuid_jprobe)) <0) { printk("register_jprobe failed, returned %d\n", ret);

return (-1);

}

return (0);

}

void cleanup_module(void) {

unregister_jprobe(&setuid_jprobe);

printk("jprobe unregistered\n");

}

MODULE_LICENSE("GPL");

Wie bereits erwähnt, befindet sich unsere JProbe (wie im Framework kprobes allgemein üblich) in einem Kernelmodul, das die Sonde mithilfe der Funktionen register_jprobe() und unregister_jprobe() im Speicher platziert und aktiviert. Die Sonde wird als jprobe struct beschrieben, und gefüllt wird diese Struktur mit dem Namen des zugehörigen Sondenhandlers (kp_setuid) und der Adresse der Kernelzielfunktion. Hier verwenden

141 4.2 Die Mitglieder der UNIX-Familie

wir callsys_lookup_name() [2], um die Adresse von sys_setuid() zur Laufzeit zu bestim-men, aber ebenso gut funktionieren auch andere Verfahren wie die Hartkodierung der Adresse, ein Dump von vmlinuz oder der Abruf von System.map. Das Einzige, was die JProbe interessiert, ist die virtuelle Adresse.

Bei [1] bereiten wir den Handler vor. Für JProbes müssen wir die genaue Signatur der Zielfunktion wiedergeben. In diesem Fall ist es besonders wichtig, das Tag asmlinkage zu verwenden, um korrekt auf die Parameter zuzugreifen, die der Funktion übergeben wer-den. Hier verwenden wir einen sehr einfachen Handler, da wir nur zeigen wollen, wie wir auf globale Kernelstrukturen (wie current) und lokale Parameter (uid) zugreifen können.

Alle JProbes müssen mit einem Aufruf von jprobe_return() enden.8

Nun ist es an der Zeit, unseren Code zu testen. Dazu bereiten wir ein einfaches Makefile vor:

obj-m := kp-setuid.o

KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd)

default:

$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean:

rm -f *.mod.c *.ko *.o

Außerdem schreiben wir sehr einfachen Testcode, der sys_setuid() aufruft:

int main() { setuid(0);

}

Damit kann es auch schon losgehen:

linuxbox# make

make -C /lib/modules/2.6.31.3/build SUBDIRS=/home/luser/kprobe mod make[1]: Entering directory '/usr/src/linux-2.6.31.3'

CC [M] /home/luser/kprobe/kp-setuid.o Building modules, stage 2.

MODPOST 1 modules

CC /home/luser/kprobe/kp-setuid.mod.o make[1]: Leaving directory '/usr/src/linux-2.6.31.3'

8 Das ist erforderlich, um den Stack und die Register für die ursprüngliche Funktion wiederherzustellen, und liegt an der Implementierung von JProbes. Mehr über die Implementierung des Frameworks kprobes finden Sie in der bereits erwähnten Datei Documentation/kprobes.txt.

linuxbox# insmod kp-setuid.ko linuxbox#

[...]

linuxbox# gcc -o setuid-test setuid.c linuxbox# ./setuid-test

linuxbox# dmesg [...]

[ 1402.389175] process master [0] attempted setuid to -1 [ 1402.389283] process master [0] attempted setuid to -1 [ 1402.389302] process master [0] attempted setuid to 0 [ 1410.162081] process setuid-test [0] attempted setuid to 0 [...]

Wie Sie sehen, funktioniert unsere JProbe: Sie verfolgt Aufrufe von sys_setuid() und meldet die korrekten Informationen.

JProbes und KRetProbes sind ein wenig anspruchsvoller als normale KProbes, aber auch für sie ist es erforderlich, ein C-Modul zu schreiben, zu kompilieren und zu laden (mit insmod). Für eine weiträumige Verwendung ist das immer noch nicht optimal, insbe-sondere was die Benutzerfreundlichkeit angeht (stellen Sie sich einmal einen System-administrator vor, der das Kernelverhalten beobachten möchte). Daher wurden einige Frameworks erstellt, die auf dem Kprobes-Teilsystem aufbauen. Eines davon, SystemTap, hat sich zum De-facto-Standard für Kernelinstrumentierung und -debugging zur Lauf-zeit entwickelt. Da wir uns bei der Besprechung von Solaris auf LaufLauf-zeit-Instrumentie- Laufzeit-Instrumentie-rungssysteme konzentrieren werden (DTrace), stellen wir SystemTap hier nicht vor. Eine ausführliche Beschreibung sowie Beispiele sind in verschiedenen Quellen im Internet zu finden.

In unserem Beispiel haben wir Laufzeitdebugging und -beobachtung umfassend und detailliert durchgeführt, doch in manchen Fällen brauchen wir das Gegenteil, nämlich lediglich die Untersuchung des Werts einer Variable oder eines Teils des Kernelarbeits-speichers, etwa um zu prüfen, ob ein willkürlicher Schreibvorgang oder ein Pufferüber-lauf das Ziel erreicht hat. Dazu printk() einzusetzen, kann ineffizient sein, insbesondere wenn wir erst die Speicherbereiche ableiten müssen, die wir zur Laufzeit überprüfen wol-len, oder wenn wir einen Wert zu bestimmten Zeitpunkten abrufen möchten. Für diese Zwecke können wir den GDB-Debugger in Kombination mit einem exportierten Dump des Kernelspeichers verwenden, den Linux als /proc/kcore bereitstellt.9

linuxbox# gdb /usr/src/linux-2.6.31.3/vmlinux /proc/kcore GNU gdb (GDB) SUSE (6.8.91.20090930-2.4)

Copyright (C) 2009 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later

9 Rubini, A., Corbet, J., 2002. Linux-Gerätetreiber. O’Reilly Verlag.

143 4.2 Die Mitglieder der UNIX-Familie

<http://gnu.org/licenses/gpl.html>

[...]

Reading symbols from /usr/src/linux-2.6.31.3/vmlinux...done.

Core was generated by 'root=/dev/disk/by-id/ata-ST9120822AS_5LZ2P37Npart2 resume=/dev/disk/by-id/ata-S'.

#0 0x00000000 in ?? ()

Im vorstehenden Beispiel ist vmlinux das unkomprimierte Ergebnis einer Kernelkompi-lierung. Es enthält alle Symbole für den laufenden Kernel. (Je mehr Debugginginforma-tionen wir zur Kompilierungszeit einschließen, umso wirkungsvoller können wir GDB verwenden.) Bei /proc/kcore handelt es sich um eine Pseudodatei, die den gesamten ver-fügbaren physischen Arbeitsspeicher in Form einer klassischen Kerndumpdatei enthält.

Zur Untersuchung des Kernelarbeitsspeichers können wir die verschiedenen gdb-Befehle heranziehen:

(gdb) info address mmap_min_addr

Symbol "mmap_min_addr" is static storage at address 0xc1859f54.

(gdb) print mmap_min_addr

$4 = 65536

(gdb) print /x mmap_min_addr

$5 = 0x10000 (gdb)

Hier fragen wir die Speicheradresse der Variable mmap_min_addr ab. (Sie enthält die Adresse der niedrigsten Adresse im virtuellen Speicher, die wir mit einem mmap()-Aufruf anfor-dern können und die zur Schadenminimierung bei NULL-Dereferenzierungen dient.) Unmittelbar darauf erstellen wir einen Dump ihres Inhalts. Die Werte sehen zwar gültig aus, aber wir sollten uns trotzdem vergewissern, dass wir auch in die richtige Stelle des Speichers hineinblicken:

linuxbox# cat /proc/kallsyms | grep mmap_min_addr c117d9f0 T mmap_min_addr_handler

c16e1848 D dac_mmap_min_addr c176bd99 t init_mmap_min_addr

c17a49a8 t __initcall_init_mmap_min_addr0 c1859f54 B mmap_min_addr

linuxbox# cat /proc/sys/vm/mmap_min_addr 65536

linuxbox#

Wie Sie stehen, stimmen sowohl die Adresse (0xC1859F54) als auch der Wert (65536) von mmap_min_addr.

Die bis jetzt beschriebenen Vorgehensweisen sind nützlich und sollten es Ihnen erlau-ben, die meisten Ihrer Exploits auszuarbeiten. Manchmal jedoch müssen wir noch etwas mehr tun, beispielsweise den Kernel mithilfe von Haltepunkten und Einzelschritten zu durchlaufen. Dabei verspüren wir den Mangel eines standardmäßigen Kerneldebuggers am meisten. Wir müssen nach Notlösungen suchen, und dabei stehen drei Möglichkeiten zur Verfügung:

• Sie können den Kernel mit dem KDB-Patch versehen, der einen Laufzeit-Kernel-debugger implementiert. Herunterladen können Sie diesen Patch von http://oss.sgi.

com/projects/kdb/. Die Autoren haben bei der Anwendung des Patches und der Arbeit damit wechselhaftes Glück gehabt.

• Sie können die abgespeckte (Light-)Version von KGDB verwenden, die seit Version 2.6.26 im Linux-Kernel vorhanden ist.10 Im Prinzip exportiert KGDB einen GDB-Remotestub über die serielle Verbindung (oder über Ethernet, was jedoch in der Light-Version nicht möglich ist). Auf diesen Stub können Sie dann von einem anderen Computer aus über GDB zugreifen. Der große Nachteil besteht darin, dass Sie dazu zwei Computer mit einem seriellen Anschluss benötigen, der auf modernen Laptops nur selten vorhanden ist. Abgesehen davon ist diese Vorgehensweise jedoch ziemlich stabil. Da sie inzwischen zum »Mainstream« gehört, wurde sie auch ordnungsgemäß auf Regression getestet und ist bei Vanilla-Kernels im Lieferumfang verfügbar. Um das KGDB-Framework einzuschalten, müssen Sie über einen der {x|menu|}config-Befehle (die .config-Variablen sind CONFIG_HAVE_ARCH_KGDB, CONFIG_KGDB und CONFIG_KGDB_

SERIAL_CONSOLE) die Option Kernel Hacking | KGDB: Kernel Debugging with remote gdb auswählen. Es wird auch empfohlen, den Kernel mit Debuginformationen (Kernel Hacking | Compile the kernel with debug info) und ohne Verzicht auf den Framezeiger (Kernel Hacking | Compile the kernel with frame pointers) zu kompilieren.

• Sie können eine virtuelle Maschine oder einen Emulator verwenden, der einen GDB-Stub exportiert, und den Linux-Kernel in dieser virtuellen Umgebung laden. Das Debugging nehmen Sie dann von »außen« vor. Zwei beliebte Produkte dafür sind QEMU und VMware. Diese Vorgehensweise bietet den zusätzlichen Vorteil, dass Sie den Kernel von der ersten Anweisung an in Einzelschritten durchlaufen können.

Außerdem lässt sich die gesamte Debuggingumgebung auch für andere Betriebssys-teme nutzen. Diese Art von Debugging sehen wir uns in Kapitel 6 am Beispiel von Windows an, weshalb wir hier nicht in die Einzelheiten gehen.