Die seltsame Eigenart der Pseudodatei
/proc/*/mem
liegt in ihrer druckvollen Semantik. Schreibvorgänge durch diese Datei sind auch dann erfolgreich, wenn der virtuelle Zielspeicher als nicht beschreibbar markiert ist. Dies ist beabsichtigt und wird von Projekten wie dem Julia JIT-Compiler oder dem rr-Debugger aktiv verwendet .
Die Frage ist jedoch: Befolgen privilegierte Codes die Berechtigungen für den virtuellen Speicher? Inwieweit kann Hardware den Kernel-Speicherzugriff beeinflussen?
Wir werden versuchen, diese Fragen zu beantworten und die Nuancen der Interaktion zwischen dem Betriebssystem und der Hardware, auf der es ausgeführt wird, zu berücksichtigen. Lassen Sie uns die Prozessorgrenzen untersuchen, die sich auf den Kernel auswirken können, und sehen, wie der Kernel sie umgehen kann.
Patch libc mit / proc / self / mem
Wie sieht diese schlagkräftige Semantik aus? Betrachten Sie den Code:
#include <fstream>
#include <iostream>
#include <sys/mman.h>
/* Write @len bytes at @ptr to @addr in this address space using
* /proc/self/mem.
*/
void memwrite(void *addr, char *ptr, size_t len) {
std::ofstream ff("/proc/self/mem");
ff.seekp(reinterpret_cast<size_t>(addr));
ff.write(ptr, len);
ff.flush();
}
int main(int argc, char **argv) {
// Map an unwritable page. (read-only)
auto mymap =
(int *)mmap(NULL, 0x9000,
PROT_READ, // <<<<<<<<<<<<<<<<<<<<< READ ONLY <<<<<<<<
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mymap == MAP_FAILED) {
std::cout << "FAILED\n";
return 1;
}
std::cout << "Allocated PROT_READ only memory: " << mymap << "\n";
getchar();
// Try to write to the unwritable page.
memwrite(mymap, "\x40\x41\x41\x41", 4);
std::cout << "did mymap[0] = 0x41414140 via proc self mem..";
getchar();
std::cout << "mymap[0] = 0x" << std::hex << mymap[0] << "\n";
getchar();
// Try to writ to the text segment (executable code) of libc.
auto getchar_ptr = (char *)getchar;
memwrite(getchar_ptr, "\xcc", 1);
// Run the libc function whose code we modified. If the write worked,
// we will get a SIGTRAP when the 0xcc executes.
getchar();
}
Es wird hier verwendet
/proc/self/mem
, um auf zwei nicht beschreibbare Speicherseiten zu schreiben. Der erste enthält den Code selbst und der zweite gehört zu
libc
(der Funktion
getchar
). Der letzte Teil ist von größerem Interesse: Der Code schreibt das Byte 0xcc (ein Haltepunkt in x86-64-Anwendungen), was bei Ausführung dazu führt, dass der Kernel unserem Prozess ein SIGTRAP zur Verfügung stellt. Dies ändert buchstäblich die ausführbare libc-Datei. Und wenn
getchar
wir beim nächsten Anruf SIGTRAP erhalten, werden wir wissen, dass die Aufzeichnung erfolgreich war.
So sieht es aus, wenn Sie das Programm ausführen:
Funktioniert! In der Mitte werden Ausdrücke gedruckt, die beweisen, dass der Wert 0x41414140 erfolgreich geschrieben und aus dem Speicher gelesen wurde. Die letzte Ausgabe zeigt, dass unser Prozess nach dem Patchen als Ergebnis unseres Aufrufs ein SIGTRAP erhalten hat
getchar
.
In dem Video:
Wir haben gesehen, wie diese Funktion aus Sicht des Benutzerraums funktioniert. Lassen Sie uns tiefer graben. Um zu verstehen, wie dies funktioniert, müssen Sie sich ansehen, wie Hardware Speicherbeschränkungen auferlegt.
Ausrüstung
Auf der x86-64-Plattform gibt es zwei Prozessoreinstellungen, die die Fähigkeit des Kernels steuern, auf den Speicher zuzugreifen. Sie werden von der Speicherverwaltungseinheit (MMU) verwendet.
Die erste Einstellung ist das Schreibschutzbit (CR0.WP). Aus dem Intel-Handbuch (Band 3, Abschnitt 2.5) wissen wir:
Schreibschutz (16. Bit CR0). Wenn dies angegeben ist, wird verhindert, dass Prozeduren auf Supervisor-Ebene auf schreibgeschützte Seiten schreiben. Wenn das Bit leer ist, können Prozeduren auf Supervisor-Ebene auf schreibgeschützte Seiten schreiben (unabhängig von den Einstellungen des U / S-Bits; siehe Abschnitte 4.1.3 und 4.6).
Dies verhindert, dass der Kernel auf schreibgeschützte Seiten schreibt, was natürlich standardmäßig zulässig ist .
Die zweite Einstellung ist SMAP (Supervisor Mode Access Prevention) (CR4.SMAP). Die vollständige Beschreibung in Band 3, Abschnitt 4.6, ist ausführlich. Kurz gesagt, SMAP beraubt den Kernel vollständig der Fähigkeit, in den Speicher des Benutzerraums zu schreiben oder aus diesem zu lesen. Dies verhindert Exploits, die den Benutzerbereich mit schädlichen Daten überfluten, die der Kernel während der Ausführung lesen muss.
Wenn Ihr Kernel-Code nur genehmigte Kanäle verwendet (
copy_to_user
usw.), dann kann SMAP sicher ignoriert werden. Diese Funktionen verwenden es automatisch vor und nach dem Zugriff auf den Speicher. Was ist mit Schreibschutz?
Wenn CR0.WP nicht angegeben ist, kann die
/proc/*/mem
Kernel- Implementierung tatsächlich kurzerhand in den schreibgeschützten Speicherbereich schreiben.
CR0.WP wird jedoch beim Booten eingestellt und gilt normalerweise für die gesamte Betriebszeit der Systeme. In diesem Fall wird beim Versuch zu schreiben ein Seitenfehler ausgegeben. Es ist eher ein Copy-on-Write-Tool als ein Sicherheitstool, sodass dem Kernel keine wirklichen Einschränkungen auferlegt werden. Mit anderen Worten ist eine unbequeme Fehlerbehandlung erforderlich, die für ein gegebenes Bit nicht erforderlich ist.
Lassen Sie uns jetzt die Implementierung herausfinden.
Wie / proc / * / mem funktioniert
/proc/*/mem
Es ist in fs / proc / base.c implementiert .
Die Struktur
file_operations
enthält die Handlerfunktionen , und die Funktion mem_rw () unterstützt den Schreibhandler vollständig.
mem_rw()
verwendet access_remote_vm () für Schreibvorgänge . Und
access_remote_vm()
das macht es:
- Ruft
get_user_pages_remote()
auf, um einen physischen Frame zu finden, der der virtuellen Zieladresse entspricht. - Ruft
kmap()
auf, um diesen Frame im virtuellen Adressraum des Kernels als beschreibbar zu markieren. - Fordert die
copy_to_user_page()
endgültige Ausführung von Schreibvorgängen auf.
Diese Implementierung umgeht das Problem der Fähigkeit des Kernels, in nicht beschreibbaren Benutzerbereich zu schreiben, vollständig! Durch die Kontrolle des Kernels über das Subsystem für den virtuellen Speicher kann die MMU vollständig umgangen werden, sodass der Kernel einfach in seinen eigenen beschreibbaren Adressraum schreiben kann. Die Diskussion über CR0.WP wird also irrelevant. Schauen
wir
uns jeden der Schritte an: get_user_pages_remote ()
Um die MMU zu umgehen, muss der Kernel manuell das tun, was die MMU in der Hardware in der Anwendung tut. Zunächst müssen Sie die virtuelle Zieladresse in eine physische Adresse konvertieren. Dies geschieht durch die Funktionsfamilie
get_user_pages()
... Sie durchlaufen die Seitentabellen und suchen nach physischen Speicherrahmen, die einem bestimmten Bereich virtueller Adressen entsprechen.
Der Aufrufer stellt den Kontext bereit und verwendet Flags, um das Verhalten zu ändern
get_user_pages()
. Besonders interessant
FOLL_FORCE
ist die Flagge , die übertragen wird
mem_rw()
. Das Flag löst check_vma_flags (Zugriffsprüflogik
get_user_pages()
) aus, um Schreibvorgänge auf nicht beschreibbare Seiten zu ignorieren und die Suche fortzusetzen. Die "druckvolle" Semantik bezieht sich vollständig auf
FOLL_FORCE
(meine Kommentare):
static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags)
{
[...]
if (write) { // If performing a write..
if (!(vm_flags & VM_WRITE)) { // And the page is unwritable..
if (!(gup_flags & FOLL_FORCE)) // *Unless* FOLL_FORCE..
return -EFAULT; // Return an error
[...]
return 0; // Otherwise, proceed with lookup
}
get_user_pages()
Es hält sich auch an die CoW-Semantik (Copy-on-Write). Wenn ein Schreibvorgang in eine nicht beschreibbare Seitentabelle angegeben wird, wird ein Seitenfehler durch Aufrufen des
handle_mm_fault
Hauptseitenfehler-Handlers emuliert . Dies startet die entsprechende Copy-on-Write-Verarbeitungsroutine
do_wp_page
, die die Seite nach Bedarf kopiert. Wenn also Einträge bis
/proc/*/mem
durch private Shared Mapping ausgeführt werden, z. B. libc, sind sie nur innerhalb des Prozesses sichtbar.
kmap ()
Nachdem ein physischer Frame gefunden wurde, muss er dem beschreibbaren virtuellen Adressraum des Kernels zugeordnet werden. Dies geschieht mit Hilfe von
kmap()
.
Auf einer 64-Bit-x86-Plattform wird der gesamte physische Speicher über den Inline-Zuordnungsbereich des virtuellen Adressraums des Kernels zugeordnet. In diesem Fall
kmap()
funktioniert es sehr einfach: Es muss nur die Startadresse der linearen Zuordnung zur physischen Adresse des Rahmens hinzugefügt werden, um die virtuelle Adresse zu berechnen, der dieser Rahmen zugeordnet ist.
Auf einer 32-Bit-x86-Plattform enthält die Inline-Zuordnung eine Teilmenge des physischen Speichers.
kmap()
Daher muss eine Funktion möglicherweise einen Frame zuordnen, indem sie Highmem-Speicher zuweist und Seitentabellen bearbeitet.
In beiden Fällen werden die Linienzuordnung und die Highmem- Zuordnung mit Schutz durchgeführt. PAGE_KERNEL, mit dem geschrieben werden kann.
copy_to_user_page ()
Der letzte Schritt besteht darin, den Schreibvorgang auszuführen. Dies geschieht mit dem,
copy_to_user_page()
was im Wesentlichen memcpy ist. Dies funktioniert, da das Ziel eine beschreibbare Zuordnung von ist
kmap()
.
Diskussion
Zunächst konvertiert der Kernel unter Verwendung der zum Programm gehörenden Speicherseitentabelle die virtuelle Zieladresse im Benutzerbereich in den entsprechenden physischen Frame. Der Kernel ordnet diesen Frame dann seinem eigenen beschreibbaren virtuellen Raum zu. Schließlich schreibt es mit einfachem memcpy.
Auffallenderweise wird CR0.WP hier nicht verwendet. Die Implementierung umgeht diesen Punkt elegant, indem sie die Tatsache ausnutzt, dass sie nicht über einen User-Space-Zeiger auf den Speicher zugreifen muss . Da der Kernel die vollständige Kontrolle über den virtuellen Speicher hat, kann er den physischen Frame einfach mit beliebigen Auflösungen in seinen eigenen virtuellen Adressraum umwandeln und damit tun, was er will.
Es ist wichtig zu beachten, dass die Berechtigungen, die eine Speicherseite schützen, sich auf die virtuelle Adresse beziehen, die für den Zugriff auf diese Seite verwendet wird, nicht auf den physischen Frame, der der Seite zugeordnet ist . Die Speicherberechtigungsnotation bezieht sich ausschließlich auf den virtuellen Speicher, nicht auf den physischen Speicher.
Fazit
Indem
/proc/*/mem
wir die Details der druckvollen Semantik in der Implementierung untersuchen, können wir die Beziehung zwischen dem Kern und dem Prozessor widerspiegeln. Auf den ersten Blick wirft die Fähigkeit des Kernels, in nicht beschreibbaren Speicher zu schreiben, die Frage auf: Inwieweit kann der Prozessor den Speicherzugriff des Kernels beeinflussen? Das Handbuch beschreibt Steuerungsmechanismen, die die Aktionen des Kernels einschränken können. Bei näherer Betrachtung sind die Einschränkungen jedoch bestenfalls oberflächlich. Dies sind einfache Hindernisse, um herumzukommen.