Linux-systemanrop. Systemanrop

VLADIMIR MESHKOV

Avlytte systemanrop i Linux OS

De siste årene har Linux-operativsystemet tatt en solid ledende posisjon som serverplattform, foran mange kommersielle utviklinger. Likevel slutter ikke spørsmålene om å beskytte informasjonssystemer bygget på grunnlag av dette operativsystemet å være relevante. Det finnes et stort antall tekniske virkemidler, både programvare og maskinvare, som kan sikre systemsikkerhet. Dette er midler for å kryptere data og nettverkstrafikk, avgrense tilgangsrettigheter til informasjonsressurser, beskytte e-post, webservere, antivirusbeskyttelse osv. Listen er, som du forstår, ganske lang. I denne artikkelen inviterer vi deg til å vurdere en beskyttelsesmekanisme basert på avskjæring av systemanrop fra Linux-operativsystemet. Denne mekanismen lar deg ta kontroll over driften av enhver applikasjon og dermed forhindre mulige ødeleggende handlinger som den kan utføre.

Systemanrop

La oss starte med en definisjon. Systemanrop er et sett med funksjoner implementert i OS-kjernen. Enhver brukerapplikasjonsforespørsel blir til slutt oversatt til et systemanrop som utfører den forespurte handlingen. En fullstendig liste over Linux-systemanrop finnes i filen /usr/include/asm/unistd.h. La oss se på den generelle mekanismen for å utføre systemanrop med et eksempel. La applikasjonskilden din kalle opp creat()-funksjonen for å lage en ny fil. Etter å ha møtt et kall til denne funksjonen, konverterer kompilatoren den til monteringskode, laster systemanropsnummeret som tilsvarer denne funksjonen og dens parametere inn i prosessorregistrene og kaller deretter avbrudd 0x80. Følgende verdier lastes inn i prosessorregistrene:

  • for å registrere EAX– systemanropsnummer. Så, for vårt tilfelle, vil systemanropsnummeret være 8 (se __NR_creat);
  • til EBX-registeret– den første parameteren til funksjonen (for opprettelse er dette en peker til en linje som inneholder navnet på filen som opprettes);
  • til ECX-registeret– andre parameter (filtilgangsrettigheter).

Den tredje parameteren lastes inn i EDX-registeret; i dette tilfellet har vi den ikke. For å utføre et systemanrop i Linux OS, bruk system_call-funksjonen, som er definert i filen /usr/src/liux/arch/i386/kernel/entry.S. Denne funksjonen er inngangspunktet for alle systemanrop. Kjernen reagerer på avbrudd 0x80 ved å kalle system_call-funksjonen, som i hovedsak er en behandler for avbrudd 0x80.

For å være sikker på at vi er på rett spor, la oss skrive et lite testfragment på assemblerspråk. I den vil vi se hva creat()-funksjonen blir til etter kompilering. La oss kalle filen test.S. Her er innholdet:

Globl_start

Tekst

Start:

Vi laster inn systemnummeret i EAX-registeret:

movl $8, %eax

I EBX-registeret – den første parameteren, en peker til en linje med filnavnet:

movl $filnavn, %ebx

I ECX-registeret – den andre parameteren, tilgangsrettigheter:

movl $0, %ecx

Ring et avbrudd:

int $0x80

Vi går ut av programmet. For å gjøre dette, kall opp exit(0)-funksjonen:

movl $1, %eax movl $0, %ebx int $0x80

I datasegmentet angir vi navnet på filen som skal opprettes:

Data

filnavn: .string "fil.txt"

Kompilerer:

gcc -c test.S

ld -s -o test test.o

Den kjørbare filtesten vil vises i gjeldende katalog. Ved å kjøre den vil vi lage en ny fil kalt file.txt.

La oss nå gå tilbake til å se på systemanropsmekanismen. Så, kjernen kaller interrupt handler 0x80 - system_call-funksjonen. System_call plasserer kopier av registrene som inneholder kalleparameterne på stabelen ved å bruke SAVE_ALL makroen og kaller opp den ønskede systemfunksjonen med call-kommandoen. Tabellen med pekere til kjernefunksjoner som implementerer systemanrop er plassert i sys_call_table-matrisen (se filen arch/i386/kernel/entry.S). Systemanropsnummeret, som er i EAX-registeret, er en indeks i denne matrisen. Derfor, hvis verdien 8 finnes i EAX, vil kjernefunksjonen sys_creat() bli kalt. Hvorfor er SAVE_ALL-makroen nødvendig? Forklaringen her er veldig enkel. Siden nesten alle kjernesystemfunksjoner er skrevet i C, ser de etter parameterne sine på stabelen. Og parameterne blir skjøvet inn på stabelen ved å bruke SAVE_ALL-makroen! Returverdien til systemanropet lagres i EAX-registeret.

La oss nå finne ut hvordan du avskjærer et systemanrop. Mekanismen til lastbare kjernemoduler vil hjelpe oss med dette. Selv om vi tidligere har vurdert utvikling og bruk av kjernemoduler, vil vi av hensyn til konsistensen i presentasjonen av materialet kort vurdere hva en kjernemodul er, hva den består av og hvordan den samhandler med systemet.

Lastbar kjernemodul

En lastbar kjernemodul (la oss betegne det LKM - Loadable Kernel Module) er programkode som kjøres i kjernerommet. Hovedfunksjonen til LKM er muligheten til å dynamisk laste og losse uten å måtte starte hele systemet på nytt eller kompilere kjernen på nytt.

Hver LKM består av to hovedfunksjoner (minimum):

  • modulinitieringsfunksjon. Kalles når LKM er lastet inn i minnet:

int init_module(void) ( ... )

  • modulavlastingsfunksjon:

void cleanup_module(void) ( ... )

Her er et eksempel på en enkel modul:

#define MODUL

#inkludere

int init_module(void)

printk("Hei verden");

returner 0;

void cleanup_module(void)

printk("Hei");

Kompiler og last inn modulen. Modulen lastes inn i minnet ved å bruke insmod-kommandoen:

gcc -c -O3 helloworld.c

insmod helloworld.o

Informasjon om alle moduler som for øyeblikket er lastet inn i systemet finnes i /proc/modules-filen. For å bekrefte at modulen er lastet, skriv inn kommandoen cat /proc/modules eller lsmod. Kommandoen rmmod laster ut en modul:

rmmod helloworld

Algoritme for avlytting av systemanrop

For å implementere en modul som avskjærer et systemanrop, er det nødvendig å definere en avlyttingsalgoritme. Algoritmen er som følger:

  • lagre en peker til det opprinnelige (originale) kallet slik at det kan gjenopprettes;
  • lage en funksjon som implementerer et nytt systemkall;
  • i systemanropstabellen sys_call_table, erstatte anrop, dvs. sette opp en passende peker til det nye systemanropet;
  • etter fullført arbeid (når du losser modulen), gjenopprett det opprinnelige systemanropet ved å bruke den tidligere lagrede pekeren.

Sporing lar deg finne ut hvilke systemanrop som brukes når brukerens applikasjon kjører. Ved å spore kan du bestemme hvilket systemanrop som skal avlyttes for å ta kontroll over applikasjonen. Et eksempel på bruk av sporingsprogrammet vil bli diskutert nedenfor.

Nå har vi nok informasjon til å begynne å studere eksempler på implementeringer av moduler som avskjærer systemanrop.

Eksempler på avlytting av systemanrop

Forbud mot opprettelse av kataloger

Når en katalog er opprettet, kalles kjernefunksjonen sys_mkdir. Parameteren er en streng som inneholder navnet på katalogen som skal opprettes. La oss se på koden som fanger opp det tilsvarende systemanropet.

#inkludere

#inkludere

#inkludere

Eksporter tabellen over systemanrop:

ekstern void *sys_call_table;

La oss definere en peker for å lagre det opprinnelige systemanropet:

int (*orig_mkdir)(const char *bane);

La oss lage vårt eget systemanrop. Samtalen vår gjør ingenting, returnerer bare en nullverdi:

int own_mkdir(const char *path)

returner 0;

Under modulinitialisering lagrer vi en peker til det opprinnelige anropet og erstatter systemkallet:

int init_module()

orig_mkdir=sys_call_table;

sys_call_table=own_mkdir; returner 0;

Ved lossing gjenoppretter vi det opprinnelige anropet:

void cleanup_module()

Sys_call_table=orig_mkdir;

Vi vil lagre koden i filen sys_mkdir_call.c. For å få tak i en objektmodul, lag en Makefile med følgende innhold:

CC = gcc

CFLAGS = -O3 -Vegg -fomit-ramme-peker

sys_mkdir_call.o: sys_mkdir_call.c

$(CC) -c $(CFLAGS) $(MODFLAGS) sys_mkdir_call.c

Bruk make-kommandoen for å lage en kjernemodul. Etter å ha lastet den ned, vil vi prøve å lage en katalog med mkdir-kommandoen. Som du ser skjer det ingenting. Kommandoen virker ikke. For å gjenopprette funksjonaliteten er det bare å fjerne modulen.

Hindre å lese en fil

For å lese en fil, må den først åpnes ved hjelp av åpne-funksjonen. Det er lett å gjette at denne funksjonen tilsvarer systemkallet sys_open. Ved å avskjære den kan vi beskytte filen mot å bli lest. La oss se på implementeringen av en interceptormodul.

#inkludere

#inkludere

#inkludere

#inkludere

#inkludere

#inkludere

#inkludere

ekstern void *sys_call_table;

Peker for å lagre det opprinnelige systemanropet:

int (*orig_open)(const char *banenavn, int-flagg, int-modus);

Den første parameteren til åpne-funksjonen er navnet på filen som skal åpnes. Det nye systemkallet må sammenligne denne parameteren med navnet på filen vi ønsker å beskytte. Hvis navnene samsvarer, vil en filåpningsfeil simuleres. Vår nye systemsamtale ser slik ut:

int own_open(const char *pathname, int flagg, int mode)

La oss legge navnet på filen som skal åpnes her:

char *kjernebane;

Navnet på filen vi ønsker å beskytte:

char hide="test.txt"

La oss tildele minne og kopiere navnet på filen som skal åpnes der:

kernel_path=(char *)kmalloc(255,GFP_KERNEL);

copy_from_user(kjernebane, banenavn, 255);

La oss sammenligne:

if(strstr(kernel_path,(char *)&hide) != NULL) (

Vi frigjør minne og returnerer en feilkode hvis navnene stemmer overens:

kfree(kjernebane);

returnere -ENOENT;

ellers(

Hvis navnene ikke stemmer, kaller vi det opprinnelige systemkallet for å utføre standardprosedyren for å åpne en fil:

kfree(kjernebane);

return orig_open(banenavn, flagg, modus);

int init_module()

orig_open=sys_call_table;

sys_call_table=own_open;

returner 0;

void cleanup_module()

sys_call_table=orig_open;

La oss lagre koden i filen sys_open_call.c og lage en Makefile for å hente objektmodulen:

CC = gcc

CFLAGS = -O2 -Vegg -fomit-ramme-peker

MODFLAGS = -D__KERNEL__ -DMODULE -I/usr/src/linux/include

sys_open_call.o: sys_open_call.c

$(CC) -c $(CFLAGS) $(MODFLAGS) sys_open_call.c

I gjeldende katalog, lag en fil kalt test.txt, last inn modulen og skriv inn kommandoen cat test.txt. Systemet vil rapportere at det ikke finnes noen fil med det navnet.

For å være ærlig er slik beskyttelse lett å omgå. Det er nok å bruke mv-kommandoen til å gi nytt navn til filen og deretter lese innholdet.

Skjuler en filoppføring i en katalog

La oss finne ut hvilket systemanrop som er ansvarlig for å lese innholdet i katalogen. For å gjøre dette, la oss skrive et annet testfragment som leser gjeldende katalog:

/* Fil dir.c*/

#inkludere

#inkludere

int main()

DIR *d;

struct dirent *dp;

d = opendir(“.”);

dp = readdir(d);

Returner 0;

La oss få den kjørbare modulen:

gcc -o dir dir.c

og spor det:

strace ./dir

La oss ta hensyn til den nest siste linjen:

getdents(6, /* 4 oppføringer*/, 3933) = 72;

Innholdet i katalogen leses av getdents-funksjonen. Resultatet lagres som en liste over strukturer av typen struct dirent. Den andre parameteren i denne funksjonen er en peker til denne listen. Funksjonen returnerer lengden på alle oppføringer i katalogen. I vårt eksempel bestemte getdents-funksjonen tilstedeværelsen av fire oppføringer i gjeldende katalog – “.”, “..” og våre to filer, den kjørbare modulen og kildeteksten. Lengden på alle oppføringer i katalogen er 72 byte. Informasjon om hver post lagres, som vi allerede har sagt, i strukturstrukturen. Av interesse for oss er to felt av denne strukturen:

  • d_reclen– rekordstørrelse;
  • d_navn- filnavn.

For å skjule en oppføring om en fil (med andre ord, gjøre den usynlig), må du avskjære systemanropet sys_getdents, finne den tilsvarende oppføringen i listen over mottatte strukturer og slette den. La oss se på koden som utfører denne operasjonen (forfatteren av den originale koden er Michal Zalewski):

ekstern void *sys_call_table;

int (*orig_getdents)(u_int, struct dirent *, u_int);

La oss definere systemanropet vårt.

int own_getdents(u_int fd, struct dirent *dirp, u_int count)

usignert int tmp, n;

int t;

Hensikten med variablene vil bli vist nedenfor. I tillegg trenger vi strukturer:

struct dirent *dirp2, *dirp3;

Navnet på filen vi ønsker å skjule:

char hide="vår.fil";

La oss bestemme lengden på oppføringer i katalogen:

tmp=(*orig_getdents)(fd,dirp,count);

if(tmp>0)(

La oss allokere minne for strukturen i kjerneplass og kopiere innholdet i katalogen inn i den:

dirp2=(struktur dirent *)kmalloc(tmp,GFP_KERNEL);

kopi_fra_bruker(dirp2,dirp,tmp);

La oss bruke den andre strukturen og lagre lengden på oppføringene i katalogen:

dirp3=dirp2;

t=tmp;

La oss begynne å lete etter filen vår:

mens(t>0) (

Vi leser lengden på den første oppføringen og bestemmer gjenværende lengde på oppføringer i katalogen:

n=dirp3->d_reclen;

t-=n;

Vi sjekker om filnavnet fra gjeldende oppføring samsvarer med det vi ser etter:

if(strstr((char *)&(dirp3->d_name),(char *)&hide) != NULL) (

Hvis ja, overskriv oppføringen og beregn den nye lengden på oppføringene i katalogen:

memcpy(dirp3,(char *)dirp3+dirp3->d_reclen,t);

tmp-=n;

Vi plasserer pekeren til neste oppføring og fortsetter søket:

dirp3=(struct dirent *)((char *)dirp3+dirp3->d_reclen);

Vi returnerer resultatet og frigjør minnet:

kopi_til_bruker(dirp,dirp2,tmp);

kfree(dirp2);

Returner lengden på oppføringer i katalogen:

returnere tmp;

Funksjonene for initialisering og lossing av en modul har en standardform:

int init_module(void)

orig_getdents=sys_call_table;

sys_call_table=own_getdents;

returner 0;

void cleanup_module()

sys_call_table=orig_getdents;

La oss lagre kildeteksten i filen sys_call_getd.c og lage en Makefile med følgende innhold:

CC = gcc

modul = sys_call_getd.o

CFLAGS = -O3 -Vegg

LINUX = /usr/src/linux

MODFLAGS = -D__KERNEL__ -DMODULE -I$(LINUX)/inkluder

sys_call_getd.o: sys_call_getd.c $(CC) -c

$(CFLAGS) $(MODFLAGS) sys_call_getd.c

I gjeldende katalog, lag en fil our.file og last inn modulen. Filen forsvinner, som var det som måtte bevises.

Som du forstår, er det ikke mulig å vurdere et eksempel på avlytting av hvert systemanrop i én artikkel. Derfor, for de som er interessert i dette problemet, anbefaler jeg å besøke nettstedene:

Der kan du finne mer komplekse og interessante eksempler på systemsamtaleavlytting. Skriv om alle kommentarer og forslag på magasinforumet.

Ved utarbeidelse av artikkelen ble materialer fra nettstedet brukt


Systemanrop

Så langt har alle programmene vi har laget måttet bruke veldefinerte kjernemekanismer for å registrere /proc-filer og enhetsdrivere. Dette er flott hvis du vil gjøre noe allerede levert av kjerneprogrammererne, for eksempel å skrive en enhetsdriver. Men hva om du vil gjøre noe uvanlig, endre oppførselen til systemet på en eller annen måte?

Det er her kjerneprogrammering blir farlig. Da jeg skrev eksemplet nedenfor, ødela jeg det åpne systemkallet. Dette betydde at jeg ikke kunne åpne noen filer, jeg kunne ikke kjøre noen programmer, og jeg kunne ikke slå av systemet med shutdown-kommandoen. Jeg må slå av strømmen for å stoppe det. Heldigvis ble ingen filer ødelagt. For å sikre at du ikke mister filer, vennligst utfør en synkronisering før du utsteder kommandoene insmod og rmmod.

Glem /proc-filer og enhetsfiler. De er bare små detaljer. Den virkelige prosessen med kommunikasjon med kjernen, brukt av alle prosesser, er systemanrop. Når en prosess ber om tjeneste fra kjernen (som å åpne en fil, starte en ny prosess eller be om mer minne), brukes denne mekanismen. Hvis du vil endre kjerneadferd på interessante måter, er dette stedet å være. Forresten, hvis du vil se hvilke systemanrop som brukes av et program, kjør: strace .

Generelt har ikke prosessen tilgang til kjernen. Den kan ikke få tilgang til kjerneminne og kan ikke kalle opp kjernefunksjoner. CPU-maskinvaren dikterer denne tilstanden (det er en grunn til at det kalles "beskyttet modus"). Systemanrop er unntaket fra denne generelle regelen. Prosessen fyller registrene med de riktige verdiene og kaller deretter en spesiell instruksjon som hopper til en forhåndsdefinert plassering i kjernen (selvfølgelig leses den av brukerprosesser, men ikke overskrives av dem.) Under Intel-prosessorer oppnås dette gjennom interrupt 0x80. Maskinvaren vet at når du hopper til den plasseringen, kjører du ikke lenger i brukerbegrenset modus. I stedet kjører du som kjernen til operativsystemet, og derfor har du lov til å gjøre hva du vil.

Plasseringen i kjernen som en prosess kan ringe til kalles system_call. Prosedyren som er der sjekker systemets anropsnummer, som forteller kjernen nøyaktig hva prosessen ønsker. Den slår deretter opp systemanropstabellen (sys_call_table) for å finne adressen til kjernefunksjonen som skal kalles. Den ønskede funksjonen kalles da opp, og etter at den returnerer en verdi, foretas flere systemkontroller. Resultatet returneres deretter tilbake til prosessen (eller til en annen prosess hvis prosessen er avsluttet). Hvis du vil se koden som gjør alt dette, er det i arch/source-filen< architecture >/kernel/entry.S , etter linjen ENTRY(system_call) .

Så hvis vi ønsker å endre hvordan noen systemanrop fungerer, er det første vi må gjøre å skrive vår egen funksjon for å gjøre den riktige tingen (vanligvis ved å legge til litt av vår egen kode, og deretter kalle den opprinnelige funksjonen), så endre pekeren til sys_call_table for å peke på funksjonen vår. Siden vi kan bli slettet senere og ikke ønsker å forlate systemet i en inkonsekvent tilstand, er det viktig for cleanup_module å gjenopprette tabellen til sin opprinnelige tilstand.

Kildekoden gitt her er et eksempel på en slik modul. Vi ønsker å "spionere" på en bestemt bruker, og sende en melding via printk hver gang den brukeren åpner en fil. Vi erstatter filåpne systemkallet med vår egen funksjon kalt our_sys_open. Denne funksjonen sjekker uid (bruker-ID) til gjeldende prosess, og hvis den er lik uid vi spionerer på, kaller printk for å vise navnet på filen som skal åpnes. Deretter kaller den den opprinnelige åpne funksjonen med de samme parameterne, og åpner faktisk filen.

Funksjonen init_module endrer den tilsvarende plasseringen i sys_call_table og lagrer den opprinnelige pekeren i en variabel. Cleanup_module-funksjonen bruker denne variabelen for å gjenopprette alt tilbake til det normale. Denne tilnærmingen er farlig på grunn av muligheten for at to moduler kan endre samme systemanrop. Tenk deg at vi har to moduler, A og B. La oss kalle det åpne systemkallet til modul A A_open og kalle det samme kallet til modul B B_open. Nå som kjernen satt inn syscall er erstattet med A_open, som vil kalle den opprinnelige sys_open når den gjør det den trenger å gjøre. Deretter vil B settes inn i kjernen, og erstatte systemkallet med B_open, som kaller det den tror er det opprinnelige systemkallet, men som faktisk er A_open.

Nå hvis B fjernes først, vil alt være bra: det vil ganske enkelt gjenopprette systemkallet på A_open som kaller originalen. Men hvis A fjernes og deretter B fjernes, vil systemet kollapse. Fjerning av A vil gjenopprette systemkallet til originalen, sys_open, og kutte B ut av løkken. Så, når B fjernes, vil den gjenopprette systemkallet til det den tror er originalen.Anropet vil faktisk bli dirigert til A_open, som ikke lenger er i minnet. Ved første øyekast ser det ut til at vi kan løse dette spesielle problemet ved å sjekke om systemanropet er lik vår åpne funksjon og i så fall ikke endre verdien på det anropet (slik at B ikke endrer systemkallet når det blir slettet ), men det ville forårsake et annet verste problem. Når A fjernes, ser den at systemkallet er endret til B_open slik at det ikke lenger peker til A_open, så det vil ikke gjenopprette pekeren til sys_open før det fjernes fra minnet. Dessverre vil B_open fortsatt prøve å kalle A_open, som ikke lenger er i minnet, så selv uten å fjerne B, vil systemet fortsatt krasje.

Jeg ser to måter å forhindre dette problemet på. Først: gjenopprett tilgang til den opprinnelige verdien av sys_open. Sys_open er dessverre ikke en del av kjernetabellen i /proc/ksyms, så vi kan ikke få tilgang til den. En annen løsning er å bruke en referanseteller for å hindre at modulen losses. Dette er bra for vanlige moduler, men dårlig for "pedagogiske" moduler.

/* syscall.c * * Systemkall "stjele"-eksempel */ /* Copyright (C) 1998-99 av Ori Pomerantz */ /* De nødvendige header-filene */ /* Standard i kjernemoduler */ #include /* Vi gjør kjernearbeid */ #include /* Spesifikt, en modul */ /* Deal with CONFIG_MODVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include #endif #inkluder /* Listen over systemanrop */ /* For gjeldende (prosess) struktur trenger vi * denne for å vite hvem den aktuelle brukeren er. */ #inkludere /* I 2.2.3 inkluderer /usr/include/linux/version.h en * makro for dette, men 2.0.35 gjør det ikke - så jeg legger den til * her om nødvendig. */ #ifndef KERNEL_VERSION #define KERNEL_VERSION(a ,b,c) ((a)*65536+(b)*256+(c)) #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #include #endif /* Systemanropstabellen (en tabell med funksjoner). Vi * bare definerer dette som eksternt, og kjernen vil * fylle det opp for oss når vi er insmod"ed */ extern void *sys_call_table; /* UID vi ønsker å spionere på - vil bli fylt fra * kommandolinjen */ int uid; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) MODULE_PARM(uid, "i"); #endif /* En peker til det opprinnelige systemkallet. Grunnen * vi beholder denne, i stedet for å kalle den opprinnelige funksjonen * (sys_open), er fordi noen andre kan ha * erstattet systemkallet før oss. Merk at dette * ikke er 100 % trygt, for hvis en annen modul * erstattet sys_open før oss, så ringer vi når vi er satt inn * funksjonen i den modulen - og den * kan bli fjernet før vi er. * * En annen grunn til dette er at vi ikke kan få sys_open. * Det er en statisk variabel, så den eksporteres ikke. */ asmlinkage int (*original_call)(const char *, int, int); /* Av en eller annen grunn, i 2.2.3 ga strøm->uid meg * null, ikke den virkelige bruker-IDen. Jeg prøvde å finne hva som gikk * galt, men jeg klarte ikke gjøre det på kort tid, og * jeg er lat - så jeg bruker bare systemkallet for å få * uid, måten en prosess ville gjort. * * Av en eller annen grunn forsvant dette problemet etter at jeg kompilerte kjernen på nytt. */ asmlinkage int (*getuid_call)(); /* Funksjonen vi skal erstatte sys_open (funksjonen * kalles når du kaller det åpne systemkallet) med. For å * finne den eksakte prototypen, med antall og type * argumenter, finner vi den opprinnelige funksjonen først * (den" s på fs/open.c). * * I teorien betyr dette at vi er bundet til den * gjeldende versjonen av kjernen. I praksis endres * systemkallene nesten aldri (det vil ødelegge * og kreve at programmer rekompileres, siden systemkallene * er grensesnittet mellom kjernen og *-prosessene). */ asmlinkage int our_sys_open(const char *filnavn, int-flagg, int-modus) ( int i = 0; char ch; /* Sjekk om dette er brukeren vi spionerer på */ if (uid == getuid_call()) ( /* getuid_call er getuid-systemkallet, * som gir uid-en til brukeren som * kjørte prosessen som kalte systemet * kallet vi fikk */ /* Rapporter filen, hvis relevant */ printk("Åpnet fil av %d: ", uid); gjør ( #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) get_user(ch, filename+i); #else ch = get_user(filename+ i) ); #endif i++; printk("%c", ch); ) while (ch != 0); printk("\n"); ) /* Kall den opprinnelige sys_open - ellers mister vi * muligheten til å åpne filer */ return original_call(filnavn, flagg, modus); ) /* Initialiser modulen - erstatt systemkallet */ int init_module() ( /* Advarsel - for sent for det nå, men kanskje til * neste gang. .. */ printk("Jeg er farlig. Jeg håper du gjorde en "); printk("synkronisering før du endret meg.\n"); printk("Min motpart, cleanup_module(), er jevn"); printk("farligere. Hvis\n"); printk("du verdsetter filsystemet ditt, det vil"); printk("være \"sync; rmmod\" \n"); printk("når du fjerner denne modulen.\n"); /* Hold en peker til den opprinnelige funksjonen i * original_call, og erstatt deretter systemkallet * i system call-tabellen med our_sys_open */ original_call = sys_call_table[__NR_open]; sys_call_table[__NR_open] = vår_sys_open; /* For å få adressen til funksjonen for system * ring foo, gå til sys_call_table[__NR_foo]. */ printk("Spionerer på UID:%d\n", uid); /* Hent systemkallet for getuid */ getuid_call = sys_call_table[__NR_getuid]; returner 0; ) /* Cleanup - avregistrer den aktuelle filen fra /proc */ void cleanup_module() ( /* Returner systemkallet tilbake til normal */ if (sys_call_table[__NR_open] != our_sys_open) ( printk("Noen andre har også spilt med "); printk("åpen systemanrop\n"); printk("Systemet kan bli værende i "); printk("en ustabil tilstand.\n"); ) sys_call_table[__NR_open] = original_call; )

Dette materialet er en modifikasjon av artikkelen med samme navn av Vladimir Meshkov, publisert i magasinet "System Administrator"

Dette materialet er en kopi av artikler av Vladimir Meshkov fra magasinet "System Administrator". Disse artiklene finner du ved å bruke lenkene nedenfor. Noen eksempler på kildekoder til programmer ble også endret - forbedret, ferdigstilt. (Eksempel 4.2 ble kraftig endret, siden vi måtte avlytte et litt annet systemanrop) URLer: http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/uploaded /a3. pdf

Har du spørsmål? Så her går du: [e-postbeskyttet]

  • 2. Lastbar kjernemodul
  • 4. Eksempler på avlytting av systemanrop basert på LKM
    • 4.1 Forbud mot opprettelse av kataloger

1. Generelt syn på Linux-arkitektur

Den mest generelle visningen lar oss se en to-nivå modell av systemet. kjerne<=>progs I midten (til venstre) er systemkjernen. Kjernen samhandler direkte med maskinvaren, og isolerer applikasjonsprogrammer fra arkitektoniske funksjoner. Kjernen har et sett med tjenester levert til applikasjonsprogrammer. Kjernens tjenester inkluderer I/O-operasjoner (åpne, lese, skrive og administrere filer), opprette og administrere prosesser, deres synkronisering og kommunikasjon mellom prosesser. Alle applikasjoner ber om kjernetjenester gjennom systemanrop.

Det andre nivået består av applikasjoner eller oppgaver, både system, som bestemmer funksjonaliteten til systemet, og applikasjoner, som gir Linux-brukergrensesnittet. Til tross for den eksterne heterogeniteten til applikasjonene, er imidlertid interaksjonsskjemaene med kjernen de samme.

Interaksjon med kjernen skjer gjennom et standard systemanropsgrensesnitt. Systemanropsgrensesnittet representerer et sett med kjernetjenester og definerer formatet på tjenesteforespørsler. En prosess ber om en tjeneste gjennom et systemkall til en spesifikk kjerneprosedyre, som ligner på et vanlig bibliotekfunksjonskall. Kjernen, på vegne av prosessen, utfører forespørselen og returnerer nødvendige data til prosessen.

I eksemplet ovenfor åpner programmet en fil, leser data fra den og lukker filen. I dette tilfellet utføres operasjonen med å åpne (åpne), lese (lese) og lukke (lukke) en fil av kjernen på forespørsel fra oppgaven, og åpne(2), les(2) og lukk(2) ) funksjoner er systemanrop.

/* Kilde 1.0 */ #inkludere main () ( int fd; char buf; /* Åpne filen - få en lenke (filbeskrivelse) fd */ fd = open("file1",O_RDONLY); /* Les 80 tegn inn i bufferbufferen */ read( fd, buf , sizeof(buf)); /* Lukk filen */ close(fd); ) /* EOF */ En komplett liste over OS Linux-systemanrop finnes i filen /usr/include/asm/unistd .h. La oss nå se på mekanismen for å utføre systemanrop ved å bruke dette eksemplet. Kompilatoren, etter å ha støtt på open()-funksjonen for å åpne en fil, konverterer den til monteringskode, laster systemanropsnummeret som tilsvarer denne funksjonen og dens parametere inn i prosessorregistrene og kaller deretter avbrudd 0x80. Følgende verdier lastes inn i prosessorregistrene:

  • i EAX-registeret - nummeret til systemanropet. Så, for vårt tilfelle, er systemanropsnummeret 5 (se __NR_open).
  • inn i EBX-registeret - den første parameteren i funksjonen (for open() - dette er en peker til en streng som inneholder navnet på filen som skal åpnes.
  • til ECX-registeret - andre parameter (filtilgangsrettigheter)
Den tredje parameteren lastes inn i EDX-registeret; i dette tilfellet har vi den ikke. For å utføre et systemanrop i OS Linux, bruk system_call-funksjonen, som er definert (avhengig av arkitekturen, i dette tilfellet i386) i filen /usr/src/linux/arch/i386/kernel/entry.S. Denne funksjonen er inngangspunktet for alle systemanrop. Kjernen reagerer på avbrudd 0x80 ved å kalle system_call-funksjonen, som i hovedsak er en behandler for avbrudd 0x80.

For å være sikker på at vi er på rett spor, la oss se på koden for open()-funksjonen i libc-systembiblioteket:

# gdb -q /lib/libc.so.6 (gdb) disas open Dump av assembler-kode for funksjon åpen: 0x000c8080 : ring 0x1082be< __i686.get_pc_thunk.cx >0x000c8085 : legg til $0x6423b,%ecx 0x000c808b : cmpl $0x0,0x1a84(%ecx) 0x000c8092 :jne 0xc80b1 0x000c8094 : trykk %ebx 0x000c8095 : mov 0x10(%esp,1),%edx 0x000c8099 : mov 0xc(%esp,1),%ecx 0x000c809d : mov 0x8(%esp,1),%ebx 0x000c80a1 : mov $0x5,%eax 0x000c80a6 : int $0x80 ... Som det ikke er vanskelig å legge merke til i de siste linjene, overføres parametere til EDX, ECX, EBX-registrene, og systemanropsnummeret plasseres i det siste EAX-registeret, lik, som vi allerede vet , 5.

La oss nå gå tilbake til å se på systemanropsmekanismen. Så, kjernen kaller avbruddsbehandleren 0x80 - system_call-funksjonen. System_call plasserer kopier av registrene som inneholder kalleparameterne på stabelen ved å bruke SAVE_ALL makroen og kaller opp den ønskede systemfunksjonen med call-kommandoen. Tabellen med pekere til kjernefunksjoner som implementerer systemanrop er plassert i sys_call_table-matrisen (se filen arch/i386/kernel/entry.S). Systemanropsnummeret, som ligger i EAX-registeret, er en indeks i denne matrisen. Derfor, hvis EAX inneholder verdien 5, vil kjernefunksjonen sys_open() bli kalt. Hvorfor er SAVE_ALL-makroen nødvendig? Forklaringen her er veldig enkel. Siden nesten alle kjernesystemfunksjoner er skrevet i C, ser de etter parameterne sine på stabelen. Og parameterne blir skjøvet inn på stabelen ved å bruke SAVE_ALL! Returverdien til systemanropet lagres i EAX-registeret.

La oss nå finne ut hvordan du avskjærer et systemanrop. Mekanismen til lastbare kjernemoduler vil hjelpe oss med dette.

2. Lastbar kjernemodul

Loadable Kernel Module (vanlig forkortelse LKM - Loadable Kernel Module) er programkode som kjøres i kjernerommet. Hovedfunksjonen til LKM er muligheten til å dynamisk laste og losse uten å måtte starte hele systemet på nytt eller kompilere kjernen på nytt.

Hver LKM består av to hovedfunksjoner (minimum):

  • modulinitieringsfunksjon. Kalles når LKM er lastet inn i minnet: int init_module(void) ( ... )
  • modulavlastingsfunksjon: void cleanup_module(void) ( ... )
Her er et eksempel på en enkel modul: /* Source 2.0 */ #include int init_module(void) ( printk("Hello World\n"); return 0; ) void cleanup_module(void) ( printk("Bye\n"); ) /* EOF */ Kompiler og last inn modulen. Å laste en modul inn i minnet gjøres med insmod-kommandoen, og visning av lastede moduler med lsmod-kommandoen: # gcc -c -DMODULE -I/usr/src/linux/include/ src-2.0.c # insmod src-2.0.o Advarsel: lasting av src-2.0 .o vil skjemme kjernen: ingen lisens Modul src-2.0 lastet, med advarsler # dmesg | hale -n 1 Hello World # lsmod | grep src src-2.0 336 0 (ubrukt) # rmmod src-2.0 # dmesg | hale -n 1 Bye

3. Systemanropsavlyttingsalgoritme basert på LKM

For å implementere en modul som avskjærer et systemanrop, er det nødvendig å definere en avlyttingsalgoritme. Algoritmen er som følger:
  • lagre en peker til det opprinnelige (originale) kallet slik at det kan gjenopprettes
  • opprette en funksjon som implementerer et nytt systemkall
  • i systemanropstabellen sys_call_table, erstatt anrop, dvs. sett opp en tilsvarende peker til et nytt systemkall
  • etter fullført arbeid (når du losser modulen), gjenopprett det opprinnelige systemanropet ved å bruke den tidligere lagrede pekeren
Sporing lar deg finne ut hvilke systemanrop som brukes under driften av brukerapplikasjonen. Ved å spore kan du bestemme hvilket systemanrop som må avlyttes for å ta kontroll over applikasjonen. # ltrace -S ./src-1.0 ... open("fil1", 0, 01 SYS_open("fil1", 0, 01) = 3<... open resumed>) = 3 lest(3, SYS_read(3, "123\n", 80) = 4<... read resumed>"123\n", 80) = 4 lukke(3 SYS_close(3) = 0<... close resumed>) = 0 ... Nå har vi nok informasjon til å begynne å studere eksempler på implementeringer av moduler som avskjærer systemanrop.

4. Eksempler på avlytting av systemanrop basert på LKM

4.1 Forbud mot opprettelse av kataloger

Når en katalog er opprettet, kalles kjernefunksjonen sys_mkdir. Parameteren er en streng som inneholder navnet på katalogen som skal opprettes. La oss se på koden som fanger opp det tilsvarende systemanropet. /* Kilde 4.1 */ #inkludere #inkludere #inkludere /* Eksporter systemanropstabellen */ ekstern void *sys_call_table; /* Definer en peker for å lagre det opprinnelige kallet */ int (*orig_mkdir)(const char *path); /* La oss lage vårt eget systemanrop. Vårt kall gjør ingenting, returnerer bare en nullverdi */ int own_mkdir(const char *path) ( return 0; ) /* Under modulinitialisering lagrer vi en peker til det opprinnelige kallet og erstatter systemkallet */ int init_module(void ) ( orig_mkdir =sys_call_table; sys_call_table=own_mkdir; printk("sys_mkdir erstattet\n"); return(0); ) /* Ved avlasting, gjenopprett det opprinnelige anropet */ void cleanup_module(void) ( sys_call_table=orig_m(k) sys_mkdir flyttet tilbake\n "); ) /* EOF */ For å få tak i objektmodulen, kjør følgende kommando og utfør en serie eksperimenter på systemet: # gcc -c -DMODULE -I/usr/src/linux/include / src-3.1.c # dmesg | tail -n 1 sys_mkdir erstattet # mkdir test # ls -ald test ls: test: Ingen slik fil eller katalog # rmmod src-3.1 # dmesg | tail -n 1 sys_mkdir flyttet tilbake # mkdir test # ls -ald test drwxr-xr-x 2 root root 4096 2003-12-23 03:46 test Som du kan se, fungerer ikke "mkdir"-kommandoen, eller rettere sagt, ingenting skjer. For å gjenopprette systemfunksjonaliteten er det bare å fjerne modulen. Det er det som ble gjort ovenfor.

4.2 Skjule en filoppføring i en katalog

La oss finne ut hvilket systemanrop som er ansvarlig for å lese innholdet i katalogen. For å gjøre dette, la oss skrive et annet testfragment som leser gjeldende katalog: /* Source 4.2.1 */ #include #inkludere int main() ( DIR *d; struct dirent *dp; d = opendir("."); dp = readdir(d); return 0; ) /* EOF */ Hent den kjørbare filen og spor den: # gcc -o src-3.2.1 src-3.2.1.c # ltrace -S ./src-3.2.1 ... opendir("." SYS_open(".", 100352, 010005141300) = 3 SYS_fstat64(3, 0xbffff79c, 0x4014c2c0, 3, 0xbffff874) = 0 SYS_fcntl64(3, 1, 01, 01, 01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 k(NULL) = 0x080495f4 SYS_brk(0x0806a5f4 ) = 0x0806a5f4 SYS_brk(NULL) = 0x0806a5f4 SYS_brk(0x0806b000) = 0x0806b000<... opendir resumed>) = 0x08049648 readdir(0x08049648 SYS_getdents64(3, 0x08049678, 4096, 0x40014400, 0x4014c2c0) = 528<... readdir resumed>) = 0x08049678 ... Vær oppmerksom på den siste linjen. Innholdet i katalogen leses av getdents64-funksjonen (getdents er mulig i andre kjerner). Resultatet lagres som en liste over strukturer av typen struct dirent, og selve funksjonen returnerer lengden på alle oppføringer i katalogen. Vi er interessert i to felt av denne strukturen:
  • d_reclen - rekordstørrelse
  • d_name - filnavn
For å skjule en filpost om en fil (med andre ord, gjøre den usynlig), må du avskjære systemanropet sys_getdents64, finne den tilsvarende posten i listen over mottatte strukturer og slette den. La oss se på koden som utfører denne operasjonen (forfatteren av den originale koden er Michal Zalewski): /* Kilde 4.2.2 */ #inkludere #inkludere #inkludere #inkludere #inkludere #inkludere #inkludere #inkludere ekstern void *sys_call_table; int (*orig_getdents)(u_int fd, struct dirent *dirp, u_int count); /* Definer systemkallet vårt */ int own_getdents(u_int fd, struct dirent *dirp, u_int count) ( usignert int tmp, n; int t; struct dirent64 ( int d_ino1,d_ino2; int d_off1,d_off2; usignert; usignert kort d_recl char d_type; char d_name; ) *dirp2, *dirp3; /* Navnet på filen vi ønsker å skjule */ char hide = "fil1"; /* Bestem lengden på katalogoppføringene */ tmp = (*orig_getdents)( fd,dirp,count); if (tmp>0) ( /* Tildel minne for strukturen i kjerneplass og kopier innholdet i katalogen inn i den */ dirp2 = (struct dirent64 *)kmalloc(tmp,GFP_KERNEL); copy_from_user (dirp2,dirp,tmp); /* La oss bruke den andre strukturen og lagre lengden på oppføringene i katalogen */ dirp3 = dirp2; t = tmp; /* La oss begynne å søke etter filen vår */ while (t>0 ) ( /* Les lengden på den første oppføringen og bestem den gjenværende lengden på oppføringene i katalogen */ n = dirp3->d_reclen; t -= n; /* Sjekk om filnavnet fra gjeldende oppføring samsvarer med den søkte */ if (strstr((char *)&(dirp3->d_name), (char * )&hide) != NULL) ( /* Hvis dette er tilfelle, så overskriv oppføringen og beregn den nye lengden på oppføringene i katalogen */ memcpy(dirp3, (char *)dirp3+dirp3->d_reclen, t); tmp -= n; ) /* Plasser pekeren til neste post og fortsett å søke */ dirp3 = (struct dirent64 *)((char *)dirp3+dirp3->d_reclen); ) /* Returner resultatet og frigjør minnet */ copy_to_user(dirp,dirp2,tmp); kfree(dirp2); ) /* Returner lengden på oppføringer i katalogen */ return tmp; ) /* Funksjoner for initialisering og utlasting av en modul har en standardform */ int init_module(void) ( orig_getdents = sys_call_table; sys_call_table=own_getdents; return 0; ) void cleanup_module() ( sys_call_dentable=eOForig; ) / Having*dentsOForig kompilerte denne koden, la oss legge merke til hvordan "fil1" forsvinner, noe vi trengte å bevise.

5. Direkte tilgangsmetode til kjerneadresseområdet /dev/kmem

La oss først vurdere teoretisk hvordan avlytting utføres ved å bruke direkte tilgang til kjerneadresserommet, og deretter vil vi gå videre til praktisk implementering.

Direkte tilgang til kjerneadresseområdet er gitt av enhetsfilen /dev/kmem. Denne filen viser all tilgjengelig virtuell adresseplass, inkludert swap-partisjonen. For å jobbe med kmem-filen brukes standard systemfunksjoner - open(), read(), write(). Ved å åpne /dev/kmem på standard måte, kan vi få tilgang til hvilken som helst adresse i systemet, og sette den som en offset i denne filen. Denne metoden ble utviklet av Silvio Cesare.

Systemfunksjoner åpnes ved å laste funksjonsparametere inn i prosessorregistre og deretter kalle programvareavbrudd 0x80. Behandleren for dette avbruddet, system_call-funksjonen, skyver anropsparameterne på stabelen, henter adressen til den kalte systemfunksjonen fra sys_call_table-tabellen og overfører kontrollen til denne adressen.

Ved å ha full tilgang til kjerneadresserommet, kan vi få hele innholdet i systemanropstabellen, dvs. adresser til alle systemfunksjoner. Ved å endre adressen til et systemanrop, vil vi dermed avlytte det. Men for dette må du vite tabelladressen, eller, med andre ord, forskyvningen i /dev/kmem-filen som denne tabellen er plassert på.

For å bestemme adressen til sys_call_table-tabellen, må du først beregne adressen til system_call-funksjonen. Siden denne funksjonen er en avbruddsbehandler, la oss se på hvordan avbrudd håndteres i beskyttet modus.

I reell modus, når du registrerer et avbrudd, får prosessoren tilgang til avbruddsvektortabellen, som alltid er plassert helt i begynnelsen av minnet og inneholder to-ords adresser til avbruddsbehandlingsprogrammer. I beskyttet modus er analogen til avbruddsvektortabellen avbruddsbeskrivelsestabellen (IDT, Interrupt Descriptor Table), som ligger i operativsystemet for beskyttet modus. For at prosessoren skal få tilgang til denne tabellen, må adressen lastes inn i IDTR-registeret (Interrupt Descriptor Table Register). IDT-tabellen inneholder beskrivelser av avbruddsbehandlere, som spesielt inkluderer adressene deres. Disse beskrivelsene kalles porter. Prosessoren, etter å ha registrert et avbrudd, henter gatewayen fra IDT ved å bruke nummeret, bestemmer adressen til behandleren og overfører kontrollen til den.

For å beregne adressen til system_call-funksjonen fra IDT-tabellen, er det nødvendig å trekke ut avbruddsporten int $0x80, og fra den adressen til den tilsvarende behandleren, dvs. adressen til system_call-funksjonen. I system_call-funksjonen får man tilgang til system_call_table-tabellen ved hjelp av call-kommandoen<адрес_таблицы>(,%eax,4). Etter å ha funnet opkoden (signaturen) til denne kommandoen i filen /dev/kmem, vil vi også finne adressen til systemanropstabellen.

For å bestemme op-koden, vil vi bruke feilsøkeren og demontere system_call-funksjonen:

# gdb -q /usr/src/linux/vmlinux (gdb) disas system_call Dump av assembler-kode for funksjon system_call: 0xc0194cbc : trykk %eax 0xc0194cbd : cld 0xc0194cbe : trykk %es 0xc0194cbf : trykk %ds 0xc0194cc0 : trykk %eax 0xc0194cc1 : trykk %ebp 0xc0194cc2 : trykk %edi 0xc0194cc3 : trykk %esi 0xc0194cc4 : trykk %edx 0xc0194cc5 : trykk %ecx 0xc0194cc6 : trykk %ebx 0xc0194cc7 : mov $0x18,%edx 0xc0194ccc : mov %edx,%ds 0xc0194cce : mov %edx,%es 0xc0194cd0 : mov $0xffffe000,%ebx 0xc0194cd5 : og %esp,%ebx 0xc0194cd7 : testb $0x2.0x18(%ebx) 0xc0194cdb :jne 0xc0194d3c 0xc0194cdd : cmp $0x10e,%eax 0xc0194ce2 :jae0xc0194d69 0xc0194ce8 : ring *0xc02cbb0c(,%eax,4) 0xc0194cef : mov %eax,0x18(%esp,1) 0xc0194cf3 : nei Slutt på assemblerdump. Linjen "call *0xc02cbb0c(,%eax,4)" er et kall til sys_call_table-tabellen. Verdien 0xc02cbb0c er tabelladressen (mest sannsynlig vil tallene dine være forskjellige). La oss få opkoden til denne kommandoen: (gdb) x/xw system_call+44 0xc0194ce8 : 0x0c8514ff Vi fant opkoden til kommandoen for å få tilgang til sys_call_table-tabellen. Det er lik \xff\x14\x85. De neste 4 bytene er tabelladressen. Du kan bekrefte dette ved å skrive inn kommandoen: (gdb) x/xw system_call+44+3 0xc0194ceb : 0xc02cbb0c Når vi finner sekvensen \xff\x14\x85 i /dev/kmem-filen og leser de neste 4 bytene, får vi adressen til systemkallingstabellen sys_call_table. Når vi kjenner adressen, kan vi få innholdet i denne tabellen (adresser til alle systemfunksjoner) og endre adressen til et hvilket som helst systemanrop ved å avskjære det.

La oss se på pseudokoden som utfører avlyttingsoperasjonen:

Readaddr(old_syscall, scr + SYS_CALL*4, 4); writeaddr(ny_syscall, scr + SYS_CALL*4, 4); Readaddr-funksjonen leser systemanropsadressen fra systemanropstabellen og lagrer den i old_syscall-variabelen. Hver oppføring i sys_call_table-tabellen tar 4 byte. Den nødvendige adressen er plassert ved offset sct + SYS_CALL*4 i filen /dev/kmem (her er sct adressen til sys_call_table-tabellen, SYS_CALL er serienummeret til systemanropet). Writeaddr-funksjonen overskriver adressen til SYS_CALL-systemanropet med adressen til new_syscall-funksjonen, og alle anrop til SYS_CALL-systemkallet vil bli betjent av denne funksjonen.

Det ser ut til at alt er enkelt og målet er nådd. La oss imidlertid huske at vi jobber i brukerens adresseområde. Hvis vi plasserer en ny systemfunksjon i dette adresserommet, vil vi motta en hyggelig feilmelding når vi kaller denne funksjonen. Derav konklusjonen - det nye systemkallet må plasseres i kjerneadresserommet. For å gjøre dette, må du: få en minneblokk i kjerneplass, plassere et nytt systemkall i denne blokken.

Du kan tildele minne i kjerneplass ved å bruke kmalloc-funksjonen. Men det er umulig å kalle en kjernefunksjon direkte fra brukerens adresserom, så vi vil bruke følgende algoritme:

  • Når vi kjenner adressen til sys_call_table-tabellen, får vi adressen til et systemanrop (for eksempel sys_mkdir)
  • Vi definerer en funksjon som kaller opp kmalloc-funksjonen. Denne funksjonen returnerer en peker til en minneblokk i kjerneadresserommet. La oss kalle denne funksjonen get_kmalloc
  • lagre de første N bytene av sys_mkdir-systemanropet, der N er størrelsen på get_kmalloc-funksjonen
  • overskriv de første N bytene av sys_mkdir-kallet med get_kmalloc-funksjonen
  • vi kaller systemkallet sys_mkdir, og starter dermed get_kmalloc-funksjonen
  • gjenopprett de første N bytene av sys_mkdir-systemanropet
Som et resultat vil vi ha til rådighet en minneblokk som ligger i kjernerommet.

Men for å implementere denne algoritmen trenger vi adressen til kmalloc-funksjonen. Det er flere måter å finne den på. Det enkleste er å lese denne adressen fra System.map-filen eller bestemme den ved å bruke gdb-feilsøkeren (skriv ut &kmalloc). Hvis modulstøtte er aktivert i kjernen, kan kmalloc-adressen bestemmes ved å bruke get_kernel_syms()-funksjonen. Dette alternativet vil bli diskutert videre. Hvis det ikke er støtte for kjernemoduler, må adressen til kmalloc-funksjonen ses etter av opkoden til kommandoen kmalloc-kalling - tilsvarende det som ble gjort for sys_call_table-tabellen.

kmalloc-funksjonen tar to parametere: størrelsen på det forespurte minnet og GFP-spesifikatoren. For å søke etter op-koden, vil vi bruke feilsøkeren og demontere enhver kjernefunksjon som inneholder et kall til kmalloc-funksjonen.

# gdb -q /usr/src/linux/vmlinux (gdb) disas inter_module_register Dumping av assemblerkode for funksjon inter_module_register: 0xc01a57b4 : trykk %ebp 0xc01a57b5 : push %edi 0xc01a57b6 : trykk %esi 0xc01a57b7 : push %ebx 0xc01a57b8 : sub $0x10,%esp 0xc01a57bb : mov 0x24(%esp,1),%ebx 0xc01a57bf : mov 0x28(%esp,1),%esi 0xc01a57c3 : mov 0x2c(%esp,1),%ebp 0xc01a57c7 : movl $0x1f0,0x4(%esp,1) 0xc01a57cf : movl $0x14,(%esp,1) 0xc01a57d6 : ring 0xc01bea2a ... Det spiller ingen rolle hva funksjonen gjør, hovedsaken i den er hva vi trenger - et kall til kmalloc-funksjonen. Vær oppmerksom på de siste linjene. Først lastes parameterne inn i stabelen (esp-registeret peker til toppen av stabelen), etterfulgt av et funksjonskall. GFP-spesifikatoren lastes inn på stabelen først ($0x1f0,0x4(%esp,1). For kjerneversjoner 2.4.9 og høyere er denne verdien 0x1f0. La oss finne opkoden til denne kommandoen: (gdb) x/xw inter_module_register +19 0xc01a57c7 : 0x042444c7 Hvis vi finner denne opkoden, kan vi beregne adressen til kmalloc-funksjonen. Ved første øyekast er adressen til denne funksjonen et argument for anropsinstruksjonen, men dette er ikke helt sant. I motsetning til system_call-funksjonen, inneholder instruksjonen her ikke kmalloc-adressen, men forskyvningen til den i forhold til gjeldende adresse. La oss verifisere dette ved å definere opkoden til kommandokallet 0xc01bea2a: (gdb) x/xw inter_module_register+34 0xc01a57d6 : 0x01924fe8 Den første byten er e8 - dette er opkoden til anropsinstruksjonen. La oss finne verdien av argumentet til denne kommandoen: (gdb) x/xw inter_module_register+35 0xc01a57d7 : 0x0001924f Nå hvis vi legger til gjeldende adresse 0xc01a57d6, forskyv 0x0001924f og 5 byte av kommandoen, vil vi få den ønskede adressen til kmalloc-funksjonen - 0xc01bea2a.

Dette avslutter de teoretiske beregningene, og ved å bruke metoden ovenfor vil vi avskjære sys_mkdir-systemkallet.

6. Eksempel på avlytting med /dev/kmem

/* kilde 6.0 */ #inkludere #inkludere #inkludere #inkludere #inkludere #inkludere #inkludere #inkludere /* Systemanropsnummer for å avskjære */ #define _SYS_MKDIR_ 39 #define KMEM_FILE "/dev/kmem" #define MAX_SYMS 4096 /* Beskrivelse av IDTR-registerformatet */ struct ( usignert kort grense; usignert int base; ) __attribut__ (( pakket) ) idtr; /* Beskrivelse av IDT-tabellen interrupt gate format */ struct ( unsigned short off1; unsigned short sel; unsigned char none, flags; unsigned short off2; ) __attribute__ ((packed)) idt; /* Beskrivelse av strukturen for get_kmalloc-funksjonen */ struct kma_struc ( ulong (*kmalloc) (uint, int); // - adresse til kmalloc-funksjonen int size; // - minnestørrelse for allokering int-flagg; // - flagg, for kjerner > 2.4.9 = 0x1f0 (GFP) ulong mem; __attribute__ ((pakket)) kmalloc; /* En funksjon som bare tildeler en blokk med minne i kjerneadresserommet */ int get_kmalloc(struct kma_struc *k) ( k->mem = k->kmalloc(k->størrelse, k->flagg); return 0 ; ) /* En funksjon som returnerer adressen til funksjonen (nødvendig for kmalloc-søk) */ ulong get_sym(char *n) ( struct kernel_sym tab; int numsyms; int i; numsyms = get_kernel_syms(NULL); if (numsyms > MAX_SYMS || numsyms< 0) return 0; get_kernel_syms(tab); for (i = 0; i < numsyms; i++) { if (!strncmp(n, tab[i].name, strlen(n))) return tab[i].value; } return 0; } /* Наша новая системная функция, ничего не делает;) */ int new_mkdir(const char *path) { return 0; } /* Читает из /dev/kmem с offset size данных в buf */ static inline int rkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset){ printf("lseek err\n"); return 0; } if (read(fd, buf, size) != size) return 0; return size; } /* Аналогично, но только пишет в /dev/kmem */ static inline int wkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset) return 0; if (write(fd, buf, size) != size) return 0; return size; } /* Читает из /dev/kmem данные размером 4 байта */ static inline int rkml(int fd, uint offset, ulong *buf) { return rkm(fd, offset, buf, sizeof(ulong)); } /* Аналогично, но только пишет */ static inline int wkml(int fd, uint offset, ulong buf) { return wkm(fd, offset, &buf, sizeof(ulong)); } /* Функция для получения адреса sys_call_table */ ulong get_sct(int kmem) { ulong sys_call_off; // - адрес обработчика // прерывания int $0x80 (функция system_call) char *p; char sc_asm; asm("sidt %0" : "=m" (idtr)); if (!rkm(kmem, idtr.base+(8*0x80), &idt, sizeof(idt))) return 0; sys_call_off = (idt.off2 << 16) | idt.off1; if (!rkm(kmem, sys_call_off, &sc_asm, 128)) return 0; p = (char *)memmem(sc_asm, 128, "\xff\x14\x85", 3) + 3; printf("call for sys_call_table at %08x\n",p); if (p) return *(ulong *)p; return 0; } /* Функция для определения адреса функции kmalloc */ ulong get_kma(ulong pgoff) { uint i; unsigned char buf, *p, *p1; int kmemz; ulong ret; ret = get_sym("kmalloc"); if (ret) { printf("\nZer gut!\n"); return ret; } kmemz = open("/dev/kmem", O_RDONLY); if (kmemz < 0) return 0; for (i = pgoff+0x100000; i < (pgoff + 0x1000000); i += 0x10000){ if (!rkm(kmemz, i, buf, sizeof(buf))) return 0; p1=(char *)memmem(buf,sizeof(buf),"\x68\xf0\x01\x00",4); if(p1) { p=(char *)memmem(p1+4,sizeof(buf),"\xe8",1)+1; if (p) { close(kmemz); return *(unsigned long *)p+i+(p-buf)+4; } } } close(kmemz); return 0; } int main() { int kmem; // !! - пустые, нужно подставить ulong get_kmalloc_size; // - размер функции get_kmalloc !! ulong get_kmalloc_addr; // - адрес функции get_kmalloc !! ulong new_mkdir_size; // - размер функции-перехватчика!! ulong new_mkdir_addr; // - адрес функции-перехватчика!! ulong sys_mkdir_addr; // - адрес системного вызова sys_mkdir ulong page_offset; // - нижняя граница адресного // пространства ядра ulong sct; // - адрес таблицы sys_call_table ulong kma; // - адрес функции kmalloc unsigned char tmp; kmem = open(KMEM_FILE, O_RDWR, 0); if (kmem < 0) return 0; sct = get_sct(kmem); page_offset = sct & 0xF0000000; kma = get_kma(page_offset); printf("OK\n" "page_offset\t\t:\t0x%08x\n" "sys_call_table\t:\t0x%08x\n" "kmalloc()\t\t:\t0x%08x\n", page_offset,sct,kma); /* Найдем адрес sys_mkdir */ if (!rkml(kmem, sct+(_SYS_MKDIR_*4), &sys_mkdir_addr)) { printf("Cannot get addr of %d syscall\n", _SYS_MKDIR_); perror("er: "); return 1; } /* Сохраним первые N байт вызова sys_mkdir */ if (!rkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Cannot save old %d syscall!\n", _SYS_MKDIR_); return 1; } /* Перепишем первые N байт, функцией get_kmalloc */ if (!wkm(kmem, sys_mkdir_addr,(void *)get_kmalloc_addr, get_kmalloc_size)) { printf("Can"t overwrite our syscall %d!\n",_SYS_MKDIR_); return 1; } kmalloc.kmalloc = (void *) kma; //- адрес функции kmalloc kmalloc.size = new_mkdir_size; //- размер запращевоемой // памяти (размер функции-перехватчика new_mkdir) kmalloc.flags = 0x1f0; //- спецификатор GFP /* Выполним сис. вызов sys_mkdir, тем самым выполним нашу функцию get_kmalloc */ mkdir((char *)&kmalloc,0); /* Востановим оригинальный вызов sys_mkdir */ if (!wkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Can"t restore syscall %d !\n",_SYS_MKDIR_); return 1; } if (kmalloc.mem < page_offset) { printf("Allocated memory is too low (%08x < %08x)\n", kmalloc.mem, page_offset); return 1; } /* Оторбразим результаты */ printf("sys_mkdir_addr\t\t:\t0x%08x\n" "get_kmalloc_size\t:\t0x%08x (%d bytes)\n\n" "our kmem region\t\t:\t0x%08x\n" "size of our kmem\t:\t0x%08x (%d bytes)\n\n", sys_mkdir_addr, get_kmalloc_size, get_kmalloc_size, kmalloc.mem, kmalloc.size, kmalloc.size); /* Разместим в пространстве ядра наш новый сис. вызво */ if(!wkm(kmem, kmalloc.mem, (void *)new_mkdir_addr, new_mkdir_size)) { printf("Unable to locate new system call !\n"); return 1; } /* Перепишем таблицу sys_call_table на наш новый вызов */ if(!wkml(kmem, sct+(_SYS_MKDIR_*4), kmalloc.mem)) { printf("Eh ..."); return 1; } return 1; } /* EOF */ Скомпилируем полученый код и определим адреса и размеры функций get_kmalloc и new_mkdir. Запускать полученое творение рано! Для вычисления адресов и размеров воспользуемся утилитой objdump: # gcc -o src-6.0 src-6.0.c # objdump -x ./src-6.0 >dump La oss åpne dumpfilen og finne dataene vi er interessert i: 080485a4 g F .text 00000032 get_kmalloc 080486b1 g F .text 0000000a new_mkdir La oss nå legge inn disse verdiene i programmet vårt: ulong get_kmalloc_s; ulong get_kmalloc_addr=0x080485a4 ; ulong new_mkdir_size=0x0a; ulong new_mkdir_addr=0x080486b1; La oss nå kompilere programmet på nytt. Ved å starte den, vil vi avskjære sys_mkdir-systemanropet. Alle anrop til sys_mkdir-anropet vil nå betjenes av new_mkdir-funksjonen.

Slutt på papir/EOP

Funksjonaliteten til koden fra alle seksjoner ble testet på kjerne 2.4.22. Ved utarbeidelse av rapporten ble det brukt materialer fra stedet

Oftest er systemanropskoden med nummer __NR_xxx, definert i /usr/include/asm/unistd.h, finnes i Linux-kjernens kildekode i funksjonen sys_xxx(). (Anropstabellen for i386 finner du i /usr/src/linux/arch/i386/kernel/entry.S.) Det er mange unntak fra denne regelen, hovedsakelig på grunn av at de fleste gamle systemanrop erstattes med nye, uten noe system. På proprietære OS-emuleringsplattformer som parisc, sparc, sparc64 og alpha, er det mange ekstra systemkall; mips64 har også et komplett sett med 32-biters systemanrop.

Over tid har det skjedd endringer i grensesnittet til noen systemanrop, om nødvendig. En av grunnene til disse endringene var behovet for å øke størrelsen på strukturer eller skalarverdier som ble sendt til systemanropet. På grunn av disse endringene, på noen arkitekturer (nemlig den eldre 32-bit i386) dukket det opp forskjellige grupper av lignende systemanrop (f.eks. avkorte(2) og avkorte64(2)), som utfører de samme oppgavene, men varierer i størrelsen på argumentene. (Som nevnt påvirker dette ikke applikasjoner: glibc wrapper-funksjonene gjør en del arbeid for å utløse riktig systemkall, og dette sikrer ABI-kompatibilitet for eldre binærfiler.) Eksempler på systemkall som har flere versjoner:

*For tiden er det tre forskjellige versjoner stat(2): sys_stat() (plass __NR_oldstat), sys_newstat() (plass __NR_stat) Og sys_stat64() (plass __NR_stat64), sistnevnte er for tiden i bruk. En lignende situasjon med lstat(2) og fstat(2). * Definert tilsvarende __NR_oldolduname, __NR_olduname Og __NR_uname for samtaler sys_olduname(), sys_uname() Og sys_newuname(). * Linux 2.0 har en ny versjon vm86(2), kalles den nye og gamle versjonen av kjernefysiske prosedyrer sys_vm86old() Og sys_vm86(). * Linux 2.4 har en ny versjon getrlimit(2) de nye og gamle versjonene av kjernefysiske prosedyrer kalles sys_old_getrlimit() (plass __NR_getrlimit) Og sys_getrlimit() (plass __NR_ugetrlimit). * I Linux 2.4 er størrelsen på bruker- og gruppe-ID-feltene økt fra 16 til 32 biter. Flere systemanrop er lagt til for å støtte denne endringen (f.eks. chown32(2), getuid32(2), getgroups32(2), setresuid32(2)), eliminerer tidligere anrop med samme navn, men uten suffikset "32". * Linux 2.4 har lagt til støtte for tilgang til store filer (hvis størrelser og forskyvninger ikke passer inn i 32 bits) i applikasjoner på 32-bits arkitekturer. Dette krevde endringer i systemkallene som fungerer med filstørrelser og forskyvninger. Følgende systemanrop er lagt til: fcntl64(2), getdents64(2), stat64(2), stats64(2), avkorte64(2) og deres analoger, som håndterer filbeskrivelser eller symbolske lenker. Disse systemanropene eliminerer de gamle systemkallene, som, med unntak av "stat"-kall, heter det samme, men ikke har suffikset "64".

På nyere plattformer som kun har 64-biters filtilgang og 32-biters UID/GID (f.eks. alpha, ia64, s390x, x86-64), er det kun én versjon av systemoppkall for UID/GID og filtilgang. På plattformer (vanligvis 32-bits plattformer) der *64 og *32 samtaler er tilgjengelige, er de andre versjonene foreldet.

* Utfordringer rt_sig* lagt til kjerne 2.2 for å støtte ytterligere sanntidssignaler (se kjerne 2.2). signal(7)). Disse systemanropene overstyrer de gamle systemanropene med samme navn, men uten prefikset "rt_". * I systemanrop plukke ut(2) og mmap(2) fem eller flere argumenter brukes, noe som forårsaket problemer med å bestemme hvordan argumenter ble sendt på i386. Som en konsekvens av dette, mens på andre arkitekturer kaller sys_select() Og sys_mmap() kamp __NR_velg Og __NR_mmap, på i386 samsvarer de old_select() Og old_mmap() (prosedyrer som bruker en peker til en blokk med argumenter). For øyeblikket er det ikke lenger et problem med å bestå mer enn fem argumenter, og det er det __NR__nyhetsvalg, som samsvarer nøyaktig sys_select(), og samme situasjon med __NR_mmap2.