
Dieser Artikel konzentriert sich auf eine kleine Sicherheitsfunktion, die in GNU ld zur Version 2.30 im Dezember 2018 hinzugefügt wurde. Auf Russisch wurde diese Verbesserung im Opennet mit folgender Anmerkung erwähnt:
"-z separater Code" -Modus, der die Sicherheit ausführbarer Dateien auf Kosten einer geringfügigen Erhöhung der Größe und des Speicherverbrauchs erhöht
Lass es uns herausfinden. Um zu erklären, um welche Art von Sicherheitsproblem es sich handelt und um welche Lösung es sich handelt, beginnen wir mit den allgemeinen Funktionen von Exploits für binäre Sicherheitslücken.
Probleme mit dem Kontrollfluss ausnutzen
Ein Angreifer kann Daten an das Programm übertragen und auf diese Weise mithilfe verschiedener Sicherheitsanfälligkeiten bearbeiten: Schreiben nach Index über die Grenzen eines Arrays hinaus, unsicheres Kopieren von Zeichenfolgen und Verwenden von Objekten nach der Freigabe. Solche Fehler sind typisch für C- und C ++ - Programmcode und können bei bestimmten Eingabedaten für das Programm zu einer Speicherbeschädigung führen.
Sicherheitslücken in Bezug auf SpeicherbeschädigungCWE-20: Unsachgemäße Eingabevalidierung
CWE-118: Falscher Zugriff auf indizierbare Ressourcen ('Bereichsfehler')
CWE-119: Unsachgemäße Einschränkung von Operationen innerhalb der Grenzen eines Speicherpuffers
CWE-120: Pufferkopie ohne Überprüfung der Eingabegröße ('Klassischer Pufferüberlauf')
CWE-121: Stapelbasierter Pufferüberlauf
CWE-122: Heap-basierter Pufferüberlauf
CWE-123: Write-what-where-Bedingung
CWE-124: Buffer Underwrite ('Buffer Underflow')
CWE-125: Lesen außerhalb der Grenzen
CWE-126: Pufferüberlesen
CWE-127: Puffer unterlesen
CWE-128: Umlauffehler
CWE-129: Unsachgemäße Validierung des Array-Index
CWE-130: Unsachgemäße Behandlung von Inkonsistenzen bei Längenparametern
CWE-131: Falsche Berechnung der Puffergröße
CWE-134: Verwendung einer extern gesteuerten Formatzeichenfolge
CWE-135: Falsche Berechnung der Multi-Byte-Stringlänge
CWE-170: Unsachgemäße Nullbeendigung
CWE-190: Integer Overflow oder Wraparound
CWE-415: Double Free
CWE-416: Nach kostenlos verwenden
CWE-476: NULL-Zeiger-Dereferenzierung
CWE-787: Schreiben außerhalb der Grenzen
CWE-824: Zugriff auf nicht initialisierten Zeiger
...
Das klassische Exploit-Element speicherbeschädigungsähnlicher Sicherheitslücken ist das Überschreiben eines Zeigers im Speicher. Der Zeiger wird dann vom Programm verwendet, um die Steuerung auf einen anderen Code zu übertragen: eine Klassenmethode oder -funktion von einem anderen Modul aufzurufen, von einer Funktion zurückzukehren. Und da der Zeiger überschrieben wurde, wird die Kontrolle vom Angreifer abgefangen - das heißt, der von ihm vorbereitete Code wird ausgeführt. Wenn Sie an Variationen und Details dieser Techniken interessiert sind, empfehlen wir Ihnen, das Dokument zu lesen.
Dieser gemeinsame Moment des Betriebs solcher Exploits ist bekannt, und hier für den Angreifer sind die Barrieren seit langem gesetzt:
- Überprüfen der Integrität von Zeigern vor dem Übergeben der Kontrolle: Stapel-Cookies, Kontrollflussschutz, Zeigerauthentifizierung
- Randomisierung von Segmentadressen mit Code und Daten: Randomisierung des Adressraumlayouts
- Verhindern, dass Code außerhalb von Codesegmenten ausgeführt wird: Schutz des ausführbaren Speicherplatzes
Als nächstes konzentrieren wir uns auf den Schutz des letzteren Typs.
ausführbarer Speicherplatzschutz
Der Programmspeicher ist heterogen und in Segmente mit unterschiedlichen Rechten unterteilt: Lesen, Schreiben und Ausführen. Dies wird durch die Fähigkeit des Prozessors sichergestellt, Speicherseiten mit Zugriffsflags in Seitentabellen zu markieren. Die Idee des Schutzes basiert auf einer strikten Trennung von Code und Daten: Die vom Angreifer während des Verarbeitungsprozesses empfangenen Daten sollten in nicht ausführbaren Segmenten (Stapel, Heap) und der Code des Programms selbst in separaten unveränderlichen Segmenten abgelegt werden . Dies sollte dem Angreifer daher die Möglichkeit nehmen, Fremdcode im Speicher abzulegen und auszuführen.
Um das Verbot der Codeausführung in Datensegmenten zu umgehen, werden Techniken zur Wiederverwendung von Code verwendet. Das heißt, der Angreifer überträgt die Kontrolle auf die Codefragmente (im Folgenden als Gadgets bezeichnet), die sich auf den ausführbaren Seiten befinden. Techniken dieser Art sind in aufsteigender Reihenfolge von unterschiedlicher Schwierigkeit:
- Übertragen der Kontrolle an eine Funktion, die das tut, was für den Angreifer ausreicht: an die Funktion system () mit einem kontrollierten Argument zum Ausführen beliebiger Shell-Befehle (ret2libc)
- Übertragen der Steuerung an eine Funktion oder Kette von Gadgets, die den Schutz deaktivieren oder einen Teil des Speichers ausführbar machen (z. B. Aufrufen von
mprotect()
), gefolgt von der Ausführung von beliebigem Code - Ausführung aller gewünschten Aktionen mit einer langen Kette von Gadgets
Somit steht der Angreifer vor der Aufgabe, den vorhandenen Code in dem einen oder anderen Volume wiederzuverwenden. Wenn dies etwas komplizierter ist als die Rückkehr zu einer einzelnen Funktion, ist eine Reihe von Gadgets erforderlich. Um nach Gadgets nach ausführbaren Segmenten zu suchen, gibt es folgende Tools: Ropper , Ropgadget .
Loch READ_IMPLIES_EXEC
Manchmal können jedoch Speicherbereiche mit Daten ausführbar sein, und die oben beschriebenen Prinzipien der Trennung von Code und Daten werden eindeutig verletzt. In solchen Fällen bleibt dem Angreifer die Mühe erspart, Gadgets oder Funktionen zur Wiederverwendung des Codes zu finden. Ein interessanter Fund dieser Art war der ausführbare Stack und alle Datensegmente auf derselben „Industrial Firewall“.
Listing /proc/$pid/maps
:
00008000-00009000 r-xp 00000000 08:01 10 /var/flash/dmt/nx_test/a.out 00010000-00011000 rwxp 00000000 08:01 10 /var/flash/dmt/nx_test/a.out 00011000-00032000 rwxp 00000000 00:00 0 [heap] 40000000-4001f000 r-xp 00000000 1f:02 429 /lib/ld-linux.so.2 4001f000-40022000 rwxp 00000000 00:00 0 40027000-40028000 r-xp 0001f000 1f:02 429 /lib/ld-linux.so.2 40028000-40029000 rwxp 00020000 1f:02 429 /lib/ld-linux.so.2 4002c000-40172000 r-xp 00000000 1f:02 430 /lib/libc.so.6 40172000-40179000 ---p 00146000 1f:02 430 /lib/libc.so.6 40179000-4017b000 r-xp 00145000 1f:02 430 /lib/libc.so.6 4017b000-4017c000 rwxp 00147000 1f:02 430 /lib/libc.so.6 4017c000-40b80000 rwxp 00000000 00:00 0 be8c2000-be8d7000 rwxp 00000000 00:00 0 [stack]
Hier sehen Sie die Speicherkarte des Testdienstprogramms. Eine Karte besteht aus Speicherbereichen - Tabellenzeilen. Achten Sie zunächst auf die rechte Spalte - sie erklärt den Inhalt des Bereichs (Codesegmente, Daten von Funktionsbibliotheken oder des Programms selbst) oder seinen Typ (Heap, Stack). Auf der linken Seite befindet sich in der angegebenen Reihenfolge der Adressbereich, den jeder Speicherbereich belegt, und außerdem die Zugriffsrechte-Flags: r (Lesen), w (Schreiben), x (Ausführen). Diese Flags bestimmen das Verhalten des Systems beim Versuch, Speicher an diesen Adressen zu lesen, zu schreiben und auszuführen. Wenn der angegebene Zugriffsmodus verletzt wird, wird eine Ausnahme ausgelöst.
Beachten Sie, dass fast der gesamte Speicher im Prozess ausführbar ist: der Stapel, der Heap und alle Datensegmente. Das ist ein Problem. Offensichtlich erleichtert das Vorhandensein von rwx-Speicherseiten einem Angreifer das Leben, da er seinen Code in einem solchen Prozess an jedem Ort frei ausführen kann, an dem sein Code beim Übertragen von Daten (Paketen, Dateien) an ein solches Programm zur Verarbeitung abgerufen wird.
Warum ist eine solche Situation bei einem modernen Gerät aufgetreten, das das Verbot der Codeausführung auf Datenseiten mit Hardware unterstützt, hängt die Sicherheit von Unternehmens- und Industrienetzwerken vom Gerät ab, und das Problem und seine Lösung sind seit langem bekannt?
Dieses Bild wird durch das Verhalten des Kernels während der Initialisierung des Prozesses (Zuweisen eines Stapels, Heaps, Laden des Haupt-ELF usw.) und während der Ausführung der Aufrufe des Kernprozesses bestimmt. Das Schlüsselattribut, das dies beeinflusst, ist das Persönlichkeitsflag READ_IMPLIES_EXEC
. Dieses Flag bewirkt, dass jeder lesbare Speicher auch ausführbar wird. Ein Flag kann aus mehreren Gründen für Ihren Prozess gesetzt werden:
- Legacy kann explizit vom Software-Flag im ELF-Header angefordert werden, um einen sehr interessanten Mechanismus zu implementieren: ein Sprungbrett auf dem Stapel ( 1 , 2 , 3 )!
- Es kann von untergeordneten Prozessen vom übergeordneten Prozess geerbt werden.
- Es kann vom Kernel unabhängig für alle Prozesse installiert werden! Erstens, wenn die Architektur keinen nicht ausführbaren Speicher unterstützt. Zweitens, nur für den Fall, einige andere alte Krücken zu stützen. Dieser Code befindet sich im Kernel 2.6.32 (ARM), der eine sehr lange Lebensdauer hatte. Dies war nur unser Fall.
Platz zum Auffinden von Gadgets in einem ELF-Bild
Funktionsbibliotheken und ausführbare Programmdateien liegen im ELF-Format vor. Der gcc-Compiler übersetzt Sprachkonstrukte in Maschinencode und fügt sie in einen Abschnitt und die Daten, die dieser Code verarbeitet, in andere Abschnitte ein. Es gibt viele Abschnitte, die vom ld-Linker in Segmente gruppiert werden. Somit enthält ELF ein Programmbild, das zwei Darstellungen aufweist: eine Tabelle mit Abschnitten und eine Tabelle mit Segmenten.
$ readelf -l /bin/ls Elf file type is EXEC (Executable file) Entry point 0x804bee9 There are 9 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 RE 0x4 INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x1e40c 0x1e40c RE 0x1000 LOAD 0x01ef00 0x08067f00 0x08067f00 0x00444 0x01078 RW 0x1000 DYNAMIC 0x01ef0c 0x08067f0c 0x08067f0c 0x000f0 0x000f0 RW 0x4 NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x018b74 0x08060b74 0x08060b74 0x00814 0x00814 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x01ef00 0x08067f00 0x08067f00 0x00100 0x00100 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got
Hier sehen Sie die Zuordnung von Abschnitten zu Segmenten in einem ELF-Bild.
Die Abschnittstabelle wird von Dienstprogrammen zum Analysieren von Programmen und Bibliotheken verwendet, von Ladern jedoch nicht zum Projizieren von ELFs in den Prozessspeicher. Die Abschnittstabelle beschreibt die ELF-Struktur detaillierter als die Segmenttabelle. Innerhalb eines Segments können sich mehrere Abschnitte befinden.
Ein speicherinternes ELF-Bild wird von ELF-Ladern basierend auf dem Inhalt der Segmenttabelle erstellt . Die Partitionstabelle wird nicht mehr zum Laden von ELF in den Speicher verwendet.
Es gibt jedoch Ausnahmen von dieser Regel.In der Natur gibt es beispielsweise einen Debian-Entwickler-Patch für den ELF ld.so-Loader für die ARM-Architektur, der nach einem speziellen Abschnitt ".ARM.attributes" wie SHT_ARM_ATTRIBUTES sucht, und Binärdateien mit einer abgesägten Abschnittstabelle in einem solchen System werden nicht geladen ...
Ein ELF-Segment verfügt über Flags, die bestimmen, welche Berechtigungen das Segment im Speicher haben wird. Traditionell wurde der größte Teil der Software für GNU / Linux so erstellt, dass zwei PT_LOAD
Segmente (im Speicher PT_LOAD
) in der Segmenttabelle deklariert wurden - wie in der obigen Auflistung:
Segment mit RE
Flags
1.1. Ausführbarer ELF-Code: Abschnitte .init
, .text
, .fini
1.2. Unveränderliche Daten in ELF: .rodata
.symtab
, .rodata
Segment RW
Flaggen
2.1. Variable Daten in ELF: Abschnitte .plt
, .got
, .data
, .bss
Wenn Sie auf die Zusammensetzung des ersten Segments und seiner Zugriffsflags achten, wird deutlich, dass ein solches Layout den Platz für die Suche nach Gadgets für Techniken zur Wiederverwendung von Code erweitert. In großen ELFs wie libcrypto können Servicetabellen und andere unveränderliche Daten bis zu 40% des ausführbaren Segments einnehmen. Das Vorhandensein von etwas ähnlichem wie Codeteilen in diesen Daten wird durch Versuche bestätigt, solche Binärdateien mit einer großen Datenmenge im ausführbaren Segment ohne Abschnittstabellen und Symbole zu zerlegen. Jede Folge von Bytes in diesem einzelnen ausführbaren Segment kann als nützlich für das angreifende Fragment von Maschinencode und Sprungbrett angesehen werden - sei es diese Folge von Bytes mit mindestens einem Teil der Zeile der Debugging-Nachricht aus dem Programm, einem Teil des Funktionsnamens in der Symboltabelle oder der konstanten Anzahl des kryptografischen Algorithmus ...
Ausführbare PE-HeaderDie ausführbaren Header und Tabellen am Anfang des ersten Abschnitts des ELF-Images ähneln der Situation mit Windows vor etwa 15 Jahren. Es gab eine Reihe von Viren, die Dateien infizierten und ihren Code in ihren PE-Header schrieben, der auch dort ausführbar war. Ich habe es geschafft, ein solches Beispiel im Archiv zu finden:

Wie Sie sehen können, wird der Viruskörper direkt nach der Abschnittstabelle im Bereich der PE-Header gequetscht. Bei einer Projektion einer Datei auf den virtuellen Speicher stehen hier normalerweise etwa 3 KB freier Speicherplatz zur Verfügung. Nach dem Körper des Virus gibt es ein leeres Feld und dann beginnt der erste Abschnitt mit dem Programmcode.
Für Linux gab es jedoch viel interessantere Werke der VX-Szene: Vergeltung .
Lösung
- Das oben beschriebene Problem ist seit langem bekannt.
- Behoben 12. Januar 2018 : Der Schlüssel `ld -z separater Code:" Separaten Code erstellen "PT_LOAD" Segment-Header im Objekt wird hinzugefügt. Dies gibt ein Speichersegment an, das nur Anweisungen enthalten sollte und sich in vollständig getrennten Seiten von anderen Daten befinden muss. Erstellen Sie kein separates Code-Segment "PT_LOAD", wenn Noseparate-Code verwendet wird. "). Die Funktion wurde in Version 2.30 veröffentlicht .
- Darüber hinaus war diese Funktion in der nächsten Version 2.31 standardmäßig enthalten.
- Präsentiert in frischen
binutils
Paketen, zum Beispiel in den Ubuntu 18.10-Repositorys. Viele Pakete wurden bereits mit dieser neuen Funktion zusammengestellt, die ElfMaster- Forscher kennengelernt und dokumentiert haben
Durch Änderungen am Layoutalgorithmus wird ein neues ELF-Bild erhalten:
$ readelf -l ls Elf file type is DYN (Shared object file) Entry point 0x41aa There are 11 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x00000034 0x00000034 0x00160 0x00160 R 0x4 INTERP 0x000194 0x00000194 0x00000194 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x00000000 0x00000000 0x01e6c 0x01e6c R 0x1000 LOAD 0x002000 0x00002000 0x00002000 0x14bd8 0x14bd8 RE 0x1000 LOAD 0x017000 0x00017000 0x00017000 0x0bf80 0x0bf80 R 0x1000 LOAD 0x0237f8 0x000247f8 0x000247f8 0x0096c 0x01afc RW 0x1000 DYNAMIC 0x023cec 0x00024cec 0x00024cec 0x00100 0x00100 RW 0x4 NOTE 0x0001a8 0x000001a8 0x000001a8 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x01c3f8 0x0001c3f8 0x0001c3f8 0x0092c 0x0092c R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x0237f8 0x000247f8 0x000247f8 0x00808 0x00808 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt 03 .init .plt .plt.got .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss 06 .dynamic 07 .note.ABI-tag .note.gnu.build-id 08 .eh_frame_hdr 09 10 .init_array .fini_array .data.rel.ro .dynamic .got
Die Grenze zwischen Code und Daten ist jetzt genauer. Das einzige ausführbare Segment enthält wirklich nur Codeabschnitte: .init, .plt, .plt.got, .text, .fini.
Was genau wurde in ld geändert?Wie Sie wissen, wird die Struktur der Ausgabe-ELF-Datei durch das Linker-Skript beschrieben . Sie können das Standardskript folgendermaßen sehen:
$ ld --verbose GNU ld (GNU Binutils for Ubuntu) 2.26.1 * * * using internal linker script: ================================================== /* Script for -z combreloc: combine and sort reloc sections */ /* Copyright (C) 2014-2015 Free Software Foundation, Inc. * * *
Viele andere Skripte für verschiedene Plattformen und Optionskombinationen befinden sich im Verzeichnis ldscripts
. Für die Option " separate-code
neue Skripte erstellt.
$ diff elf_x86_64.x elf_x86_64.xe 1c1 < /* Default linker script, for normal executables */ --- > /* Script for -z separate-code: generate normal executables with separate code segment */ 46a47 > . = ALIGN(CONSTANT (MAXPAGESIZE)); 70a72,75 > . = ALIGN(CONSTANT (MAXPAGESIZE)); > /* Adjust the address for the rodata segment. We want to adjust up to > the same address within the page on the next page up. */ > . = SEGMENT_START("rodata-segment", ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)));
Hier sehen Sie, dass eine Direktive hinzugefügt wurde, um ein neues Segment mit schreibgeschützten Abschnitten nach dem Codesegment zu deklarieren.
Zusätzlich zu den Skripten wurden jedoch Änderungen an den Linkerquellen vorgenommen. In der Funktion _bfd_elf_map_sections_to_segments
- siehe commit . Wenn Sie jetzt Segmente für Abschnitte auswählen, wird ein neues Segment hinzugefügt, wenn sich der Abschnitt durch das SEC_CODE
Flag vom vorherigen Abschnitt unterscheidet.
Fazit
Nach wie vor empfehlen wir Entwicklern, bei der Entwicklung von Software nicht zu vergessen, die im Compiler und Linker integrierten Sicherheitsflags zu verwenden. Nur eine so kleine Änderung kann das Leben des Angreifers erheblich verkomplizieren und Ihr Leben viel ruhiger machen.