FiND HiDDEN RESiDENT PR0CESSES -
Pubblicato da twiz <[email protected]> & sgrakkyu <[email protected]> il 04/02/2003
Livello avanzato

Introduzione

Questo articolo si divide fondamentalmente in due parti: una prima parte che cercherà di analizzare le basi teoriche di FHRP, spaziando tra concetti, implementazioni pratiche all'interno del kernel di linux e così via e una seconda parte "pratica" che presenterà gli obiettivi e l'implementazione effettiva del codice.
La prima parte non è strettamente necessaria per utilizzare FHRP, ma fornisce le basi (e magari spunti) per capirlo a fondo e, magari, migliorarlo e/o adattarlo alle proprie esigenze :)

Giusto per dare due coordinate, FHRP è un modulo scritto per il kernel 2.4 di linux, implementato unicamente per sistemi uniprocessore, con l' obiettivo di trovare eventuali processi nascosti su una macchina utilizzando il valore del registro cr3 come signature.
Nonostante ciò una (direi buona :)) parte delle idee e del codice dovrebbe essere applicabile anche a altri sistemi operativi, a sistemi SMP e, eventualmente, a altre architetture.

Iniziamo

Al tutorial è allegato il codice sorgente dei programmi.

I processi e lo scheduler
Scovare processi nascosti, abbiamo detto. Do per scontato che tutti sappiate cosa è un processo e che vi sia chiara l'"astrazione" che viene fatta nel kernel di linux, dove sarebbe più corretto parlare di task, visto che linux vede e gestisce kernel thread e processi in userland fondamentalmente allo stesso modo, ovvero con una struct task_struct.
[nota: Questo non vuol dire che non ci sia differenza tra kthread e processi in userland, infatti, ad esempio, la struct mm_struct di un kthread sarà sempre uguale a NULL, in quanto la virtual memory che accede è quella direttamente mappata in kernel space. Un altro esempio importante è che attualmente nel thread "base" del kernel 2.4 la full preemption patch non è applicata, dunque i kernel thread e, comunque, qualunque cosa giri in kernel space non è pre-emptable]

Allo stesso modo non ci dilunghiamo su cos'è e come funziona uno scheduler, è pieno di libri e testi per la rete che ne trattano l'argomento (per una minima lista consultate la reference al fondo)... in una nutshell possiamo definire lo scheduler come quella parte del sistema operativo che si occupa di scegliere, tra più processi che competono per la CPU, quello da far girare effettivamente, in base ad un determinato algoritmo (molti sono gli algoritmi possibili e diversi possono essere gli obiettivi.. pensate solo alla differenza tra un batch system e un real time system).
Diverse inoltre possono essere le situazioni in cui lo scheduler viene richiamato, ad esempio quando un processo termina o blocca in attesa di una determinata risorsa (per es. i/o su una porta) o quando questa diventa disponibile o, ancora, nel caso forki o abbia esaurito il suo time quantum.

Un'ultima definizione che può essere utile dare è la differenza tra *non-preemtive* e *preemptive* scheduling, nel primo caso lo scheduler sceglie un processo e lo lascia in esecuzione finchè non ha terminato il suo lavoro, blocchi o volontariamente rilasci la cpu, mentre nel secondo caso il processo ha assegnato un determinato time quantum, esaurito il quale viene sospeso e un altro processo viene scelto. Qualora venga implementata la "priorità" tra i processi (Priority Scheduling Algorithm), un processo a più alta priorità che diventa disponibile (ad esempio poichè s'è liberata una risorsa sulla quale bloccava) viene schedulato e il processo precedente viene sospeso.
Conditio sine qua non del preemptive scheduling è ovviamente la presenza di un timer interrupt.

Visto che, come detto, ci troveremo a lavorare col kernel di linux (thread di sviluppo 2.4) e su un sistema x86 a 32bit uniprocessore vediamo di addentrarci oltre e di vedere come tutto ciò venga implementato.
Ci sono almeno 3 testi reperibili online (Reference [2] [3] e [4]) che analizzano, anche molto a fondo, lo scheduler nel kernel di linux, sia UP sia SMP, quindi, anche in questo caso ci limiteremo ad analizzare i punti che più ci interessano, cercando di andare più a fondo possibile su questi e lasciando a chi fosse interessato ad approfondire oltre la lettura di questi testi.
Il miglior modo per capire lo scheduler di linux rimane tuttavia leggersi kernel/sched.c e alcune parti di kernel/timer.c (sys_alarm e sys_nanosleep sono implementate in questo file) oltre a include/linux/sched.h e time[r].h .

Ad un dato momento un task può trovarsi in uno di questi 5 stati (membro task->state della struct task_struct ) :


<include/linux/sched.h>
[snip]

#define TASK_RUNNING            0
#define TASK_INTERRUPTIBLE      1
#define TASK_UNINTERRUPTIBLE    2
#define TASK_ZOMBIE             4
#define TASK_STOPPED            8

[snip]
TASK_RUNNING -> il task è sulla runqueue e compete dunque per la CPU. Tutti i processi sulla runqueue sono in TASK_RUNNING state, mentre può non essere vero il contrario, visto che l'azione di settare un processo in TASK_RUNNING e metterlo nella runqueue non è atomica.

TASK_INTERRUPTIBLE -> Il task è in sleeping, ma può essere risvegliato da un signal oppure se finisce il timer per lo sleep settatogli con schedule_timeout() . Quando un processo va in sleep in TASK_INTERRUPTIBLE la sua task_struct viene inserita nella waitqueue legata alla risorsa su cui sta bloccando.

TASK_UNINTERRUPTIBLE -> Il task è in sleeping, ma viene garantito che ci resterà fino all'expire del timer settatogli dalla schedule_timeout() .
Questa opzione viene raramente usata all'interno del kernel di linux, ma si rivela utile nel caso di device driver che debbano aspettare che una determinata operazione finisca e che, se interrotta, potrebbe ritornare un valore errato o lasciare il device in uno stato impredicibile e/o corrotto.

TASK_STOPPED -> il task è stato stoppato o da un signal o poichè si sta cercando di tracciarlo con ptrace (ad un PTRACE_ATTACH viene inviato un SIGSTOP al child). Un task in TASK_STOPPED state non è ovviamente nella runqueue ed in nessuna waitqueue.

Del TASK_ZOMBIE state non ci interessa granchè, semplicemente il task ha terminato la sua esecuzione, ma il padre non ha eseguito una wait() sul suo status. Questi processi diventano figli "adottivi" di init, che, periodicamente, esegue delle wait(), eliminandoli de facto.

Cosa ci interessa notare è che *unicamente* i TASK_RUNNING competono per la CPU, mentre a tutti gli altri non viene data CPU (a meno che, ovviamente, non vengano risvegliati da un expire, per i processi in timeout, o, salvo i TASK_UNINTERRUPTIBLE, da un signal).
L'importanza di ciò e del fatto che i TASK_STOPPED non finiscano in alcuna waitqueue sarà più chiara quando analizzeremo le basi di FHRP e soprattuto il sistema delle signature dei processi.

Case study n.1 -> schedule_timeout()
Già che abbiamo introdotto il concetto di timeout e di schedule_timeout() , vediamo come il kernel di linux lo gestisce. La funzione da cui partire è senza dubbio schedule_timeout() , contenuta in kernel/sched.c .

signed long schedule_timeout(signed long timeout)
Questa funzione riceve come parametro il "tempo" in jiffies durante il quale dovrà stare in sleep il processo. I jiffies altro non sono che il numero di clock ticks dall'avvio della macchina, per questo il valore della variabile jiffies viene incrementato ad ogni timer interrupt.
FHRP basa una buona parte del suo lavoro sul timer interrupt e questo argomento verrà approfondito più avanti.

A questo punto, dopo aver dichiarato una struct timer_list (usata per settare il timeout) e un long expire viene eseguito uno switch sul valore di timeout (per comodità i commenti all'interno di sched.c sono stati rimossi):


        switch (timeout)
        {
        case MAX_SCHEDULE_TIMEOUT:
                schedule();
                goto out;
        default:
                {
                        printk(KERN_ERR "schedule_timeout: wrong timeout "
                               "value %lx from %p\n", timeout,
                               __builtin_return_address(0));
                        current->state = TASK_RUNNING;
                        goto out;
                }
        }
Dei due casi ci interessa principalmente MAX_SCHEDULE_TIMEOUT : in questo caso infatti non verrà settato alcun timer, ma verrà semplicemente invocato lo scheduler, in modo che il processo, precedentemente messo in TASK_INTERRUPTIBLE (generalmente ;)) o TASK_UNINTERRUPTIBLE esca dalla runqueue e ne venga schedulato un altro. Il processo dunque *NON* si sveglierà dopo un qualsivoglia periodo.
Il caso di MAX_SCHEDULE_TIMEOUT è di diretto interesse in FHRP in quanto la sys_accept (nella wait_for_connect() ) passa proprio questo parametro alla schedule_timeout , creandoci qualche problema per trovare processi nascosti che siano in listen su una determinata porta. La soluzione e l'importanza di ciò, come al solito, vi saranno chiare più avanti, dopo l'analisi pratica di FHRP.

Per tutti gli altri casi (default :) viene semplicemente fatto un check (come scritto tra i commenti nel source *PARANOICO* :)) per controllare che non venga passato un valore negativo a schedule_timeout (cosa che non dovrebbe *mai* accadere comunque). In tal caso comunque schedule_timeout ritornerà 0 .

Qualora niente di tutto ciò si verifichi (ed è il caso più comune) la funzione si comporta così:


        expire = timeout + jiffies;

        init_timer(&timer);
        timer.expires = expire;
        timer.data = (unsigned long) current;
        timer.function = process_timeout;

        add_timer(&timer);
        schedule();
        del_timer_sync(&timer);

        timeout = expire - jiffies;
Niente di strano, semplicemente viene calcolato il valore di expire (sommando il valore attuale della variabile jiffies al valore di delay contenuto in timeout) e vengono riempiti i campi della struct timer_list . Quello che ci interessa notare è che la funzione che verrà richiamata per prima per risvegliare il processo in sleep è process_timeout .
add_timer(&timer) aggiunge il processo nella global list dei timer attivi, mentre la del_timer_sync(&timer) viene usata per evitare race condition in caso la funzione ritorni prima del tempo (es. siamo stati svegliati da un signal). In tal caso verrà ritornato 'timeout', ovvero il tempo trascorso dal set del timer.
Per ogni altro approfondimento sulla schedule_timeout() i commenti in sched.c dovrebbero essere sufficienti :)

Prima di proseguire due parole su come un processo può essere messo in sleep, con o senza timeout. I casi sono fondamentalmente due: un'invocazione che definirei "manuale" di schedule_timeout() o l'uso delle varie interruptible_sleep_on_timeout / interruptible_sleep_on / sleep_on_timeout / sleep_on .

Un esempio di invocazione "manuale" lo abbiamo sia nella sys_nanosleep() , la syscall che viene invocata quando nei codici in C scriviamo, ad esempio, sleep(10) .
Tralasciando le parti relative ai processi in realtime ( task->policy settata a SCHED_RR o SCHED_FIFO) le linee che ci interessano sono:


        current->state = TASK_INTERRUPTIBLE;
        expire = schedule_timeout(expire);
In questo caso non c'è nessuna necessità di settare una waitqueue, poichè il processo non sta aspettando (ovvero bloccando per) una determinata risorsa, ma semplicemente deve rimanere "in sleep" per un certo periodo.
Nel caso il processo blocchi in attesa di un qualche evento, se viene "manualmente" invocata schedule_timeout() , nelle righe precedenti viene settata e aggiunta a una waitqueue_head la wait_queue .
Un esempio lo vedremo quando analizzeremo, brevemente, la wait_for_connect() .

Le varie *sleep_on* (asterischi usati come regexp ;)) fanno esattamente lo stesso, semplicemente settano sempre una waitqueue aggiungendola alla wait_queue_head_t struct passata come argomento e si occupano all'interno della funzione di settare lo state del processo e di invocare, se necessario, schedule_timeout() .
La differenza tra timeout o meno viene ottenuta, con schedule_timeout(), a seconda che il valore di timeout sia diverso o uguale a MAX_SCHEDULE_TIMEOUT .

Case study n.2 -> Le tappe di un processo che si risveglia
Come abbiamo visto poco fa, all'expire del timer settato da schedule_timeout() , la funzione richiamata è process_timeout() , che riceve come parametro un unsigned long che altro non è che il puntatore alla task_struct andata in sleep.
Da questo movimento in poi ci ritroveremo a saltellare tra varie funzioni, ognuna che funge da "wrapper" alla successiva, fino ad arrivare alla try_to_wake_up() , che è poi la funzione che effettivamente risveglierà il nostro processo.
Quello che faremo in questo secondo case study sarà percorrere le tappe cercando di mettere in evidenza le parti più interessanti per FHRP e i motivi per cui si è scelto di hookare in determinati punti del codice.

La prima funzione che viene richiamata è appunto la process_timeout ed è anche la funzione che FHRP hooka per controllare questo tipo di processi. La funzione in sè, come tutti i wrapper, è molto semplice:


static void process_timeout(unsigned long __data)
{
        struct task_struct * p = (struct task_struct *) __data;

        wake_up_process(p);
}
Viene dichiarato un puntatore e con un cast lo si fa puntare al processo che era andato in sleep e successivamente viene richiamata la wake_up_process .
process_timeout() è anche la funzione che viene hookata all'interno di FHRP, questo per alcuni motivi: - E' la prima funzione richiamata quando si tratta di risvegliare un processo, il che si traduce nel non dover dipendere da altre funzioni che avrebbero potuto essere hookate dall'attaccante e quindi riportare risultati errati.
- E' molto breve ed è quindi possibile riscriverla completamente nell'hook, in modo da avere la certezza che niente si "metterà in mezzo".
- Se noi mettiamo un processo in schedule_timeout e, per qualche ragione, questo processo non viene passato a wake_up_process e, quindi, non arriva alla try_to_wake_up quel processo non si risveglierà più... questo in FHRP viene usato come metodo un po' "rude" (ma efficace) per rendere innocuo un eventuale processo "maligno".

La wake_up_process() è anch'essa una funzione wrapper, che richiama la try_to_wake_up() .
Viene utilizzata, ad esempio, quando viene inviato un SIGCONT a un processo ( kernel/signal.c ).


inline int wake_up_process(struct task_struct * p)
{
        return try_to_wake_up(p, 0);
}
Come detto poco fa altro non fa che richiamare la try_to_wake_up, passando come secondo parametro (int synchronous , come vedremo tra pochissimo) "0", ovvero la richiesta di richiamare reschedule_idle() , oltre a inserire il modulo nella runqueue.
Il risultato di tutto ciò è che, in reschedule_idle() , verrà calcolata la "goodness" (attraverso la dynamic priority) del processo risvegliato e, se questo si rivelerà avere una maggiore priorità rispetto al processo corrente, avverà un context switch e il processo appena svegliato otterrà subito la CPU.
Vediamo comunque la try_to_wake_up() :

static inline int try_to_wake_up(struct task_struct * p, int synchronous)
{
        unsigned long flags;
        int success = 0;

        spin_lock_irqsave(&runqueue_lock, flags);
        p->state = TASK_RUNNING;
        if (task_on_runqueue(p))
                goto out;
        add_to_runqueue(p);
        if (!synchronous || !(p->cpus_allowed & (1 << smp_processor_id())))
                reschedule_idle(p);
        success = 1;
out:
        spin_unlock_irqrestore(&runqueue_lock, flags);
        return success;
}
Anche questa funzione è abbastanza semplice: La reschedule_idle() è la funzione che si occupa, come detto un paio di paragrafi fa, di controllare se la goodness del processo svegliato è migliore di quella del processo in esecuzione e, se così è, di "portare" a un context switch a favore del processo risvegliato.
Il codice è abbastanza complesso in SMP (infatti ha come obiettivo di "trovare" una idle cpu per farci girare il processo sopra), mentre si riduce a poche righe su UP:

        int this_cpu = smp_processor_id();
        struct task_struct *tsk;

        tsk = cpu_curr(this_cpu);
        if (preemption_goodness(tsk, p, this_cpu) > 1)
                tsk->need_resched = 1;
In tsk viene recuperato il processo corrente sulla CPU, mentre preemption_goodness altro non fa che sottrarre la goodness del processo risvegliato a quella del processo corrente. Se il valore è maggiore di uno significa che la priorità del processo svegliato è maggiore e need_resched viene settata a 1, forzando così una chiamata allo scheduler al primo ret_from_intr o syscall.
La need_reschedule() , come viene detto anche in un commento in kernel/sched.c è assolutamente timing critical, infatti se ricordate dalla try_to_wake_up viene richiamata con il lock settato sulla runqueue e non è possibile richiedere il tasklist_lock .

Visto il buon numero di testi approfonditi reperibili online (Reference [2] [3] e [4]... oltre che sched.c ) non ci dilungheremo su altre parti dello scheduler di linux, quali ad esempio la goodness (che altro non è che il core dello scheduling algorithm) o schedule() in sè in quanto sono ampiamente trattate sia in Linux Kernel Internals 2.4 [2] sia nel capitolo di Understanding the linux kernel disponibile per il download [3].

Case study n.3 -> PIT e dintorni, alzare la frequenza del clock
Come abbiamo già avuto modo di dire il timer interrupt è la conditio sine qua non di uno scheduling preemptive. E' infatti grazie al timer interrupt che possiamo periodicamente diminuire il time quantum di un processo ( task->counter ) e settare, se uguale a 0, task->need_resched a 1 in modo da invocare lo scheduler alla successiva ret_from_intr o ret_from_sys_call e ottenere così un context switch.

Prendendo in esame il kernel di linux, la frequenza (modificabile a compile time semplicemente cambiando il valore di HZ in asm/param.h ... di default uguale a 100) è settata a 1 tick ogni 10ms.
Va da sè che se noi aumentiamo il valore di HZ otteniamo un sistema con un maggiore response time, cioè il lasso di tempo che intercorre tra l'invio del comando e l'esecuzione dello stesso, ma anche un maggior overhead, dovuto al fatto che aumentano considerevolmente i context switch e ogni processo ha globalmente ad ogni epoch "meno tempo a disposizione" per girare, in quanto il suo counter si esaurirebbe in un tempo minore.
Allo stesso modo va da sè che se diminuiamo il valore di HZ otteniamo tempi di risposta via via più lunghi.
Entrambe le azioni (aumentare e diminuire la frequenza di timer interrupt) portano vantaggi a determinati processi, mentre ne penalizzano altri. Una shell o una qualsiasi applicazione interattiva trae vantaggio dall'aumento, mentre un'operazione come un find sull'intero disco fisso o un backup è avvantaggiata da un maggiore delay tra i context switch.

FHRP, quando caricato, alza la frequenza del timer interrupt, portandola a 1 tick ogni millisecondo (valore che ovviamente è modificabile, come vedremo quando analizzeremo quella parte del codice), richiamando però la routine originale per l'handling del timer interrupt con la frequenza classica di 10ms. Tutto questo ci permette di controllare più volte tra un "effettivo" timer interrupt e l'altro cosa effettivamente stia girando sulla CPU.

Vediamo ora come tutto questo è possibile e soprattutto cosa permette il raising del timer interrupt e come il kernel di linux gestisce tutto questo.
Al termine dell'analisi verrà anche proposto un semplice modulo che permette di alzare e abbassare la frequenza a piacere.

[nota: questa parte non è strettamente necessaria per comprendere il funzionamento di FHRP e, probabilmente, interesserà maggiormente gli appassionati di architettura e devices a basso livello. Se non siete interessati potete tranquillamente saltarla o leggerla per curiosità senza soffermarvi troppo sui dettagli.
Inoltre, sebbene una parte della descrizione valga anche per sistemi SMP, le parti analizzate e il codice proposto sono unicamente per UP.]
Partiamo dall'architettura. Analizzeremo l'8253/8254 (anche se de facto analizzeremo solamente l'8253, in quanto non prenderemo in esame le estensioni del 8254) Programmable Interrupt Timer chip.
Dei 3 canali disponibili sul PIT, l'unico che prenderemo in esame è il canale/timer 0, ovvero il device che il kernel di linux utilizza per tener conto del tempo (timer interrupt).

[nota: per un approfondimento su tutto ciò che non verrà trattato in questo spazio e, in questo caso, sugli altri due canali, ovvero il canale/timer 1 che controlla il refresh della DRAM e il canale/timer 2 che è legato allo speaker potete consultare le Reference [5] e [6]]

Tutti e tre i timer del chip 8253 sono controllati dallo stesso clock signal, che deriva dall'oscillazione del quarzo sulla scheda madre. La frequenza di questo clock signal è di circa 1.1931 MHz.
Ciascun timer ha un contatore, programmabile, che, in soldoni, calcola ogni quanto tempo o dopo quanto tempo (a seconda dell'Operation Mode, descritto più avanti) inviare il proprio "segnale".
Il timer 0 del chip 8253 è infatti collegato al PIC (Programmable Interrupt Controller) 8259 che di base si occupa di ascoltare su 8 sources di interrupt e di passare questi stessi, uno alla volta, alla CPU secondo un meccanismo di priorità grazie al quale un interrupt più importante può interromperne un altro e ricevere la CPU per sè.
L'8259 PIC permette di "maskare" determinati interrupt grazie agli 8 bit (uno per interrupt) del IRM (Interrupt Mask Register), infatti un bit settato a 1 nell'IRM impedirà a quell'interrupt di "raggiungere" la CPU per essere gestito.

[nota: effettivamente gli IRQ attualmente sono più di 8: sono il doppio, cioè 16, divisi tra 8 Master, direttamente collegati alla CPU, e 8 Slave, che passano attraverso l'IRQ2. Tuttavia non mi sembra il caso di approfondire oltre, chi fosse interessato può consultare la Reference [5]]

Il timer 0 del chip 8253 è legato all'IRQ0 dell'8259, ovvero l'Interrupt Request (così viene definita una interrupt source che passa per l' 259) a più alta priorità.
Come abbiamo detto più volte, la frequenza di default all'interno del kernel linux di questo interrupt è di 100 Hz, ovvero 1 tick ogni 10ms.
Ciò si ottiene settando nel contatore del timer il valore di 11932 (dalla semplice divisione otteniamo *circa* 100Hz o 10 ms), vediamo come nel kernel di linux questo venga calcolato.
La macro che restituisce il valore è LATCH:


<include/linux/timex.h>

#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) /* For divider */
CLOCK_TICK_RATE è definita in include/asm/timex.h ed è uguale a 1193180. Il motivo di questa divisione è facilmente intuibile. LATCH è una macro generica, che si adatta a tutti i controller di tutte le architetture supportate, mentre CLOCK_TICK_RATE , cioè la frequenza a cui gli interrupt arrivano dal quarzo (ok... non è esattamente dal quarzo ;)) cambia da architettura a architettura.
Il risultato è dunque 11932.

Prima di vedere come questo valore viene messo nel contatore relativo al canale/timer 0 è necessario dire due parole relative alle porte a cui è legato il PIT.
Il PIT è legato a 4 porte:

La porta 0x43 è quella che ci interessa di più, infatti, prima di poter settare il contatore dobbiamo "istruirlo" su come comportarsi. Questo viene fatto con una out sulla porta 0x43.
Il Mode Control Register è composto da 8 bit:
 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
   |___|   |___|   |_______|   _
Ricapitolando, dunque, la bitmask da passare sarà 00110100, ovvero 0x34 in hex, ed è esattamente quello che fa il kernel in arch/i386/kernel/i8259.c :

        outb_p(0x34,0x43);              /* binary, mode 2, LSB/MSB, ch 0 */
        outb_p(LATCH & 0xff , 0x40);    /* LSB */
        outb(LATCH >> 8 , 0x40);        /* MSB */
Il tutto dovrebbe essere sufficientemente chiaro e non necessitare di spiegazione.
L'ultima cosa che resta da fare è mostrare un'applicazione pratica, il modulo che pasto di seguito è scritto in sintassi intel at&t (quella del gas per intenderci) e permette di passare come parametro il valore del counter da settare. Il valore di default aumenterà la frequenza del clock a 1000Hz...
provate a insmodarlo e a lanciare alcuni programmi interattivi, tipo top o eseguire comandi della shell. Se avete il framebuffer il vostro cursore sembrerà impazzito :)

<-| fhrp/citf.s |->
/*
 * citf.s                                                                                 *
 * Change the interrupt timer frequency via lkm                                           *
 *                                                                                        *
 * This module gets the value to set in the counter of channel 0 via parameter 'freq' and *
 * sets it. Default value is 0x4a9, the value you'll get setting HZ to 1000 inside kernel *
 * I list there some value you could find useful to know :                                *
 *    for HZ == 100  (default in linux kernel)  -> 0x2e9c                                 *
 *    for HZ == 1000 (default in this lkm)      -> 0x4a9                                  *
 *    for HZ == 50                              -> 0x5d38                                 *
 *
 * Example : insmod citf.o freq=0x5d38
 */

.globl init_module
.globl cleanup_module

.globl freq
.data
.align 4
.size freq, 4
freq:
 .long 0x4a9

.text
.align 4

start:

init_module:
        pushl   %ebp
        movl    %esp,%ebp
        xorl    %eax, %eax
        pushfl
        cli
        movb    $0x34, %al
        outb    %al, $0x43
        movw    freq, %ax
        outb    %al, $0x40
        movb    %ah, %al
        outb    %al, $0x40
        popfl
        xorl    %eax, %eax
        leave
        ret

cleanup_module:
        pushl   %ebp
        movl    %esp,%ebp
        xorl    %eax, %eax
        pushfl
        cli
        movb    $0x34, %al
        outb    %al, $0x43
        movw    $0x2e9c, %ax
        outb    %al, $0x40
        movb    %ah, %al
        outb    %al, $0x40
        popfl
        leave
        ret


.globl __module_parm_freq
.section .modinfo
__module_kernel_version:
.ascii "kernel_version=2.4.9\0"
__module_parm_freq:
.ascii "parm_freq=i\0"
Aggiungo di seguito un semplice codice C per calcolare i valori da passare come parametro, più che altro per comodità, dal momento che spero vi sia chiaro come questo venga calcolato :)

<-| fhrp/freq.c |->
#define MY_LATCH(x)  ((1193180  + x/2) / x)  /* For divider */

main(int argc, char **argv)
{
 int i = atoi(argv[1]);
 printf("%x\n", MY_LATCH(i) );
}
<-X->

FHRP: gli obiettivi, le idee e l'implementazione
Dopo questa parte "teorica" è il momento di presentare il tool in sè e di analizzarne gli obiettivi e le idee che hanno portato alla sua stesura. Al termine della presentazione verranno anche proposti eventuali spunti per migliorare o estendere alcune funzioni.

Gli obiettivi
L'obiettivo di FHRP alla fin fine è uno solo, trovare processi che siano stati nascosti dall'attacker. Si parte da un certo principio, i processi sono stati nascosti venendo eliminati *almeno* dalla task_struct double chained list (il motivo vi sarà chiaro a breve) e si vuole scovare anche un ipotetico processo che, staccato da qualsiasi lista del kernel (vale a dire runqueue, pidhash list e task list), riceva "quantum" di CPU ad esempio da un manual switching a basso livello o da una modifica "pesante" allo scheduler in sè (pur con tutte le limitazioni e le difficoltà che ci sono ad implementare uno switch manuale o un hook consistente allo scheduler).

La prima cosa che ci serve per raggiungere il nostro scopo è avere un metodo per riconoscere i processi, una sorta di signature che distingua i processi 'buoni' dai processi 'cattivi'.
La scelta è stato il valore di cr3, il quarto dei control register, perchè: E' indispensabile per l'esecuzione del processo.

Il registro cr3 (anche detto PDBR - Page Directory Base Register) contiene l'indirizzo fisico della base della page directory e due flag (PCD e PWT).
Solamente i 20 most-significative bits sono specificati, mentre per gli altri 12 si assume che il valore sia 0.
Proprio perchè indispensabile per l'esecuzione del processo non può essere "fakato".

E' veloce da recuperare.

Il registro cr3 è contenuto in ogni task_struct in task->mm->pgd (per confrontarlo dobbiamo ricordarci di "tradurlo" in indirizzo fisico con __pa() ) ed è quindi veloce da recuperare per creare il database di cr3-noti.
Inoltre è molto veloce anche da recuperare mentre il processo è in esecuzione sulla CPU (movl %cr3, %eax) e, questa, è una buona notizia, visto che siamo in interrupt time e il clock è alzato a 1000Hz.

Il cr3 viene caricato dal kernel al momento del context switch da switch_mm() , con una semplice istruzione inline assembly:


                asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));
Ora che avevamo trovato la nostra signature ci serviva un punto nel quale andare a piazzarci per poter costantemente monitorare la CPU e il valore del cr3 register.

[nota: per comodità abbiamo diviso il codice di FHRP in 4 file: 3 sorgenti .c e un include .h, dei quali andiamo ora a presentare le caratteristiche principali e i punti più interessanti, il resto leggetevelo nei file stessi ;)]

cr3-timer.c
All'interno di questo file sono presenti le funzioni raise_timer() e restore_timer() che permettono di alzare all'insmod e riportare al valore standard la frequenza dei timer interrupt. Se avete letto il case study n.3 dovreste capirle al volo (non sono che, in fondo, la trasposizione in C del codice di citf.s); se l'avete saltato, l'unica cosa che vi interessa ora come ora è che durante il periodo in cui il modulo sarà linkato al kernel si verificherà un timer interrupt ogni ms.
La scelta di alzare a 1000Hz è stata presa per avere più possibilità di scovare un processo sulla CPU e si è dimostrata ben bilanciata con il possibile overhead che le nostre funzioni di check creano durante l'interrupt time.

handler_new() è il nuovo handler che andiamo a settare per il timer interrupt. Questo handler calcola grazie a HZ e MY_HZ la frequenza con cui richiamare lo scheduler in modo che nulla cambi nel succedersi dei context switch.
Sempre all'interno di handler_new viene eseguito il controllo tra il cr3 correntemente caricato e la lista di cr3 validi.

Come si scopre andando a vedere in timer.c e seguendo le varie funzioni chiamate, sono molti i punti in cui potevamo hookare per ottenere pressapoco lo stesso effetto. Si è scelto di andare a piazzarsi sulla cima della catena, sostituendo all'indirizzo contenuto nella struct irqhandler irq0 (l'indirizzo per l'handler del timer interrupt) l'indirizzo della nostra nuova funzione.
In questo modo finiremo col sovrascrivere anche altri eventuali hook con l'obiettivo di *gestire* in qualche modo i processi nascosti.
La procedura che avviene per gestire un interrupt non è il target di questo articolo e quindi non mi soffermo su come viene gestita la IDT e sul meccanismo legato allo switching in kernel land, ma piuttosto è interessante, per capire il funzionamento di FHRP, come vengono invocate le ISRs (Interrupt Service Routine).

La struttura fondamentale che contiene la ISR è la struct irqaction


</include/linux/interrupt.h>

struct irqaction {
        void (*handler)(int. void *, struct pt_regs *);
        unsigned long flags;
        unsigned long mask;
        const char *name;
        void *dev_id;
        struct irqaction *next;
};
Già che ci troviamo a lavorare con la irq0, vediamo come questa viene dichiarata:

static struct irqaction irq0  = { timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL};
L'indirizzo della struct irq0, ricavato con nm o dalla System.map , è alla base delle funzioni set_irq e restore_irq , che si occupano di settare la nostra nuova struct irquaction (e quindi in pratica il nostro nuovo handler) e di ripristinare lo stato originale.

cr3-func.c
In questo file si trovano alcune delle funzioni determinanti per il funzionamento di FHRP e che aumentano sensibilmente la probabilità già cmq abbastanza alta di trovare un eventuale processo nascosto.
Le prime due funzioni che andiamo a vedere sono la stop_all_process_safe() e la resume_all_process() , relative, rispettivamente, allo "STOP" dei processi e al "risveglio" di questi ultimi a lavoro finito.
Il vantaggio che bloccare tutti i processi 'buoni' ci dà è quello di aumentare le probabilità di scheduling di un processo nascosto, permettendo allo scheduler, de facto, di dare la CPU unicamente ai kthread, ai processi 'safè (che analizzeremo a breve) e, ovviamente, agli eventuali processi occultati.

La funzione stop_all_process_safe() lista tutta la serie dei processi 'legali' attivi sulla macchina al momento del caricamento del modulo (scorrendo la task_struct double chained list con list_for_each ) e stoppa uno ad uno tutti i processi inviandogli una force_sig(SIGSTOP, p) .
Tuttavia non *tutti* i processi vengono stoppati, come si vede dai controlli:


        ....
        if(p->mm)
        ....
        if((t !=  pid_bash_safe) && (t != SAFE_P_KLOGD) && (t != SAFE_P_SYSLOGD) && (t != SAFE_P_INIT))
        ....
        if((p->state != TASK_UNINTERRUPTIBLE) && (p!=current))
        ....
Vengono infatti lasciati attivi:
Nel nostro caso current è insmod quindi stopparlo non permetterebbe al nostro modulo di caricarsi e, con molta probabilità, porterebbe a un freeze della macchina. Inoltre non ci interessa più di tanto di insmod, infatti il suo destino è quello di terminare subito dopo aver caricato il nostro modulo.

resume_all_process() altro non è che l'opposto della stop_all_process_safe() e utilizza nuovamente force_sig per inviare un SIGCONT (in caso di qualche problema nel riacquistare la tty da parte di alcuni processi, qualche "fg" dovrebbe essere sufficiente).

L'ultima, ma cruciale, funzione che troviamo qui dentro (e che è già stata "anticipata") è take_global_page_dir() , che si occupa di recuperare il cr3 del processo corrente.


unsigned long int take_global_page_dir()
{
        __asm__ __volatile__ ("movl %cr3, %eax");
}

------] cr3-main.c
Questo è il cuore del modulo e, oltre alle init_module e cleanup_module (che organizzano il funzionamento del modulo) ci sono alcune altre funzioni interessanti.
Innanzitutto viene costruita la tabella dei processi 'autorizzati', con la funzione routine_set_table() , dopodichè viene fatto l'hook alla process_timeout .
Questo hook è molto semplice (come tutti gli hook a wrapper) e ci dà modo di controllare tutti i processi che si "svegliano" (e che magari tornerebbero subito a dormire, non essendoci la risorsa disponibile, senza ricevere cpu in userspace). Viene permesso solo ai processi 'autorizzati' di risvegliarsi, mentre gli altri finiscono nel dimenticatoio e al 99% (a meno che l'attacker non abbia creato un controllo parallelo) non riceveranno più CPU, diventando innocui.

La seconda funzione che analizziamo è la check_listening_socket() .
Questa funzione ci dà la possibilità di trovare eventuali processi nascosti che siano in ascolto, dopo una accept() , su una determinata porta.
Innanzitutto vediamo di chiarirne la necessità. Prendiamo ad esempio un "socket tcp" in ascolto su una porta (una normalissima backdoor ad esempio).
L'ultimo passo della accept sulla quale poi sleeperà è in net/ipv4/tcp.c la wait_for_connect()


static int wait_for_connect(struct sock * sk, long timeo)
{
        DECLARE_WAITQUEUE(wait, current);
        int err;

    add_wait_queue_exclusive(sk->sleep, &wait);
        for (;;) {
                current->state = TASK_INTERRUPTIBLE;
                release_sock(sk);
                if (sk->tp_pinfo.af_tcp.accept_queue == NULL)
                        timeo = schedule_timeout(timeo);
[...]
Questa è la parte che ci interessa. Viene dichiarata la waitqueue, si controlla se c'è qualcuno in 'accept_queue' per il socket e, se così non è, si invoca schedule_timeout .
Tuttavia l'hook sulla process_timeout in questo caso non ci è di nessun aiuto, infatti, nel caso classico che stiamo prendendo in considerazione, timeo è settato a MAX_SCHEDULE_TIMEOUT .
Come abbiamo visto nel case study relativo alla schedule_timeout , un valore di MAX_SCHEDULE_TIMEOUT non setta alcun timer, ma, semplicemente, invoca lo scheduler e manda il socket a dormire, fintantochè un signal (generalmente SIGIO) non si occuperà di svegliarlo.

A questo punto abbiamo bisogno di:

La soluzione dovrebbe essere abbastanza chiara osservando il codice, innanzitutto si prendono le varie hash table di listening socket (tcp, udp e raw), dopodichè, risaliti alla sock struct, si ottiene wait_queue_head struct da sk->sleep . Listando ancora questa si ottengono le 'possibili' struct wait_queue dalle quali è possibile risalire alla struct task_struct e lì fare il check.
Questo check viene fatto da check_wait_process() . Nel report del risultato, all'unload del modulo, viene notificata anche la porta e il tipo di socket connesso.
Un'altra possibile idea (e forse la più naturale) era quella di risalire alla struct file da sock e fare una sorta di pattern matching via via con le struct file raggiungibili dalla task_struct , ma questo avrebbe snaturato l'idea alla base di FHRP, cioè il check del cr3.

[nota: se date un veloce sguardo a net/netsyms.c vedrete che due "simboli" che ci interessano, ovvero udp_hash e tcp_hashinfo , sono esportate solo se almeno uno tra CONFIG_IPV6_MODULE , CONFIG_KHTTPD , CONFIG_KHTTPD_MODULE è settato.
Il problema si risolve velocemente hookando altre due funzioni, ma, visto che si tratta di un tool lato admin, ciò non viene aggiunto nel codice (se volete aggiungerlo un paio di #ifdef dovrebbero essere sufficienti ;)). La soluzione più veloce rimane ricompilare con, ad esempio, CONFIG_IPV6_MODULE settato.]

config.h
L'header di fhrp contiene innanzitutto i #define per l'hook di alcune funzioni (es. process_timeout() ) o per accedere a alcune struct a livello kernel (es.
irq0 o la listening raw socket hash table), che devono essere correttamente settati usando nm o una System.map aggiornata. Il nome della "stringa" da ricercare è messo tra i commenti di fianco a ogni #define .

Sempre in questo file dovrete settare i pid di syslogd e klogd, essendo questi due demoni avviati allo startup della macchina dovrebbero mantenere sempre lo stesso pid anche dopo i reboot.

Le ultime due cose che potrebbero interessarvi da settare sono MY_HZ , che decide a che frequenza portare il clock e MAX_RESULTS che decide qual è il numero massimo di risultati da riportare. Entrambe sono settate a due valori che di default dovrebbero andare bene. Tutt'al più qualora venissero riportati 10 risultati potrebbe essere utile, per sicurezza, rifare la prova con più risultati.
Volendo potete anche, con una piccola modifica, cambiare MAX_RESULTS e rendere il totale dei results settabile a insmod-time con un MODULE_PARM .

L'ultima parte su cui ci soffermiamo in config.h è compare_cr3() . La funzione è stata dichiarata static inline così da evitare una CALL a questa (siamo in interrupt time e qualche ciclo in meno ci fa comodo... senza dimenticare che comunque una CALL tende a flushare la pipeline).
La funzione è stata strutturata per essere il più "ottimizzata" possibile, ad esempio con l'implementazione di una sorta di cache che tiene in memoria l'ultimo cr3 trovato (infatti, avendo il clock alzato di dieci volte è molto probabile che lo stesso cr3 maligno, nel momento in cui il programma runni sulla cpu per un quantum o più, venga ripetitivamente trovato molte volte. La cache ci permette di evitare di dover scorrere la lista dei cr3 trovati ogni volta... non dimentichiamo che siamo in interrupt time e abbiamo il clock a una frequenza più alta).

Conclusioni

Conclusioni, possibili modifiche e miglioramenti
Iniziamo col funzionamento... come vi sarà chiaro, questo non è un modulo studiato per essere residente, anzi, dovrebbero essere sufficienti pochi secondi (a meno che non vogliate essere sicuri contro sleep(100) o comunque sleep lunghe... ma controlli incrociati le scoverebbero), il tempo di un paio di epochs di finire, e dovreste avere una fotografia di ciò che gira sulla vostra macchina.
Inolte il modulo restituisce sempre un FALSO POSITIVO. E' il valore del cr3 di rmmod... abbiamo preferito farlo riportare, stampandone prima a video il valore, per maggiore sicurezza. Va da sè che un semplice hidden_task - 1 rimuova questo falso positivo... ma conviene essere paranoici.
Vediamo comunque un esempio pratico del funzionamento del modulo:

root@twiz:/home/twiz/cr3/cr3-dev# insmod fhrp.o
[snip]
Pid: 79 Context: 2577000
Pid: 83 Context: 25da000
Pid: 85 Context: 2523000
Pid: 93 Context: 3c9c000
Pid: 94 Context: 3e1a000
Pid: 95 Context: 3cd1000
Pid: 96 Context: 3bd4000
Pid: 97 Context: 3b75000
Pid: 98 Context: 3ccd000
Pid: 99 Context: 24af000
Pid: 100 Context: 233d000
Pid: 101 Context: 23cc000
Pid: 194 Context: 3a36000
[snip]
Setting up process_timeout hook..

root@twiz:/home/twiz/cr3/cr3-dev# rmmod fhrp
Restoring process_timeout..

 Ripristining all process...

Leaving Module
Hidden Processes Foud : 1
Cuurent-> deve essere rmmod: 2139000
 cr3 malign : 2139000  pid : 672  got from Interrupt handler
root@twiz:/home/twiz/cr3/cr3-dev# 
Il cr3 malign riportato altro non è che il falso positivo di cui parlavamo.
Se volete testare l'efficacia del modulo su una backdoor in listen o un processo che eseguite potete fargli "dimenticare" di raccogliere il cr3 durante il collect della tabella (un semplice if (p->pid == piddadimenticare) ) oppure provare qualche modulo che occulti processi rimuovendoli dalla task list. I nostri test hanno dato risultati positivi :)

Continuiamo con una cosa che abbiamo già detto, ma che è particolarmente importante: questo modulo non è una panacea, se un ps troiano o un codice che modifica proc e alcune syscall sta nascondendo il processo questo modulo non può fare granchè, al massimo listarvi tutti i processi a insmod time. Ci sono altri modi per controllare, primo tra tutti listare la lista di task_struct (come fa il modulo a insmod time), utilizzare un ps safe o controllare i md5sum (qualora il redirect non sia a kernel level), utilizzare KSTAT per controllare le syscall.

Insomma non c'è *il* tool, ma un lavoro combinato di vari tool quando si tratta di controllare l'integrità di una macchina. Questo modulo viene in aiuto scovando gli hide più complessi, quelli che staccano il processo dalle liste conosciute, andando a agire molto a basso livello e appoggiandosi pochissimo a funzioni del kernel.

Niente è 100% valido quando sia l'attacker che il sysadmin possono lavorare a kernel space. L' attacker potrebbe aver modificato la create_module() e potrebbe fare pattern matching tra gli opcodes del modulo in caccia di un movl %cr3, %eax o altri punti. A quel punto noi potremmo offuscare il codice, renderlo automodificante, fare semplicemente pushl %cr3, popl %eax... inserire random 'nop-like' all'interno del codice stesso (non necessariamente un NOP è \x90 ;)).
Ancora, l'attacker potrebbe fare un'analisi statistica dell'accesso a force_sig , controllare se è incrementale e frequente (il che significherebbe che è in caricamento il nostro modulo) e invia solamente SIGSTOP e quindi stoppare fino alla ricezione dei SIGCONT il processo nascosto.
Ma a quel punto noi potremmo *manualmente* stoppare i processi e riavviarli... avremmo qualche piccolo problema in più (anche se è stato testato... ed è abbastanza sicuro anche così ;)), ma aggireremmo anche questo controllo.
Insomma, sono scenari possibili una volta che si sa cosa 'potrebbe succedere', ma ciò non toglie che questo modulo sia utile e valido in molte situazioni.
Inoltre modifiche pesanti come il check di opcode o della force_sig dovrebbero essere veloci da vedere dumpando e disassemblando quelle funzioni e, magari, notando jmp *%eax o pushl/ret o movl/ret "sospette" ;)

Il fatto stesso che il modulo sia stato scritto molto a basso livello e non sia residente ci dà già un buon grado di protezione (andiamo a sovrascrivere noi la struct irq0, modificando quindi anche un possibile hook dell'attacker, e così agiamo in molte situazioni), ma, ovviamente, non il 100% di sicurezza :)

Il modulo così com'è lascia aperte alcune migliorie che non vengono incluse nella versione di release. Tra queste:

Detto questo crediamo (e speriamo) troverete questo tool interessante, così come speriamo abbiate trovato le informazioni contenute in questo articolo utili e/o valide. Per ogni dubbio, critica, migliorie, patch & co i contatti via email sono scritti di fianco al titolo :)

Prima di passare alle References, un paio di ringraziamenti/shoutouts a vecna, Dark-Angel, albe e rene @ irc.kernelnewbies.org per qualche discussione sullo scheduler, i sistemi SMP e altro.
Un saluto va ai ragazzi del racl (la parte del PIT è su misura per voi :), ndtwiz), a _oink (grazie per la cartolina ;) ndtwiz) e a "tutti quelli che ci conoscono" (che fa molto telefonata a show televisivo). Direi che abbiamo detto abbastanza cazzate :)

References

[1] - Modern Operating Systems - Second Edition - Andrew S. Tanenbaum

[2] - Linux Kernel Internals 2.4 - Tigran Aivazian
      http://www.moses.uklinux.net/patches/lki.html 

[3] - Understanding the Linux Kernel - Bovet, Cesati - Ch10 "Scheduling"
      http://www.oreilly.com/catalog/linuxkernel/chapter/ch10.html

[4] - For Kernel_Newbies By a Kernel_Newbie - A.R.Karthick 
      http://www.freeos.com/articles/4536/

[5] - http://www.nondot.org/sabre/os/articles/MiscellaneousDevices/

[6] - Timer-related functionality in Linux kernels 2.x.x - Andre Derric Balsa
      http://www.cse.msu.edu/~zhengpei/tech/Linux/timerin2.2.htm

[7] - Linux Kernel Sources 2.4.*