- Most people, I think, don't even know what a rootkit is, so why should they care about it?
- -- Thomas Hesse, President of Sony BMG's global digital business division
Neulich habe ich etwas gebraucht, was ich im Nachhinein "poor man's rootkit" getauft habe. Wir wollten auf einem Linux System feststellen, welches Prozess an Sachen rumfingert, an die er eigentlich gar nicht ran sollte. Als Beispiel nehme ich mal an, dass es sich dabei um eine Konfigurationsdatei handelt.
Ich habe dabei ein paar Sachen kombiniert, die ich schon Monaten, das Meiste davon sogar schon seit Jahren kenne. Interessant ist dabei die Mächtigkeit, die aus den Kombination der Einzelteile entsteht. Der Trick besteht im Wesentlichen daraus, dass man dem Linker beim Starten eines Programms anweisen kann, neben den angeforderten Bibliotheken noch weitere "Shared Objects" hinzu zu laden, die dann eine höhere Priorität aufweisen. Das wollen wir uns zu nutze machen, um den Aufruf der Funktion open() aus der C-Bibliothek "abzufangen".
Der C-Quellcode für eine Funktionen zum Abfangen sieht dabei so aus:
int creat(const char \*pathname, mode_t mode)
{
/\*1\*/ typedef int (*creat_t)(const char*,mode_t);
/\*2\*/ creat_t real_creat = (creat_t) dlsym(RTLD_NEXT, "creat");
/\*3\*/ logentry( "creat", pathname );
/\*4\*/ return real_creat( pathname, mode );
}
Wenden wir uns erst kurz der Funktion creat() zu. Es ist eine Kurzform, um eine Datei zu erzeugen und zum Schreiben zu öffnen, also ein Spezialfall von open(). Weil open() eine Funktion ist, die mit zwei oder drei Argumenten aufgerufen werden kann, wird es dort deutlich komplexer. In /\*1\*/ wird der Typ für den Funktionspointer definiert, der unter /\*2\*/ beim Linker die Adresse der eigentlichen Funktion abfragt und unter /\*4\*/ dann auch aufgerufen wird. Unter /\*3\*/ wird dann der Aufruf geloggt. So sieht als das "Abfangen" einer Bibliotheksfunktion im einfachen Fall aus.
Nun zum schwierigeren open():
int open(const char *pathname, int flags, ...)
{
if( flags & O_CREAT )
{
typedef int (*open_t)(const char*,int,mode_t);
va_list ap;
mode_t mode;
open_t real_open = (open_t) dlsym(RTLD_NEXT, "open");
va_start( ap, flags );
mode = va_arg( ap, mode_t );
va_end( ap );
logentry( "open3", pathname );
return open3( pathname, flags, mode );
}
else
{
typedef int (*open_t)(const char*,int);
open_t real_open = (open_t) dlsym(RTLD_NEXT, "open");
logentry( "open2", pathname );
return real_open( pathname, flags );
}
}
Sieht deutlich komplexer aus als creat(), ist es aber nur auf den ersten Blick. Für den Fall, dass open() mit den flags O_CREAT aufgerufen wird, um zu signalisieren, dass eine Datei bei Bedarf erzeugt werden soll, muss ein dritter Parameter angegeben werden, der besagt, welche Zugriffsrechte diese Datei haben sollte. Dieser dritte Parameter muss gesondert abgeholt werden, das ist auch schon alles. Außerdem muss noch eine andere Signatur der beim Linker angefragten open()-Funktion angegeben werden, schließlich soll der dritte Parameter ja auch mit durch gereicht werden.
Abschließend noch ein Blick auf die logentry()-Funktion, schließlich müssen wir uns ja noch von irgendwoher aussagekräftige Informationen holen:
static void logentry(const char \*target,const char *pathname)
{
typedef int (\*open_t)(const char\*,int,mode_t);
static const char colon = ':';
static const char newline = '\n';
static const char filename[] = "config.txt";
int fd;
open_t real_open = (open_t) dlsym(RTLD_NEXT, "open");
if( strcmp( basename( pathname ), &filename[0] ) )
{
return;
}
fd = real_open( "/tmp/open.log", O_WRONLY | O_APPEND | O_CREAT, 0600 );
if( fd > 0 )
{
int size;
char link[PATH_MAX];
write( fd, target, strlen( target ) );
write( fd, &colon, 1 );
write( fd, pathname, strlen( pathname ) );
write( fd, &colon, 1 );
size = readlink("/proc/self/cwd", &link[0], sizeof(link));
write( fd, link, size );
write( fd, &colon, 1 );
size = readlink("/proc/self/exe", &link[0], sizeof(link));
write( fd, link, size );
write( fd, &colon, 1 );
size = readlink("/proc/self", &link[0], sizeof(link));
write( fd, link, size );
write( fd, &newline, 1 );
close( fd );
}
}
Um nicht unseren eigenen open()-Aufruf zu loggen, und so in einer Endlosschleife zu landen, muss auch hier wieder die "eigentliche" open()-Funktion beim Linker angefragt werden. Die eigentlichen Informationen werden alle aus dem "/proc"-Dateisystem von Linux geholt. Praktischerweise werden alles, was wir wissen wollen in symbolischen Links abgebildet, und das Lesen des Inhalts eines symbolischen Links ist einfacher als das Lesen des Inhalts einer Datei.
Nun bleibt aber die Frage, wie schieben wir diese Bibliothek dem System unter? Bei einem einzelnen Programm geht dies über das setzen der Umgebungsvariablen "LD_PRELOAD" mit dem kompletten Pfad der Bibliothek. Es gibt aber auch noch einen anderen Weg. Genauso wie man den Pfad in dem die Bibliotheken gesucht werden soll, sowohl über die Umgebungsvariable "LD_LIBRARY_PATH" und die Datei "/etc/ld.so.conf" gesetzt werden kann, kann man auch eine Datei "/etc/ld.so.preload" anlegen, in der dann der komplette Pfad unseres Shared Objects steht, und schon lädt der Linker bei jedem Aufruf unseren Code dazu.
Es fehlt in diesem Beispiel noch das Abfangen von fopen() und freopen(). Außerdem funktioniert das Abfangen bei statisch gelinkten Programmen nicht. Wenn man also diese Technik wirklich dazu verwenden möchte ein Rootkit zu basteln, ist noch viel zu tun: die Datei /etc/ld.so.preload muss versteckt werden, indem sowohl bei open(), als auch bei den Funktionen zum Auslesen von Verzeichnissen manipulierte Werte zurückgegeben werden müssen. Und trotzdem ist die Gegenwehr recht einfach: man verwendet eine statisch gelinkte
busybox um das System zu säubern.
Mein Proof-Of-Concept ist unter git://git.svolli.org/mischacks.git zu finden, dort in dem Verzeichniss "c/open-logger", der dann auch noch fopen() abfängt.
Kommentare
http://sourceware.org/systemtap/wiki/WSFileMonitor
Aber dort macht man es zur Compile-Zeit klar und irgendwas mit der Platform ist auch anders :-----)