Laborator 5: Exerciții

Pregătirea laboratorului

Pentru rezolvarea laboratorului, vom lucra în același director din care pornim mașina virtuală (~/so2/linux/tools/labs).

Pașii de rezolvare sunt următorii:

  • pregătirea scheletului de laborator
  • compilarea modulelor de Kernel
  • copierea modulelor pe mașina virtuală
  • pornirea mașinii virtuale și testarea modulelor

Pregătirea scheletului de laborator

Scheletul de laborator este generat din sursele din directorul tools/labs/templates. Putem genera scheletele pentru toate laboratoarele folosind următoarea comanda:

tools/labs $ make skels

Pentru a genera scheletul pentru un singur laborator, vom folosi variabila de mediu LABS:

tools/labs $ make clean
tools/labs $ LABS=<lab name> make skels

Numele laboratorului curent este interrupts.

Similar, putem genera și scheletul pentru un singur exercițiu, atribuind valoarea <lab_name>/<task_name> variabilei LABS.

Scheletul este generat în directorul tools/labs/skels.

Compilarea modulelor

Comanda make build compilează toate modulele din directorul skels.

student@eg106:~/so2/linux/tools/labs$ make build
echo "# autogenerated, do not edit " > skels/Kbuild
echo "ccflags-y += -Wno-unused-function -Wno-unused-label -Wno-unused-variable " >> skels/Kbuild
for i in ./interrupts; do echo "obj-m += $i/" >> skels/Kbuild; done
...

Copierea modulelor pe mașina virtuală

Putem copia modulele generate pe mașina virtuală folosind target-ul copy al comenzii make, atunci când mașina virtuală este oprită.

student@eg106:~/so2/linux/tools/labs$ make copy
student@eg106:~/so2/linux/tools/labs$ make boot

Alternativ, putem copia fișierele prin scp, pentru e evita repornirea mașinii virtuale. Pentru detalii despre folosirea interacțiunea prin rețea cu mașina virtuală citiți Interacțiunea cu mașina virtuală.

Testarea modulelor

Modulele generate sunt copiate pe mașina virtuală în directorul /home/root/skels/<lab_name>.

root@qemux86:~/skels/interrupts# ls
kbd.ko

După pornirea mașinii virtuale QEMU vom putea folosi comenzi în fereastra QEMU (sau în minicom) pentru a încărca și descărca modulul de kernel:

root@qemux86:~# insmod skels/<lab_name>/<task_name>/<module_name>.ko 
root@qemux86:~# rmmod skels/<lab_name>/<task_name>/<module_name>.ko 

Pentru dezvoltarea laboratorului, este recomandat să folosim patru terminale sau, mai bine, patru tab-uri de terminal. Pentru a deschide un nou tab de terminal folosim combinația de taste Ctrl+Shift+t. Cele trei tab-uri de terminal îndeplinesc următoarele roluri:

  1. În primul tab de terminal dezvoltăm modulul de kernel: editare, compilare, copiere în directorul dedicat pentru mașina virtuală QEMU. Lucrăm în directorul aferent rezultat în urma decomprimării arhivei de sarcini a laboratorului.
  2. În al doilea tab de terminal pornim mașina virtuală QEMU și apoi testăm modulul de kernel: încărcare/descărcare modul, rulare teste. Lucrăm în directorul aferent mașinii virtuale: ~/so2/qemu-so2/.
  3. În al treilea tab de terminal accesăm sursele nucleului Linux din directorul ~/so2/linux-4.9/ și folosim Vim și cscope pentru parcurgerea codului sursă.
  4. În al treilea tab de terminal pornim minicom sau un server UDP care să primească mesajele de netconsole. Nu contează în ce director ne aflăm. Folosim comanda
    student@eg106:~$ netcat -lup 6666

Citiți cu atenție toate precizările unui exercițiu înainte de a începe rezolvarea acestuia.

[0.5p] Intro

Găsiți definițiile următoarelor simboluri în nucleul Linux:

  • structura resource;
  • funcția request_region și de acolo funcția __request_region;
  • funcția request_irq și de acolo funcția request_threaded_irq;
  • funcția inb, varianta de pe arhitectura x86.

Urmăriți în codul nucelului Linux:

  • funcția de inițializare a driver-ului de tastatură i8042_setup_kbd
  • funcția de tratarea a întreruperii pentru tastatură AT sau PS/2, înregistrată cu funcția de inițializare: atkbd_interrupt

[10.5p] Keylogger

Obiectivul exercițiilor următoare este să creați un driver pentru a intercepta tastele apăsate (keylogger). Driver-ul va intercepta IRQ-ul destinat controller-ului de tastatură și va inspecta codurile tastelor primite, stocându-le într-un buffer.

1. [2.5p] Alocare de porturi I/O

Pentru început ne propunem să alocăm spații din zona I/O pentru dispozitive hardware. Vom vedea că nu putem să alocăm spațiu pentru tastatură întrucât regiunea aferentă e deja alocată. Apoi vom aloca spațiu I/O pentru porturi care nu sunt folosite.

În fișierul kbd.c avem un schelet de implementare a unui driver de tastatură. Parcurgeți codul sursă din acest fișier și urmăriți funcția kbd_init. Observați că porturile de care avem nevoie sunt I8042_STATUS_REG și I8042_DATA_REG.

Urmăriți comentariile marcate cu TODO 1 pentru a înregistra porturile de I/O și asigurați-vă că ați tratat corect erorile. De asemenea, deînregistrați porturile în funcția kbd_exit.

Parcurgeți secțiunea Alocarea porturilor I/O din laborator.

Compilați fișierul sursă kdb.c în modulul kdb.ko folosind, pe sistemul fizic, comanda

make build

și, după ce l-ați copiat pe mașina virtuală, încărcați-l în nucleu, în mașina virtuală QEMU, folosind comanda

insmod kbd.ko

Observați că obțineți eroare la încărcare: insmod: Can't insert 'kbd.ko': Device or resource busy. Acest lucru se întâmplă întrucât avem deja un driver care a întregistrat spațiul de I/O și kernel-ul poate deja accesa porturile I/O aferente. Ca să validăm că, într-adevăr, adresele aferente celor două registre (STATUS_REG și DATA_REG) sunt înregistrate, rulăm, în mașina virtuală QEMU comanda

cat /proc/ioports | grep keyboard

Observăm că cele două porturi (0x60: DATA_REG și 0x64:STATUS_REG) corespunzătoare registrelor pentru tastatură sunt înregistrate de kernel la boot, deci nu vom putea să eliminăm modulul care le deține.

Pentru a putea testa cu succes alocarea de porturi I/O, vom păcăli kernelul prin înregistrarea porturilor 0x61 și 0x65.

Folosiți funcția request_region (în cadrul funcției kbd_init) pentru a aloca porturile aferente și folosiți funcția release_region (în cadrul funcției kbd_exit). Apoi compilați și inserați modulul în nucleu. Verificați conținutul fișierului /proc/ioports și verificați acum că cele două porturi I/O sunt alocate driverului nostru.

root@qemux86:~# insmod skels/interrupts/kbd.ko
kbd: loading out-of-tree module taints kernel.
Driver kbd loaded
root@qemux86:~# cat /proc/ioports | grep kbd
0061-0061 : kbd
0065-0065 : kbd

Descărcați modulul din kernel folosind comanda

rmmod kbd

și verificați acum conținutul fișierului /proc/ioports; observți că a dispărut alocarea spațiului I/O pentru cele două porturi.

Comentați liniile care înregistrează și eliberează cele două porturi înainte de a trece la exercițiul următor.

2. [2p] Rutina de tratare a întreruperii

Urmărim să înregistrăm o rutină de tratare a întreruperii de tastatură. Veți scrie o funcție (rutină) de tratare a înteruperii și o veți înregistra folosind request_irq.

Implementarea o veți face în fișierul kbd.c. Zonele din program în care trebuie să acționați au comentarii marcate cu /* TODO 2: ... */ și indicații despre ce trebuie să urmăriți.

Pentru început definiți rutina de tratare a întreruperii (numită kbd_interrupt_handle) ca nefăcând nimic. Rutina trebuie să lase întreruperea să fie procesată de handler-ul inițial; în caz contrar, veți pierde controlul tastaturii în mașina virtuală.

Rutina trebuie să întoarcă IRQ_NONE.

Pentru informații legate de definirea rutinei, revedeți secțiunea Implementarea rutinei de tratare a întreruperii.

Apoi înregistrați și deînregistrați rutina de tratare a întreruperii. Linia de întrerupere (IRQ line) este definită de macro-ul I8042_KBD_IRQ. Rutina de tratare a întreruperii trebuie înregistrată cu IRQF_SHARED, pentru a partaja linia de întrerupere cu driverul de tastatură (i8042).

Pentru întreruperi partajate, parametrul dev_id nu poate fi NULL. Folosiți &devs[0], adică pointer la structura de dispozitiv de tipul struct kbd. Această structură va conține toate informațiile necesare pentru gestiunea dispozitivului.

Pentru a putea urmări ulterior înregistrarea validă a întreruperii în /proc/interrupts, nu folosiți NULL nici pentru parametrul dev_name. Puteți folosi macro-ul MODULE_NAME.

Dacă înregistrarea întreruperii eșuează să aveți în vedere să deînregistrați regiunea de char device, folosiți label-ul out_unregister (label de goto)/

Pentru înregistrarea rutinei, parcurgeți secțiunea Obținerea unei întreruperi.

Compilați modulul și încărcați-l în nucleu. Ulterior încărcării în nucleu, puteți observa înregistrarea liniei de întrerupere prin consultarea fișierului /proc/interrupts. Urmăriți linia din output aferentă IRQ line-ului pentru tastatură (definit în macro-ul I8042_KBD_IRQ). Observați că sunt două drivere înregistrate la această linie de întreruper (deci avem o linie de întrerupere partajată): driverul inițial i8042 și driverul nostru kbd. Descărcați modulul din nucleu.

Mai multe detalii despre formatul fișierului /proc/interrupts găsiți în secțiunea Statistici despre întreruperi

Afișați un mesaj în rutina de tratare a întreruperii pentru a verifica dacă este apelată. Compilați și reîncărcați modulul în nucleu. Verificați folosind dmesg că rutina de tratatre a întreruperii este apelată în momentul în care apăsați pe tastatură în mașina virtuală.

3. [2.5p] Colectarea tastelor apăsate în buffer

Pentru implementarea keylogger-ului vrem să colectăm tastele apăsate într-un buffer al cărui conținut să îl transmitem apoi în user space. Pentru aceasta în rutina de tratare a întreruperii veți face următoarele:

  1. Veți captura tastele apăsate (doar pressed nu și released)/
  2. Veți identifica tastele ce reprezintă caractere ASCII.
  3. Veți copia caracterele ASCII corespunzătoare tastelor apăsate și le veți stoca în buffer-ul buf din structura de tip struct kbd aferentă dispozitivului.

Implementarea o veți face în fișierul kbd.c. Zonele din program în care trebuie să acționați au comentarii marcate cu /* TODO 3: ... */ și indicații despre ce trebuie să urmăriți.

La fiecare pas compilați modulul, încărcați modulul în kernel, apăsați în mașina virtuală taste pentru a verifica funcționalitatea, și apoi descărcați modulul.

a. [0.5p] Citirea registrului de date

Pentru început completați funcția i8042_read_data() pentru citirea registrului I8042_DATA_REG al controller-ului de tastatură. Funcția trebuie doar să returneze valoarea registrului. Valoarea registrului se mai numește scancode, este ceea ce este generat la fiecare apăsare de tastatură.

Citiți valoarea registrului I8042_DATA_REG folosind inb, rețineți valoarea citită în variabila locală val și apoi returnați valoarea variabilei val.

Parcurgeți secțiunea Operații de scriere și citire a porturilor I/O din laborator.

Apelați funcția i8042_read_data() în handler-ul de întrerupere kbd_interrupt_handler() (adică la fiecare tastă apăsată) și afișați valoarea citită din registru.

Pentru afișare în cadrul handler-ului de întrerupere folosiți o construcție de forma:

	pr_info("IRQ %d: scancode=0x%x (%u) pressed=%d ch=%c\n",
		irq_no, scancode, scancode, pressed, ch);

unde scancode este valoarea registrului citit cu ajutorul funcției i8042_read_data().

Observați că scancode-ul (valoarea registrului citit) nu este un caracter ASCII aferent tastei apăsate. Va trebui să interepretăm scancode-ul.

b. [1p] Interpretarea scancode-ului

Observați că valoarea registrului este un cod al tastei apăsate (se mai numește scancode), nu reprezintă valoarea ASCII a caracterului apăsat. De asemenea, este trimisă o întrerupere la apăsarea tastei și este trimisă o întrerupere la eliberarea tastei. Pentru aceasta, va trebui să selectați doar cazurile de apăsare de tastă și să decodificați tastele apăsate în caractere ASCII.

Pentru a urmări scancode-urile tastelor apăsate putem folosi comanda showkey în forma:

showkey -s

Poate fi rulată pe mașina virtuală, sau pe sistemul fizic prefixată de sudo.

În această formă comanda va afișa scancode-urile tastelor timp de 10 secunde de la ultima tastă apăsată după care se va închide. Dacă apăsați și veți elibera o tastă veți obține două scancode-uri: unul pentru tastă apăsată și unul pentru tastă eliberată. De exemplu:

  • dacă apăsați tasta ENTER veți obține scancode-urile 0x1c (pentru tastă apăsată) si 0x9c (pentru tastă eliberată)
  • dacă apăsați tasta a veți obține scancode-urile 0x1e (pentru tastă apăsată) si 0x9e (pentru tastă eliberată)
  • dacă apăsați tasta b veți obține scancode-urile 0x30 (pentru tastă apăsată) si 0xb0 (pentru tastă eliberată)
  • dacă apăsați tasta c veți obține scancode-urile 0x2e (pentru tastă apăsată) si 0xae (pentru tastă eliberată)
  • dacă apăsați tasta Shift veți obține scancode-urile 0x2a (pentru tastă apăsată) si 0xaa (pentru tastă eliberată)
  • dacă apăsați tasta Ctrl veți obține scancode-urile 0x1d (pentru tastă apăsată) si 0x9d (pentru tastă eliberată)

Așa cum este indicat și în acest articol un scancode pentru eliberare de tastă este mai mare cu 128 (0x80) decât un scancode de apăsare de tastă. Acesta este modul în care vom putea face diferența între un scancode de tastă apăsată și un scancode de tastă eliberată.

Un scancode este tradus într-un keycode care corespunde unei taste. Un scancode de tastă apăsată și un scancode de tastă eliberată are același keycode. Pentru tastele indicate mai sus avem următorul tabel:

Key Key Press Scancode Key Release Scancode Keycode
ENTER 0x1e 0x9e 0x1e (30)
a 0x1e 0x9e 0x1e (30)
b 0x3e 0x9e 0x30 (48)
c 0x2e 0x9e 0x2e (46)
Shift 0x2a 0xaa 0x2a (42)
Ctrl 0x1d 0x9d 0x1d (29)

Verificarea de tastă apăsată/eliberată are loc în funcția is_key_press() iar obținerea caracterului ASCII aferent unui scancode are loc în funcția get_ascii().

În handler-ul de întrerupere (kbd_interrupt_handler) interpretați scancode-ul pentru a vedea dacă este o apăsare sau o ridicare de tastă și obțineți caracterul ASCII aferent. Afișați informațiile obținute.

Pentru verificarea de tastă apăsată/eliberată folosiți funcția is_key_press(). Pentru obținerea caracterului ASCII aferent folosiți funcția get_ascii(). Ambele funcții primesc ca parametru scancode-ul citit din funcția i8042_read_data().

Pentru afișare în cadrul handler-ului de întrerupere folosiți o construcție de forma:

	pr_info(LOG_LEVEL "IRQ %d: scancode=0x%x (%u) pressed=%d ch=%c\n",
		irq_no, scancode, scancode, pressed, ch);

unde scancode este valoarea registrului citit cu ajutorul funcției i8042_read_data(), iar ch este valoarea întoarsă de funcția get_ascii().

Tastele săgeți nu mai funcționează pentru că apăsarea acestora trimite scancode-uri care sunt mai dificil de tratat în handler-ul de întrerupere.

c. [1p] Colectarea caracterelor în buffer

Dorim să colectăm caracterele apăsate (nu și celelalte taste) într-un buffer circular care apoi va fi citit din user space.

Actualizați handler-ul de tratare a întreruperii (kbd_interrupt_handler) astfel încât un caracter ASCII citit să fie adăugat la sfârșitul buffer-ului aferent dispozitivului. Dacă buffer-ul este plin, caracterul cel mai vechi o sa fie suprascris.

Buffer-ul aferent dispozitivului este câmpul buf din structura de tip struct kbd aferentă dispozitivului. Această structură o puteți obține în cadrul handler-ului de tratare a întreruperii folosind construcția

	struct kbd *data = (struct kbd *) dev_id;

Dimensiunea curentă a buffer-ului este dată de câmpul count din structura de tip struct kbd aferentă dispozitivului. Câmpul put_idx specifică următoarea poziție de scriere, iar get_idx următoarea poziție de citire. Urmăriți implementarea funcției put_char pentru a observa cum sunt adăugate datele în buffer-ul circular.

4. [2p] Citirea bufferului printr-un apel ''read''

Pentru a avea acces la datele colectate de keylogger va trebui să le transmitem în user space. Facem acest lucru prin intermediului unui dispozitiv de tip caracter (/dev/kbd). La citirea din acest dispozitiv de tip caracter vom primi datele din buffer-ul din kernel space în care am colectat tastele apăsate.

Implementarea o veți face în fișierul kbd.c. Zonele din program în care trebuie să acționați au comentarii marcate cu /* TODO 4: ... */ în funcția kbd_read() și indicații despre ce trebuie să urmăriți.

La fiecare pas compilați modulul, încărcați modulul în kernel, apăsați în mașina virtuală taste pentru a verifica funcționalitatea, și apoi descărcați modulul.

Este permis un singur cititor la un moment dat astfel încât protejăm codului funcției kbd_read() cu un mutex.

Întrucât nu dorim modificări la nivelul buffer-ului din partea handler-ului de întrerupere în timp ce facem citirea sa în funcția kbd_read() veți folosiți un spinlock și veți dezactiva întreruperile în funcția kbd_read.

Definiți spinlock-ul în structura de dispozitiv (struct kbd) și inițializați-l în kbd_init. Folosiți funcțiile spin_lock_irqsave și spin_unlock_irqrestore pentru locking cu dezactivarea întreruperilor pe parcursul lucrului cu buffer-ul în rutina kbd_read().

Folosiți funcțiile spin_lock si spin_unlock pentru protejarea buffer-ulu în rutina de tratare a întreruperii.

Parcurgeți secțiunea Locking din laborator.

Completați funcția get_char pentru a obține următorul caracter care trebuie citit din buffer. Aveți grijă la implementarea buffer-ului circular (să nu citiți dincolo de dimensiunea buffer-ului și de poziția de scriere).

Pentru copierea în user space veți folosi apelul put_user pentru fiecare caracter în parte, obținut folosind funcția get_char.

Nu puteți folosi copy_to_user sau put_user în timp ce aveți un spinlock luat.

Urmăriți în laboratorul 4 secțiunea Accesul la spațiul de adresă al procesului.

Variabilele atomice nu pot fi folosite în această situație. Indiferent dacă folosim sau nu variabile atomice, doar rutina de tratare a întreruperii modifică câmpul dimensiune și folosirea variabilelor atomice nu ar ajuta.

Suplimentar, este nevoie de un spinlock pentru protejarea buffer-ului în momentul citirii informațiilor din acesta în buffer-ul temporar.

Pentru testare, va trebui să creați fișierul de tip caracter /dev/kbd folosind utilitarul mknod cu majorul și minorul dispozitivului sunt definiți în macro-urile din kbd.c: KBD_MAJOR și KBD_MINOR. Adică va trebui să rulați comanda

mknod /dev/kbd c 42 0

Compilați modulul pe sistemul fizic, apoi porniți mașina virtuală și încărcați modulul în nucleul mașinii virtuale. Testați folosind comanda

cat /dev/kbd

O să obțineți conținutul bufferului keylogger-ului din kernel space.

5. [1.5p] Curățarea buffer-ului la scriere

Dorim să "curățăm" buffer-ul atunci când se scrie în device node-ul asociat driver-ului. În acel moment buffer-ul va fi resetat: conținutul său va fi resetat și dimensiunea va fi 0.

Implementarea o veți face în fișierul kbd.c. Urmăriți comentariile marcate cu TODO 5. Implementați funcția reset_buffer și adăugați operația write la kbd_fops.

În momentul resetării buffer-ului, va trebui să folosiți spinlock-ul cu dezactivarea întreruperilor pentru accesul exclusiv la buffer.

Parcurgeți secțiunea Utilizarea spinlock-uri.

Pentru testare, va trebui să creați fișierul de tip caracter /dev/kbd folosind utilitarul mknod cu majorul și minorul dispozitivului sunt definiți în macro-urile din kbd.c: KBD_MAJOR și KBD_MINOR. Adică va trebui să rulați comanda

mknod /dev/kbd c 42 0

Compilați modulul pe sistemul fizic, apoi porniți mașina virtuală și încărcați modulul în nucleul mașinii virtuale. Consultați conținutul buffer-ului folosind comanda

cat /dev/kbd

Apăsați pe taste și apoi rulați comanda echo “clear” > /dev/kbd și apoi consultați din nou conținutul buffer-ului. Ar trebui să fie resetat.

Extra

[3 karma] Utilizare kfifo

Implementați colectarea de date de la tastatură, și transmiterea lor în user space folosind API-ul kfifo.

Folosiți exemplele de utilizare a API-ului kfifo din codul nucleului. De exemplu bytestream-example.c.

Soluții

so2/laboratoare/lab05/exercitii.txt · Last modified: 2019/03/20 11:49 by gabriel.bercaru
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0