Guida Leggere memoria da un altro task in usermode (aggiungendo una nuova syscall)

DispatchCode

Moderatore
24 Maggio 2016
507
15
382
264
Quando un'applicazione in modalità utente accede a un indirizzo di memoria virtuale, questo fa parte dello spazio degli indirizzi di quel processo. Ogni task in modalità utente ha il proprio spazio degli indirizzi: la traduzione da un indirizzo virtuale a un indirizzo fisico inizia dal valore di CR3. Quanto riportato di seguito vale per Linux e in x64.

Utilizzando questa syscall è possibile leggere la memoria di un altro task in esecuzione.
Ho scritto il codice solo come esempio, quindi è tutt'altro che privo di bugs.

1    Aggiunta della syscall



Io ho scelto di aggiungere una mia cartella al sorgente Linux, chiamata memory/.

Gli step sono descritti di seguito:
  • aggiungere al file Kbuild la nuova cartella (io ho aggiunto questa riga sotto a kernel/):
    • obj-y += memory/
  • se si vuole aggiungere la propria configurazione (per disabilitare la syscall in fase di compilazione, eventualmente) creare anche un file chiamato Kconfig all'interno di memory/, con il seguente contenuto: source "memory/Kconfig"
Sotto a include/linux/vminfo.h aggiungere la struttura:

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

Creare il file vminfo.c all'interno della cartella memory/, con il seguente contenuto:

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 error;
        }
        if(read_at_page_offset(address, pte_v, buffer, buff_len)) {
                err = -ENOMEM;
                goto error;
        }

        put_task_struct(task);

        if(copy_to_user(vminfo, &vminfo_tmp, sizeof(vminfo_tmp))) {
                return -EFAULT;
        }

        pr_info("Copy to user worked successfully");

        return 0;

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

}

Se si vuole la configurazione menzionata sopra, creare un file chiamato Kconfig all'interno di memory/ con questo contenuto:

Codice:
menu "Hack the process address space"

config VMINFO_SYSCALL
       prompt "Hack the process address space"
       def_bool y
       help
         Walk the page table starting from an address in the
         current address space or another one

endmenu

Aggiungere COND_SYSCALL(vminfo); al file kernel/sys_ni.c
Aggiungere la syscall alla tabella situata nel file arch/x86/entry/syscalls/syscall_64.tbl, nel mio caso:

Codice:
548    64      vminfo                  sys_vminfo

Aggiungere la dichiarazione in include/linux/syscalls.h:

Codice:
asmlinkage long sys_vminfo(unsigned long address, pid_t pid, struct vminfo_struct __user *vminfo, void __user *buffer, unsigned int buff_len);

E infine creare il Makefile in memory/ con il seguente contenuto:

Codice:
obj-$(CONFIG_VMINFO_SYSCALL) += vminfo.o

2    Sorgenti usati a scopo di test



Per provare la syscall abbiamo bisogno di due task: uno in esecuzione, che attende un input (fittizio, giusto per bloccare il loop); e il secondo, che andrà a leggere a uno specifico indirizzo in questo task.

Il task di test (che verrà letto) è:

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;
}

Mentre l'altro task:

C:
#include <stdio.h>
#include <stdlib.h> 
#include <errno.h> 

#include <linux/kernel.h> 
#include <sys/syscall.h> 
#include <unistd.h> 
#include <linux/unistd.h> 
#include <linux/types.h>

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


int main(int argc, char *argv[]) {
        if(argc != 5) {
                printf("Usage: ./vminfo <pid> <address> <len_buffer> <n_elements>\n");
                exit(1);
        }

        struct vminfo_struct vminfo;
        pid_t pid;
        unsigned long address;
        // Size of the variable to access
        int buffer_len, n_elements;

        pid =  atoi(argv[1]);
        address = atol(argv[2]);
        buffer_len = atoi(argv[3]);
        n_elements = atoi(argv[4]);
        
        // stack allocation, but will be the same with malloc
        void *buffer = malloc(buffer_len);
        
        printf("[ pid: %d, addr: %lu, 0x%llx, size of data: %d, n_elements %d]\n\n", pid, address, address, buffer_len, n_elements);
        printf("Starting Page Structures walk...\n\n");

        char dummy;
        int *buff = (int*) buffer;

        while(1) {
                int ret = syscall(548, address, pid, &vminfo, buffer, buffer_len); 

                printf("PGD: 0x%llx P4D: 0x%llx PUD: 0x%llx PMD: 0x%llx PTE: 0x%llx\n",
                        vminfo.pgd, vminfo.p4d, vminfo.pud, vminfo.pmd, vminfo.pte);
       
                printf("Buffer content:\n");
                for(int i=0; i<n_elements; i++) {
                        printf("[%d] = %d\n", i, buff[i]);
                }

                printf("\nRET syscall: %d, errno: %d\n\n", ret, errno);
                printf("Enter to keep reading the address, or CTRL+C");
                scanf("%c",&dummy);
        }

        return 0; 
}

Io ho scelto di usare un buffer, andando a leggere in pratica un array di interi.

3    Eseguire il test



Ho preferito usare virtme-ng per compilare ed eseguire le prove; per far ciò ho usato una singola shell, quindi ho dovuto mettere il task in background.

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

A questo punto avviamo il secondo task:

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

Come si nota compare un 37, che è il valore generato randomicamente e visualizzato nell'output sopra.
Ho scritto qualche altro carattere in data/in (usando un echo), così da avere qualche altro valore (non è un vero new line, lo so):

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

Eseguendo di nuovo 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 nota ha letto con successo i nuovi valori.

4    Spiegazione della syscall



Non penso sia immediata la comprensione, quindi cerco di spiegare brevemente il funzionamento.

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

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

In questa parte viene ovviamente preso il task (task_struct) partendo dal PID.

C:
        get_task_struct(task);

        if(get_pte(task->mm, address, &vminfo_tmp, &pte_v)) {
                err = -EINVAL;
                goto error;
        }
        if(read_at_page_offset(address, pte_v, buffer, buff_len)) {
                err = -ENOMEM;
                goto error;
        }

        put_task_struct(task);

Questa parte di codice è molto importante. La funzione get_task_struct() è utilizzata in quanto incrementa il refcounter del task, in pratica stiamo dicendo che stiamo usando quel task. put_task_struct() decrementa questo contatore, è l'operazione inversa.

get_pte() effettua quello che viene definito "page walk":

C:
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;
}

ciò che viene fatto è quindi usare "mm", che è di tipo mm_struct, e che appartiene al task, per accedere al campo pgd che viene memorizzato poi nella variabile pgd.
A questo punto si procede con il vero e proprio page walk nelle varie strutture (come si nota, assumo la presenza di determinati livelli di tabelle).

Arrivato al termine, abbiamo il valore del PTE. Dal PTE si può ottenere la pagina, ed è ciò che avviene nell'altra funzione:

C:
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;
}

viene allocato un buffer di lunghezza adeguata in kernel space, prima di tutto. Fatto ciò, il PTE ottenuto prima viene usato per ottenere il Page Frame Number (PFN); si tratta di uno shift del valore del PTE (per farla breve). Questo numero è poi usato per ottenere la pagina.

Questa pagina viene mappata nello spazio degli indirizi temporaneamente (per permettere l'accesso) e in seguito avviene la copia da un determinato offset (ottenuto dall'indirizzo virtuale medesimo, in quanto è proprio l'offset nella pagina) in direzione del buffer allocato.

A questo punto il buffer in kernel space è pieno e può essere copiato nel buffer utente (allocato in user mode).

La parte finale copia poi i valori del page walk all'interno della struttura:

C:
        if(copy_to_user(vminfo, &vminfo_tmp, sizeof(vminfo_tmp))) {
                return -EFAULT;
        }

Questi campi sono poi quelli che vediamo stampati da vminfo qui:

Codice:
PGD: 0x38bf8067 P4D: 0x38bf8067 PUD: 0x3f03b067 PMD: 0x5e24067 PTE: 0x8000000003213867

Ho scritto un pò di fretta e traducendone una parte dall'inglese (e scrivendo direttamente l'altra metà qui); spero sia tutto comprensibile, se avete domande fate pure. :D
 
  • Love
Reazioni: JunkCoder