Open Source im professionellen Einsatz
Linux-Magazin 04/2008
© Yevgeniya Ponomareva, Fotolia

© Yevgeniya Ponomareva, Fotolia

Perl-Skript nutzt Ptrace zur Prozessüberwachung

Prozess-Spion

Einem Prozess bei der Arbeit auf die Finger sehen - unter Linux erlaubt das Ptrace. Das eingebaute Tool nutzen hilfreiche Debugger wie feindselige Prozess-Kidnapper gleichermaßen. Ein CPAN-Modul führt die Technik in Perl ein, und wo das nicht reicht, helfen in C geschriebene Erweiterungen weiter.

1007

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.

Listing 1: »uninstd.h« (Auszug)

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.

Listing 2: »WriteTracer.pm«

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, &registers);
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.

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

comments powered by Disqus

Ausgabe 09/2017

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.