This is an old revision of the document!
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.
Ctrl+Shift+t
. Cele trei tab-uri de terminal îndeplinesc următoarele roluri:
~/so2/qemu-so2/
.~/so2/linux-4.9/
și folosim Vim și cscope pentru parcurgerea codului sursă.student@eg106:~$ netcat -lup 6666
Găsiți definițiile următoarelor simboluri în nucleul Linux:
resource
;request_region
și de acolo funcția __request_region
;request_irq
și de acolo funcția request_threaded_irq
;inb
, varianta de pe arhitectura x86.Urmăriți în codul nucelului Linux:
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.
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:
-EBUSY
) variabilei err
;goto
) la label-ul corespunzător (out_unregister
).
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ă.
Pentru a folosi cu succes alocarea de porturi I/O, alocați pentru portul serial COM1
. Pentru portul serial COM1
aveți nevoie de:
COM1
; este vorba de 0x3F8
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.
COM1
înainte de a trece la exercițiul următor.
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ă.
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
).
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.
/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ă.
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:
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.
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ă.
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.
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.
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.
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:
ENTER
veți obține scancode-urile 0x1c
(pentru tastă apăsată) si 0x9c
(pentru tastă eliberată)a
veți obține scancode-urile 0x1e
(pentru tastă apăsată) si 0x9e
(pentru tastă eliberată)b
veți obține scancode-urile 0x30
(pentru tastă apăsată) si 0xb0
(pentru tastă eliberată)c
veți obține scancode-urile 0x2e
(pentru tastă apăsată) si 0xae
(pentru tastă eliberată)Shift
veți obține scancode-urile 0x2a
(pentru tastă apăsată) si 0xaa
(pentru tastă eliberată)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.
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()
.
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()
.
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.
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.
BUFFER_SIZE
.
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
.
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.
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.
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.
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.
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.
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.
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.
Implementați colectarea de date de la tastatură, și transmiterea lor în user space folosind API-ul kfifo.