Laborator 5: Exerciții

Pentru desfășurarea laboratorului pornim de la arhiva de sarcini a laboratorului. Descărcăm și decomprimăm arhiva în directorul so2/ din directorul home al utilizatorului student de pe sistemul fizic:

student@eg106:~$ cd so2/
student@eg106:~/so2$ wget http://elf.cs.pub.ro/so2/res/laboratoare/lab05-tasks.zip
student@eg106:~/so2$ unzip lab05-tasks.zip
student@eg106:~/so2$ tree lab05-tasks

În cadrul directorului lab05-tasks/ se găsesc resursele necesare pentru dezvoltarea exercițiilor de mai jos: fișiere schelet de cod sursă, fișiere Makefile și Kbuild, scripturi și programe de test.

Vom dezvolta exercițiile pe sistemul fizic și apoi le vom testa pe mașina virtuală QEMU. După editarea și compilarea unui modul de kernel îl vom copia în directorul dedicat pentru mașina virtuală QEMU folosind o comandă de forma

student@eg106:~/so2$ cp /path/to/module.ko ~/so2/qemu-so2/fsimg/root/modules/

unde /path/to/module.ko este calea către fișierul obiect aferent modulului de kernel. Apoi vom porni, din directorul ~/so2/qemu-so2/, mașina virtuală QEMU folosind comanda

student@eg106:~/so2/qemu-so2$ make

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

# insmod modules/module-name.ko
# rmmod module/module-name

unde module-name este numele modulului de kernel.

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. [1.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 portul serial.

În fișierul so2_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 so2_kbd_init. Codul din cadrul funcției so2_kbd_init care alocă porturile I/O (STATUS_REG și DATA_REG, folosind request_region) este comentat pentru că ar returna o eroare.

Decomentați liniile din cadrul funcției so2_kbd_init și returnați cod de eroare la nevoie, prin:

  1. atribuirea unui cod de eroare specific (de exemplu -EBUSY) variabilei err;
  2. salt (goto) la label-ul corespunzător (out_unregister).

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

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

make

și încărcați-l în nucleu, în mașina virtuală QEMU, folosind comanda

insmod so2_kbd.ko

Observați că obțineți eroare la încărcare: insmod: Can't insert '.../so2_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ă sunt înregistate cele două porturi (0x60: DATA_REG și 0x64:STATUS_REG) corespunzătoare registrelor pentru tastatură.

Comentați la loc liniile ce generează eroare înainte de a trece mai departe.

Pentru a folosi cu succes alocarea de porturi I/O, alocați pentru portul serial COM1. Pentru portul serial COM1 aveți nevoie de:

  • adresa de start a portului serial COM1; este vorba de 0x3F8
  • numărul de registre accesibile de la adresa de mai sus; este vorba de 8 registre

Folosiți funcția request_region (în cadrul funcției so2_kbd_init) pentru a aloca porturile aferente și folosiți funcția release_region (în cadrul funcției so2_kbd_exit). Apoi compilați și inserați modulul în nucleu. Verificați conținutul fișierului /proc/ioports și verificați acum că spațiul I/O 0x3f8-0x3ff este alocat driverului nostru.

Descărcați modulul din kernel folosind comanda

rmmod so2_kdb

și verificați acum conținutul fișierului /proc/ioports; observți că a dispărut alocarea spațiului I/O pentru COM1 (0x3f8-0x3ff).

Încărcați și descărcați de mai multe ori modulul de kernel pentru a vă asigura că totul merge cum trebuie.

Pentru a verifica tratarea corectă a cazului de eroare, încărcați driverul de port serial din sistem, folosind comanda:

modprobe 8250.ko

Acum încercați din nou încărcarea modulului so2_kbd.ko. Observăm eroare la înregistrarea spațiului de I/O care este acum adjudecat de driverul de port serial. Putem valida acest lucru urmărind /proc/ioports.

Descărcați din kernel modulele pentru portul serial, verificați că spațiul I/O pentru COM1 este liber și inserați din nou modulul so2_kbd.ko. Ar trebui ca totul să funcționeze.

Ștergeți liniile care înregistrează și eliberează spațiul pentru portul serial COM1 înainte de a trece la exercițiul următor.

2. [1.5p] 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 so2_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ă so2_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 so2_device_data. 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 so2_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 so2_device_data aferentă dispozitivului.

Implementarea o veți face în fișierul so2_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 so2_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:

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

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 (so2_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:

	printk(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 care apoi va fi citit din user space.

Actualizați handler-ul de tratare a întreruperii (so2_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 va fi ignorat.

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

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

Dimensiunea curentă a buffer-ului este dată de câmpul buf_idx din structura de tip struct so2_device_data aferentă dispozitivului. Va trebui să incrementați valoarea acestei variabile la fiecare caracter adăugat în buffer.

Capacitatea buffer-ului, peste care nu puteți scrie, este dată de macro-ul BUFFER_SIZE.

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/so2_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 so2_kbd.c. Zonele din program în care trebuie să acționați au comentarii marcate cu /* TODO 4: ... */ în funcția so2_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 so2_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 so2_kbd_read() veți folosiți un spinlock și veți dezactiva întreruperile în funcția so2_kbd_read.

Definiți spinlock-ul în structura de dispozitiv (struct so2_device_data) și inițializați-l în so2_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 so2_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.

Pentru copierea în user space veți folosi apelul copy_to_user. Întrucât nu puteți folosi copy_to_user în timp ce aveți un spinlock luat, trebuie să folosiți un buffer temporar.

Buffer-ul temporar și dimensiunea acestuia sunt indicate de câmpurile tmp_buf și tmp_buf_idx din structura de tip dispozitiv.

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/so2_kbd folosind utilitarul mknod cu majorul și minorul dispozitivului sunt definiți în macro-urile din so2_kbd.c: SO2_KBD_MAJOR și SO2_KBD_MINOR. Adică va trebui să rulați comanda

mknod /dev/so2_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/so2_kbd

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

5. [3p] Curățarea buffer-ului dacă este tastată o parolă

Dorim să "curățăm" buffer-ul dacă este tastată o parolă. Î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 so2_kbd.c. În rutina de tratare a întreruperii veți reține în câmpul passcnt câte caractere din parolă au fost potrivite. Parola este definită de macro-urile MAGIC_WORD și MAGIC_WORD_LEN.

Când se detectează întreaga parolă, curătați buffer-ul. Pentru curățare, reinițializați câmpul buf_idx la valoarea 0 și umpleți cu 0-uri buffer-ul.

La primirea unui caracter nou, verificați dacă acesta corespunde cu caracterul de pe poziția curentă din parolă. În caz afirmativ, incrementați contorul; altfel, resetați counterul la 0.

În momentul resetării buffer-ului, în handler-ul de întrerupere va trebui să folosiți spinlock-ul pentru accesul exclusiv la buffer. Este posibil ca rutina de read să fie procesată de pe alt procesor și să acceseze buffer-ul în momentul în care handler-ul de întrerupere îl modifică. Veți folosi funcțiile spin_lock și spin_unlock.

Parcurgeți secțiunea Utilizarea spinlock-uri.

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

mknod /dev/so2_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/so2_kbd

Apăsați pe taste și apoi apăsați conținutul macro-ului MAGIC_WORD (adică root) și apoi consultați din nou conținutul buffer-ului. Ar trebui să fie resetat.

Extra

[1 karma] Resetarea buffer-ului în rutina de read

Implementarea curentă este problematică pentru că rutina de tratare a întreruperii este cea care face resetarea buffer-ului, lucru care este consumator de timp și nerecomandat să fie făcut în context întrerupere.

O soluție este să actualizăm comportamentul astfel încât resetarea buffer-ului să se producă la apelul funcției so2_kbd_read(). După ce informația este copiată în user space, buffer-ul este resetat. Nu mai resetăm în momentul tastării

Simplificați implementarea în așa fel încât resetarea buffer-ului să se producă în funcția so2_kbd_read() imediat după copierea datele în user space.

În momentul în care copiați datele în buffer-ul temporare tmp_buf resetați buffer-ul principal: adică umpleți buffer-ul buf cu zero-uri și inițialiați buf_idx la 0.

Pentru verificarea după citirea din /dev/so2_kbd veți avea buffer-ul resetat. Citiri ulterioare vor găsi buffer-ul gol.

[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: 2017/03/20 16:42 by octavian.purdila
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