Programmverhalten mit LD_PRELOAD ändern

Aus LinuxUser 07/2023

Programmverhalten mit LD_PRELOAD ändern

© Chris Up / Photocase.com

Bitte abbiegen!

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 <code>ls</code> nutzt nur wenige Bibliotheken; bei grafischen Anwendungen f&auml;llt die Liste deutlich l&auml;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&ouml;&szlig;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 PDF
LinuxUser 07/2023 KAUFEN
EINZELNE AUSGABE
ABONNEMENTS
TABLET & SMARTPHONE APPS
E-Mail Benachrichtigung
Benachrichtige mich zu:

Hinweis: Dieser Artikel ist älter als ein Jahr, enthaltene Informationen sind möglicherweise veraltet.

0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben