Neulich wollte ich die Schreib-Aktivitäten eines Linux-Prozesses etwas genauer untersuchen und entdeckte bei der Gelegenheit, dass es im CPAN sogar ein Modul für Ptrace gibt! Ptrace ist eine im Linux-Kernel verankerte Technik, die Prozesse schrittweise ablaufen lässt und Informationen über ihre Daten einholt. Debugger wie zum Beispiel der GDB und andere benutzen diese Technik und bauen ein komfortables Benutzer-Interface darum herum.
Um nur herauszufinden, welche Dateien ein Prozess zum Schreiben öffnet, reicht es jedoch schon, »ptrace« mit »PTRACE_SYSCALL« dazu zu überreden, jedes Mal anzuhalten, wenn der Prozess einen »open()«-Systemaufruf absetzt. Anschließend ruft er die in der Manpage von Open näher beschriebene Funktion der Standard-C-Bibliothek Libc im Schreibmodus auf. Was diese Bibliothek intern alles anstellt, damit der Kernel die angegebene Datei öffnet und einen File-Deskriptor zurückgibt, lässt sich mit »objdump -d /lib/libc.so.6« ergründen (siehe Abbildung 1).
Abbildung 1: Der Code der Libc, der den Kernel darum bittet, den Systemcall »open()« auszuführen. Über einen Interrupt wechselt der Prozessor dafür in den Kernel-Mode.
Eingang zur Unterwelt
Was der Disassembler ausspuckt, ist nur Wenigen auf den ersten Blick verständlich. Das gilt auch für den x86-Assemblercode, der die Funktionsparameter für »open()« vom Stack (»%esp«) holt und mit der Anweisung »mov« (für Move) in die Prozessorregister EBX, ECX und EDX schreibt (im Assemblercode mit vorangestelltem Prozentzeichen).
Wie die Include-Datei »adm/unistd.h« (Listing 1) verrät, führt der Kernel den Systemcall »open()« intern unter der Nummer 5, die Libc schreibt diesen Wert mit »mov $0x5,%eax« ins Register EAX des Prozessors. Den Sprung in den Kernel führt dann die Anweisung »int $0x80« aus. Sie löst einen Software-Interrupt aus, der Prozessor wechselt in den Priviledged Mode und bearbeitet den Systemcall auf der anderen Seite der Mauer, im Kernel-Land. Die Parameter holt er sich aus den Prozessorregistern, in die sie die Libc vorher gelegt hat.
01 #ifndef _ASM_I386_UNINSTD_H_
02 #define _ASM_I386_UNINSTD_H_
03 /*
04 * This file contains the system call numbers.
05 */
06 #define __NR_exit 1
07 #define __NR_fork 2
08 #define __NR_read 3
09 #define __NR_write 4
10 #define __NR_open 5
11 #define __NR_close 6
|
Die Funktion »open()« nimmt bis zu drei Parameter entgegen: »int open(const char *pathname, int flags, mode_t mode)«. Der String, der den Dateipfad angibt, passt natürlich nicht in ein 32-Bit-Register, also liegt im Register EBX nur die Speicheradresse, an der die Zeichenkette zu finden ist.
Um nun zu untersuchen, ob ein zufällig aufgeschnappter Systemcall ein »open()« mit Schreiboption ist, muss der Überwachungscode prüfen, ob EAX den Wert 5 aufweist und ob die UND-Verknüpfung des Registers ECX mit der in »sys/fcntl.h« definierten Konstanten »O_WRONLY« den Wert »wahr« ergibt. Theoretisch ließe sich eine Datei zum Schreiben auch mit »O_RDWR« (Schreib-/Lesezugriff) oder »O_APPEND« (ans Dateiende anfügen) öffnen, aber das soll der Einfachheit halber unterbleiben.
Es ist übrigens egal, in welcher Sprache der zu überwachende Code geschrieben ist - unter der Haube verwenden C, Perl, Java, Ruby und wie sie alle heißen immer den Systemcall »open()« aus der Libc in »WriteTracer.pm«.
Der Überwacher dockt an
Listing 2 zeigt den Perl-Code, mit dem ein Skript alle Systemcalls eines Prozesses abfängt und auf »open()«-Requests mit Schreibmodus untersucht. Abbildung 2 illustriert das Zusammenspiel von Eltern- und Kindprozess während der Überwachung. Nach einem »fork()« setzt der neu entstandene Kindprozess das Ptrace-Kommando »PTRACE_TRACEME« ab und führt das zu überwachende Programm mit »exec()« aus.
001 ###########################################
002 package WriteTracer;
003 use strict;
004 use POSIX;
005 use Inline "C";
006 use Fcntl;
007
008 use Sys::Ptrace qw(ptrace
009 PTRACE_SYSCALL PTRACE_TRACEME);
010
011 ###########################################
012 sub run {
013 ###########################################
014 my($prg, @params) = @_;
015
016 my @files = ();
017 my %files = ();
018
019 if((my $pid = fork()) < 0) {
020 die "fork failed";
021
022 } elsif($pid == 0) {
023 # child
024 ptrace(PTRACE_TRACEME, $$, 0, 0);
025 exec($prg, @params);
026
027 } else {
028 # parent
029 {
030 my $rc = waitpid($pid, 0);
031 last if $rc < 0;
032
033 if( WIFSTOPPED($?) ) {
034 my($eax, $orig_eax, $ebx, $ecx,
035 $edx) = ptrace_getregs($pid);
036
037 if($eax == -ENOSYS()) {
038 if($orig_eax == 5 and
039 $ecx & O_WRONLY) {
040 my $str = ptrace_string_read(
041 $pid, $ebx);
042 push @files, $str
043 unless $files{$str}++;
044 }
045 }
046
047 ptrace(PTRACE_SYSCALL, $pid,
048 undef, undef);
049 redo;
050 }
051 }
052 }
053 return @files;
054 }
055
056 1;
057
058 __DATA__
059 __C__
060 #include <sys/ptrace.h>
061 #include <linksm/user.h>
062
063 #define IVPUSH(x) Inline_Stack_Push( \
064 sv_2mortal(newSViv(x)));
065
066 /* ------------------------------------- */
067 void ptrace_getregs(int pid) {
068 int rc;
069 struct user_regs_struct registers;
070 Inline_Stack_Vars;
071
072 rc = ptrace(PTRACE_GETREGS, pid,
073 0, ®isters);
074 if(rc == -1) {
075 return;
076 }
077
078 if( registers.eax == -ENOSYS ) {
079 Inline_Stack_Reset;
080 IVPUSH(registers.eax);
081 IVPUSH(registers.orig_eax);
082 IVPUSH(registers.ebx);
083 IVPUSH(registers.ecx);
084 IVPUSH(registers.edx);
085 Inline_Stack_Done;
086 }
087 }
088
089 /* ------------------------------------- */
090 int ptrace_aligned_word_read_c(int pid,
091 void *addr, char *buf, int *len) {
092 char *aligned_addr;
093 long word;
094 void *ptr;
095
096 aligned_addr = (char *) (
097 (long)addr & ~ (sizeof(long) - 1) );
098
099 word = ptrace(PTRACE_PEEKDATA, pid,
100 aligned_addr, NULL);
101
102 if(word == -1) {
103 return -1;
104 }
105
106 *len = sizeof(long) - ( (long) addr -
107 (long) aligned_addr );
108 ptr = &word;
109 ptr += (sizeof(long) - *len);
110 memcpy(buf, ptr, *len);
111
112 return 0;
113 }
114
115 /* ------------------------------------- */
116 void ptrace_string_read(int pid,
117 void *addr) {
118 char word_buf[ sizeof(long) ];
119 int word_len;
120 SV *pv;
121 int rc;
122 int i;
123 Inline_Stack_Vars;
124
125 pv = newSVpv((const char *)"", 0);
126
127 while(1) {
128 rc = ptrace_aligned_word_read_c(pid,
129 addr, word_buf, &word_len);
130 if(rc < 0) {
131 return;
132 }
133
134 for(i=0; i<word_len; i++) {
135 if(word_buf[i] == '\0') {
136 goto FINISH;
137 }
138 sv_catpvn(pv, (const char *)
139 &word_buf[i], 1);
140 }
141 addr += word_len;
142 }
143
144 FINISH:
145 Inline_Stack_Reset;
146 Inline_Stack_Push(sv_2mortal(pv));
147 Inline_Stack_Done;
148 }
|
Abbildung 2: Eltern- und Kindprozess im Zusammenspiel während einer Überwachung mit Ptrace.
Der Elternprozess wartet mit »waitpid()« darauf, dass der Kernel das Kind mit einem Stopp-Signal anhält. Danach schickt er es mit »PTRACE_SYSCALL« wieder ins Rennen, weist den Kernel aber damit an, das Kind beim nächsten Aufruf eines Systemcalls sofort wieder anzuhalten. Beim nächsten Stopp erwischt er das Kind dann beim Tuscheln mit dem Kernel und der Elternprozess kann in aller Ruhe mit weiteren Ptrace-Kommandos untersuchen, welcher Systemcall auszuführen war. Sogar dessen Parameter liegen offen.