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:
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
interrupts
.
Similar, putem genera și scheletul pentru un singur exercițiu, atribuind valoarea <lab_name>/<task_name>
variabilei LABS
.
tools/labs/skels
.
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 ...
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ă.
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
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 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
.
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.
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ă.
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 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.
/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 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.
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 kbd_interrupt_handler()
(adică la fiecare tastă apăsată) și afișați valoarea citită din registru.
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.
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 (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()
.
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()
.
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.
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.
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
.
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
.
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.
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.
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
.
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.
Implementați colectarea de date de la tastatură, și transmiterea lor în user space folosind API-ul kfifo.