Kernel hacken
------------------------------------------------------- Kernel hacking ------------------------------------------------------- * Disclaimer * Inleiding * System calls * Loadable kernel modules * Verbergen van modules * Afsluiting * Bronnen
- Disclaimer
Het kan zijn dat als je informatie uit de artikel gaat uitvoeren op je OS en je gaat zelf dingen uit proberen met betrekking tot kernel modules en dergelijke dan is het mogelijk dat je dingen verkeerd doet, zodat je kernel (tijdelijk?) niet meer goed werkt. Alles is dus op eigen risico en ik ben nergens voor aansprakelijk.
- Inleiding
Zoals velen van jullie wel weten bestaan er mogelijkheden om de kernel van een OS te hacken. Dit is natuurlijk een zeer gevaarlijke attack, want wat moet je als sysop als je je eigen kernel niet eens meer kan vertrouwen? Een kernel kan je echter pas hacken als je in een systeem bent binnengedrongen en root hebt verkregen. Maar hoe goed je je systeem ook beveiligd, er kan altijd iemand binnenkomen. En hoe slimmer de hacker is die jouw geweldige beveiliging omzeild, hoe groter de kans dat hij je kernel hackt om later nog eens terug op bezoek te kunnen komen. Deze worden vaak als een kernelmodule gecompileerd en geinstal- leerd, terwijl ze op een vernuftige manier in elkaar gezet zijn, zodat je ook met lsmod de desbetreffende backdoor niet kan zien. In dit artikel zal ik eens wat dieper ingaan op het hacken van de kernel. Alles is van toepassing op de Linux-kernel, maar de principes zijn hetzelfde voor alle UNIX-based systemen. Je zult dan wel even uit moeten vogelen welke functies de kernel gebruikt om systemcalls uit te voeren. Dit gaat natuurlijk het makkelijkst als je de sources van de kernel van je OS hebt. In principe zijn er ook wel slimme backdoors te schrijven voor windows, maar dit kost heel veel tijd en je zult je door alle assembly-sources van Windows 9x/2K/ME heen moeten worstelen. :)
- System calls
Voordat we kunnen bekijken wat system calls eigenlijk zijn moet er eerst wat uitgelegd worden over hoe het geheugen in Linux is ingedeeld. Het geheugen is in twee gedeeltes verdeeld; kernelspace en userspace. Een programma kan niet naar de kernelspace schrijven, omdat die voor de kernel is gereserveerd *doh*. Ook kun je niet zonder meer de kernel access tot het usergeheugen laten geven. Als je een system call uitvoert doet de kernel enkele dingen. Hij zet DS en ES (geheugenpointers, voor meer info lees wat over Assembler) zodat het geheugen wijst naar het kernelsegment. Hierna zet het FS en die wijst dan naar het usersegment in het geheugen. Vanuit de kernelspace kun je dus toch het stukje geheugen dat gereserveerd is voor de user aanroepen.
Elk programma dat je in C hebt geschreven gebruikt libc. Dit is een library die alle basisfuncties, die je nodig hebt om te programmeren, vastlegd. In feite zijn bijna alle functies in libc gebaseerd op de onderliggende system calls. Dit zijn de simpelste kernelfuncties die een userprogramma kan aanroepen. Alle system calls van een OS zijn geimplementeerd in de kernel zelf of in kernel modules die je pas in het geheugen laat als je ze nodig hebt (insmod, rmmod, lsmod...)
In Linux zijn de system calls geimplementeerd door door een interrupt. Deze is 'int 0x80'. Als je deze instructie uitvoert wordt de controle aan de kernel overgedragen met behulp van de functie _system_call(). Deze functie saved alle registers en daarna wordt de inhoud van het %eax register gecheckt en vergeleken met de zogeheten 'global system calls'-table. In deze tabel staan alle system calls en hun geheugenadressen. Als de system call in %eax niet in de tabel staat retouneert _system_call een error. In alle andere gevallen zal de kernel het geheugenadres dat in de tabel stond achter de system call uitvoeren. Hier volgt even een klein voorbeeldje dat de system call aanroept met een system call die niet bestaat, zodat de kernel een foutmelding zal geven! Let er wel op dat je bij jouw systeem wel een waarde voor 'movl $165,%eax' geeft die niet bestaat, want anders kan je kernel hele rare dingen gaan doen. Voor een overzicht van alle system calls kijk in: "/usr/include/asm/unistd.h". Daar staan ze tenminste bij mij. Anders kan je OS ze ook in: "/usr/include/sys/syscall.h" hebben staan. Daar staan alle gedefineerde system calls van je OS in. Laten we eens gaan kijken wat er gebeurd als we een systemcall uit proberen die dus NIET bestaat!
[gorny@localhost artikel]$ vi ./syscall1.c /* Voorbeeld van een systemcall */ #include #include #include
extern void *sys_call_table[];
functie() { __asm__( /* wel gecheckt of systemcall 165 NIET bestaat? */ "movl $165,%eax int $0x80"); }
main() { errno = functie(); perror("Invalid syscall"); } [gorny@localhost artikel]$ gcc -O2 -o syscall1 ./syscall1.c [gorny@localhost artikel]$ ./syscall1 Invalid syscall: Unknown error 4294967282 [gorny@localhost artikel]$ su Password: [root@localhost artikel]# ./syscall1 Invalid syscall: Unknown error 4294967282 [root@localhost artikel]# exit [gorny@localhost artikel]$
En zoals we al verwacht hadden geeft de kernel een keurig nette foutmelding. Normaal zal de controle terug gegeven worden gegeven aan de gespecificeerde systemcall. Hierna roept _system_call() de functie _ret_from_sys_call() aan, om verschillende informatie te checken en uiteindelijk weer terug te keren naar het usersegment in het geheugen. Zoals je ziet, maakt het ook geen ver- schil of je het programmaatje uitvoert als gewone user of als root. NOTE: Om de een of andere vage reden geeft het voorbeeldproggie een core-dump als ik hem gecompiled heb met de optimatie-optie '-O3'. Vraag me niet waarom, want '-O1' en '-O2' werken wel gewoon :(
- Loadable kernel modules
Als je nou zelf je eigen system calls wilt maken dan kun je dit doen door de source van je kernel handmatig aan te passen en een nieuwe kernel te compilen. Het is echter heel wat makkelijker als je een loadable kernel module gebruikt. Dit betekent domweg dat het een objectfile is die gelinked zal worden als de kernel de module nodig heeft. Hierdoor kun je ook een kleinere kernel krijgen, door alles wat je niet vaak nodig hebt, als aparte module te compileren. In feite is het enige wat je moet doen in je module om system calls toe te voegen is het veranderen van de sys_call_table array die al eerder is beschreven. Elke loadable kernel module (lkm) heeft in ieder geval de functies;
int init_module(void) void cleanup_module(void)
De eerste functie wordt aangeroepen bij het laden van de module en de tweede bij (je raadt het al :) het unloaden van de module. Nog even een waarschuwing voor mensen die nu hun eigen loadable kernel modules willen gaan schrijven. Begrijp goed dat je met kernel-code bezig bent en dat je hele systeem in de war kan lopen als je echt rare system calls gaat uitvoeren. Let dus goed op en zorg ervoor dat de code goed is voordat je de gecompilede code gaat uitvoeren. Een ander nadeel van kernel modules is dat ze niet libc kunnen gebruiken. Ze moeten direct via system calls hun opdrachten uitvoeren. Hier bestaat ook een handige macro voor genaamd _syscallX(), maar het kan ook gewoon met de sys_call_table array. Ook is het een goed idee om je module te compileren met een regel als:
[gorny@localhost artikel]$ gcc -c -O3 blaat.c
Met de optie O3 wordt namelijk alles geoptimaliseerd en duurt het compilen wat langer, maar uiteindelijk levert het wel wat snelheidswinst op :) Je krijgt dan een module genaamd blaat.o. Deze kun je laden met 'insmod blaat.o' en dan is je lkm actief. Wil je hem weer uit het geheugen laden dan doe je 'rmmod' en voor een overzicht van alle geladen kernel modules typ je 'lsmod'. Laten we eens een voorbeeld module gaan bekijken. Deze heb ik gehaald uit het artikel van plaguez.
#define MODULE #define __KERNEL__ #include #ifdef MODULE #include #include #else #define MOD_INC_USE_COUNT #define MOD_DEC_USE_COUNT #endif #include #include #include #include #include #include #include #include
int errno;
/* Hier wordt een pointer gedefineerd die naar de oude setreuid */ int (*o_setreuid) (uid_t, uid_t);
/* In de onderstaande array staan de systemcalls en pointers */ extern void *sys_call_table[];
int new_setreuid(uid_t ruid, uid_t euid) { printk("uid %i trying to seteuid to euid=%i", current->uid, euid); return (*o_setreuid) (ruid, euid); }
int init_module(void) { o_setreuid = sys_call_table[SYS_setreuid]; sys_call_table[SYS_setreuid] = (void *) new_setreuid; printk("swatch loaded.\n"); return 0; }
void cleanup_module(void) { sys_call_table[SYS_setreuid] = o_setreuid; printk("swatch unloaded.\n"); }
Je compileert deze module met 'gcc -c -O3 plaguez.c'. Wat doet deze module eigenlijk precies? Als hij wordt aangeroepen en in het geheugen wordt geladen zal de pointer naar de oude setreuid functie opgeslagen worden in o_setreuid. Hierna wordt de sys_call_table gewijzigd zodat de systemcall 'setreuid' nu wijst naar de functie 'new_setreuid'. De informatie wordt dus onderschept en daarna wordt pas de goede functie aangeroepen. Bij het unloaden van de module wordt alle informatie weer terug gezet zoals het was en zal de system call 'setreuid' weer gewoon wijzen naar de goede functie. Zoals je hier ook duidelijk ziet kun je niet de gewone printf-functie gebruiken van libc. Daarom wordt hier de functie printk gebruikt die wel aangeroepen kan worden door loadable kernel modules.
- Verbergen van modules
Maar natuurlijk zal de module ook verborgen moeten worden. Anders is de module veel te gauw gespot en daar heb je hem niet voor geinstalleerd :). Dit kan ge- daan worden op verschillende manieren. Een mogelijkheid is om '/proc/modules' te wijzigen zodra een programma die file probeert te lezen, maar dit is erg moeilijk om te programmeren en het schijnt dat je dan met: 'dd if=/proc/modules bs=1' wel gewoon de file kan lezen inclusief de naam van de module die je wilt verbergen. Er is wel een oplossing. De module lijst wordt niet direct geëxporteerd vanuit de kernel zodat er geen manier is om deze lijst direct aan te spreken. Dit laatste kan echter alleen als sys_init_module() wordt aangeroepen, want dat is namelijk de module die onze eigen lkm moet gaan aan- roepen, dus die moet toegang hebben tot de moduleinfolijst. Hierbij moeten we er wel van uitgaan dat GCC de registers niet verneukt tussen het aanroepen van 'sys_init_module' en het aanroepen van 'init_module()'. Dat maakt het mogelijk om na te gaan wat het register was waarin de moduleinfolijst begon. De module- lijsten zijn opgeslagen in een register struct van het gedefineerd type module. De naam van deze struct is mp. Je kan dan nagaan wat het adres is van 1 van de items van de struct. Als je dan een stukje code als het onderstaande schrijft in je lkm dan zal de module niet getoond worden in /proc/modules, omdat de kernel denkt dat het een kernel module is, aangezien die ook geen referenties en een naam hebben. Bij het onderstaande stukje wordt er wel vanuit gegaan dat het adres van die struct is opgeslagen in %ebx. Dit zul je dus wel even moeten checken om daar het goede register in te vullen.
int init_module() { register struct module *mp asm("%ebx"); *(char*)mp->name=0; mp->size=0; mp->ref=0; /* De rest van je code */ }
Naast het verbergen van de module zelf zijn er trojans geschreven met allerlei opties. Zoals: PID-hiders, execve redirection, file hiders, socket recvfrom() en gemodificeerde setuid()-calls. Vooral itf.c van plaguez heeft veel opties. Maar het is natuurlijk wel lame om niet je eigen trojans te gebruiken, maar die van anderen (zie artikel van Nynex in H4H-0C over lameheid :). Bovendien leer je gigantisch veel van het zelf schrijven van je trojans en tools. Als je van het zelf maken van je programma's niets leert dan leer je nergens wat van. Kun je beter je computer verkopen ofzo en postzegels gaan verzamelen :)
|