Inhaltsverzeichnis
1 Einleitung
1.1 Abstract
1.2 Situationsbeschreibung
1.3 Materialauswahl
1.4 Zielbeschreibung des dynamic function call tracings - fctrace
1.5 Präzisierung des Projektes
1.5.1 Vaterprojekt
1.5.2 Vergleiche der bis dato vorhandenen Linux Trace Projekte
2 Wurm
2.1 Theorie
2.1.1 User-Space
3 KProbes
3.1 Funktionsweise
3.1.1 KProbe Struktur
3.1.2 Registrierung einer KProbe
3.2 Abhandlung einer KProbe
3.3 Vorteile von KProbes
4 Interrupts im Linux Kernel
4.1 Softwareinterrupts
4.1.1 Darstellung im Linux Kernel
4.1.2 Interrupt 3
5 Darstellung der Programme im RAM
5.1 lineare Adressierung
5.2 Logische und physikalische Adressierung
5.2.1 Adressberechnung
6 Prozess Speicherabbilder
6.1 Prozessspeicher Organisation
6.2 Speicherabbild
6.2.1 ELF
6.3 ABI
6.3.1 ABI
6.3.2 ABI Sprung-Arten
7 Realisierung
7.1 One Shot Trace
7.1.1 Sprungadressen
7.1.2 Userland Proben
7.2 Dateien
7.2.1 fct_funktions_adressen.pl
7.2.2 fct_nasse_erde.c
7.2.3 fct_wurm.c
7.2.4 fct_wurm_kprobe.h
7.3 Abschliessende Betrachtung
7.3.1 letzter Stand
8 Abbildungen
1 Einleitung
1.1 Abstract
In der folgenden Projekt III Arbeit soll die Möglichkeit der Erweiterung des Linux Tra- cings um die Fähigkeit Funktionen dynamisch zurück zu verfolgen, untersucht werden. Dabei soll auf vorhandene Open-Source Projekte zurückgegri en und eine Machbar- keitstudie angefertigt werden. Ziel ist es Aussagen über eine generelle Implementierung sowie mögliche Risiken zu tre en. Die Theorie sieht einenWurm vor, der mithilfe einer vorherigen Programmcode-Analyse mögliche Sprungpunkte erkennt und diese manipu- lieren kann. Dabei sollen die Kriterien der Plattformunabhängigkeit und der einfachen Maintenance vorrangig sein.
1.2 Situationsbeschreibung
Mit den gröÿer und komplexer werdenden Systemen wird die Anforderung nach hoher Qualität, hoher Entwicklungsgeschwindigkeit und Sicherheit immer deutlicher[1]. Dieser Umstand verlangt ein Werkzeug, das Analysen schnell und einfach erstellt als auch nicht vorhergesehenes Verhalten visualisieren kann. Bevorzugter gedachter Einsatzbereich ist das Rapid Application Learning. Es soll einem erfahrenem, mit grundlegendem Wis- sen der Programmiersprache C ausgestattetem Benutzer die Möglichkeit gegeben wer- den, Parameterübergaben auf der Funktionsebene zu sehen. Dennoch ist dieses Projekt streitbar, da ein Teil der Zielgruppe den zu observierenden Quellcode entweder selbst geschrieben hat oder ein sehr detailliertes Wissen über den Kernel besitzt und benötigte Informationen aus dem Stack-Dump lesen kann.1
1.3 Materialauswahl
Dieses Gebiet der Sichtbarmachung von Abläufen mit übergebenen Strukturen/Para- metern ist, im Zuge der Nachvollziehbarkeit (Debugging), Bestandteil des sich rasch entwickelnten Kernel Software Entwicklungszyklusses. Für dieses Projekt ist vor allem der Linux Kernel Quellcode in der Version 2.6.9 verwendet worden. Durch diese Vor- arbeit und dem Vorzug des Open Sources ist es möglich und auch unabdingbar, den bestehenden Code weiter zu verwenden und Redundanzen damit zu vermeiden. Einen maÿgeblichen Ein uss haben vergleichbare etablierte Projekte wie SUN's Dtrace und KProbes gehabt. Neben dem Quellcode sind Teile der Intel x86 Prozessor Dokumenta- tion sowie die Fachliteratur Understanding the Linux Kernel 2nd Edition in die Theorie eingebunden. An die Mailingliste von http://kernelnewbies.org2 wurden kernelspezi sche Fragen gestellt.
1.4 Zielbeschreibung des dynamic function call tracings - fctrace
Ziel ist es, nicht nur die begrenzte Anzahl der System Calls sondern auch die FunctionCalls (einer beliebigen Ausführbaren) zu sehen. Genau das ist in bisher keinem Linux Trace Tool realisiert. Dabei nutzt man den Vorteile des Open Sources: der vorhandene einsehbare Quellcode. Der gcc-Compiler kennt nämlich bei der Übersetzung des Programms die möglichen Wegsprungadressen. Diese werden im Zusammenhang mit einem wurmartigen dynamischen Einp anzen von Interrupt-Traps benutzt, um einen Aufruf vollständig nachzuverfolgen. Damit ist das Projekt zwischen dem GNU Debugger und dem strace einzuordnen. Mit dem wesentlichem Vorteil, gegenüber dem GNU Debugger, auch Abläufe in Kernelmodulen sichtbar zu machen.
1.5 Präzisierung des Projektes
Die Zielbeschreibung innerhalb des gegebenen zeitlichen Rahmens umzusetzen, ist nicht realisierbar. Nach Absprache mit dem Betreuer Herrn Olaf Dabrunz wurde im erstem Schritt die Projektdokumentation mit einer Implementation, vergleichbar des strace, vereinbart. In diesem Sinne, dass nach der Übergabe der Startfunktionsadresse nur die Aufrufe der gerufenen Funktion im syslog3 sichtbar ist. Während der Meilensteinpla- nung waren schon folgender Knackpunkt als nicht scha bar ersichtlich: der Quellcode Parser[1]. Aus dem derzeitigen Standpunkt wird dieser für die eindeutige Ermittlung der Funktions-Parameter benötigt. Der Parser müsste einen ähnlichen Funktionsumfang wie der Quellcode-Parser des gcc-Compilers haben4.
1.5.1 Vaterprojekt
SUN's DTrace ist ein dynamisches Analyse Tool, allerdings nur auf SPARC, x86 und x86-64. Das DTrace (Dynamic Trace) wurde auf der Granularität des Prozesses ausge- richtet. Diese Implementierung dient dazu einen schnellen, aussagekräftigen sowie sich selbst erklärenden Überblick auf einem Produktionssystem zu erhalten. Ausgehend von dem dynamic tracing module [5] möchte man mit diesem Projekt ein ähnlich hilfreiches Tracing Werkzeug für Linux bereitstellen. SUN's DTrace ermöglicht unter SOLARIS X das dynamische Beobachten von User bzw. Kernel Level Software. Ist DTrace nicht ak- tiviert so stellt sich der zero-probe E ekt ein. Das System verhält sich dann genau so, als ob überhaupt kein DTrace geladen wäre. DTrace stellt im SOLARIS Kernel mehr als 30.000 abfragbare Informationenspunkte zur Verfügung [6]. Die Aufbereitung der ge- sammelten Daten wird durch eine C-ähnliche Sprache mittels thread-lokalen Variablen sowie assoziativen Arrays unterstützt. Eine Portierung des Solaris DTrace erscheint zu diesem Zeitpunkt für nicht möglich. Das Problem stellt der Lizenzkon ikt dar. Das kom- plette Betriebssystem Solaris X einschlieÿlich dem Modul DTrace wurde unter CDDL [7] verö entlicht. Der Linux-Kernel restriktive unter der GPL [2]. Hintergrund ist, dass für die Portierung des DTraces GPL Quellcode benutzt werden muss. Gemäÿ der General Public License muss dann das entstehende Modul wiederum unter der GPL verö ent- licht werden. Aufgrund der Inkompatibilität der beiden Lizenzen sind die Folgen eines Verstoÿes durch Portierung nicht abzusehen. SUN's DTrace dient hiermit lediglich als gedankliche Grundlage sowie als theoretisches Ziel.
1.5.2 Vergleiche der bis dato vorhandenen Linux Trace Projekte
Die Untersuchung der bisher bekannten Tracing Projekte ergab, dass entweder hart- kodierte Tracing Funktionen oder der prozessor-spezi sche Breakpoint Mechanismus benutzt wird. Fest im Quellcode verankerte Debug Funktionen kennzeichnen PTrace (kernel/ptrace.c). Der Nachteil dieser Methodik ist ihre permanente Anwesenheit im Quellcode des Kernels, der damit verbundene Overhead (Beispiel: Platzhalter sowie Aktivierungs-Abfragen), aber auch die Starrheit (nur vorher gespickte Funktionen können überwacht werden).
Einzig das DTrace auf der SPARC Plattform hat die performanteste Lösung. Durch das Einsetzen eines Branch-Always Befehls (RISC-Besonderheit) ist man in der Lage, den heraus gelösten Assembler-Quellcode an einer anderen (Speicher-)Stelle auszuführen5. Andere bedeutende Arbeiten sind: Linux Trace Toolkit (LTT), K42, Kerninst, Dprobes, Dtrace.
Der LTT zeichnet sich durch die eine statische Implementation sowie durch eine fest gelegte Anzahl von Proben aus. Durch die geringe Anzahl der Proben soll auch der Performanceverlust in Grenzen(<2,5%) gehalten werden. Es besteht die Möglichkeit nach bestimmten Prozessen/Usern/Gruppen-ID zu ltern und sich bestimmte Daten aufbereitet anzeigen zu lassen.
Das IBM Projekt K42 stellt den Ansatz des Tracings durch einen universitären Research Kernel bereit. Zurzeit ist der Quellcode nur für PowerPC-Linux verfügbar. Geplant wurde der Kernel für large-scale applications und für 64bit Power Prozessoren. Ansonsten ähnelt K42 dem LTT, in der statischen Festlegung der Proben im Quellcode, konkret wird in jeder interessanten Funktion ein Trap vermerkt.
Kerninst ist ein kompletter Framework, bedient sich der INT36 Traps auf der x86 Architektur. Insbesondere bedient man sich nicht der Kernel-Noti er-Chain, sondern man sieht in der Interrupt Descriptor Table nach, ob der Aufruf ein INT3 war. Dadurch kann sich das Kerninst noch vor die Abarbeitung des DProbes setzen. Potenziell unsicher scheint dabei die Berechnung der Rücksprungadresse zu sein.
2 Wurm
Der Wurm besteht aus dem Erkennen der möglichen Wegsprungadressen, tauschen des ersten Bytes durch ein INT3, beim Erreichen des Instruction Pointers ein Zurückschrei- ben des originalen Codes, ein Single Stepping (Ausführung der einen Instruktion) über den einen Operationscode. Das raupenartige Setzen von Breakpoints in einem Ausfüh- rungspfad kennzeichnet diesen Mechanismus. Der Wurm setzt allerdings nur auf KProbes auf. Es ndet in verminderten Maÿen keine Codeduplizierung statt. Vorerst soll durch die Implementierung des Wurms die Funktionalität des strace auf der Funktionsebene nach- gebildet werden. Das angesprochene Verhalten bedingt die Kenntnisse des Aufbaus von Programmen im Speicher. Hinsichtlich der Schichten: Kernelseitige Strukturierung des Speichers und Organisation der ausführbaren Programme. Dieses Wissen setzt voraus, das mittels der Umwandlung von logischen linearen Adresse in die physikalische Adresse umgegangen werden kann. Das letztere Problem ist gleichzeitig das interessanteste.
2.1 Theorie
Für eine Implementierung des Wurms wird ein Kernelmodul benötigt, welches ein Proc- Interface1 zur User-Kommunikation bereitstellt. Das zu testende Beispiel soll ein Ker- nelmodul sein, das drei Funktionen implementiert und diese mittels Export_Symbol() im globalen Namensraum2 verfügbar macht. Es wird ebenfalls ein Proc-Interface benötigt, dass eine Schalterfunktion einnimmt. In Folge der Aktivierung soll der Aufrufablauf im syslog erkennbar sein.
Durch diese getro ene Vereinfachung kann das Proof-of-Concept gezeigt werden. Der Wurm setzt voraus, dass er alle möglichen3 Funktionen und deren Subfunktionen kennt. Dies läÿt zwei Möglichkeiten der Realisierung zu. Die Eine ist, der Wurm bekommt eine Startadresse und liest direkt im Speicher die gerufenen Funktionen aus und die Andere ist, der Wurm kennt vor dem Aufruf alle möglichen anspringbaren Funktionen.
Der Wurm verliert an Dynamik, denn es muss der gesamte Kernelmoduldump4 an das /proc/fctrace übertragen werden. Der Wurm registriert zuerst alle Kernelfunktionen die in der main()/init_module stehen. Bei dem Aufruf einer Funktion fängt der Wurm an, die bekannte Call's bzw. Subfunktionen mit Proben zu versetzen. Zu dem Zeitpunkt ist nicht bekannt, welche davon gerufen werden wird. Wird eine der Subfunktionen gerufen, muss die Vater-Funktion unregistriert werden5.
2.1.1 User-Space
Das beschriebene Verfahren setzt ein User-Space-Programm zur Erst-Fütterung des Wurms voraus. Dieses Skript muss dem Wurm mitteilen, welche Vater-Funktionen wel- che möglichen Sohn-Funktionen aufrufen kann. In diesem Sinn müssen alle möglichen Ausführungspfade vorwärtsgerichtet nachgebildet werden. Durch die Di erenzierung in Vater- und Sohnfunktionen müssen dem angedachten Proc-Interface zur Kernelseiti- gen Kommunikation einige Steuersignale ergänzt werden. Die Benutzung eines Proc- Interfaces zum einseitigen Datenaustausch erkauft eine Plattformunabhängigkeit, aber auch einen Performanceverlust. Unausgesprochenes Ziel ist, diesen Overhead zu sparen, in dem man nicht beim Start, sondern beim Eintre en des Interrupts zu dem ASM- Quellcode der gerufenen Funktion springt und diesen Quellcode auswertet. Es entsteht ein Overhead durch die Verwaltung der Sprungadressen. Generell stellt sich die Frage nach einem schnellen Zugri auf den entsprechenden Sprungpunkte .6 KProbes verwaltet seine gesetzten Proben mit den Hash-Funktionen des Linux Kernels. Die KProbes interne Probenverwaltung zu nutzen wäre nur bedingt vorteilhaft, denn der Wurm soll nicht nur momentan gesetzte Proben kennen7.
An dieser Stelle wird ersichtlich, dass die Wurmlogik in dem pre_handler der struct_kprobe erfolgen sollte. Gemäÿ dieser Formulierung lässt sich das Projekt in die Eckpunkte glie- dern: Erweiterung des KProbes, Interruptabarbeitung, Speicherorganisation und Binär- formate.
3 KProbes
Für die geforderte genaue Untersuchung wurde die bestehende, im Linux Kernel inte- grierte Kernel-Probes (KProbes)[17] benutzt. KProbe ist ursprünglich für OS/2 (unter dem Namen: Dynamic Probe ) entwickelt, dann für Linux portiert und erweitert wor- den. Anknüpfen will man an die Möglichkeit, dynamische breakpoint-basierende Kernel Proben während der Laufzeit einzup anzen und zu entnehmen. KProbes ist im Mo- ment nicht in der Lage automatisiert Funktionen zu tracen. Es kann lediglich in der System-Map erzeugte System-Calls überwachen [18]. Damit ist es nicht in der Lage einen automatisierten, vollständigen Programm uss unter der Anzeige aller Funktionsaufrufe wiederzugeben.
3.1 Funktionsweise
Die Grundlage für den Wurm ist die Implementation der Kernel Probes im Linux Kernel. Demzufolge wird auf die wichtigsten Funktionen und Abhandlungschritte eingegangen. Interessant sind die KProbe Struktur und die Funktionen der Registrierung einer Probe.
3.1.1 KProbe Struktur
Die Kprobe Struktur enthält die Attribute, die einer Probe wie folgt zugeordnet sind:
Listing 3.1: Ausschnitt aus dem Source 2.6.9 include/linux/kprobes.h
Abbildung in dieser Leseprobe nicht enthalten
Wie man aus dem Quellcode erkennen kann, wird diese Probe einem Listeneintrag zugeordnet. Die Originalinstruktion, die logische Proben-Adresse sowie der Zeiger sind auf der zu rufenden Funktion gespeichert.
3.1.2 Registrierung einer KProbe
Ausgehend von der Struktur möchte ich nun den Ablauf der Registrierung einer Probe beleuchten. Wie im Anhang [siehe Quellcodedatei fct_wurm_kprobes.h bzw. Sektion 7.2.4] ersichtlich, wird eine Adresse an die Funktion kprobe_init übergeben. An dieser Stelle werden alle benötigten Initialisierungsschritte unternommen. Diese sind: Zuweisen der pre_handler, post_handler sowie fault_handler Funktionen (ab Zeile 8) sowie der zu probenden Stelle. Die Handler-Funktionen dienen der einseitigen Interaktion (Kernel ⇒ User). Diese Funktionen werden unmittelbar vor bzw. nach dem gesetzten Interrupt ausgeführt.
Listing 3.2: Ausschnitt aus dem Source 2.6.9 kernel/kprobes.c
Abbildung in dieser Leseprobe nicht enthalten
In [kernel/kprobes.c] be ndet sich die Funktion register_kprobe, diese nimmt die Si- cherung der IRQ-Flags vor (spin_lock_irq_save zeigt nach include/asm-i386/system.h local_irq_save()). Dabei wird der eventuell vorhandenen Multiprozessor-Maschine Tri- but gezollt, denn in der [include/linux/spinlock.h] wird der Aufruf spin_lock_irq_save nach Symmetric-Multi-Processing Maschinen di erenziert. Die register_kprobe prüft auf das Vorhandensein der angeforderten Probe. Dabei sei angemerkt, dass das Suchen in- nerhalb der KProbes (internen) Listen optimal mit dem im Linux Kernel integriertem Hashverfahren implementiert ist (get_kprobe benutzt hlist-Funktionen aus der [inclu- de/linux/hash.h]). Arch_copy_kprobe kopiert die komplette Anweisung, die sich an der angegeben Adresse be ndet. Die beiden folgenden Quellcodezeilen zeigen die Kernkom- ponente des KProbes, an dieser Stelle wird die Probe mittels des architekturspezi schen Breakpoint-Codes gesetzt und der Originalwert kopiert. Diese Zuweisung verdeutlicht auch, dass keine Einschränkung (an dieser Stelle) hinsichtlich der linearen Adresse ge- macht wird. Somit eignet sich das KProbes zum Setzen von frei de nierbaren unabhän- gigen Proben.
3.2 Abhandlung einer KProbe
Um den Einblick in KProbes zu vervollständigen, wird der Ablauf nach dem Auslösen einer Probe geschildert. Der dazugehörige Quellcode be ndet sich im architekturab- hängigen Teil von Kprobes (e.g. arch/i386/kernel/kprobes.c). Nach dem der Interrupt 3 mittels der in der IDT1 beschriebenen do_int3 Funktion ausgelöst wurde, folgt der Aufruf des kprobe_handlers. Der kprobe_handler sorgt für die Ausführung des mit der KProbe assozierten pre_handler Funktion. Die kprobe_handler Funktion deaktiviert die Preemption2, sieht nach ob eine Probe mit derselben Adresse vorhanden und ge- setzt ist (ein Abbruch erfolgt, wenn die Breakpoint-Anweisung keiner Probe zugordnet ist), führt den Single-Step über die Anweisung aus. Das Single Stepping besteht aus dem zurück kopieren der Original Instruktion, Deaktivierung des Trap Flags, Setzen des Instruction Pointers auf den zurückgeschriebenen Operationscode. Dieser beschrie- bene Schritt sichert die Integrität des Originalprogrammes, da jetzt die ursprüngliche Handlungsabfolge ausgeführt wird. Es folgt der Aufruf des mit der KProbe assozierten post_handler Funktion. Die allgemeine post_kprobe_handler Funktion ruft die resu- me_execution Funktion. Deren Aufgabe es ist, den durch den Interrupt veränderten Stack wieder herzustellen. Die drei behandelten Fälle sind im Quellcode als Kommentar gut beschrieben, der Fall 1 absolute bzw. relationale Sprünge werden im Abschnitt 7 ABI genauer erklärt. Nach der Wiederherstellung des Stacks wird die Preemption aktiviert und die KProbes internen Locks gelöst. Das Programm kann fortgesetzt werden.
3.3 Vorteile von KProbes
KProbe ist SMP sicher. Es verwendet eine Überwachung von schon gesetzten Break- points, damit wird einer Schleifenbildung vorgebeugt (realisiert durch die Deaktivierung der Preemption3 ). Es ist bereits plattformunabhängig realisiert. Eine Auseinanderset- zung mit dem Bytecode verschiedener Architekturen muss nicht mehr erfolgen. Der beschriebene Ablauf besitzt einen Single-Step-Mechanismus, der den veränderten Stack wiederherstellen kann. Optimal ist KProbes durch die Bestätigung des o ziellen Ein- satzes im 2.6.9 Linux Kernels.
4 Interrupts im Linux Kernel
In diesem Anwendungsfall sind synchrone, von Software erzeugte, Interrupts relevant. Die Intel Dokumentation[9] teilt die Exceptions generell in Prozessor- und Softwareereig- nis. Dabei wird eine Unterteilung der Prozessor-Exeption anhand des Wertes von EIP[1] in Fault,Trap und Abort vorgenommen. Das Softwareereignis ist eine explizite Anforde- rung/Aufruf einer Exception. Dieser Softwareinterrupt wird als Trap implementiert (Bei- spiel: System-Calls benutzen Interrupt [0]x[80]). Für dieses Projekt ist die Klasse der Traps interessant. Traps zeichnen sich durch die zeitnahe Ausführung der Trap-Instruktion so- wie des damit verbundenen Programmestücks (bzw. Task) aus. Interessant sind in diesem Sinn auch die EFLAGs Register[2] . Im Besonderen das RF[3]-Bit, dieses wird benutzt um loops durch gesetzte Instruktion-Breakpoints zu vermeiden. Benötigt wird das Bearbeiten dieses Flags, da Interrupts auf der IA-[32] Plattform nicht reentrant4 sind[[10]]. An dieser Stelle wird nicht auf Hardware Interrupts5 eingegangen.
4.1 Softwareinterrupts
Softwareinterrupts stehen an der Abarbeitung aller verfügbaren Interrups nicht an aller erster Stelle: gemäÿ Tabelle Abbildung: 'Prioritäten zwischen den Interrupts' an Stelle 4. Diese können nur durch einen Reset-Befehl, den Trap eines Task-Switches und einem externen, hardware generiertem Interrupt überlagert werden6. Der hohe Stellenwert des Softwareinterrupts unterstützt die Methodik des dynamischen Einp anzens von Break- points. Zum Beispiel sind NMI7,Page Faults, Invalid OpCodes8 oder Over ows niedri- ger priorisiert (theoretisch sieht man zuerst den Zugri, über den Wurm, und dann den
Abbildung in dieser Leseprobe nicht enthalten
Abbildung 4.1: IA-32 EFLAGS Prozessor Register
Fehler. Dies garantiert die feste Abarbeitungsreihenfolge für den Wurm, zuerst soll der Zugri und dann erst der Fehler protokolliert werden.
4.1.1 Darstellung im Linux Kernel
Wie gelangt man vom Setzen des magischen INT3 zu der Funktion, die das Logging übernimmt? Die Koordination der Interrupts erfolgt über die globale IDT9. Diese Ta- belle klärt den Zusammenhang: Interrupt ⇒ Prozessor ⇒ Funktion. Folgend werden die Schritte aufgezeigt, die benötigt werden, um zu der eigentlichen eingeplanzten Funktion zu kommen.
Die globale IDT wird in dem IDT-Register festgehalten und kann damit an einer belie- bigen logischen Adresse stehen. Der Prozessor, der den Interrupt bearbeitet, kann über den O set das entsprechende Interruptgate rufen. In unserem konkreten Fall ist das das Gate für den Interrupt Nummer 3, der wie aus dem Bild zu erkennen, bei der Basis- adresse + 16 Byte liegt. Auf dem Bild 'IDT Gates' erkennt man den konkreten Aufbau aller Task Gates. Hierbei ist wiederum nur das Trap Gate für uns von Bedeutung. Aus diesem Bild und aus dem Hinweis, dass dem Prozessorkern jeweils nur einmal Interrupts mitgeteilt werden, schlieÿt man, dass eine korrekte Initialisierung der Speicherstrukturen notwendig ist.
Abbildung in dieser Leseprobe nicht enthalten
Abbildung 8.6: Lineare Adress Umsetzung
4.1.2 Interrupt 3
Der Interrupt Nummer 3 gehört zur Exception Klasse Trap, ist ein Byte lang (Opera- tionCode '0xcc' für Intel Architektur 32) und wird benutzt, in dem man im Speicher eine Anweisung mit dieser Breakpoint Exception austauscht. Dies hat zur Folge, dass die Register CS10 und EIP, die auf die nächste Anweisung zeigen, auf den Stack gelagert werden[11]. Um die Interrupt-Handler Prozedur ausführen zu können, erhält der Pro- zessor den Interruptvektor (also die Interruptzahl, in unserem Fall die 3, per Software do_int3 [12]). Mit Hilfe der Interrupt Nummer kann der Prozessor, über den Index (als O set), den Eintrag in der IDT bestimmen. Da der Interruptvektor 3 ein Trap Gate ist, wird die entsprechende Funktion wie Prozeduraufruf behandendelt (quasi wie CALL Gate[?] die angesprochenen Adressen werden direkt geladen). Zwecks Vereinfachung wird an diesem Punkt die vom Prozessor ausgeführten Privilegienvergleiche (CPL mit DPL aus dem IDT-Trap Gate) unterschlagen11.
Kernel Control Path
Erreicht ein Interrupt Signal ein Prozess, so unterbricht die CPU dessen Ausführung und beginnt mit der Sicherung des Programm-Counters (Register CS und EIP) in den Kernel Stack. In die Register wird dann die Adresse relativ zum ausgelösten Interrupt- typs geschrieben. Der beschriebene Ablauf ähnelt dem eines Prozess-Switches. Der Vor- zug des Interruptes ist, dass der Interruptcode direkt in die nicht veränderte Stack- Umgebung des Prozesses geschrieben und ausgeführt wird [arch/i386/kernel/entry.S common_interrupt ]. Diese eingesetzte Methotik Kernel Control Path ist schneller als ein kompletter Prozesswechsel.
5 Darstellung der Programme im RAM
Daten die den Prozess Adressspeicher beschreiben, stehen in der Struktur mm_struct [include/linux/sched.h]. Interessant ist hierbei die unsigned long start_code und end_code De nitionen, diese enthalten die entsprechenden logischen Adressen, die den ausführbaren Quellcode enthalten. Die Struktur mm in der task_struct zeigt auf den Memory Deskriptor des jeweiligen Prozesses. Active_mm zeigt auf den Memory Deskriptor des Prozesses während seiner Ausführung. Diese beiden Strukturen sollten für normale Prozesse den gleichen Pointer enthalten.
5.1 lineare Adressierung
Die Struktur mm_struct enthält alle Speicherregionen, die einem Prozess zugeordnet sind. Realisiert wird das durch eine verlinkte Liste, implementiert durch die Struktur vm_area_struct. Die Struktur vm_area_struct enthält in sich die Strukturen vm_area_struct vm_next. Die Übersicht wird durch die Struktur mmap in task_struct bewahrt. Das folgende Bild fasst das Zusammenspiel der Strukturen erkennbar zusam- men: Fragt ein User-Space Programm nach Speicher, so bekommt es diesen nicht prompt, sondern es erwirbt erst einmal einen Adressbereich innerhalb der linearen Adresse (be- kannt als 'memory region').
5.2 Logische und physikalische Adressierung
In dieser Sektion wird deutlich, dass die Plattformunabhängigkeit und die Portabilität verloren geht. Das Abfragen der Register muss architekturspezi sch abgekapselt werden. Wie auf dem Bild 'Segment O set' zu erkennen, unterteilt man die Adressierung in lo-
Abbildung in dieser Leseprobe nicht enthalten
Abbildung 5.1: Segment O set[13]
gische Adressen1, lineare Adressen2 und die physikalische Adresse3. Die logische Adresse besteht aus Segment 16bit und O set 32bit. Speziell auf dem Intel sind die Register CS-Code Segment SS-Stack Segment DS-Data Segment bedeutsam. Jedes Segment wird durch einen 8 Byte langen Segmentdeskriptor-Eintrag beschrieben. Dieser Deskriptor steht in der GDT bzw. in der LDT 4. Die Register bauen sich wie folgt auf: Interessant sind die Code-Segmentregister, da direkt Ein uss auf den auszuführenden Code genom- men werden soll. Nun wurde gesagt, dass die logische Adresse aus 16bit Segmentwahl + 32bit O set besteht, allerdings im Deskriptor nur 32bit hinterlegt werden. Die Seg- mentierung wird benutzt um die lineare Adresse (dem Adressbereich des Prozessors) in kleinere handelbare sowie beschützbare Bereiche zu zerlegen. Dieses Bild 'Segmen- tatierung und Paging' verdeutlicht die Einordnung bzw. die Verwendung der linearen Adresse. Interessant ist der rechte Teil des Bildes, die Schritte der Berechnung von der linearen Adresse zu der physikalischen Adresse. Schlieÿlich ist es das Ziel, Programme auÿerhalb ihres Kontextes (wenn diese nicht im Status running sind) mit INT3's zu bestücken. Wie man sieht, besteht die logische Adresse aus Segment und O set. Die Segmente werden vom Prozessor intern verwaltet und sind nicht extern beschreibbar [14]. Im weiteren Verlauf spielt die Segmentverwaltung des Intel Prozessors in diesem Dokument keine Rolle und wird als 'magic' abgetan5. In der GDT wird der O set wie in Bild 'Segment O set' gespeichert. In dem linearen Adressraum spricht man damit eine bestimmte Page an, Instruktionen werden als Teilelemente eines Blockes gespeichert. Es soll verdeutlicht werden, dass die lineare Adresse eine Abstrahierung ist, die benötigt wird, um zum Beispiel lückenlos Swap-Speicher einzublenden. Wenn das Paging aktiviert ist, teilt man die Segmente in z.B. 4KByte groÿe Pages. Dabei muss das Betriebssystem das Page Directory und die Page Table verwalten. Unter Angabe dieser beiden Informationen (also der linearen Adresse) und der prozessorinternen Segmentdarstellung wird die physikalische Adresse berechnet. Das bekannteste Beispiel ist das Page Fault: der Versuch des Zugri s eines Prozessors auf eine nicht im RAM vorhandene Page, in dem Fall muss das Betriebssystem diese Page manuell nachladen.
5.2.1 Adressberechnung
Um nun eine physikalische Adresse zu berechnen werden, zwei Stufen der Adressum- setzung benutzt: die logische Segment Adressierung und das linear adressierte Paging. Die exakte Zerlegung der linearen Adresse wird gemäÿ Bild 'Lineare Adress Umsetzung' vorgenommen. Die Bits 22 bis 31 beschreiben den O set zu einem Eintrag im Page Direc- tory. Die Bits 21 bis 12 beschreiben den Page Table Eintrag. Die restlichen 12 Bits (also 2 12=4KByte) adressieren das konkrete Byte im real vorhandenen Speicher. Die Aufga- be des Speichermanagaments ist es die Page Directorys für alle Prozesse zu verwalten. Nützlich scheint auch das Task State Segment (siehe Bild: 'Task State Segment'). Hier sieht man den Task-Kontext der bei einem Task Switch gesichert wird. Dabei hilft das Register CR36 weiter. Zusammen mit dem Aufbau der Page Directory, der Page Table und dem EIP kann man die Einträge bestimmen, welche im Ergebnis eine physikalische Page im RAM bestimmen.
Umsetzung. Im vorigen Absatz wurde die theoretische Berechnung hergeleitet. Im konkreten Fall muss eine Verknüpfung zwischen dem Task Deskriptor und dem TSS gesucht und benutzt werden. Verfolgt man einen Prozess, kann die task_struct als Ausgangspunkt benutzt werden: Task_struct de niert eine Struktur struct_mm, die beeinhaltet einen Verweis auf pgd_t dem Page Global Directory.
Zusammenfassend kann aus den start_code und end_code aus der mm_struct mit dem Register CR3 die physikalische Speicherstelle berechnet werden, bei der der Wurm
Abbildung in dieser Leseprobe nicht enthalten
Abbildung 5.2: Task State Segment
seine ASM-Quellcode Analyse beginnen kann. Folgendes Problem trat bei dem Ver- such der Realisierung auf, folgende Abfrage: 'current->thread.cr3' funktioniert im Ker- nel 2.6.9 nicht mehr7. Damit fällt dieser Weg zur Ermittlung des CR3 Registers weg. Ein anderer Weg ist die Ermittlung des CR3 über die Funktion switch_mm() [asm- i386/mmu_context.h], die die Funkion load_cr3 ruft. In dem Rumpf wird eine Di erenz zwischen der übergebenen Adresse der PGT und dem O set des Kernels gebildet.
Prozessiterierung. Ein Randproblem bei der Realisierung ist die Zuordnung Speicher- stelle - Prozess. Die einfachste Lösung ist: alle Prozesse der Reihe nach durchzusuchen.
siehe: current.h inline Makro get_current()
Eine Prozessiteration kann mit folgendem Makro durchgeführt werden:
Listing 5.1: Ausschnitt aus dem Source 2.6.9 kernel/kprobes.c
Abbildung in dieser Leseprobe nicht enthalten
6 Prozess Speicherabbilder
6.1 Prozessspeicher Organisation
Als Aufgabe gilt es zu zeigen, wie der Compiler den Quellcode hinterlegt. Technisch ist die PID eines Prozesses der O set der logischen Adresse. Die Prozessliste ist eine doppelt verkette Liste. Der Hardware Kontext einen Prozesses wird in der TSS (Task State Segment) abgelegt, realisiert wird dies durch die 'tss_struct'-Struktur. Der Linux- Kernel sieht nur ein TSS je Prozess vor[15], deswegen ist das 'busy' Bit immer 1. Die TSSD werden in der GDT gespeichert1, das Register TR beinhaltet den Selektor für das jeweilige TSS Element in der GDT. Librarys werden mit in den Prozesskontext einbettet. Prinzipiell führt der Systemcall 'execve()' eine Executable zum Start des gewählten Programms. Der Ablauf beinhaltet die Änderung der GID und EGID, das Laden der Ausführbaren in den Speicher und das verlinken der shared Librarys mit dem Binärcode der Executable. Das Linken spielt eine groÿe Rolle bei Programmen. Es gilt zu prüfen ob diese drei Arten des Linkens problematisch für den Wurm werden könnten. Die drei Arten des Linkens sind: statisch, dynamisch und Laufzeit[19].
6.2 Speicherabbild
Für Speicherabbild gilt: Programme werden segmentiert im Speicher abgebildet. Der vorhandene lineare Adressraum wird partitioniert in: Text, Data und Stack Segment. Das Text Segment enthält den ausführbaren Code. Das Data Segment unterteilt sich in initialisierte und nicht inititalisierte. Das initialisierte Segment enthält die mit Werte belegten globalen sowie statischen Variablen. Das nicht initialisierte Segment (auch als BSS, historischer Herkunft) enthält die globalen Variablen, deren Werte erst zur Laufzeit bestimmt werden. Das Stack Segment sichert den Stack, der zum Beispiel die return Adressen, Parameter, programmlokale Variablen der gerufenen Funktionen sichert.
6.2.1 ELF
Das standardisierte ELF (Electronic Linker Format) benutzt man, um den binären Pro- grammcode zu strukturieren bzw. um Parameter zu beschreiben. Die Startadresse ei- nes auszuführenden Programmes ist nicht frei wählbar, diese hängt vom Format (bina- ry_handler) des auszuführenden Prozesses ab. Der initial auszuführende Code[16] ist nicht relokierbar, weil dieser Mutmaÿungen über Lokation der Variablen tre en muss. Globale initialisierte Variablen zum Beispiel werden an bestimmten Stellen vermutet. Speziell für das ELF-Format ist die erste auszuführende Instruktion an der linearen Adresse 0x08084000 erwartet[22].
Folgt man der Idee, die Sprungadressen vor dem Start einer Applikation herauszulesen, so ist die Funktion load_elf_binary [linux/fs/binfmt_elf.c] zu beachten. Die Initialise- rung der mm_struct erfolgt über 'current->mm->start_data = 0;'. Ausgehend von der C-Routine execve(), welche der einzigste Linux System Call ist mit dem Programme ausgeführt werden können, wird folgender gekürzter Pfad wie folgt gegangen. Aus 'li- nux/fs/exec.c' wird ersichtlich, dass Speicher für den Binary Handler allokiert wird 'bprm = kmalloc(sizeof(*bprm), GFP_KERNEL);', die Datei nicht im Moment geschrieben wird ' le = open_exec( lename);', 'retval = prepare_binprm(bprm);'[linux/fs/exec.c] prüft nach den Nutzerrechen (UID, GID) und kopiert die ersten 128 Byte (die Magic2 )des Programms in die buf-Variable 'memset(bprm->buf,0,BINPRM_BUF_SIZE);'. Die Funk- tion prepare_binprm probiert anhand der gespeicherten Magic alle registrierten Bi- nary Handler aus. Der Einfachheit halber wird festgelegt, dass der Binary Handler load_elf_binary [linux/fs/binfmt_elf.c] ist.
6.3 ABI
Das System V Application Binary Interface (ABI) de niert eine Schnittstelle mit dem kompilierte Programme auf einem System V kompatiblen System ausgeführt werden können. Die Schnittstelle liegt im binär Format vor.
6.3.1 ABI
Bei dem Erstellen eines Prozesses werden logische Datei-Segmente (e.g. text,data,stack) in ein Speichersegment abgebildet. Dabei werden erst die physischen Seiten im RAM allokiert, wenn eine logische Referenz innerhalb des Programm-Codes erfolgt. Dadurch vermeidet man unnötiges physikalische Lesen im Speicher. Diese E zienz kann nur ge- währleistet werden, wenn die Segmente der shared librarys sowie des ausführbaren Code Vielfache der Page Size sind und die benannten Segmente kongruent sind.
Abbildung in dieser Leseprobe nicht enthalten
Abbildung 6.1: Aufbau Executable File[23]
6.3.2 ABI Sprung-Arten
Folgende Call Arten sind in der Intel 386 ABI de niert:
- absoluter direkter Funktionsaufruf,
Abbildung in dieser Leseprobe nicht enthalten
- positions-unabhängiger direkter Funktionsaufruf,
Abbildung in dieser Leseprobe nicht enthalten
- absolut indirekter Funktionsaufruf,
Abbildung in dieser Leseprobe nicht enthalten
- positions-unabhängiger indirekter Funktionsaufruf
Diese Auszüge zeigen die mögliche Assembler-Darstellung von Funktionsaufrufen. Der Wurm sollte diesen Standard nutzen und verarbeiten können. Von der vollständigen Erkennung aller Sprungsequenzen im fortlaufenden Programm uss hängt der Wurm schlieÿlich ab. Wie erkennbar, realisieren Programme Funktionssprünge durch die 'call' Instruktion. Mit 'call' kann man den EIP(i386 Instruktion Pointer) soweit verändern, das dieser an jede beliebige Stelle innerhalb der linearen Adresse springen kann. Dies gilt insbesondere auch wenn der angesprungene Code in shared librarys liegt.
Kann das Aussehen der ABI das Verhalten des Wurmes beein ussen? Ja, wenn Funktionssprünge anders de niert werden. Solange aber eine Binärkompatibilität (x86 und x86-64) besteht sollte zumindest an dieser Stelle keine Probleme auftreten.
7 Realisierung
Das Kapitel behandelt einige ausgewählte Themen aus Verlauf der Realisierung.
7.1 One Shot Trace
Im ersten Schritt der Erstellung des Prototypen wurde versucht den Groÿteil in Assemb- ler zu verwirklichen. Dieser Ansatz landete schnell in der Sackgasse. Die Idee erwies sich als unbrauchbar, da ein Prozess nicht problemlos gezielt in den Speicher eines anderen schreiben kann. Weiterhin fehlt dem Ansatz das Prinzip der Sperrung vor Preemption und die Multiprozessorsicherheit. Dies hat den Vorteil der Schnelligkeit, man verliert aber dadurch die Plattformunabhängigkeit und einen Teil der Wartbarkeit. Letztend- lich wurde die Entscheidung getro en auf die Optimierungseigentschaften des Compilers zu vertrauen und den Weg über die Kernel-Noti er-Chain zu gehen. Der mitgelieferte Quellcode soll eine Diskussionsbasis bereitstellen. Es wird in diesem Kernel-Modul eine Speicheradresse von einem beliebigen System-Call hart kodiert. Der System-Call kann aus der Systemmap (nicht mehr standardmäÿig ab 2.6.x beim Kernelbuild aktiviert) oder aus '/proc/kallsyms' gewonnen werden. Der strukturelle Ablauf sieht vor: Setze an gegebene Speicheradresse ein INT3, benutze die noti er-Warteschlange des Kernels für die Benachrichtigung, tausche den INT3 Befehl mit dem vorher ausgelesenen Wert aus, setze den Instruction Pointer auf die Speicheradresse des System-Calls. Der Aufruf wird nun ohne Unterbrechung abgearbeitet.
7.1.1 Sprungadressen
Als erste erkennbare Schwierigkeit stellt sich das Herausbekommen der Informatio- nen über die Wegsprungadressen dar. Der gcc-Compiler benutzt zur Optimierung die RTL (Register Transfer Language1 ), dieser plattformunabhängiger Pseudo Assembler Code[20] könnte erweitert werden. Ziel wäre es bei der Ausgabe der Debuginforma- tionen die Sprungadressen (die Call Instruktionen[21])als Kommentar anzuzeigen. Es wurde bei einem Test2 deutlich, wenn der Kernel als Grundlage dient entsteht ein Groÿ an Informationen. Ein schlüssiges Konzept zur performanten Verwaltung muss noch erarbeitet werden. Neben der RTL gibt es die einfachere Möglichkeit: die Ausgabe des dissasembliertem Binärcodes zu parsen.
7.1.2 Userland Proben
Bei der Entwicklung des Beispiel Kernel Moduls drängt sich die Frage auf, wie kann man als externer Prozess/Handlungsfolge innerhalb des Speichers eines beliebigen Prozesses lesen/schreiben ohne wichtige Qualitätsmerkmale(Performanz, Stabilität) zu verletzen. Ziel des Kernels ist es die Speicherbereiche zweier Prozesse nicht überlappen zu lassen. Jeder Prozess kann die lineare adressierte Speicherstelle 0xb 988 lesen und schrei- ben, die genannte Adresse wird durch das Kernelinterface in eine andere physikalisch vorhandene Speicheradresse abgebildet. Der saubere Weg wäre also ein erzwungener Task Switch(über die Funktion schedule()). Diese Variante stellt sich aber als sehr un- praktikabel und unperformant für dieses Projekt heraus. Der Beweis liegt im Quellcode vor(schedule() in kernel/sched.c, bzw. das Exekutive switch_to in asm-i386/system.h). Der Blick in die Quellcodedateien bescheinigt einen intensiven Aufwand beim Wechsel einer Task. So wird beim Context Switch des Prozessors die gesamte Prozessumgebung hergestellt (siehe context_switch() in sched.c).
Eine Betrachtung wert ist die Frage, ob man Tracing im User und Kernelspace trennen sollte. Für die Trennung spricht die Möglichkeit, den zu tracenden Prozess als Child- prozess zu starten. Dieser Vorteil spiegelt sich in dem Zugri auf den Speicher des zu überwachenden Programmes wieder. Eine einfach zu realisierende Möglichkeit wäre alle programmexternen Sprünge anzuzeigen (siehe Mapping des gcc-Compilers). Ebenfalls vereinfachen würde sich der Datenaustausch zwischen Status zur Breakpointaulösung und z.B. der Anzeige in einer Shell. Die dazu verwendeten Datenstrukturen könnten entlang der Prozesserblinie geteilt werden, ein umfangreich gesichertes Locking ist ver- mieden. Das Minimalgerüst besteht aus einem Ausführungspfad, der sich anordnet: aus dem Setzen der Sicherung des Originalen Operationscodes, dem Setzen der Breakpoint Anweisung (da Breakpoint einen Interruptaufruf impliziert, steht die Frage, ob auf die kernelseitige Implementierung von Interrupts zurückgegri en werden sollte oder eine Trampolin Funktion favorisiert werden sollte). Fakt ist, dass der Interrupt Aufruf einen kleinen Overhead durch das nachsehen in der IDT hat. Ziel ist es allerdings diesen zu vermeiden und bestenfalls auf die übergebenen Argumente (platziert im Stack) anzeigen zu können.
7.2 Dateien
Die für das Projekt erstellten Dateien sind nötig gewesen, um den Inhalt der Projektarbeit zu nden.
7.2.1 fct_funktions_adressen.pl
Das Perlskript benutzt die 'objdump -d <executable>' Ausgabe um die Sprungadressen zu der Proc-Schnittstelle '/proc/fctrace_nasse_erde' zu übertragen. Dabei übergibt es die Kommandos '0' und '1' für Vaterfunktion und abgeleitete Sohnfunktionen. Durchsucht wird schrittweise nach dem String 'call'.
7.2.2 fct_nasse_erde.c
Den Basiscode für das Kernelmodul wurde aus Linux Kernel Modul Programming Guide[3] entlehnt und erweitert. In der proc le_write Funktion wurde eine Abfrage hinzugefügt, die bei Übermittlung von Zeichen an '/proc/fctrace_nasse_erde' die Funktion 'eins' startet. Eins ist Bestandteil des Ablauftestes/Proof-of-Concepts. Mit den insgesamt drei Funktionen, die sich nacheinander aufrufen und einen Eintrag im Syslog hinterlassen, soll die prinzipielle Machbarkeit bewiesen werden.
7.2.3 fct_wurm.c
Das Kernelmodul stellt die Managementebene des Wurmes dar, es wurde wiederum auf die Vorlage vom Linux Documentation Project[3] aufgebaut. Bereitgestellt wird das Proc-Interface '/proc/fctrace', dies dient unter anderem zur Kommunikation mit dem User-Space. Die globale, zwei dimensionale Tabelle enthält die Zuordnung: Funktion(y- Richtung) und die dazugehörigen möglichen rufbaren Funktionen(x-Richtung). Weiter- hin gibt es einen Zeichen-Zahl Extraktionsfunktion strtoint, diese ist erforderlich, weil die User-Space Bibliotheken nicht im Kernel-Space verfügbar sind. Problemmatisch an dieser Stelle ist, das keine Prüfung der übergebenen Daten statt ndet, so ist es möglich einen Kernel-Oops herbeizuführen. Nach der Übernahme der Funktionsadressen, kann mit dem Befehl '%' und Adresse die erste KProbe, damit der Anfangspunkt des Wur- mes, gesetzt werden. Das Absetzen der '%' Anweisung erzwingt ein iteratives Suchen in y-Richtung der globalen Tabelle. Sollte die Gleichheit festgestellt werden, so wird die Probe über kprobe_init, aus der folgenden Header Datei, gesetzt.
7.2.4 fct_wurm_kprobe.h
Diese Bibliothek basiert auf einem KProbe-Beispiel[4]. Die pre_handler Funktion wird vor der gesetzte Probe3 ausgeführt. Hier be ndet sich die Wurm-Logik, anhand des ausgelösten Interrupts wird durch die globale Tabelle in y-Richtung iteriert. Diese y- Richtung enthält die Vaterfunktionen, die Schleife durchsucht diese und wird bei einem Tre er alle zugehörigen long-Werte4 als Probenadresse interpretieren und damit regis- trieren5. Der Schalter für das Deaktivieren der gesetzten Proben ist funktionell vorhan- den.
7.3 Abschliessende Betrachtung
Das Projekt zeigt das von der eigentlichen Idee eine einfache Verfolgung von Aufrufen entlang eines Ausführungspfades eine Menge Grundlagenarbeit zu leisten ist. Als Schwie- rigkeit ist auch das Testen der Implementation zu vermerken. Einserseits können nicht alle im User-Space gewöhnten Funktionen und Bibliotheken zur Kernelprogrammierung benutzt werden. Es ist so zum Beispiel nicht möglich eine Datei(eingebunden über die ' le' Struktur aus 'stdio.h') im Kernel-Space zum lesen zuönen. Dies erzwingt zur Kommunikation in Richtung Kernel ein mit user-Rechten schreibbares proc-Interface. Bedeutend an dieser Stelle ist das andere Funktionen für zum Beispiel die Umrechnung von char-Arrays in einen Integer Wert benutzt werden müssen. Zwingend ist auch die Be- nutzung von printk anstelle von printf. Die Unterscheidung wird in der Art wie man den Namen der Funktion au öst vorgenommen. Prinzipiell gilt das die Namen der Funk- tionen von Kernelmodul beim 'insmod' aufgelöst werden. Es können nur Funktionen verwendet werden, die also im Kernel de niert sind (die also über 'cat /proc/kallsyms' angezeigt werden). Die Funktionsnamen im User-Space werden beim Linken des Com- pilers aufgelöst. An dieser Stelle wird sicher gegangen, dass der Code in einer Library verfügbar ist. Library Funktionen laufen aus jenem Grund komplett im UserSpace, weil diese einen einfacheren Zugri auf Kernelseitige Funktionen(besser: System-Calls) bieten sollen. Eine andere Schwierigkeit ist die lange 'turn-around' Zeit. Anders als im Userspace verhindert ein Segmentation Fault in einem Kernelmodul das Entladen dieses Modules. Das zeigt sich durch den 'usage count' (siehe Ausgabe von 'lsmod'). Solang der 'usage count' grösser null ist, wird eine Benutzung dieses Modules durch andere angezeigt[in module_refcount Funktion kernel/module.c]. Dem nach muss nach einem gemachtem programmierten Fehler entweder der Kernel komplett neu gestartet werden oder ein an- deres Kernelmodul setzt den 'usage counter' des nicht mehr entladenbaren Modules auf 0. Der Vollständigkeit wird auch der Fallstrick der Lizensen erwähnt. Das Kernelmodul muss MODULE_LICENSE(GPL) [include/linux/module.h] enthalten. Entfällt der ge- samte String wird angenommen, das das Modul unter einer proprietären Lizenz verfasst wurde. Das äuÿert sich beim Laden des Moduls mit der Warnung 'Kernel tainted'. Aber um die Aussage zu unterlegen: in kprobes.c werden die spezi schen KProbe-Funktionen (e.g. register_kprobe ) per EXPORT_SYMBOL_GPL exportiert. Diese Makro sichert die Interessen der General Public License. Die Vereinbarung in der Lizenz legt fest, das jede von der GPL abgeleitete Arbeit (=Quellcode) wieder unter der GPL verö entlicht werden muss. Das heiÿt der Quellcode muss der Allgemeinheit verfügbar gemacht wer- den. Das Ergebnis der Aussparung dieser Quellcodezeile bedeutet, dass beim insmod des resultierende Modules die unresolved Symbol Warnung kommt, weil auf die GPL signierten Funktionen nicht zugegri en werden kann.
7.3.1 letzter Stand
Zu den beiden erwähnten Kriterien in 1.1 ist auswertend zu sagen, das keine Plattformunabhängigkeit gewahrt werden kann. Die Speicherzugri e und die Umrechnung der linearen Adresse in eine phyikalische sind prozessorspezi sch. Die einfache Maintenance wäre durch das KISS6 Prinzip zu realiseren. Das Herauslesen der nächsten WegSprungadressen aus den Folge-Instruktionen relativ zur Adresse des letzten gesetzten INT3 erwies sich als langwährig und nicht erfolgreich. Um das FCtrace eine ungefähre Handlung wie dem strace zu geben, wurde deswegen das Heraus nden der Funktionsadressen der Sprungpunkte in den UserSpace verlagert. Aber ohne eine vorrausschauende, speicherbasierende Fortplanzung und die Erkennung der möglichen Wegsprungpunkte des Wurmes ist ein realistischer Einsatz nicht denkbar.
Speziell im Quellcode des Wurmes wird deutlich, dass die Verwaltung der einzelnen Sprungpunkt-Adressen in einer zweidimensionalen Tabelle naiv ist. Hier wäre eine HashFunktion angebracht, da einerseits das Suchen nach der Vaterfunktion im Worst-Case einen nahezu kompletten Durchlauf bedeutet und andererseits Hashing-Funktionen im Kernel schon verfügbar sind.
Im ersten Schritt des Projektes sollten Performanceuntersuchungen angefertigt werden. Diese können nur theoretisch behandelt werden. Der Wurm basiert auf einem Schleifen- konstrukt der Komplexität O(n) für die Vaterfunktionen und O(m) für die Sohnfunk- tionen. Der obere, feste Grenzwert ist im Quellcode durch die globale Tabellende nition von 256 gegeben. Diese lineare Suche nach den zu registrierenden Sohnfunktionen erfolgt bei jedem INT3 Aufruf. Zugegebenermaÿen wäre es interessant zu wissen, wie sich diese Verzögerung auf eine Applikation auswirkt. An dieser Stelle müsste di erenziert werden in die Dauer einer Interruptverarbeitung, die Dauer der Registrierung der Sohnfunk- tionen sowie das die Dauer des Logging vom syslog Mechanismusses. An dieser Stelle wird der Zeitverlust durch das Heraus nden des Funktionsendes (dem Wegsprungpunkt) weggelassen. Diese Zeit wäre abhängig von der Implementierung7 der zu observierenden Funktion und die beeinhaltenden Wegsprungpunkte.
Die Idee das KProbe um einen Patch zu erweitern wurde nicht weiter verfolgt. Zur möglichen realen Anwendbarkeit sei gesagt, dass ein Patch für das bestehende KProbes erstellt werden sollte, um eine minimale Chance auf eine Aufnahme in den Stable Linux Kernel Source Tree zu wahren. Bislang wurde ein kompletter Framework um KProbes herum geschrieben.
Literaturverzeichnis
[1] Olaf Dabrunz, SUSE Technical Engineer, odabrunz@suse.de, 24.06.2004
[2] GNU General Public License, 21.09.2005, http://www.gnu.org/copyleft/gpl.html
[3] Linux Documentation Project, Kernel Modul 2.6, http://www.tldp.org/LDP/lkmpg/2.6/html/x773.htm
[4] Linux Documentation Project, Kernel Modul 2.6, http://www.tldp.org/LDP/lkmpg/2.6/html/x773.htm
[5] Homepage DTrace, Firma SUN Microsystems, 24.09.2005, http://www.sun.com/bigadmin/content/dtrace/
[6] dtrace_usenix.pdf, 21.09.2005, http://www.sun.com/bigadmin/content/dtrace/dtrace_usenix.pdf
[7] CDDL, Opensolaris Lizenz, 23.09.2005, http://www.opensolaris.org/os/licensing/opensolaris_license/
[8] Intel Architecture Manual, Kernel Ring, 16.02.2006, Seite 131, ftp://download.intel.com/design/Pentium4/manuals/25366817.pdf
[9] Intel Architecture Manual, 16.02.2006, Seite 170, ftp://download.intel.com/design/Pentium4/manuals/25366817.pdf
[10] Intel Architecture Manual, 16.02.2006, Seite 187, ftp://download.intel.com/design/Pentium4/manuals/25366817.pdf
[11] Intel Architecture Manual, 16.02.2006, Seite 198, ftp://download.intel.com/design/Pentium4/manuals/25366817.pdf
[12] Bovet, Daniel, Cesati, Marco: Understanding the Linux Kernel 2nd, O'Reilly, 2002,
Seite 152
[13] Bovet, Daniel, Cesati, Marco: Understanding the Linux Kernel 2nd, O'Reilly, 2002, Seite 53
[14] Bovet, Daniel, Cesati, Marco: Understanding the Linux Kernel 2nd, O'Reilly, 2002, Seite 54
[15] Bovet, Daniel, Cesati, Marco: Understanding the Linux Kernel 2nd, O'Reilly, 2002, Seite 110
[16] Bovet, Daniel, Cesati, Marco: Understanding the Linux Kernel 2nd, O'Reilly, 2002, Seite 741
[17] KProbes Homepage, Firma RedHat, 24.10.2005, http://sources.redhat.com/systemtap/kprobes/
[18] IBM KProbes HowTo, 12.11.2005, http://www- 106.ibm.com/developerworks/library/l-kprobes.html?ca=dgr-lnxw07Kprobe
[19] Compilation Process, University of Illinois, 15.11.2005, http://www.acm.uiuc.edu/sigmil/RevEng/ch02.html
[20] GNU, RTL Spezi kation, 03.02.2006, http://gcc.gnu.org/onlinedocs/gcc-4.0.0/gccint/RTL.html
[21] GNU, RTL Spezi kation: call, 03.02.2006, http://gcc.gnu.org/onlinedocs/gcc-4.0.0/gccint/Calls.html
[22] ABI Intel 386 DRAFT, http://www.caldera.com/developers/devspecs/abi386-4.pdf
[23] ABI Intel 386 DRAFT, Seite 81, http://www.caldera.com/developers/devspecs/abi386-4.pdf
8 Abbildungen
Die folgenden Abbildungen sind Ausschnitte aus dem Intel Architecture Manual[9].
Abbildung in dieser Leseprobe nicht enthalten
Abbildung 8.1: Prioritäten zwischen den Interrupts
Abbildung in dieser Leseprobe nicht enthalten
Abbildung 8.2: Systemregister und deren Datenstrukturen
Abbildung in dieser Leseprobe nicht enthalten
Abbildung 8.3: Zusammenhang IDT und IDTR
Abbildung in dieser Leseprobe nicht enthalten
Abbildung 8.4: IDT Gates
Abbildung in dieser Leseprobe nicht enthalten
Abbildung 8.5: Segmentierung und Paging
[...]
1 http://216.239.51.104/search?q=cache:BCri1pIBLYgJ:kroah.com/ persönlicher Blog, mit persönli- cher Meinung von Greg Kroah-Hartman einem renomiertem Kernel Developer
2 Alias: kernelnewbies@nl.linux.org
3 siehe die Log-File: /var/log/messages
4 vgl. Kapitel Sprungarten 7.0.3
5 abstrahiert betrachtet ähnelt das einer Trampolin Funktion
6 Interrupt Nummer 3, prozessor spezi sches Funktionsbyte, siehe 4.1.2
1 eine dedizierte Datei innerhalb des /proc Verzeichnisbaumes
2 besser: Adressraum
3 anspringbaren
4 Ausgabe von objdump -d <Beispielkernelmodul.ko>, die Sprungadressen werden aus dem User- space in den Kernelspace kopiert
5 Hierbei unterschlägt man gewisse Fälle, zum Beispiel: Rekursion
6 tabellenartige, hashbasierende Organisation
7 ein Erweiterungs-Patch für die Proben-Verwaltung bedarf den Zuspruch der KProbe Entwickler
1 Interrupt Deskriptor Table, siehe
2 Fähigkeit eines Betriebssystems die Abarbeitung eines Prozesses zu unterbrechen
3 über die spin_lock Funktion
1 Extended Instruction Pointer, IA-32 spezi sch
2 prozessorspezi sch, zur Initialiserung des Prozessors benötigt
3 Resume Flag
4 wiedereintrittsfähig, Vermeidung von globalen Variablen
5 asynchron
6 zum Beispiel die FLUSH-Anweisung
7 Nicht maskierbare (besser: abfangbare) Interrupts
8 Operation Code
9 Interrupt Deskriptor Table
10 Codesegment
11 Der Wurm muss mit Root-Rechten ausgeführt werden, damit wird der ausgeführte Vergleich (3>0)[8] immer wahr wird
1 segment:o set
2 32 Bit, universale Adresse
3 Intel x86 32bit, Adressierung für 4GByte
4 die Adressen der globalen Register sind bei x86 aus den Registern ldtr und gdtr zu entnehmen
5 mit dem Verweis auf die prozessorspezi sche Intel Dokumentation, http://www.intel.com/design/Pentium4/documentation.htm
6 beeinhaltet die physikalische Adresse des Page Global Directory
7 siehe: current.h inline Makro get_current()
1 siehe GDT-Register
2 umgangssprachlich für die Identi kationsnummer des Formates
1 Bestandteil der GNU C-Compiler Collection
2 gcc mit Option '-dr' 'Dump after RTL generation'
3 der Breakpoint-Anweisung
4 in der globalen Tabelle die x-Richtung
5 mittels kprobe_init
6 keep it simple and stupid
7 die CALL Instruktion auf x86 ist ein Byte lang, es müsste byteweise gesucht werden
- Arbeit zitieren
- Patrick Kirsch (Autor:in), 2006, Dynamic function call tracing im Linux Kernel, München, GRIN Verlag, https://www.grin.com/document/110097