/proc/$pid/maps
/proc/$pid/mem
mostra il contenuto della memoria di $pid mappato allo stesso modo del processo, cioè il byte all'offset x nello pseudo-file è uguale al byte all'indirizzo x nel processo. Se un indirizzo non è mappato nel processo, la lettura dall'offset corrispondente nel file restituisce EIO
(Errore di ingresso/uscita). Ad esempio, poiché la prima pagina in un processo non viene mai mappata (in modo che la dereferenziazione di un NULL
puntatore fallisce in modo pulito piuttosto che accedere involontariamente alla memoria effettiva), leggendo il primo byte di /proc/$pid/mem
restituiscono sempre un errore di I/O.
Il modo per scoprire quali parti della memoria del processo sono mappate è leggere /proc/$pid/maps
. Questo file contiene una riga per regione mappata, simile a questa:
08048000-08054000 r-xp 00000000 08:01 828061 /bin/cat
08c9b000-08cbc000 rw-p 00000000 00:00 0 [heap]
I primi due numeri sono i confini della regione (indirizzi del primo byte e del byte dopo l'ultimo, in esadecimale). La colonna successiva contiene i permessi, quindi ci sono alcune informazioni sul file (offset, dispositivo, inode e nome) se si tratta di un file mapping. Vedi il proc(5)
pagina man o Understanding Linux /proc/id/maps per ulteriori informazioni.
Ecco uno script proof-of-concept che scarica il contenuto della propria memoria.
#! /usr/bin/env python
import re
maps_file = open("/proc/self/maps", 'r')
mem_file = open("/proc/self/mem", 'rb', 0)
output_file = open("self.dump", 'wb')
for line in maps_file.readlines(): # for each mapped region
m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line)
if m.group(3) == 'r': # if this is a readable region
start = int(m.group(1), 16)
end = int(m.group(2), 16)
mem_file.seek(start) # seek to region start
chunk = mem_file.read(end - start) # read region contents
output_file.write(chunk) # dump contents to standard output
maps_file.close()
mem_file.close()
output_file.close()
/proc/$pid/mem
[Quanto segue è per interesse storico. Non si applica ai kernel correnti.]
Dalla versione 3.3 del kernel, puoi accedere a /proc/$pid/mem
normalmente fintanto che accedi accedi solo agli offset mappati e hai il permesso di rintracciarlo (stesse autorizzazioni di ptrace
per l'accesso in sola lettura). Ma nei kernel più vecchi c'erano alcune complicazioni aggiuntive.
Se provi a leggere dal mem
pseudo-file di un altro processo, non funziona:ottieni un ESRCH
(Nessun processo di questo tipo) errore.
I permessi su /proc/$pid/mem
(r--------
) sono più liberali di quanto dovrebbe essere. Ad esempio, non dovresti essere in grado di leggere la memoria di un processo setuid. Inoltre, provare a leggere la memoria di un processo mentre il processo la sta modificando potrebbe dare al lettore una visione incoerente della memoria e, peggio ancora, c'erano race condition che potevano rintracciare versioni precedenti del kernel Linux (secondo questo thread lkml, sebbene io non conosco i dettagli). Quindi sono necessari ulteriori controlli:
- Il processo che vuole leggere da
/proc/$pid/mem
deve allegare al processo utilizzandoptrace
con ilPTRACE_ATTACH
bandiera. Questo è ciò che fanno i debugger quando iniziano a eseguire il debug di un processo; è anche ciò chestrace
fa alle chiamate di sistema di un processo. Una volta che il lettore ha finito di leggere da/proc/$pid/mem
, dovrebbe staccarsi chiamandoptrace
conPTRACE_DETACH
bandiera. - Il processo osservato non deve essere in esecuzione. Normalmente chiama
ptrace(PTRACE_ATTACH, …)
interromperà il processo di destinazione (invia unSTOP
segnale), ma c'è una race condition (la consegna del segnale è asincrona), quindi il tracciante dovrebbe chiamarewait
(come documentato inptrace(2)
).
Un processo eseguito come root può leggere la memoria di qualsiasi processo, senza dover chiamare ptrace
, ma il processo osservato deve essere interrotto, altrimenti la lettura restituirà comunque ESRCH
.
Nel sorgente del kernel Linux, il codice che fornisce voci per processo in /proc
è in fs/proc/base.c
e la funzione per leggere da /proc/$pid/mem
è mem_read
. Il controllo aggiuntivo viene eseguito da check_mem_permission
.
Ecco alcuni esempi di codice C da allegare a un processo e leggerne un pezzo di mem
file (controllo degli errori omesso):
sprintf(mem_file_name, "/proc/%d/mem", pid);
mem_fd = open(mem_file_name, O_RDONLY);
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
waitpid(pid, NULL, 0);
lseek(mem_fd, offset, SEEK_SET);
read(mem_fd, buf, _SC_PAGE_SIZE);
ptrace(PTRACE_DETACH, pid, NULL, NULL);
Ho già pubblicato uno script proof-of-concept per scaricare /proc/$pid/mem
su un altro thread.
Questo comando (da gdb) scarica la memoria in modo affidabile:
gcore pid
I dump possono essere grandi, usa -o outfile
se la tua directory attuale non ha abbastanza spazio.
Quando esegui cat /proc/$$/mem
la variabile $$
viene valutato da bash che inserisce il proprio pid. Quindi esegue cat
che ha un pid diverso. Ti ritroverai con cat
cercando di leggere la memoria di bash
, il suo processo genitore. Poiché i processi non privilegiati possono solo leggere il proprio spazio di memoria, questo viene negato dal kernel.
Ecco un esempio:
$ echo $$
17823
Nota che $$
restituisce 17823. Vediamo di che processo si tratta.
$ ps -ef | awk '{if ($2 == "17823") print}'
bahamat 17823 17822 0 13:51 pts/0 00:00:00 -bash
È il mio guscio attuale.
$ cat /proc/$$/mem
cat: /proc/17823/mem: No such process
Anche qui $$
valuta 17823, che è la mia shell. cat
non riesco a leggere lo spazio di memoria della mia shell.