GUIDA Aggiungere nuova syscall e leggere memoria di un altro processo

DispatchCode

Moderatore
Staff Forum
Utente Èlite
2,251
1,881
CPU
Intel I9-10900KF 3.75GHz 10x 125W
Dissipatore
Gigabyte Aorus Waterforce X360 ARGB
Scheda Madre
Asus 1200 TUF Z590-Plus Gaming ATX DDR4
HDD
1TB NVMe PCI 3.0 x4, 1TB 7200rpm 64MB SATA3
RAM
DDR4 32GB 3600MHz CL18 ARGB
GPU
Nvidia RTX 3080 10GB DDR6
Audio
Integrata 7.1 HD audio
Monitor
LG 34GN850
PSU
Gigabyte P850PM
Case
Phanteks Enthoo Evolv X ARGB
Periferiche
MSI Vigor GK30, mouse Logitech
Net
FTTH Aruba, 1Gb (effettivi: ~950Mb / ~480Mb)
OS
Windows 10 64bit / OpenSUSE Tumbleweed
Piccola panoramica su cosa aspettarsi da questo articolo, schematicamente:
- introduzione alla traduzione di un indirizzo
- tutti i passi per aggiungere una nuova syscall al kernel linux (si, cosa non raccomandata, ma tanto non pushiamo il codice nel repo di Linus :) )
- creazione di 2 codici di test per provare la syscall

0 - VAS, syscall, indirizzi virtuali e fisici, CR3... cosa sono?​


L'utente Linux tecnico di professione (eg informatico) o no è sicuramente portato a indagare il funzionamento di alcune cose, ma questi concetti potrebbero esservi oscuri o non proprio conosciuti. Cerco di introdurli senza andare troppo nel dettaglio, ma abbastanza da capire cosa fa questa syscall.

Iniziamo dal VAS, acronimo che sta per Virtual Address Space. In un sistema operativo - e questo vale per Windows anche - esistono più livelli di privilegio per eseguire il codice. Questi dipendono dall'architettura; prendendo come esempio x64, ne abbiamo ben 4: ring0, ring1, ring2, ring3. Di questi 4 ne vengono usati praticamente 2 dai sistemi operativi, e si tratta di ring0 e ring3.

Tutto il codice con privilegi maggiori gira in ring3. Questo codice è il kernel stesso così come tutti i driver (quelli però "kernel space"). A questo livello di privilegio si può accedere a "istruzioni assembly" che non sono accessibili da user mode; un esempio sono i Control Register(s), i vari "CR" (CR0, CR1, CR2...).

I programmi e il kernel (+ drivers) sono separati: ciò significa che un programma non può recar danno al kernel; se un programma è buggato e crasha, solo il programma stesso viene coinvolto. Se il kernel crasha... ecco.

Ora, tornando al VAS: questo spazio virtuale degli indirizzi è uno spazio nel quale ogni programma utente (user space) si trova confinato. Ciascun programma è isolato non solo dal codice privilegiato (kernel space) ma anche dagli altri programmi. Per capirci, l'indirizzo virtuale 0x12345678 (0x sta per esadecimale) del programma A e il medesimo indirizzo del programma B, sono due aree di memoria distinte.

Ok, come è possibile questa cosa? E' possibile in quanto appunto si tratta di indirizzi virtuali: ogni indirizzo virtuale viene tradotto in un indirizzo fisico, che è la locazione effettiva nel quale si trova il dato in memoria.

Il kernel in fase di avvio crea delle tabelle, note come tabelle delle pagine e ciascun programma che viene eseguito fa uso di queste tabelle. L'aspetto importante è che ciascun programma parte da una base (che è un indirizzo di memoria) diverso rispetto a tutti gli altri programmi.

L'immagine qui sotto sarà utile a chiarire questo aspetto:
Istantanea_2024-05-18_17-53-02.png

Quello che si vede in cima è un indirizzo virtuale... e qui occorre una precisazione: in un OS a 64bit in teoria ci sarebbero 2^64 bytes di memoria virtuale utilizzabile; nella pratica, non tutti questi bit vengono usati. Nell'immagine sopra vediamo che il bit più a sinistra è il numero 56. Quindi abbiamo 57bit come spazio virtuale, e si tratta di un totale di 128PB (PetaBytes) di memoria!

Non occorre andare più nel dettaglio, va solo aggiunto che come è immaginabile, seppur la memoria è virtuale... dovrà essere mappata su della memoria fisica (appunto, è poi il meccanismo che ci porta alla traduzione da virtuale a fisico).

Fate sempre riferimento all'immagine: senza complicarci la vita iniziamo con il dire che l'indirizzo fisico che si vede sulla destra, l'ultimo livello, è proprio l'indirizzo dove c'è il dato a cui stiamo accedendo. Per arrivarci si percorrono tute le tabelle delle pagine: il primo step è prendere un valore che in Linux è salvato in una struttura chiamata mm_struct, accessibile solo da kernel space; ogni programma (task) è rappresentato da una struttura all'interno del kernel, chiamata task_struct. Questa struttura tra i tanti membri ha un riferimento a mm_struct.

In pratica: ogni singolo task ha un mm_struct diverso. La cosa importante è che all'interno di mm_struct, c'è un membro, chiamato "pgd", che è esattamente quello che contiene il valore che verrà caricato nel registro della CPU che vediamo nell'immagine, CR3. La traduzione quindi inizia da CR3.

Avendo un valore di CR3 diverso per ogni processo, va da sè che ogni programma vedrà e quindi mapperà un indirizzo virtuale su uno fisico diverso dagli altri programmi, proprio perchè la base della tabella delle pagine è diversa. Vediamo quindi che la traduzione procede di livello in livello usando anche l'indirizzo virtuale: i bit dal 47 al 56 selezionano una particolare entry nella tabella PML5 (Page Memory Level 5); questa entry punta alla base della tabella successiva, quindi i bit da 39 a 47 vengono usati per selezionare la entry successiva nella tabella PML4 e così via.

Alla fine di tutto ciò ci ritroviamo con l'ultima tabella: questa è importante, perchè contiene quelle che si chiamano "pagine". Ogni pagina è grande 4KB, e anche qui, usando l'ultima porzione dell'indirizzo virtuale (offset) otteniamo l'ultima entry, che è in realtà la locazione di memoria del valore che stiamo cercando.

Non è esattamente semplice come concetto, ma spero sia sufficientemente chiaro.

1 - Aggiungiamo la nostra syscall​


Una syscall è come detto sopra una chiamata di sistema. Le si usa per richiedere qualche servizio al sistema operativo (scrivere un file, leggerlo e tante altre cose).
Sarò molto schematico qui, riportando solo i passaggi. Prima cosa, è necessario scaricarsi il kernel di Linux da github (o da kernel.org).
Successivamente:
  • creare una cartella chiamata memory nella root
  • modificare il file Kbuild nella root, aggiungendo:
obj-y += memory/
  • modificare il file Kconfig della cartella root e incollare:
source "memory/Kconfig"
  • modificare il file kernel/sys_ni.c e aggiungerci COND_SYSCALL(vminfo);
  • aggiungere un file di nome Kconfig nella cartella memory/ incollando:
Codice:
menu "Hack the process address space"

config VMINFO_SYSCALL
       prompt "Hack the process address space"
       def_bool y
       help
         [add here your text]

endmenu
  • aggiungere la syscall alla tabella situata in arch/x86/entry/syscalls/syscall_64.tbl
548 64 vminfo sys_vminfo
  • aggiungere la dichiarazione della funzione in include/linux/syscalls.h
asmlinkage long sys_vminfo(unsigned long address, pid_t pid, struct vminfo_struct __user *vminfo, void __user *buffer, unsigned int buff_len);
  • creare un file chiamato Makefile all'interno di memory, e aggiungere:
obj-$(CONFIG_VMINFO_SYSCALL) += vminfo.o

A questo punto siamo al sorgente.
Aggiungere un file chiamato vminfo.h in include/linux/

C:
struct vminfo_struct {
    unsigned long pgd;
    unsigned long p4d;
    unsigned long pud;
    unsigned long pmd;
    unsigned long pte;
};

Creare un file chiamato vminfo.c all'interno di memory/ incollando:

C:
#include <linux/syscalls.h>
#include <linux/mm.h>
#include <linux/kernel.h>
#include <linux/vminfo.h>
#include <linux/pgtable.h>
#include <linux/slab.h>
#include <linux/highmem.h>

#include <asm/processor.h>
#include <asm/io.h>
#include <asm/page.h>


static int bad_address(void *p)
{
        unsigned long dummy;

        return get_kernel_nofault(dummy, (unsigned long *)p);
}

static int get_pte(struct mm_struct *mm, unsigned long address, struct vminfo_struct *vminfo_kern, pte_t *pte_v) {
        pgd_t *pgd;
        p4d_t *p4d;
        pud_t *pud;
        pmd_t *pmd;
        pte_t *pte;

        pgd = pgd_offset(mm, address);

        if(pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
                return -EINVAL;

        p4d = p4d_offset(pgd, address);
        if(p4d_none(*p4d) || unlikely(p4d_bad(*p4d)))
                return -EINVAL;

        pud = pud_offset(p4d, address);
        if(pud_none(*pud) || unlikely(pud_bad(*pud)))
                return -EINVAL;

        pmd = pmd_offset(pud, address);
        if(pmd_none(*pmd) || unlikely(pmd_bad(*pmd)))
                return -EINVAL;

        pte = pte_offset_kernel(pmd, address);
        if(bad_address(pte))
                return -EINVAL;

        vminfo_kern->pgd = pgd_val(*pgd);
        vminfo_kern->p4d = p4d_val(*p4d);
        vminfo_kern->pud = pud_val(*pud);
        vminfo_kern->pmd = pmd_val(*pmd);
        vminfo_kern->pte = pte_val(*pte);

        *pte_v = *pte;

        pr_info("get_pte completed");

        return 0;
}

static int read_at_page_offset(unsigned long address, pte_t pte_v, void __user *buffer, unsigned int buff_len) {
        pr_info("Try to read memory");

        size_t offset;
        void *kern_buffer;
        void *kaddr;

        kern_buffer = kmalloc(buff_len, GFP_KERNEL);

        kaddr = kmap_local_page(pfn_to_page(pte_pfn(pte_v)));
        if(!kaddr) {
                return -ENOMEM;
        }

        offset = address & (PAGE_SIZE -1);
        pr_info("offset %lu", offset);

        memcpy(kern_buffer, kaddr + offset, buff_len);
        kunmap_local(kaddr);

        if(copy_to_user(buffer, kern_buffer, buff_len)) {
                kfree(kern_buffer);
                return -EFAULT;
        }

        pr_info("copy_to_user of the buffer worked successfully");

        kfree(kern_buffer);

        return 0;
}

SYSCALL_DEFINE5(vminfo, unsigned long, address, pid_t, pid, struct vminfo_struct __user *, vminfo, void __user *, buffer, unsigned int, buff_len) {
        pr_info("vminfo SYSCALL, pid %d address %lu",pid,address);

        struct vminfo_struct vminfo_tmp;
        struct task_struct *task;
        pte_t pte_v;
        int err;

        task = pid_task(find_vpid(pid), PIDTYPE_PID);
        if(!task) {
                err = -ESRCH;
                goto error;
        }

        pr_info("Task status: %u", task->__state);
        get_task_struct(task);

        if(get_pte(task->mm, address, &vminfo_tmp, &pte_v)) {
                err = -EINVAL;
                goto clean_error;
        }

        if(read_at_page_offset(address, pte_v, buffer, buff_len)) {
                err = -ENOMEM;
                goto clean_error;
        }

        put_task_struct(task);

        if(copy_to_user(vminfo, &vminfo_tmp, sizeof(vminfo_tmp))) {
                err = -EFAULT;
                goto error;
        }

        return 0;

clean_error:
        put_task_struct(task);

error:
        pr_err("SYSCALL_VMINFO error: %d", err);
        return err;
}

A questo punto ci siamo...

2 - Spiegazione del codice​


Questa è la messa in pratica di quanto detto sopra.
SYSCALL_DEFINE5(vminfo....) è la syscall. Questa riceve alcuni parametri: il PID del processo target, un indirizzo di memoria del processo target, struttura vminfo per memorizzare tutte le informazioni del page walk (la traduzione), un buffer di output, e la lunghezza del buffer di output.

Ciò che viene fatto è cercare la task_struct che corrisponde al PID; ottenuto il task viene incrementato un reference counter (vabbhe, un contatore, per dire che stiamo usando quel task) e poi c'è il page walk, effettuato da get_pte()!

Notare infatti che get_pte() riceve un address (indirizzo virtuale) e ricava la PGD (che si trova, come detto sopra, nella struttura mm_struct). I nomi purtroppo non sono gli stessi usati da Intel, ma si può osservare che ad ogni funzione con suffisso _offset() viene passato l'indirizzo virtuale e l'indirizzo della tabella precedente (che costituisce la base della tabella).
Quindi passando per esempio:

pud_offset(p4d, address);

viene presa la entry dalla tabella P4D (usando alcuni bit di address, come visto sopra) e questa entry sarà la base della tabella successiva (PUD in questo caso).

3 - Sorgenti di test​


Il programma vittima non fa altro che rimanere in esecuzione e attendere un input ("invio") per generare un numero random da aggiungere nel buffer (un array):

C:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>

int main(int argc, char *argv[]) {
        if(argc != 2) {
                printf("Usage: ./test <n_of_elements>");
                return -1;
        }

        srand(time(NULL));
        
        int n_elements = atoi(argv[1]);
        int malloc_size = n_elements * sizeof(int);

        int *buffer = malloc(malloc_size);
        memset(buffer, 0, malloc_size);

        printf("PID: %d\n", getpid());
        printf("address of buffer: %ld, buffer size: %d, n_elements: %d\n", buffer, malloc_size, n_elements);

        for(int i=0; i < n_elements; i++) {
                buffer[i] = rand() % 100;
                printf("[%d] = %d\n", i, buffer[i]);

                char dummy;
                scanf("%c", &dummy);
        }

        free(buffer);

        return 0;

}

L'altro programma fa uso della syscall e riceve in input i dati del programma test; per essere chiari, riceve come input il PID del programma test, l'indirizzo della variabile "buffer", la dimensione degli elmeneti e la lunghezza del buffer:

C:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>

int main(int argc, char *argv[]) {
        if(argc != 2) {
                printf("Usage: ./test <n_of_elements>");
                return -1;
        }

        srand(time(NULL));
        
        int n_elements = atoi(argv[1]);
        int malloc_size = n_elements * sizeof(int);

        int *buffer = malloc(malloc_size);
        memset(buffer, 0, malloc_size);

        printf("PID: %d\n", getpid());
        printf("address of buffer: %ld, buffer size: %d, n_elements: %d\n", buffer, malloc_size, n_elements);

        for(int i=0; i < n_elements; i++) {
                buffer[i] = rand() % 100;
                printf("[%d] = %d\n", i, buffer[i]);

                char dummy;
                scanf("%c", &dummy);
        }

        free(buffer);

        return 0;

}

4 - Test del codice​


Veniamo alla parte più importante, il test della syscall vista sopra.
Io sto usando virtme-ng, molto comodo per questo tipo di cose. Uso una sola shell per fare tutto, quindi mi trovo costretto a mettere in background il primo task, ./test:

Codice:
user@localhost:~/kernel/linux/linux-6.8> vng
          _      _
   __   _(_)_ __| |_ _ __ ___   ___       _ __   __ _
   \ \ / / |  __| __|  _   _ \ / _ \_____|  _ \ / _  |
    \ V /| | |  | |_| | | | | |  __/_____| | | | (_| |
     \_/ |_|_|   \__|_| |_| |_|\___|     |_| |_|\__  |
                                                |___/
   kernel version: 6.8.0-1-default-virtme x86_64
   (CTRL+d to exit)

user@virtme-ng:~/kernel/linux/linux-6.8> cd ../../
user@virtme-ng:~/kernel> mkdir data && mkfifo data/in
user@virtme-ng:~/kernel> sleep infinity > data/in &
[1] 442
user@virtme-ng:~/kernel> ./test 10 < data/in &
[2] 448
user@virtme-ng:~/kernel> PID: 448
address of buffer: 24801952, buffer size: 40, n_elements: 10
[0] = 37

Qui nell'output abbiamo tutte le info da dare in input a vminfo, e quindi possiamo procedere:
Codice:
user@virtme-ng:~/kernel> ./vminfo 448 24801952 40 10
[ pid: 448, addr: 24801952, 0x17a72a0, size of data: 40, n_elements 10]

Starting Page Structures walk...

PGD: 0x38bf8067 P4D: 0x38bf8067 PUD: 0x3f03b067 PMD: 0x5e24067 PTE: 0x8000000003213867
Buffer content:
[0] = 37
[1] = 0
[2] = 0
[3] = 0
[4] = 0
[5] = 0
[6] = 0
[7] = 0
[8] = 0
[9] = 0

RET syscall: 0, errno: 0

Enter to keep reading the address, or CTRL+C

Ok, come si vede, la lettura è andata a buon fine: nell'altro output vediamo il numero 37, che è presente anche in vminfo a seguito dalla lettura all'indirizzo passato.
Inseriamo dell'input in ./test:

Codice:
user@virtme-ng:~/kernel> echo '..' > data/in 
[1] = 14
[2] = 31
[3] = 14

Quindi i numeri generati sono 13, 31 e ancora 14. Ora eseguiamo nuovamente vminfo:

Codice:
user@virtme-ng:~/kernel> ./vminfo 448 24801952 40 10
[ pid: 448, addr: 24801952, 0x17a72a0, size of data: 40, n_elements 10]

Starting Page Structures walk...

PGD: 0x38bf8067 P4D: 0x38bf8067 PUD: 0x3f03b067 PMD: 0x5e24067 PTE: 0x8000000003213867
Buffer content:
[0] = 37
[1] = 14
[2] = 31
[3] = 14
[4] = 0
[5] = 0
[6] = 0
[7] = 0
[8] = 0
[9] = 0

RET syscall: 0, errno: 0

Enter to keep reading the address, or CTRL+C

Come si vede, leggendo l'intero buffer, abbiamo tutti i numeri situati in memoria nell'altro programma!

./vminfo grazie al parametro che passa alla syscall ha tutto il page walk effettuato, che per curiosità, può essere visto nell'output sopra (PGD, P4D, PUD,... etc).

Topic che ho sempre trovato interessante, spero lo sia stato anche per voi, e spero che il contenuto dell'articolo sia stato comprensibile :)
 

Guerriero con mazza

Utente Attivo
536
621
CPU
i7-3770K
Dissipatore
Thermalright Macho Rev.B HR-02
Scheda Madre
MSI Z77A-GD65
HDD
SSD: Samsung 870Evo 500GB || HDD: WD Caviar Blue 1TB + WD Caviar Black 2TB
RAM
Kingston HyperX Beast DDR3 2x8GB 1866MHz
GPU
MSI RX 6600 MECH 2X - 8GB DDR6
Audio
Sound Blaster Z
Monitor
AOC 24G2SPAE
PSU
EVGA SuperNOVA 650 GS 80+Gold
Case
Corsair 400R
OS
Windows 10 Pro
Prima il like per l'ottimo lavoro svolto, poi leggiamo con calma!! xD
 

DispatchCode

Moderatore
Staff Forum
Utente Èlite
2,251
1,881
CPU
Intel I9-10900KF 3.75GHz 10x 125W
Dissipatore
Gigabyte Aorus Waterforce X360 ARGB
Scheda Madre
Asus 1200 TUF Z590-Plus Gaming ATX DDR4
HDD
1TB NVMe PCI 3.0 x4, 1TB 7200rpm 64MB SATA3
RAM
DDR4 32GB 3600MHz CL18 ARGB
GPU
Nvidia RTX 3080 10GB DDR6
Audio
Integrata 7.1 HD audio
Monitor
LG 34GN850
PSU
Gigabyte P850PM
Case
Phanteks Enthoo Evolv X ARGB
Periferiche
MSI Vigor GK30, mouse Logitech
Net
FTTH Aruba, 1Gb (effettivi: ~950Mb / ~480Mb)
OS
Windows 10 64bit / OpenSUSE Tumbleweed
Prima il like per l'ottimo lavoro svolto, poi leggiamo con calma!! xD

Ti ringrazio! 😄

Se hai domande o non sono stato chiaro, commenta pure. Ho scritto l'articolo in circa 1h e mezza, quindi è probabile che specie nella prima parte mi sia perso un pò più del dovuto...
 

Entra

oppure Accedi utilizzando
Discord Ufficiale Entra ora!