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 dvojicu systémových volaní, ktoré vám pomôžu 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.

Ak spustíte make grade, zistíte, že hodnotiaci skript nemôže spustiť (execnúť) trace. Vašou úlohou je pridať potrebné systémové volania a podporný kód, aby trace fungoval. Naviac ste si mohli všimnúť, že test attacktest neprešiel.

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/initcode.S na prvý program, ktorý xv6 spustí.)

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.

Trasovanie systémových volaní (stredná)

V tomto zadaní pridáte do jadra funkcionalitu trasovania systémových volaní, ktorá vám môže byť nápomocná v neskorších cvičeniach. Vytvoríte systémové volanie trace(), ktorým bude možné riadiť trasovanie. Bude mať jeden argument, celočíselnú „masku“, ktorej bity určia, ktoré systémové volania budú trasované. Napríklad, na trasovanie systémového volania fork() program zavolá trace(1 << SYS_fork), kde SYS_fork je číslo systémového volania definované v kernel/syscall.h. Vaším cieľom je modifikovať jadro xv6 tak, aby tesne pred ukončením systémového volania jadro skontrolovalo, či je v maske nastavené číslo prebiehajúceho systémového volania. V takom prípade jadro vypíše riadok s id procesu, názvom systémového volania a jeho návratovou hodnotou. Systémové volanie trace() by malo povoliť trasovanie pre proces, ktorý ho zavolal a pre jeho detské procesy, ktoré neskôr vytvorí zavolaním fork(), ale nie pre ostatné procesy.

V repozitári nájdete užívateľský program trace, ktorý spustí iný program so zapnutým trasovaním (viď user/trace.c). Ak ste volanie správne naimplementovali, mali by ste vidieť niečo takéto:

V prvom príklade trace spustí trasovanie programu grep, konkrétne systémového volania read. Číslo 1<<SYS_read je rovné 32. V druhom príklade sa jedná o ten istý proces, ale trasujú sa všetky systémové volania. Číslo 2147483647 má nastavených všetkých 31 dolných bitov. V treťom príklade program nie je trasovaný, takže nie je ani vypísaný žiadny výstup. V štvrtom príklade sú trasovaní všetci potomkovia testu forkforkfork zo sady testov usertests. Vaše riešenie je korektné, ak program má rovnaké správanie ako je uvedené vyššie (ID procesov môžu byť rozdielne).

Pomôcky:

  • Pridajte $U/_trace do premennej UPROGS v súbore Makefile.

  • Spustite make qemu a uvidíte, že kompilátor nemôže skompilovať súbor user/trace.c, pretože užívateľské útržky kódu pre systémové volanie trace ešte neexistujú: pridajte prototyp systémového volania trace 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 trace 32 grep hello README. Uvidíte, že zlyhá, lebo samotné systémové volanie v jadre ešte nie je implementované.

  • Pridajte funkciu sys_trace() do kernel/sysproc.c. Jej úloha je uložiť argument systémového volania do novej premennej v štruktúre 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_trace() do poľa syscalls v súbore kernel/syscall.c.

  • Upravte fork() (viď kernel/proc.c), aby skopíroval trasovaciu masku z rodiča do detského procesu.

  • Upravte funkciu syscall() v kernel/syscall.c, aby vypísala trasovací výstup. Musíte pridať pole názvov systémových volaní, ktoré sa budú vypisovať.

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 ale je v implementácii systémového volania 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 xv6 odhalila 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) na riadku 272 v súbore kernel/vm.c, ktorého úlohou je vynulovať novo alokovanú 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 novo alokované stránky zachovajú pôvodný obsah z predchádzajúceho použitia.

Program user/secret.c zapíše tajné dáta s veľkosťou 8 bajtov do 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é dáta, ktoré predtým zapísal program secret.c do pamäte. Tieto dáta potom program vypíše do súborového deskriptora 2. Úloha je vyriešená, ak test attacktest vypíše OK: secret is ebb.ebb (tajné dáta sú náhodné a odlišné pri každom behu).

Upravovať môžete iba user/attack.c. Zdrojové kódy jadra, secret.c, attacktest.c a iné nesmiete upravovať.

Pomôcky:

  • Spustite attacktest v shelli xv6. Mali by ste vidieť takýto výstup:

    Aj napriek tomu, že v xv6 chýbajú 3 riadky, systém funguje korektne: spustil shell a program attacktest. Dokonca ak spustíte usertests, väčšina z nich prejde!

  • Prečítajte si zdrojový kód user/attacktest.c. Vygeneruje náhodný reťazec s dĺžkou 8 bajtov a pošle ho do programu secret, ktorý ho zapíše do svojej pamäte. Keď sa secret ukončí, attacktest spustí attack a počká, kým attack nezapíše tajné dáta do súborového deskriptora 2.
  • Po prezretí user/secret.c porozmýšľajte, ako by ste xv6 donútili prezradiť tajné dáta pomocou vášho programu attack.c.
  • Otestujte váš exploit spustením programu attacktest v shelli xv6.
user/secret.c skopíruje tajné bajty do oblasti pamäte, ktorej adresa je 32 bajtov od začiatku stránky. Zmeňte 32 na 0 a útok by už nemal fungovať. Prečo to tak je?

Malé chyby, ktoré priamo neovplyvňujú korektnosť, ale napriek tomu môžu byť zneužité na narušenie bezpečnosti (ako tá v úlohe), robia programovanie jadra náročným. xv6 takéto chyby pravdepodobne má, aj keď sa im jeho autori snažia vyhnúť. Skutočné jadrá, ktoré majú oveľa viac riadkov 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é úlohy

Prvý riešiteľ každej úlohy môže dostať bonusové body po kontrole riešenia. Informujte sa u prednášajúceho alebo cvičiaceho. Voliteľné úlohy 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.

  • Vypíšte argumenty trasovaných systémových volaní. Počet vypísaných argumentov sa musí zhodovať s počtom argumentov systémového volania. (ľahká)
  • 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á)