Einbetten in den Linux-Kernel: Abfangen von Systemaufrufen

    Der Begriff "Systemaufruf" in der Programmier- und Computertechnologie bezieht sich auf die Anwendung, die sich zum Ausführen einer Operation an den Kernel des Betriebssystems (OS) wendet. Aufgrund der Tatsache, dass eine solche Interaktion die wichtigste ist, scheint das Abfangen von Systemaufrufen die wichtigste Stufe der Integration zu sein, weil Hiermit können Sie eine Schlüsselkomponente des Betriebssystemkerns steuern - die Systemaufrufschnittstelle, über die Sie die Anforderungen der Anwendungssoftware für Kerneldienste überprüfen können.

    Dieser Artikel knüpft an den bereits angekündigten Zyklus an, der sich mit bestimmten Fragen der Implementierung auferlegter Sicherheitsfunktionen und insbesondere der Integration in Softwaresysteme befasst.



    I. Ansätze einbetten



    Es gibt verschiedene Möglichkeiten, Systemaufrufe an den Linux-Kernel abzufangen. Zunächst ist anzumerken, dass zum Abfangen einzelner Systemaufrufe die zuvor in Betracht gezogene Methode zum Abfangen von Kernelfunktionen verwendet werden kann . Da die meisten Systemaufrufe durch die entsprechenden Funktionen dargestellt werden (z. B. sys_open ), entspricht die Aufgabe, sie abzufangen, der Aufgabe, diese Funktionen abzufangen. Mit der Zunahme der Anzahl von abgefangenen Systemaufrufen und der Komplexität der "Geschäftslogik" kann dieser Ansatz jedoch begrenzt sein.

    Eine universellere Möglichkeit besteht darin, Einträge in den Systemaufruftabellen (weitere Informationen in den folgenden Tabellen) zu ändern, die Zeiger auf Funktionen enthalten, die die Logik eines Systemaufrufs implementieren. Tabellen werden vom Kernel für die Zeitplanung verwendet, wenn ein Zeiger auf eine Handlerfunktion aus der entsprechenden Tabelle anhand der Nummer des Systemaufrufs ausgewählt wird, den die Anwendung bei ihrer anschließenden Ausführung angefordert hat. Durch Ersetzen eines solchen Zeigers können Sie die Logik des Kernels in Bezug auf die Behandlung von Systemaufrufen ändern. Mit Blick auf die Zukunft ist es erwähnenswert, dass für die erfolgreiche Implementierung dieser Methode die Tabellen selbst gefunden werden müssen, weil Sie werden nicht exportiert. Das Abfangen eines Systemaufrufs besteht letztendlich darin, das Tabellenelement einfach zu überschreiben.

    Die universellste Möglichkeit zum Abfangen von Systemaufrufen bestand und besteht darin, den Code des Systemaufruf-Managers so zu ändern, dass eine Vor- und Nachbearbeitung des Kontexts des Threads erfolgt, der einen Systemdienst anfordert. Diese Option bietet eine größere Flexibilität im Vergleich zu den vorherigen Führt einzelne Punkte der Zustandsüberwachung vor und nach der Handlerfunktion ein.

    Als nächstes wird ein Beispiel im Detail besprochen, wie Linux-Kernel-Systemaufrufe in die Schnittstelle eingebettet werden, indem der Dispatcher-Code geändert wird.

    II. Dispatching von Systemaufrufen im Linux-Kernel



    Das Dispatching von Systemaufrufen ist ein ziemlich komplizierter Prozess mit vielen Nuancen. Im Rahmen dieses Artikels werden jedoch viele Details ausgelassen, da Sie außer für den Dispatching-Prozess selbst (Abrufen und Ausführen der dem Systemaufruf entsprechenden Funktion) nichts anderes für die Implementierung wissen müssen .

    Traditionell unterstützt der Linux-Kernel die folgenden Systemaufruffunktionen für die x86-Architektur:

    • INT 80h-Anweisung (32-Bit-Schnittstelle, systemeigener Aufruf oder Emulation);
    • SYSENTER-Anweisung (32-Bit-Schnittstelle, systemeigener Aufruf oder Emulation);
    • SYSCALL-Anweisung (64-Bit-Schnittstelle, systemeigener Aufruf oder Emulation).


    Im Folgenden wird leiht mir eine Darstellung eines Systemaufruf, abhängig von der Ausführungsform:

    Bild

    Wie man sehen kann, wird die 32-Bit - Anwendung Systemaufrufe durchgeführt , den INT 80h und Mechanismen SYSENTER verwendet, während 64-Bit - SYSCALL verwenden. Gleichzeitig wird die Möglichkeit unterstützt, 32-Bit-Code in einer 64-Bit-Umgebung auszuführen (sogenannter Kompatibilitätsmodus - Emulations- / Kompatibilitätsmodus; Kernel-Option CONFIG_IA32_EMULATION). Diesbezüglich gibt es im Kernel 2 nicht exportierbare Tabellen - sys_call_tableund ia32_sys_call_table(nur für den Emulationsmodus verfügbar), die die Adressen von Funktionen enthalten - Systemaufruf-Handler.

    Im allgemeinen Fall, wenn alle möglichen Mechanismen in einem 64-Bit-Kernel dargestellt werden, gibt es 4 Einstiegspunkte, die bestimmen, wie die Logik des entsprechenden Dispatchers lautet:



    Auf die eine oder andere Weise erlangt der Kernel die Kontrolle, wenn die Anwendung einen Systemaufruf durchführt. Der Systemaufruf-Manager für jeden der betrachteten Fälle weist Unterschiede zu den anderen auf. Ohne Verlust der Allgemeinheit kann jedoch deren allgemeine Struktur anhand des Beispiels system_call betrachtet werden :

       0xffffffff81731670 <+0>:	swapgs 
       0xffffffff81731673 <+3>:	mov    %rsp,%gs:0xc000
       0xffffffff8173167c <+12>:	mov    %gs:0xc830,%rsp
       0xffffffff81731685 <+21>:	sti    
       0xffffffff81731686 <+22>:	data32 data32 xchg %ax,%ax
       0xffffffff8173168a <+26>:	data32 xchg %ax,%ax
       0xffffffff8173168d <+29>:	sub    $0x50,%rsp
       0xffffffff81731691 <+33>:	mov    %rdi,0x40(%rsp)
       0xffffffff81731696 <+38>:	mov    %rsi,0x38(%rsp)
       0xffffffff8173169b <+43>:	mov    %rdx,0x30(%rsp)
       0xffffffff817316a0 <+48>:	mov    %rax,0x20(%rsp)
       0xffffffff817316a5 <+53>:	mov    %r8,0x18(%rsp)
       0xffffffff817316aa <+58>:	mov    %r9,0x10(%rsp)
       0xffffffff817316af <+63>:	mov    %r10,0x8(%rsp)
       0xffffffff817316b4 <+68>:	mov    %r11,(%rsp)
       0xffffffff817316b8 <+72>:	mov    %rax,0x48(%rsp)
       0xffffffff817316bd <+77>:	mov    %rcx,0x50(%rsp)
       0xffffffff817316c2 <+82>:	testl  $0x100801d1,-0x1f78(%rsp)
       0xffffffff817316cd <+93>:	jne    0xffffffff8173181e 
       0xffffffff817316d3 <+0>:	and    $0xbfffffff,%eax
       0xffffffff817316d8 <+5>:	cmp    $0x220,%eax /* <-------- cmp $__NR_syscall_max,%eax */
       0xffffffff817316dd <+10>:	ja     0xffffffff817317a5 
       0xffffffff817316e3 <+16>:	mov    %r10,%rcx
       0xffffffff817316e6 <+19>:	callq  *-0x7e7fec00(,%rax,8) /* <-------- call *sys_call_table(,%rax,8) */
       0xffffffff817316ed <+26>:	mov    %rax,0x20(%rsp)
       0xffffffff817316f2 <+0>:	mov    $0x1008feff,%edi
       0xffffffff817316f7 <+0>:	cli    
       0xffffffff817316f8 <+1>:	data32 data32 xchg %ax,%ax
       0xffffffff817316fc <+5>:	data32 xchg %ax,%ax
       0xffffffff817316ff <+8>:	mov    -0x1f78(%rsp),%edx
       0xffffffff81731706 <+15>:	and    %edi,%edx
       0xffffffff81731708 <+17>:	jne    0xffffffff81731745 
       0xffffffff8173170a <+19>:	mov    0x50(%rsp),%rcx
       0xffffffff8173170f <+24>:	mov    (%rsp),%r11
       0xffffffff81731713 <+28>:	mov    0x8(%rsp),%r10
       0xffffffff81731718 <+33>:	mov    0x10(%rsp),%r9
       0xffffffff8173171d <+38>:	mov    0x18(%rsp),%r8
       0xffffffff81731722 <+43>:	mov    0x20(%rsp),%rax
       0xffffffff81731727 <+48>:	mov    0x30(%rsp),%rdx
       0xffffffff8173172c <+53>:	mov    0x38(%rsp),%rsi
       0xffffffff81731731 <+58>:	mov    0x40(%rsp),%rdi
       0xffffffff81731736 <+63>:	mov    %gs:0xc000,%rsp
       0xffffffff8173173f <+72>:	swapgs 
       0xffffffff81731742 <+75>:	sysretq 
    


    Wie Sie sehen können, wechselt der erste Befehl ( swapgs) die Datenstruktur (vom Benutzer zum nuklearen). Als nächstes wird der Stapel konfiguriert, Interrupts werden zugelassen und ein Registerflusskontext (Struktur pt_regs) wird auf dem Stapel gebildet , der während der Verarbeitung erforderlich ist. Um auf die obige Auflistung zurückzukommen, sollten folgende Befehle besonders beachtet werden:

       0xffffffff817316d8 <+5>:	cmp    $0x220,%eax /* <-------- cmp $__NR_syscall_max,%eax */
       0xffffffff817316dd <+10>:	ja     0xffffffff817317a5 
       0xffffffff817316e3 <+16>:	mov    %r10,%rcx
       0xffffffff817316e6 <+19>:	callq  *-0x7e7fec00(,%rax,8) /* <-------- call *sys_call_table(,%rax,8) */
       0xffffffff817316ed <+26>:	mov    %rax,0x20(%rsp)
    


    Die erste Zeile prüft, ob die angeforderte Systemrufnummer (Register %rax) mit dem maximal zulässigen Wert ( __NR_syscall_max) übereinstimmt . Bei erfolgreicher Verifikation wird der Systemaufruf ausgelöst, dh die Steuerung geht zu einer Funktion über, die die entsprechende Logik implementiert.

    Daher ist der Schlüsselpunkt bei der Verarbeitung von Systemaufrufen der Befehl dispatch, der ein Funktionsaufruf ( call *sys_call_table(,%rax,8)) ist. Wir werden weitere Einbettungen durchführen, indem wir diesen Befehl modifizieren.

    III. Einbettungsmethode



    Wie bereits erwähnt, besteht eine universelle Möglichkeit zum Einbetten in den Dispatcher darin, den Code so zu ändern, dass der Kontext des Threads gesteuert werden kann, bevor er die Funktion zum Implementieren der Logik des Systemaufrufs (Verarbeitung) sowie nach seiner Ausführung (Nachverarbeitung) ausführt.

    Um das Einbetten mit der beschriebenen Methode zu implementieren, wird vorgeschlagen, den Dispatcher durch Ändern des Befehls dispatch ( call *sys_call_table(,%rax,8)) leicht zu patchen und einen unbedingten Sprungbefehl ( JMP REL32) in den Handler darüber zu schreiben service_stub. In diesem Fall sieht die allgemeine Struktur eines solchen Handlers wie folgt aus (im Folgenden als Pseudocode bezeichnet):

    system_call:
        swapgs
        ..
        jmp    service_stub /* <-------- ТУТ БЫЛ call *sys_call_table(,%rax,8) */
        mov    %rax,0x20(%rsp) /* <-------- СЮДА ОСУЩЕСТВЛЯЕТСЯ ВОЗВРАТ ИЗ service_stub */
        ...
        swapgs
        sysretq
    service_stub:
        ...
        call ServiceTraceEnter /* void ServiceTraceEnter(struct pt_regs *) */
        ...
        call sys_call_table[N](args)
        ...
        call ServiceTraceLeave(regs) /* void ServiceTraceLeave(struct pt_regs *) */
        ...
        jmp back
    


    Hier ServiceTraceEnter()und ServiceTraceLeave()sind die Funktionen der Vor- bzw. Nachbearbeitung. Ihre Parameter sind ein Zeiger auf pt_regs- eine Registerstruktur, die den Kontext des Streams darstellt. Die letzte Anweisung ist der Befehl, die Steuerung an den Dispatcher-Code des Systemaufrufs zu übergeben, von dem aus der Aufruf an diesen Handler früher erfolgte.

    Das Folgende ist der service_syscall64-Handlercode , der als Beispiel für das Abfangen system_call(SYSCALL-Anweisung) verwendet wird:

    .global service_syscall64
    service_syscall64:
    	SAVE_REST
    	movq	%rsp, %rdi
    	call	ServiceTraceEnter
    	RESTORE_REST
    	LOAD_ARGS 0
    	movq	%r10, %rcx
    	movq	ORIG_RAX - ARGOFFSET(%rsp), %rax
    	call	*0x00000000(,%rax,8)			// origin call
    	movq	%rax, RAX - ARGOFFSET(%rsp)
    	SAVE_REST
    	movq	%rsp, %rdi
    	call	ServiceTraceLeave
    	RESTORE_REST
    	movq	RAX - ARGOFFSET(%rsp), %rax
    	jmp	0x00000000
    


    Wie zu sehen ist, hat es die oben betrachtete Struktur. Die genauen Werte von Zeigern und Offsets werden während des Ladevorgangs des Moduls konfiguriert (dies wird später erläutert). Ferner können zusätzliche Elemente (in dem obigen Fragment SAVE_REST, RESTORE_REST, LOAD_ARGS), deren Zweck es ist in erster Linie die Thread - Kontext (zu bilden , pt_regsbevor Funktionen aufrufen) ServiceTraceEnterund ServiceTraceLeave.

    IV. Merkmale der Einbettung



    Die Implementierung der Integration in die Planungsmechanismen von Systemaufrufen des Linux-Kernels auf die eine oder andere Weise impliziert die Notwendigkeit, die folgenden praktischen Probleme zu lösen:

    • Definition von Adressen von Versendern von Systemaufrufen;
    • Ermittlung von Adressen von Versandtabellen von Systemaufrufen;
    • Änderung des Absendercodes;
    • Anpassung der Handler;
    • Entladen des Moduls.


    Ermitteln der Adressen von Dispatchern von Systemaufrufen

    Das Vorhandensein mehrerer Dispatcher im System erfordert die Ermittlung ihrer Adressen. Es wurde oben angemerkt, dass jeder Dispatcher seiner eigenen "Methode" zum Anfordern eines Systemaufrufs entspricht. Daher werden die entsprechenden Mechanismen verwendet, um die erforderlichen Adressen zu ermitteln:

    • INT 80h, Lesen eines IDT-Tabellenvektors ( more );
    • SYSENTER, Lesen des Inhalts des MSR-Registers mit der Nummer MSR_IA32_SYSENTER_EIP ( more );
    • SYSCALL32, Lesen des Inhalts des MSR-Registers mit der Nummer MSR_CSTAR ( more );
    • SYSCALL, Lesen des Inhalts des MSR-Registers mit der Nummer MSR_LSTAR ( more ).


    Somit ist jede der erforderlichen Adressen leicht zu bestimmen.

    Bestimmung Adressen versenden Tabellen syscalls

    Wie oben erwähnt, Tabellen sys_call_tableund ia32_sys_call_tablewerden nicht exportiert. Es gibt verschiedene Möglichkeiten, ihre Adressen zu ermitteln. Nachdem Sie jedoch im vorherigen Schritt die Adressen der Absender ermittelt haben, werden die Adressen der Tabellen auch einfach durch Suchen nach einer Versandanweisung des Formulars ermittelt call sys_call_table[N].

    Für diese Zwecke ist es sinnvoll, einen Disassembler ( udis86) Durch sequentielles Durchsuchen der Anweisungen, beginnend mit der allerersten, können Sie den gewünschten Befehl erreichen, dessen Argument die Adresse der entsprechenden Tabelle ist. Aufgrund der Tatsache, dass die Struktur der Dispatcher gut etabliert ist, ist es möglich, die Eigenschaften des gewünschten Befehls (CALL mit einer Länge von 7 Bytes) eindeutig zu bestimmen und mit einem hohen Maß an Zuverlässigkeit den erforderlichen Wert der Tabellenadresse daraus zu erhalten.

    Wenn dies aus irgendeinem Grund nicht ausreicht, können Sie die Überprüfung der empfangenen Adresse verstärken. Dazu können Sie beispielsweise prüfen, ob der Wert in der Zelle mit der Nummer der __NR_openvorgeschlagenen Tabelle der Adresse der Funktion sys_open entspricht. In diesem Beispiel werden solche zusätzlichen Prüfungen jedoch nicht durchgeführt.

    Änderung des Absendercodes

    Wenn Sie den Code von Systemaufruf-Dispatchern ändern, müssen Sie berücksichtigen, dass deren Code schreibgeschützt ist (ReadOnly). Zusätzlich sollte die Code-Modifikation auf einem Arbeitssystem atomar sein, d.h. Während des Änderungsprozesses gibt es keine undefinierten Zustände, wenn einer der Threads einen teilweise abgeschlossenen Datensatz sieht.

    In einem früheren Artikel wurde die richtige Methode zum Schreiben auf schreibgeschützten Seiten mithilfe der Erstellung temporärer Anzeigen erläutert. Es besteht keine Notwendigkeit, hier etwas zu wiederholen. In Bezug auf die Atomizität wurde dieses Thema auch früher diskutiert, als das Thema des Abfangens der Funktionen des Kerns betrachtet wurde.

    Daher ist es ratsam, den schreibgeschützten Code mithilfe temporärer Zuordnungen sowie einer speziellen Schnittstelle des Linux-Kernels zu ändern stop_machine.

    Konfigurieren von Handlern

    Entsprechend der vorgestellten Einbettungsmethode wird der Code jedes Dispatchers so geändert, dass der 7-Byte-Dispatch-Befehl CALL MEM32durch einen 5-Byte-Befehl für den unbedingten Sprung zum entsprechenden Handler ersetzt wird JMP REL32. Infolgedessen sind dem Bereich des Übergangs bestimmte Beschränkungen auferlegt. Der Handler sollte nicht mehr als ± 2 GB vom Standort des Teams entfernt sein JMP REL32.

    Entsprechend der Struktur der Handler enthalten sie Befehle (JMP und CALL), für die genaue Argumente angegeben werden müssen (z. B. Rückgabeadressen oder Adressen der Systemaufruftabelle). Da solche Werte beim Übersetzen oder Laden des Moduls nicht zur Verfügung stehen, müssen sie vor Arbeitsbeginn nach dem Laden „manuell“ angebracht werden.

    Ein weiteres wichtiges Merkmal beim Einrichten von Handlern ist die Notwendigkeit, sicherzustellen, dass das Modul entladen werden kann, während der Systemzustand gewahrt bleibt. Zu diesem Zweck muss der Handlercode auch nach dem Entladen des Hauptmoduls im System verbleiben (dazu später mehr).

    Modul entladen

    Das Entladen des Moduls sollte durchgeführt werden, während das System gewartet wird. Dies bedeutet, dass das System nach dem Entladen des Moduls normal funktionieren sollte. Diese Aufgabe ist nicht trivial, da mit dem Entladen des Moduls der gesamte darin verwendete Code entladen wird.

    Sie können sich beispielsweise eine Situation vorstellen, in der ein Thread, der einen Systemaufruf ausführt, im Kernel eingeschlafen ist. Bis zu dem Moment, in dem er aufwacht, versucht jemand, das Modul zu entladen. Grundsätzlich hindert nichts das System daran, dies zu tun. Wenn der betreffende Thread aufwacht und den angeforderten Systemaufruf abschließt, kehrt die Steuerung daher zum entsprechenden Handler zurück (weshalb er nicht entladen werden sollte).

    Das Nichtentladen des Codes der Handler ist jedoch nicht die einzige Voraussetzung für die Aufrechterhaltung der Funktionsfähigkeit des Systems nach dem Entladen des Moduls. Es sei daran erinnert, dass der eigentliche Systemaufruf im Handler in mehrere Aufrufe der Tracefunktionen ServiceTraceEnter und ServiceTraceLeave "eingeschlossen" wurde , deren Code sich im ausgelagerten Modul befand.

    Um nicht in eine Situation zu geraten, in der der Thread nach der Rückkehr von einem Systemaufruf versucht, eine Funktion aufzurufen, die physisch nicht mehr vorhanden ist, muss der Code jedes Handlers wiederholt geändert werden, um weitere ungültige Aufrufe zu vermeiden (mit anderen Worten, um sie mit NOPs zu verstopfen).

    V. Merkmale der Implementierung des Kernelmoduls



    Im Folgenden wird die Struktur des Kernelmoduls beschrieben, das den Aufrufverteilungsmechanismus des Linux-Kernel-Kernels implementiert.

    Die Schlüsselstruktur des Moduls ist struct scentry - eine Struktur, die die Informationen enthält, die zum Einbetten in den entsprechenden Dispatcher erforderlich sind. Die Struktur enthält folgende Felder:

    typedef struct scentry {
    	const char	*name;
    	const void	*entry;
    	const void	*table;
    	const void	*pcall;
    	void	*pcall_map;
    	void	*stub;
    	const void	*handler;
    	void	(*prepare)(struct scentry *);
    	void	(*implant)(struct scentry *);
    	void	(*restore)(struct scentry *);
    	void	(*cleanup)(struct scentry *);
    } scentry_t;
    


    Strukturen werden zu einem Array zusammengefasst , das festlegt, wie und mit welchen Parametern sie eingebettet werden sollen:

    scentry_t elist[] = {
    ...
    	{
    		.name = "system_call",				/* SYSCALL: MSR(LSTAR), kernel/entry_64.S (1) */
    		.handler = service_syscall64,
    		.prepare = prepare_syscall64_1
    	},
    	{
    		.name = "system_call",				/* SYSCALL: MSR(LSTAR), kernel/entry_64.S (2) */
    		.handler = service_syscall64,
    		.prepare = prepare_syscall64_2
    	},
    ...
    };
    


    Mit Ausnahme der angegebenen Felder werden die restlichen Elemente der Struktur automatisch ausgefüllt - die Funktion ist dafür verantwortlich prepare. Das Folgende ist eine Beispielimplementierung einer Funktion zur Vorbereitung der Einbettung in den Dispatcher des SYSCALL-Befehls:

    extern void service_syscall64(void);
    static void prepare_syscall64_1(scentry_t *se)
    {
    	/*
    	 * searching for -- 'call *sys_call_table(,%rax,8)'
    	 *     http://lxr.free-electrons.com/source/arch/x86/kernel/entry_64.S?v=3.13#L629
    	 */
    	se->entry = get_symbol_address(se->name);
    	se->entry = se->entry ? se->entry : to_ptr(x86_get_msr(MSR_LSTAR));
    	if (!se->entry) return;
    	se->pcall = ud_find_insn(se->entry, 512, UD_Icall, 7);
    	if (!se->pcall) return;
    	se->table = to_ptr(*(int *)(se->pcall + 3));
    }
    


    Wie Sie sehen, wird zunächst versucht, den Symbolnamen in die entsprechende Adresse ( se->entry) aufzulösen . Wenn die Adresse nicht auf diese Weise bestimmt werden kann, kommen die für jeden Dispatcher spezifischen Mechanismen ins Spiel (in diesem Fall Lesen des MSR-Registers mit der Nummer MSR_LSTAR).

    Als nächstes wird nach dem gefundenen Dispatcher der Dispatch-Befehl ( se->pcall) durchsucht und bei Erfolg die Adresse der vom Dispatcher verwendeten Systemaufruftabelle ermittelt.

    Der Abschluss der Vorbereitungsphase ist die Erstellung des vom Disponenten nach seiner Änderung verwendeten Handler-Codes. Unten finden Sie die stub_fixup- Funktion , die dies ausführt :

    static void fixup_stub(scentry_t *se)
    {
    	ud_t ud;
    	memset(se->stub, 0x90, STUB_SIZE);
    	ud_initialize(&ud, BITS_PER_LONG, \
    		UD_VENDOR_ANY, se->handler, STUB_SIZE);
    	while (ud_disassemble(&ud)) {
    		void *insn = se->stub + ud_insn_off(&ud);
    		const void *orig_insn = se->handler + ud_insn_off(&ud);
    		memcpy(insn, orig_insn, ud_insn_len(&ud));
    		/* fixup sys_call_table dispatcher calls (FF.14.x5.xx.xx.xx.xx) */
    		if (ud.mnemonic == UD_Icall && ud_insn_len(&ud) == 7) {
    			x86_insert_call(insn, NULL, se->table, 7);
    			continue;
    		}
    		/* fixup ServiceTraceEnter/Leave calls (E8.xx.xx.xx.xx) */
    		if (ud.mnemonic == UD_Icall && ud_insn_len(&ud) == 5) {
    			x86_insert_call(insn, insn, orig_insn + (long)(*(int *)(orig_insn + 1)) + 5, 5);
    			continue;
    		}
    		/* fixup jump back (E9.xx.xx.xx.xx) */
    		if (ud.mnemonic == UD_Ijmp && ud_insn_len(&ud) == 5) {
    			x86_insert_jmp(insn, insn, se->pcall + 7);
    			break;
    		}
    	}
    	se->pcall_map = map_writable(se->pcall, 64);
    }
    


    Wie Sie sehen, besteht die Hauptaufgabe dieser Funktion darin, eine Kopie der Handler zu erstellen und diese dann für die aktuellen Adressen zu konfigurieren. Auch hier wird Disassembler aktiv eingesetzt. Durch den einfachen Aufbau der Handler kann auf komplexe Logik verzichtet werden. Ein Signal zum Verlassen der Schleife ist die Erkennung eines Befehls JMP REL32, der die Steuerung an den Dispatcher zurückgibt.

    Auf die Vorbereitungsphase folgt die Phase der Implantation des Codes in den Kernel-Code. Diese Phase ist recht einfach und besteht aus dem Schreiben einer einzelnen Anweisung ( JMP REL32) in den Code jedes der Systemdienste.

    Wenn das Modul entladen wird, wird zuerst die Wiederherstellungsphase ausgeführt , die darin besteht, den Code der Systemaufruf-Dispatcher wiederherzustellen und den Code der Handler zu ändern:

    static void generic_restore(scentry_t *se)
    {
    	ud_t ud;
    	if (!se->pcall_map) return;
    	ud_initialize(&ud, BITS_PER_LONG, \
    		UD_VENDOR_ANY, se->stub, STUB_SIZE);
    	while (ud_disassemble(&ud)) {
    		if (ud.mnemonic == UD_Icall && ud_insn_len(&ud) == 5) {
    			memset(se->stub + ud_insn_off(&ud), 0x90, ud_insn_len(&ud));
    			continue;
    		}
    		if (ud.mnemonic == UD_Ijmp)
    			break;
    	}
    	debug("  [o] restoring original call instruction %p (%s)\n", se->pcall, se->name);
    	x86_insert_call(se->pcall_map, NULL, se->table, 7);
    }
    


    Wie Sie sehen, werden im Handler-Code alle gefundenen 5-Byte-CALL-Befehle durch eine Folge von NOPs ersetzt, wodurch Versuche, nicht vorhandenen Code auszuführen, bei der Rückkehr von einem Systemaufruf ausgeschlossen werden. Dies wurde bereits früher besprochen.

    Die den Implantations- und Wiederherstellungsphasen entsprechenden Funktionen werden im Kontext ausgeführt stop_machine, daher müssen alle verwendeten Zuordnungen im Voraus vorbereitet werden.

    Die letzte Phase beim Entladen ist die Clenup- Phase , in der interne Ressourcen ( pcall_map) freigegeben werden .

    Es sei noch einmal darauf hingewiesen, dass der Bereich, der den Code der Handler enthält , nach dem Entladen des Moduls immer im Kernel-Speicher verbleibt. Wie bereits erwähnt, ist dies eine Voraussetzung für den ordnungsgemäßen Betrieb des Systems nach dem Entladen des Moduls.

    So werden anhand eines Beispiels die Grundprinzipien der Einbettung der Kernel-Systemaufrufe in den Mechanismus der Systemaufrufe analysiert und die Möglichkeit des Abfangens aufgezeigt.

    VI. Testen und Debuggen



    Fangen Sie zu Testzwecken einen Systemaufruf ab open(2). Das Folgende ist die trace_syscall_entry- Funktion , die dieses Abfangen mithilfe des ServiceTraceEnter- Handlers implementiert :

    static void trace_syscall_entry(int arch, unsigned long major, \
    	unsigned long a0, unsigned long a1, unsigned long a2, unsigned long a3)
    {
    	char *filename = NULL;
    	if (major == __NR_open || major == __NR_ia32_open) {
    		filename = kmalloc(PATH_MAX, GFP_KERNEL);
    		if (!filename || strncpy_from_user(filename, (const void __user *)a0, PATH_MAX) < 0)
    			goto out;
    		printk("%s open(%s) [%s]\n", arch ? "X86_64" : "I386", filename, current->comm);
    	}
    out:
    	if (filename) kfree(filename);
    }
    void ServiceTraceEnter(struct pt_regs *regs)
    {
    	if (IS_IA32)
    		trace_syscall_entry(0, regs->orig_ax, \
    			regs->bx, regs->cx, regs->dx, regs->si);
    #ifdef CONFIG_X86_64
    	else
    		trace_syscall_entry(1, regs->orig_ax, \
    			regs->di, regs->si, regs->dx, regs->r10);
    #endif
    }
    


    Die Montage und Beladung des Moduls erfolgt standardmäßig:

    $ git clone https://github.com/milabs/kmod_hooking_sct
    $ cd kmod_hooking_sct
    $ make
    $ sudo insmod scthook.ko
    


    Infolgedessen dmesgsollten die folgenden Informationen im Kernelprotokoll (Befehl ) angezeigt werden :

    [ 5217.779766] [scthook] # SYSCALL hooking module
    [ 5217.780132] [scthook] # prepare
    [ 5217.785853] [scthook]   [o] prepared stub ffffffffa000c000 (ia32_syscall)
    [ 5217.785856] [scthook]       entry:ffffffff81731e30 pcall:ffffffff81731e92 table:ffffffff81809cc0
    [ 5217.790482] [scthook]   [o] prepared stub ffffffffa000c200 (ia32_sysenter_target)
    [ 5217.790484] [scthook]       entry:ffffffff817319a0 pcall:ffffffff81731a36 table:ffffffff81809cc0
    [ 5217.794931] [scthook]   [o] prepared stub ffffffffa000c400 (ia32_cstar_target)
    [ 5217.794933] [scthook]       entry:ffffffff81731be0 pcall:ffffffff81731c75 table:ffffffff81809cc0
    [ 5217.797517] [scthook]   [o] prepared stub ffffffffa000c600 (system_call)
    [ 5217.797518] [scthook]       entry:ffffffff8172fcb0 pcall:ffffffff8172fd26 table:ffffffff81801400
    [ 5217.800013] [scthook]   [o] prepared stub ffffffffa000c800 (system_call)
    [ 5217.800014] [scthook]       entry:ffffffff8172fcb0 pcall:ffffffff8172ff38 table:ffffffff81801400
    [ 5217.800014] [scthook] # prepare OK
    [ 5217.800015] [scthook] # implant
    [ 5217.800052] [scthook]   [o] implanting jump to stub handler ffffffffa000c000 (ia32_syscall)
    [ 5217.800054] [scthook]   [o] implanting jump to stub handler ffffffffa000c200 (ia32_sysenter_target)
    [ 5217.800054] [scthook]   [o] implanting jump to stub handler ffffffffa000c400 (ia32_cstar_target)
    [ 5217.800055] [scthook]   [o] implanting jump to stub handler ffffffffa000c600 (system_call)
    [ 5217.800056] [scthook]   [o] implanting jump to stub handler ffffffffa000c800 (system_call)
    [ 5217.800058] [scthook] # implant OK
    


    Die korrekte Entwicklung des Abfangens open(2)führt dazu, dass Nachrichten im gleichen Protokolltyp angezeigt werden:

    [ 5370.999929] X86_64 open(/usr/share/locale-langpack/en_US.utf8/LC_MESSAGES/libc.mo) [perl]
    [ 5370.999930] X86_64 open(/usr/share/locale-langpack/en_US/LC_MESSAGES/libc.mo) [perl]
    [ 5370.999932] X86_64 open(/usr/share/locale-langpack/en.UTF-8/LC_MESSAGES/libc.mo) [perl]
    [ 5370.999934] X86_64 open(/usr/share/locale-langpack/en.utf8/LC_MESSAGES/libc.mo) [perl]
    [ 5370.999936] X86_64 open(/usr/share/locale-langpack/en/LC_MESSAGES/libc.mo) [perl]
    [ 5371.001308] X86_64 open(/etc/login.defs) [cron]
    [ 5372.422399] X86_64 open(/home/ilya/.cache/awesome/history) [awesome]
    [ 5372.424013] X86_64 open(/dev/null) [awesome]
    [ 5372.424682] I386 open(/etc/ld.so.cache) [skype]
    [ 5372.424714] I386 open(/usr/lib/i386-linux-gnu/libXv.so.1) [skype]
    [ 5372.424753] I386 open(/usr/lib/i386-linux-gnu/libXss.so.1) [skype]
    [ 5372.424789] I386 open(/lib/i386-linux-gnu/librt.so.1) [skype]
    [ 5372.424827] I386 open(/lib/i386-linux-gnu/libdl.so.2) [skype]
    [ 5372.424856] I386 open(/usr/lib/i386-linux-gnu/libX11.so.6) [skype]
    [ 5372.424896] I386 open(/usr/lib/i386-linux-gnu/libXext.so.6) [skype]
    [ 5372.424929] I386 open(/usr/lib/i386-linux-gnu/libQtDBus.so.4) [skype]
    [ 5372.424961] I386 open(/usr/lib/i386-linux-gnu/libQtWebKit.so.4) [skype]
    [ 5372.425003] I386 open(/usr/lib/i386-linux-gnu/libQtXml.so.4) [skype]
    [ 5372.425035] I386 open(/usr/lib/i386-linux-gnu/libQtGui.so.4) [skype]
    [ 5372.425072] I386 open(/usr/lib/i386-linux-gnu/libQtNetwork.so.4) [skype]
    [ 5372.425103] I386 open(/usr/lib/i386-linux-gnu/libQtCore.so.4) [skype]
    [ 5372.425151] I386 open(/lib/i386-linux-gnu/libpthread.so.0) [skype]
    [ 5372.425191] I386 open(/usr/lib/i386-linux-gnu/libstdc++.so.6) [skype]
    [ 5372.425233] I386 open(/lib/i386-linux-gnu/libm.so.6) [skype]
    [ 5372.425265] I386 open(/lib/i386-linux-gnu/libgcc_s.so.1) [skype]
    [ 5372.425292] I386 open(/lib/i386-linux-gnu/libc.so.6) [skype]
    [ 5372.425338] I386 open(/usr/lib/i386-linux-gnu/libxcb.so.1) [skype]
    [ 5372.425380] I386 open(/lib/i386-linux-gnu/libdbus-1.so.3) [skype]
    [ 5372.425416] I386 open(/lib/i386-linux-gnu/libz.so.1) [skype]
    [ 5372.425444] I386 open(/usr/lib/i386-linux-gnu/libXrender.so.1) [skype]
    [ 5372.425475] I386 open(/usr/lib/i386-linux-gnu/libjpeg.so.8) [skype]
    [ 5372.425510] I386 open(/lib/i386-linux-gnu/libpng12.so.0) [skype]
    [ 5372.425546] I386 open(/usr/lib/i386-linux-gnu/libxslt.so.1) [skype]
    [ 5372.425579] I386 open(/usr/lib/i386-linux-gnu/libxml2.so.2) [skype]
    


    Darüber hinaus ist anzumerken, dass das Abfangen für 32-Bit-Anwendungen (z. B. Skype) ebenfalls korrekt ausgeführt wird. Dies wird durch das Vorhandensein von Nachrichten bestätigt, die mit I386 und nicht mit X86_64 beginnen. Das Beispiel open(2)zeigt also die Möglichkeit, Systemaufrufe abzufangen.

    VII. Fazit



    Die im Artikel vorgestellte Methode zum Einbetten des Linux-Kernels in die Verteilungsmechanismen von Systemaufrufen ermöglicht es uns, das Problem zu lösen, nicht nur bestimmte Systemaufrufe, sondern den gesamten Verteilungsmechanismus abzufangen. Der vorgeschlagene Ansatz für die Implementierung des Ladens und Entladens des Moduls ermöglicht die korrekte Integration in das System sowie die Gewährleistung der Funktionsfähigkeit des Systems nach dem Entladen. Durch die aktive Verwendung des Disassemblers können Sie das Problem der Suche nach versteckten und nicht exportierten Zeichen zuverlässig lösen.

    Herkömmlicherweise ist auf github Kernel- Modulcode verfügbar, der die zum Abfangen von Funktionen erforderlichen Aktionen implementiert .

    Jetzt auch beliebt: