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
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
- 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)
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
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) ( ... )
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
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 */ #inkludere4.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- d_reclen - rekordstørrelse
- d_name - filnavn
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
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
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
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 */ #inkludereSlutt 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 stedetOftest 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.