3. cvičenie – systémové volania

Systémové volania

Na minulom cvičení ste napísali niekoľko utilít, ktoré používali systémové volania. V tomto cvičení napíšete systémové volanie, ktoré vám pomôže lepšie pochopiť interné mechanizmy jadra xv6 a ako samotné systémové volania fungujú. S pridávaním systémových volaní sa stretnete aj v niektorých ďalších cvičeniach.

Než začnete programovať, preštudujte si kapitolu 2 z xv6 knižky, sekcie 4.3 a 4.4 kapitoly 4 a príslušné zdrojové súbory.

  • Útržky kódu z užívateľského módu, ktoré smerujú systémové volania do jadra sú v súbore user/usys.S, ktorý je generovaný skriptom user/usys.pl, keď spustíte make. Deklarácie systémových volaní nájdete v user/user.h.
  • Kód jadra, ktorý smeruje systémové volania do funkcií jadra nájdete v kernel/syscall.c a kernel/syscall.h.
  • Kód týkajúci sa procesov nájdete v kernel/proc.h a kernel/proc.c.

Synchronizácia s repozitárom

Na prvých troch cvičeniach musíte pracovať na školských počítačoch, aby ste si zvykli na prostredie, ktoré budete používať na všetkých troch testoch. Na prvom cvičení ste vytvorili vzdialený repozitár, ktorý si teraz musíte na virtuálku stiahnuť. Postupujte podľa pokynov na stránke Synchronizácia repozitára v sekcii Stiahnutie repozitára na cvičení.

Úvod do cvičenia

Na začiatok sa prepnite do vetvy syscall:

Riešenia úloh z tohto cvičenia ukladajte do vetvy syscall.

Používanie gdb (ľahká)

Vo väčšine prípadov si vystačite s ladením prostredníctvom výpisov, ale niekedy budete potrebovať krokovať kód po riadkoch alebo vypísať funkcie na zásobníku stack backtrace. Na tieto a iné činnosti súvisiace s ladením kódu budeme používať nástroj gdb.

Predtým, ako sa pustíte do riešení ďalších úloh si vyskúšajte prácu s GDB. V koreňovom priečinku xv6 spustite príkaz make qemu-gdb a v druhom okne/karte spustite gdb-multiarch (alebo riscv64-unknown-elf-gdb, prípadne iný príkaz pre spustenie gdb pre architektúru RISC-V; bližšie informácie si prečítajte na stránke s pomôckami v sekcii Ladiace tipy). Keď máte otvorené dve okná/karty, napíšte do okna s gbd:

Príkazom layout src alebo stlačením skratky Ctrl+x a rozdelíte okno na dve polovice, kde budete vidieť aktuálnu pozíciu v zdrojovom kóde. Príkazom backtrace vypíšete funkcie na zásobníku. Pozrite si prezentáciu Používanie gdb (EN) od našich kolegov z MIT pre ďalšie užitočné príkazy GDB.

Odpovedajte na tieto otázky (odpovede uveďte do súboru answers-syscall.txt):

Po prezretí výstupu backtrace, ktorá funkcia zavolala funkciu syscall?

Stlačte dvakrát klávesu n, aby ste prešli za riadok struct proc *p = myproc();. Zadajte print /x *p, čím vypíšete obsah štruktúry struct proc aktuálneho procesu (viď kernel/proc.h) v hexadecimálnej forme.

Aká je hodnota p->trapframe->a7, a čo reprezentuje? (Pomôcka: pozrite sa do user/init.c na prvý používateľský program, ktorý xv6 spustí; a na jeho skompilovanú verziu v jazyku assembly v user/init.asm.)

Procesor beží v móde supervízora, takže môžeme vypísať hodnoty privilegovaných registrov, ako napríklad sstatus (viď privilegované inštrukcie RISC-V pre bližší popis):

Aký je predchádzajúci mód, v ktorom sa nachádzal procesor? Podľa čoho ste to zistili?

Kód xv6 obsahuje kontroly konzistencie. Ak tieto kontroly zlyhajú, vyvolá to kernel panic. Skúsme takúto chybovú situáciu zámerne vyvolať. Najprv ukončite gdb príkazom quit a na prvej karte nezabudnite ukončiť bežiace qemu. Nahraďte výraz num = p->trapframe->a7; výrazom num = * (int *) 0; na začiatku funkcie syscall, zavolajte make qemu, a uvidíte výstup podobný tomuto:

Ukončite qemu.

Aby ste zistili miesto v kóde, kde panic nastal, hľadajte vypísanú hodnotu registra sepc v súbore kernel/kernel.asm, ktorý obsahuje inštrukcie v jazyku assembly pre skompilované jadro.

Zapíšte inštrukciu jazyka assembly na ktorej jadro vyvolalo panic. V ktorom registri je uložená hodnota num?

Aby ste mohli skontrolovať stav procesora a jadra počas zlyhania vykonania inštrukcie, spustite gdb a nastavte breakpoint na adresu epc zlyhanej inštrukcie (adresa inštrukcie sa vo vašom prípade môže líšiť):

Príkazom layout asm prepnete horné okno do zobrazenia inštrukcií jazyka assembly. Skontrolujte, či sa zlyhávajúca inštrukcia zhoduje s inštrukciou, ktorý ste našli v kernel/kernel.asm.

Prečo jadro spadlo? Pomôcka: pozrite sa na obrázok 3-3 z xv6 knižky. Je v priestore jadra namapovaná adresa 0? Potvrdzuje to obsah registra scause vyššie? (Pozrite si popis registra scause v príručke privilegovaných inštrukcií architektúry RISC-V).

Aj keď pri panicu vypíše jadro kód scause, často sa musíte pozrieť na ďalšie informácie, aby ste zistili presný problém, ktorý spôsobil panic. Napríklad, aby ste zistili, ktorý proces bežal keď nastal panic, môžete si vypísať jeho meno:

Aký je názov procesu, ktorý bežal, keď nastal panic? Aký je pid procesu?

Podľa potreby sa počas riešenia cvičení môžete vrátiť k prezentácii Používanie gdb (EN). Na stránke s pomôckami nájdete tipy k ladeniu systému.

Vykonanie príkazu v sandboxe (stredná)

V tejto úlohe vytvoríte sandbox pre proces, ktorý bude obmedzovať systémové volania, ktoré môže vyvolať. Pomocou sandboxu je možné napríklad procesu zakázať otvárať súbory. Vytvoríte nové systémové volanie interpose, cez ktoré bude možné určiť systémové volania, ktoré by malo jadro odmietnuť z procesu, ktorý ho zavolá. Volanie interpose() má dva argumenty: celočíselnú masku a cestu. Bity masky určujú, ktoré systémové volania majú byť odmietnuté. Druhý argument použijete v ďalšej úlohe. Pre túto úlohu bude jeho hodnota stále -. Napríklad, proces si môže zakázať systémové volanie open zavolaním interpose(1 << SYS_open, "-"), kde SYS_open je číslo systémového volania zo súboru kernel/syscall.h. Obmedzenia rodičovského procesu by mali zdediť detské procesy, takže vo vašej implementácii musíte zabezpečiť, aby pri systémovom volaní fork bola maska zdedená detským procesom.

Medzi používateľskými programami nájdete user/sandbox.c, ktorý zavolá fork(), následne z detského procesu zavolá interpose(), a vykoná (exec) program v detskom procese, ktorý bol zadaný v príkazovom riadku. Po úspešnej implementácii interpose(), by mal jeho výstup vyzerať takto:

V príklade vyššie sandboxujeme príkaz cat README. Číslo 32768 reprezentuje masku systémových volaní, ktoré budú odmietnuté; v tomto konkrétnom príklade sa jedná o 1 << SYS_open. Ak je vaše riešenie korektné, mali by ste vidieť cat: cannot open README.

Za riešenie získate plný počet bodov, ak prejdete testami grade-lab-syscall sandbox_mask:

Pomôcky:

  • Do premennej UPROGS v súbore Makefile pridajte riadok $U/_sandbox.

  • Po zavolaní make qemu zistíte, že kompilátor nemôže skompilovať zdrojový súbor user/sandbox.c, pretože používateľské útržky kódu pre systémové volanie interpose ešte neexistujú: pridajte prototyp systémového volania interpose do user/user.h, útržok do user/usys.pl, a číslo systémového volania do kernel/syscall.h. Súbor Makefile spustí perl skript user/usys.pl, ktorý vytvorí user/usys.S, kde sú uložené skutočné útržky systémových volaní, ktoré obsahujú inštrukciu ecall architektúry RISC-V, ktorá slúži na prechod do jadra. Potom, ako opravíte kompilačné problémy, spustite sandbox 32768 - cat README v shelli xv6. Uvidíte, že zlyhá, lebo samotné systémové volanie v jadre ešte nie je implementované.

  • Do súboru kernel/sysproc.c pridajte funkciu sys_interpose(), ktorá implementuje samotné systémové volanie. Jej úloha je uložiť masku v argumente do novej položky štruktúry proc (viď kernel/proc.h). Funkcie na prevzatie argumentov systémového volania nájdete v kernel/syscall.c a príklady ich použitia nájdete v kernel/sysproc.c. Pridajte smerník na novú funkciu sys_interpose do poľa syscalls v súbore kernel/syscall.c.

  • Upravte funkciu kfork() (viď kernel/proc.c), aby skopírovala masku z rodičovského do detského procesu.

  • Upravte funkciu syscall() v súbore kernel/syscall.c, aby kontrolovala, či je systémové volanie zakázzané.

Po úspešnom dokončení úlohy nezabudnite vytvoriť commit!

Sandbox s povolenými cestami(ľahká)

V tejto úlohe rozšírite sandox, aby podporoval maskované systémové volania open a exec podľa ciest v ich argumentoch. Druhý argument systémového volania sys_interpose je cesta, ktorá je povolená. Ak je open alebo exec maskovaný, ale požadovaná cesta zo systémového volania sa zhoduje s povolenou cestou, potom by malo byť toto systémové volanie povolené.

Po správnom vyriešení úlohy by ste mali vidieť takýto výstup:

Sandbox v príklade vyššie poloľuje iba tie volania open, ktoré pristupujú k súboru README, žiadne iné.

Vaše riešenie je korektné, ak prejdete sandbox testami po zavolaní make grade:

Pomôcky:

  • Upravte systémové volaniesys_interpose(), aby si zapamätalo povolenú cestu. Funkcia argstr() cám bude nápomocná na získanie cesty. V štruktúre proc môžete deklarovať bufer veľkosti MAXPATH.

  • Ak je systémové volanie open alebo exec maskované, skontrolujte, či sa požadovaná cesta nerovná povolenej ceste. Ak je to tak, povoľte vykonanie tohto systémového volania.

Po úspešnom dokončení úlohy nezabudnite vytvoriť commit!

Útok na xv6 (stredná)

Jadro xv6 izoluje používateľské programy navzájom a zároveň izoluje jadro od používateľských programov. Ako ste videli v úlohách vyššie, aplikácia nemôže priamo zavolať funkciu v jadre alebo v inom programe. Namiesto toho môžu interakcie prebiehať len pomocou systémových volaní. Ak je ale v implementácii systémového volania v jadre chyba, útočník môže zneužiť túto chybu na prelomenie izolačných bariér. Do xv6 sme naschvál vniesli chybu, aby ste si vyskúšali, ako môžu byť takéto chyby zneužité. Vašou úlohou je zneužiť túto chybu, aby ste ukradli citlivé dáta z iného procesu.

Chyba spočíva v tom, že pri kompilácii tohto labu je vynechané volanie memset(mem, 0, sz) vo funkcii uvmalloc()kernel/vm.c, ktorého úlohou je vynulovať novoalokovanú stránku. Podobne sú vynechané dva riadky zo súboru kernel/kalloc.c, ktorých úlohou je zneplatniť obsah uvoľňovaných stránok pomocou memset(). Kvôli vynechaniu týchto troch riadkov (všetky sú označené podmieneným príkazom ifndef LAB_SYSCALL) si novoalokované stránky zachovajú pôvodný obsah z predchádzajúceho použitia. Teda aplikácia, ktorá pre alokáciu pamäte zavolá sbrk(), môže dostať stránky obsahujúce dáta z predchádzajúcich použití. Xv6 zväčša funguje korektne aj napriek tomu, že tie tri riadky boli zmazané; dokonca prejdú aj všetky testy usertests.

Program user/secret.c zapíše tajný reťazec do svojej pamäte a potom sa ukončí (týmto je jeho pamäť uvoľnená). Vašou úlohou je napísať niekoľko riadkov kódu do súboru user/attack.c, ktoré nájdu tajný reťazec, ktorý predtým program secret.c zapísal do pamäte a vypíšu tento reťazec na samostatný riadok.

Váš program attack.c musí fungovať s nemodifikovaným systémom xv6 a nemodifikovaným súborom secret.c. V rámci experimentovania a ladenia môžete v kódoch jadra upraviť hocičo, ale pred otestovaním riešenia musíte tieto zmeny vrátiť späť.

Program secret má na vstupe jeden argument. Svoj program attack môžete otestovať spustením programu secret s ľubovoľným argumentom a potom spustením programu attack. Útok prebehol úspešne, ak vo výstupe vidíte ten istý reťazec, ktorý ste dali ako argument programu secret. Takto vyzerá úspešne vykonaný útok:

Je možné, že pre úspešnosť útoku je potrebné program attack spustiť dvakrát. Závisí to od toho, ako ste útok implementovali. Testovací skript spustí attack dvakrát. Úlohu ste vyriešili, ak bol tajný reťazec aspoň raz vypísaný.

Aby ste zistili, či váš útok prechádza testami, z konzoly Linuxu spustite testovacie skripty ./grade-lab-syscall attack alebo make grade. Tajný reťazec generovaný testami obsahuje iba čísla a malé písmená.

Ako ste mohli vidieť v tejto úlohe, aj chyby, ktoré priamo neovplyvňujú korektnosť behu, môžu byť niekedy zneužité na prelomenie bezpečnosti. Opatrným programovaním a rozsiahlym testovaním je možné znížiť počet chýb, ale ich absenciu nie je možné garantovať. Systém xv6 už nejaké chyby v minulosti mal, a nejaké neobjavené chyby sa v ňom pravdepodobne stále nachádzajú. Skutočné jadrá, ktoré majú oveľa viac kódu ako xv6, majú dlhú históriu podobných chýb. Pozrite si napríklad verejný zoznam zraniteľností Linuxu a ako nahlásiť zraniteľnosť.

Po úspešnom dokončení úlohy nezabudnite vytvoriť commit!

Týmto je cvičenie hotové. Skontrolujte, či vaše riešenie prejde všetkými testami make grade. Aby ste prešli posledným testom time, musíte vytvoriť nový súbor time.txt, v ktorom uvediete počet hodín, ktorý ste strávili nad zadaním ako celé číslo. Na záver nezabudnite svoje zmeny commitnúť do repozitára.

Voliteľná úloha

Prvý riešiteľ úlohy môže dostať bonusové body po kontrole riešenia. Informujte sa u prednášajúceho alebo cvičiaceho. Voliteľnú úlohu môžete riešiť až po vyriešení všetkých úloh základnej časti cvičenia! Riešenia prijímame iba do konca štvrtého týždňa semestra.

  • Nájdite chybu v xv6, ktorá umožní útočníkovi prelomiť izoláciu procesov alebo spôsobiť pád jadra. (Útoky postrannými kanálmi ako Meltdown neuvažujte.) (ťažká)