Aus Linux-Magazin 08/2023

Programmverhalten mit LD_PRELOAD ändern

© Chris Up / Photocase.com

Linux-Programme nutzen zahlreiche von Bibliotheken bereitgestellte Features. Mit ein wenig C-Code ersetzen Sie über die Variable LD_PRELOAD Bibliotheksfunktionen durch eigene und ändern so das Programmverhalten.

Möchten Sie herausfinden, welche Dateien ein Programm öffnet oder löscht und welche Netzwerkverbindungen es aufbaut? Mit einem Trick lassen sich Standardfunktionen wie das Öffnen von Dateien oder das Lauschen auf einem TCP-Port durch selbst programmierte Versionen ersetzen, die nicht nur protokollieren, was die Anwendung tut, sondern auf Wunsch sogar das Verhalten verändern. Den Schlüssel zu diesen Möglichkeiten bietet die Variable »LD_PRELOAD«, die den Linux-Programmlader beeinflusst.

Wenn Sie ein Programm starten, erzeugt der Linux-Kernel einen neuen Prozess und lädt die ausführbare Programmdatei in dessen Speicherbereich. Das ist aber meist nicht alles: Programme nutzen in der Regel Bibliotheken, die dynamisch hinzugeladen werden. Welche davon eine Applikation lädt, erfahren Sie über das Kommando »ldd« (Abbildung 1).

Abbildung 1: Der Kommandozeilenaufruf »ls« nutzt nur wenige Bibliotheken; bei grafischen Anwendungen fällt die Liste deutlich länger aus.

Abbildung 1: Der Kommandozeilenaufruf »ls« nutzt nur wenige Bibliotheken; bei grafischen Anwendungen fällt die Liste deutlich länger aus.

“Echte” Bibliotheken sind in Abbildung 1 nur die Einträge »libselinux.so.1« (Unterstützung für die Sicherheitserweiterung SELinux), »libc.so.6« (die Standard-C-Bibliothek) und »libpcre2-8.so.0« (Funktionen, mit denen ein Programm reguläre Ausdrücke verarbeiten kann).

Der oberste Eintrag »linux-vdso.so.1« gehört zum Kernel. Der untere (»ld-linux-x86.64.so.2«) ist der Programmlader selbst, der die benötigten Dateien lädt. Die wichtigsten Standardfunktionen, etwa für den Dateizugriff, Prozess- und Thread-Steuerung und auch alle Low-Level-System-Call-Wrapper finden sich in »libc.so.6«. Programme, die die grafische Oberfläche (also meist das X-Window-System X11) nutzen, laden zudem die X11-Bibliothek »libX11.so.6«.

Eine der Dateien in der Bibliotheksliste ist »libc.so.6«, eine GNU-C-Bibliothek [1]. Sie bringt zahlreiche häufig benötigte Funktionen mit, darunter »open()«, »read()«, »write()« für den Low-Level-Zugriff auf Dateien, »malloc()« für die dynamische Speicherverwaltung, »printf()« für die formatierte Ausgabe von Daten und »exit()« zum Beenden des Programms. Starten Sie ein grafisches Programm, kommen noch zahlreiche weitere Bibliotheken hinzu. So listet der Aufruf von »ldd /usr/bin/gedit« auf Ubuntu 22.04 beispielsweise 80 Bibliotheken auf, die der Gnome-Editor benötigt.

Eine komplette Liste aller sogenannten Symbole, die eine Bibliothek bereitstellt, rufen Sie mit »readelf« ab. So zeigt der Aufruf aus Listing 1 zum Beispiel die mit über 3000 Einträgen sehr lange Liste der Symbole der Standard-C-Bibliothek an. Nicht alle Einträge stehen für Funktionen: Zu den Symbolen gehören auch globale Variablen und Konstanten.

Listing 1

Symbole

$ readelf -Ws /lib/x86_64-linux-gnu/libc.so.6

Statisch gelinkt

Bei manchen Programmdateien listet Ldd keine Bibliotheken auf, sondern gibt die Fehlermeldung Das Programm ist nicht dynamisch gelinkt aus. Solche Programme startet Linux ohne das Laden zusätzlicher Dateien. Wenn Sie ein C-Programm mit Gcc übersetzen, erzeugen Sie über die Option »-static« eine solche statisch gelinkte Binärdatei. Abbildung 2 zeigt einige Experimente mit einem kleinen C-Programm.

Abbildung 2: Da sie alle notwendigen Bibliotheken selbst mitbringen, fallen statisch gelinkte Binaries deutlich größer aus als dynamisch gelinkte.

Abbildung 2: Da sie alle notwendigen Bibliotheken selbst mitbringen, fallen statisch gelinkte Binaries deutlich größer aus als dynamisch gelinkte.

Oben sehen Sie den Quellcode. Das Programm gibt »Hello world« und – falls vorhanden – das erste Aufruf-Argument aus. Um die Funktion »printf()« aus der Standardbibliothek zu nutzen, bindet der Code die Header-Datei »stdio.h« ein.

Die beiden Gcc-Aufrufe erzeugen eine dynamisch (»test-printf-dynamic«) und eine statisch (»test-print-static«) gelinkte Version des ausführbaren Programms. Beachten Sie den Größenunterschied: Das dynamisch gelinkte Binary belegt etwa 16 KByte, während die statische Version etwa 900 KByte groß ist, weil sie auch die Bibliotheksfunktionen enthält.

Die beiden Aufrufe von Strace prüfen, welche Dateien während des Starts und der Ausführung des Programms geöffnet werden. Die Datei »/etc/ld.so.cache« enthält eine Liste aller Bibliotheken, die bei Programmstarts automatisch geladen werden können, »libc.so.6« ist die bereits bekannte Standard-C-Bibliothek. Nur die dynamische Version des Programms öffnet die beiden Dateien. Die statische dagegen enthält schon alles an Code, was sie braucht.

Unten im Bild folgen noch Testaufrufe der beiden Programme; sie arbeiten identisch.

Eingriff

Der Programmstart lässt sich unter Linux feintunen. Insbesondere können Sie festlegen, wo der Programmlader »ld-linux-x86-64.so.2« nach Bibliotheken sucht. Das klappt entweder über eine feste Systemeinstellung in der Konfigurationsdatei »/etc/ld.so.conf« und weitere Dateien im Ordner »/etc/ld.so.conf.d/« (in diesen Dateien stehen Verzeichnisse, die Bibliotheken enthalten) oder über die Umgebungsvariable »LD_LIBRARY_PATH«: In ihr geben Sie zusätzliche Ordner mit Bibliotheken an, in denen der Loader dann mit Priorität sucht.

Diese Flexibilität ermöglicht unter anderem, dass Sie verschiedene Versionen der gleichen Bibliothek installieren und dann für einzelne Anwendungen einstellen können, welche davon sie verwenden. Am grundsätzlichen Ablauf ändert sich durch diese Anpassungen nichts: Der Loader prüft, welche Bibliotheken ein Programm benötigt, sucht diese in den vorgesehenen Ordnern und lädt sie.

Ein besonderes Ladeverhalten erzielen Sie mit der Variablen »LD_PRELOAD«: Hier tragen Sie einzelne Bibliotheken ein, die der Loader zusätzlich laden soll. Sie können auch Funktionen enthalten, die bereits in einer der regulären Bibliotheken vorkommen. Über diesen Mechanismus lässt sich beispielsweise eine einzelne Bibliotheksfunktion durch eine selbst entwickelte Variante ersetzen.

Logger

Als erstes, einfaches Beispiel für den Einsatz von »LD_PRELOAD« erstellen Sie eigene Varianten der System-Call-Wrapper »open()« und »close()«. Listing 2 zeigt den kompletten Code der Datei »openclose.c«, die Sie mit dem Kommando aus Listing 3 in eine Bibliotheksdatei »openclose.so« übersetzen. Beide Funktionen geben mit »printf()« eine Debug-Meldung auf der Konsole aus und erledigen ansonsten ihre Aufgaben, indem sie die regulären Versionen von »open()« und »close()« aufrufen. Das gelingt über einen Trick.

Listing 2

openclose.c

#define _GNU_SOURCE
#include <dlfcn.h>
#include <unistd.h>
#include <stdio.h>
#include <stdarg.h>
int (*true_close)(int fd);
int (*true_open)(const char *pathname, int flags, va_list mode);
int open (const char *pathname, int flags, va_list mode) {
  true_open = dlsym (RTLD_NEXT, "open");
  int fd = true_open (pathname, flags, mode);
  printf ("DEBUG: open(\"%s\") = %d\n", pathname, fd);
  return fd;
}
int close(int fd) {
  true_close = dlsym (RTLD_NEXT, "close");
  printf ("DEBUG: Closing fd = %d\n", fd);
  return true_close(fd);
}

Listing 3

openclose.c übersetzen

$ gcc openclose.c -o openclose.so -fPIC -shared -ldl

Die Funktion »dlsym()« findet Funktionen aus Bibliotheken über deren Namen. Darum lautet der zweite Aufrufparameter in Zeile 11 und Zeile 18 »”open”« respektive »”close”«. Das würde eigentlich Zeiger auf die hier implementierten Versionen zurückliefern, also nicht weiterhelfen. Über den Parameter »RTLD_NEXT« lässt sich aber jeweils der erste Treffer überspringen. Die fortgesetzte Suche nach den Funktionsnamen findet dann die Implementierungen in der Standardbibliothek.

Um die Funktionen dann auch aufrufen zu können, definiert der Code in den Zeilen 7 und 8 zwei Variablen, die vom richtigen Funktionstyp sein müssen. Nach den Zuweisungen in Zeile 11 und 18 ist es dann möglich, »true_open()« und »true_close()« aufzurufen.

Ein beliebiges Programm lassen Sie nun mit diesen veränderten Dateizugriffsfunktionen laufen, indem Sie dem Programmaufruf die Variablenzuweisung »LD_PRELOAD=Pfad/zur/Bibliothek« voranstellen, wobei Sie den Pfad absolut angeben müssen (Listing 4).

Listing 4

Variablenzuweisung

$ cat test.txt
Hello
$ LD_PRELOAD=$PWD/openclose.so \
cat test.txt
DEBUG: open("test.txt") = 3
Hello
DEBUG: Closing fd = 3

Verhalten anpassen

Typisch für den Einsatz von »LD_PRELOAD« ist das folgende Szenario: Sie beobachten in einer Ihrer Anwendungen ein unerwünschtes Verhalten, das Sie ändern möchten. Der Quellcode ist aber zu komplex, um dort nach der richtigen Stelle zu suchen, oder er ist nicht verfügbar. Ersatzweise überprüfen Sie mit Strace oder Ltrace (siehe Kasten “Strace und Ltrace”), in welcher Reihenfolge das Programm zum Beispiel versucht, Dateien zu öffnen oder Netzwerkverbindungen aufzubauen.

So stellt sich vielleicht heraus, dass ein Programm in einer globalen Konfigurationsdatei Informationen findet, mit denen es nicht zurechtkommt. Sie möchten aber die globale Datei nicht verändern, weil andere Anwendungen auch darauf zugreifen und diese Informationen benötigen.

Strace und Ltrace

Mit den Kommandozeilentools Strace und Ltrace aus den meist vorinstallierten Paketen strace und ltrace überwachen Sie einen Prozess bei der Ausführung und lassen dabei bestimmte Ereignisse protokollieren. Strace kümmert sich um System Calls, also Aufforderungen an den Linux-Kernel, eine Kernel-Aufgabe für den Prozess zu erledigen: Dateien öffnen, daraus lesen, Dateien schließen.

Um sämtliche System Calls eines Programms »prog« zu protokollieren, starten Sie es mit »strace -o prog.log prog« und eventuellen Aufrufparametern für »prog«. Das Protokoll landet dann in »prog.log«. Die so erstellte Liste gestaltet sich aber sehr unübersichtlich, weil ein typisches Programm in kurzer Zeit sehr viele System Calls ausführt. Besser ist es, nur bestimmte Aufrufe zu loggen. Dafür gibt es die Option »-e trace=«. Mit dem Aufruf aus der ersten Zeile von Listing 5 sehen Sie beispielsweise nur die Aufrufe der System Calls »open«, »openat« und »close«.

Lassen Sie den Schalter »-o« weg, erscheint die Ausgabe im Terminal. Das ist jedoch nur bei Programmen mit wenig eigenen Ausgaben oder bei grafischen Anwendungen sinnvoll, weil sonst die Ausgaben des Programms und die von Strace gemischt erscheinen. Eine ausführlichere Besprechung der Möglichkeiten von Strace finden Sie in einem älteren Artikel [3].

Ltrace [4] leistet für Bibliotheksaufrufe, was Strace für System Calls tut: So zeigt etwa der Aufruf aus der zweiten Zeile von Listing 5 alle Funktionsaufrufe von »open()«, »openat()« und »close()« an. Die Informationen ähneln jenen von Strace, aber hier geht es um Bibliotheksfunktionen wie »open()«, die gleichnamige System Calls (»open«, ohne Klammern) aufrufen.

Listing 5

Strace und Ltrace

$ strace -o prog.log -e trace=open,openat,close prog
$ ltrace -x open+openat+close prog

Hier hilft es, exklusiv für das problematische Programm den Zugriff auf eine andere Datei umzubiegen, die Sie mit passendem Inhalt füllen, sodass das Programm korrekt arbeitet. Dazu passen Sie die Funktion »open()« so an, dass sie jeden Versuch, eine bestimmte Datei zu öffnen, einfach mit dem Öffnen der Alternativdatei quittiert. Listing 6 zeigt, wie Sie alle Versuche, die Datei »/etc/fstab« zu öffnen, auf »/tmp/fstab.test« umbiegen.

Listing 6

openother.c

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#define SEARCH_PATH "/etc/fstab"
#define REPLACE_PATH "/tmp/fstab.test"
int (*true_open)(const char *pathname, int flags, va_list mode);
int open (const char *pathname, int flags, va_list mode) {
  true_open = dlsym (RTLD_NEXT, "open");
  char *longpath = realpath (pathname, NULL);
  if (!strncmp (longpath, SEARCH_PATH, PATH_MAX))
    pathname = REPLACE_PATH;
  int fd = true_open (pathname, flags, mode);
  free (longpath);
  return fd;
}

Die neue Fassung von »open()« erzeugt zum angegebenen Dateinamen zunächst mit »realpath()« eine absolute Pfadangabe. »open()« lässt sich auch mit relativen Pfaden aufrufen, die je nach Arbeitsverzeichnis dann sehr unterschiedlich aussehen können. Das Umwandeln stellt sicher, dass nur ein einziger Pfad zu prüfen ist. Die Funktion »strncmp()« vergleicht den Pfad dann mit einem Suchbegriff (in unserem Beispiel: »/etc/fstab«) und ersetzt ihn bei einem Treffer durch den Namen der alternativen Datei (»/tmp/fstab.test«).

Danach geht es wie gewohnt weiter, »true_open()« öffnet die Datei. Der Aufruf von »free()« am Ende ist notwendig, weil »longpath()« Speicher fürs Ablegen der Pfadangabe reserviert hat. Den sollte man wieder freigeben, bevor man die Funktion verlässt. Auch hier übernimmt das Kompilieren wieder ein Einzeiler (Listing 7, erste Zeile).

Listing 7

Tests

$ gcc openother.c -o openother.so -fPIC -shared -ldl
$ echo "Das ist nicht /etc/fstab" > /tmp/fstab.test
$ LD_PRELOAD=$PWD/openother.so cat /etc/fstab
Das ist nicht /etc/fstab

Wenn Sie jetzt mit dem Aufruf aus der zweiten Zeile eine Datei erzeugen und mit Cat und der über »LD_PRELOAD« aktivierten Bibliothek auf »/etc/fstab« zugreifen, öffnen Sie stattdessen die in »/tmp« erstellte Datei (letzte Zeile).

Zugriffsverbot

Statt den Dateizugriff umzubiegen, kann die Problemlösung auch darin liegen, den Zugriff komplett zu verbieten. Dazu brechen Sie in bestimmten Fällen ohne Aufruf der Originalfunktion ab, setzen die globale Fehlervariable »errno« und geben den Exit-Code »-1« zurück.

Listing 8 zeigt den Code, über den »open()« beim Zugriff auf die Datei »/etc/fstab« mit dem Fehlercode »ENOENT« und dem Exit-Code »-1« abbricht. Der Versuch, die Datei zu öffnen, führt dann gewünscht zum Programmabbruch (Listing 9).

Listing 8

dontopen.c

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define SEARCH_PATH "/etc/fstab"
int (*true_open)(const char *pathname, int flags, va_list mode);
int open (const char *pathname, int flags, va_list mode) {
  true_open = dlsym (RTLD_NEXT, "open");
  char *longpath = realpath (pathname, NULL);
  int check = strncmp (longpath, SEARCH_PATH, PATH_MAX);
  free (longpath);
  if (!check) {
    errno = ENOENT;  // Datei nicht gefunden
    return -1;
  }
  return true_open (pathname, flags, mode);
}

Listing 9

Programmabbruch

$ LD_PRELOAD=$PWD/dontopen.so cat /etc/fstab
cat: /etc/fstab: Datei oder Verzeichnis nicht gefunden

»ENOENT« steht für “Datei oder Verzeichnis nicht gefunden”. Die Definitionen für Fehlercodes finden Sie in »errno-base.h« und »errno.h« im Ordner »/usr/include/asm-generic/«, sodass Sie auch andere Codes auswählen können.

Wenn Sie die Zugriffssperre mit anderen Programmen testen, fällt Ihnen vielleicht auf, dass einige Editoren wie Vim, Mcedit oder Nano ebenfalls am Zugriff gehindert werden, Gedit aber die Datei öffnen kann. Eine Analyse mit Strace zeigt, dass dieser Editor Dateien nicht mit »open()« öffnet, sondern mit »openat«. Dafür gilt es, eine alternative Implementierung bereitzustellen.

Mehr Beispiele

Einige weitere mögliche Anwendungen finden Sie auf der Github-Seite Awesome LD_PRELOAD [2]. Mit Faketime gaukeln Sie Prozessen beispielsweise eine abweichende Systemzeit und damit insbesondere ein anderes Datum vor. Das erweist sich etwa als nützlich, wenn Sie ein Programm starten möchten, dessen Nutzungslizenz abgelaufen ist. Die Änderung gilt dabei nur für Prozesse, die von Faketime gestartet werden. Das Tool lädt über »LD_PRELOAD« eine Bibliothek, die verschiedene Funktionen austauscht, darunter »time()«, »ftime()« und »gettimeofday()« (Abbildung 3).

Abbildung 3: Dank Faketime ist es auf der rechten Uhr schon 2,5&nbsp;Stunden sp&auml;ter als auf der linken.

Abbildung 3: Dank Faketime ist es auf der rechten Uhr schon 2,5 Stunden später als auf der linken.

Die Bibliothek Stderred (der Name setzt sich aus der Bezeichnung stderr für die Standardfehlerausgabe und der Farbe “red” zusammen) färbt im Terminal alle Ausgaben, die ein Prozess über die Standardfehlerausgabe erzeugt, rot ein, sodass sich Fehlermeldungen leicht von anderen Ausgaben unterscheiden lassen.

Fsatrace beobachtet Dateizugriffe und erkennt dabei neben Lese- und Schreibzugriffen auch Verschieben, Löschen und Statusabfragen. Dasselbe Verhalten lässt sich aber alternativ auch mit Strace problemlos erreichen.

Fazit

Der Kreativität sind beim Einsatz von »LD_PRELOAD« keine Grenzen gesetzt: Finden Sie häufig genutzte Bibliotheksfunktionen heraus, schreiben Sie einen Wrapper dafür und bauen Sie dort kleine Veränderungen ein. Wenn Sie die Variable in einer Shell definieren und exportieren, gilt sie in allen Prozessen, die Sie von dort starten, ohne dass Sie jedem Befehl die Definition voranstellen müssen. (tle/jlu)

Infos

  1. GNU C Library: https://www.gnu.org/software/libc/
  2. Awesome LD_PRELOAD: https://github.com/gaul/awesome-ld-preload
  3. Strace: Karsten Günther, “Schlüsselloch”, LU 12/2015, S. 90, https://www.linux-community.de/35770
  4. Ltrace: https://gitlab.com/cespedes/ltrace
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben