¿Cómo leo desde / proc / $ pid / mem en Linux?

142

La página de manual de Linuxproc(5) me dice que /proc/$pid/mem"se puede usar para acceder a las páginas de la memoria de un proceso". Pero un intento directo de usarlo solo me da

$ cat /proc/$$/mem /proc/self/mem
cat: /proc/3065/mem: No such process
cat: /proc/self/mem: Input/output error

¿Por qué no catpuede imprimir su propia memoria ( /proc/self/mem)? ¿Y cuál es este extraño error de "no hay tal proceso" cuando intento imprimir la memoria del shell ( /proc/$$/mem, obviamente, el proceso existe)? ¿Cómo puedo leer /proc/$pid/mem, entonces?

Gilles
fuente
1
Hay varios otros métodos que muestran cómo hacer esto en SF en este Q&A titulado:
Volcar
respuesta
pizdelect

Respuestas:

140

/proc/$pid/maps

/proc/$pid/memmuestra el contenido de la memoria de $ pid mapeado de la misma manera que en el proceso, es decir, el byte en el desplazamiento x en el pseudoarchivo es el mismo que el byte en la dirección x en el proceso. Si una dirección no está asignada en el proceso, la lectura del desplazamiento correspondiente en el archivo devuelve EIO(error de entrada / salida). Por ejemplo, dado que la primera página de un proceso nunca se asigna (por lo que la eliminación de la referencia a un NULLpuntero falla de manera limpia en lugar de acceder involuntariamente a la memoria real), leer el primer byte /proc/$pid/memsiempre produce un error de E / S.

La manera de averiguar qué partes de la memoria de proceso están mapeadas es leer /proc/$pid/maps. Este archivo contiene una línea por región asignada, con este aspecto:

08048000-08054000 r-xp 00000000 08:01 828061     /bin/cat
08c9b000-08cbc000 rw-p 00000000 00:00 0          [heap]

Los dos primeros números son los límites de la región (direcciones del primer byte y el byte después del último, en hexa). La siguiente columna contiene los permisos, luego hay información sobre el archivo (desplazamiento, dispositivo, inodo y nombre) si se trata de una asignación de archivo. Consulte la proc(5)página de manual o Understanding Linux / proc / id / maps para obtener más información.

Aquí hay un script de prueba de concepto que volca el contenido de su propia memoria.

#! /usr/bin/env python
import re
maps_file = open("/proc/self/maps", 'r')
mem_file = open("/proc/self/mem", 'r', 0)
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
        print chunk,  # dump contents to standard output
maps_file.close()
mem_file.close()

/proc/$pid/mem

Si intentas leer el mempseudoarchivo de otro proceso, no funciona: obtienes un ESRCHerror (No hay tal proceso).

Los permisos en /proc/$pid/mem( r--------) son más liberales de lo que debería ser el caso. Por ejemplo, no debería poder leer la memoria de un proceso setuid. Además, tratar de leer la memoria de un proceso mientras el proceso lo modifica podría darle al lector una visión inconsistente de la memoria, y lo que es peor, había condiciones de carrera que podían rastrear versiones anteriores del kernel de Linux (de acuerdo con este hilo lkml , aunque yo No sé los detalles). Por lo tanto, se necesitan verificaciones adicionales:

  • El proceso que desea leer /proc/$pid/memdebe adjuntarse al proceso usando ptracecon la PTRACE_ATTACHbandera. Esto es lo que hacen los depuradores cuando comienzan a depurar un proceso; también es lo que stracehace a las llamadas al sistema de un proceso. Una vez que el lector haya terminado de leer /proc/$pid/mem, debe separarse llamando ptracecon la PTRACE_DETACHbandera.
  • El proceso observado no debe estar ejecutándose. Normalmente, la llamada ptrace(PTRACE_ATTACH, …)detendrá el proceso de destino (envía una STOPseñal), pero hay una condición de carrera (la entrega de la señal es asíncrona), por lo que el rastreador debe llamar wait(como se documenta en ptrace(2)).

Un proceso que se ejecuta como root puede leer la memoria de cualquier proceso, sin necesidad de llamar ptrace, pero el proceso observado debe detenerse o la lectura aún regresará ESRCH.

En la fuente del kernel de Linux, el código que proporciona entradas por proceso /procestá en fs/proc/base.c, y la función para leer /proc/$pid/memes mem_read. La verificación adicional es realizada por check_mem_permission.

Aquí hay un código C de muestra para adjuntar a un proceso y leer un fragmento de su memarchivo (se omite la comprobación de errores):

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

Ya publiqué un script de prueba de concepto para descargar /proc/$pid/memen otro hilo .

Gilles
fuente
2
@abc No, leer /proc/$pid/memdirectamente (ya sea con cato con ddcualquier otra cosa) no funciona. Lee mi respuesta.
Gilles
44
@abc Él está leyendo /proc/self/mem. Un proceso puede leer su propio espacio de memoria bien, está leyendo el espacio de memoria de otro proceso que requiere PTRACE_ATTACH.
Gilles
2
Tenga en cuenta que con los núcleos de Linux recientes, no necesita PTRACE_ATTACH. Este cambio viene con la process_vm_readv()llamada al sistema (Linux 3.2).
ysdx
2
Hm, con Linux 4.14.8 esto funciona para mí: iniciar un proceso de larga ejecución que esté ocupado escribiendo resultados en / dev / null. Luego, otro proceso puede abrir, buscar y leer algunos bytes de / proc / $ otherpid / mem (es decir, en algunos desplazamientos a los que se hace referencia a través del vector auxiliar), sin tener que rastrear-adjuntar / separar o detener / iniciar el proceso. Funciona si el proceso se ejecuta con el mismo usuario y para el usuario raíz. Es decir, no puedo producir un ESRCHerror en este escenario.
maxschlepzig
1
@maxschlepzig Supongo que ese es el cambio mencionado por ysdx en el comentario anterior.
Gilles
28

Este comando (de gdb) volca la memoria de manera confiable:

gcore pid

Los volcados pueden ser grandes, úselos -o outfilesi su directorio actual no tiene suficiente espacio.

Tobu
fuente
12

Cuando ejecuta cat /proc/$$/memla variable $$es evaluada por bash que inserta su propio pid. Luego ejecuta lo catque tiene un pid diferente. Terminas cattratando de leer la memoria de bashsu proceso padre. Dado que los procesos no privilegiados solo pueden leer su propio espacio de memoria, el núcleo lo niega.

Aquí hay un ejemplo:

$ echo $$
17823

Tenga en cuenta que se $$evalúa a 17823. Veamos qué proceso es ese.

$ ps -ef | awk '{if ($2 == "17823") print}'
bahamat  17823 17822  0 13:51 pts/0    00:00:00 -bash

Es mi caparazón actual.

$ cat /proc/$$/mem
cat: /proc/17823/mem: No such process

Aquí nuevamente se $$evalúa a 17823, que es mi caparazón. catNo puedo leer el espacio de memoria de mi shell.

bahamat
fuente
Terminas tratando de leer el recuerdo de lo que $pidsea. Como explico en mi respuesta, leer la memoria de un proceso diferente requiere que lo trates.
Gilles
Lo que va a ser bash. No estaba diciendo que tu respuesta fuera incorrecta. Estaba respondiendo en términos más simples "por qué esto no funciona".
bahamat
@bahamat: ¿Estás pensando $$cuando escribes (y lees) $pid?
Gilles
Sí ... comenzó preguntando refiriéndose $$y poniendo $pidal final. Lo transpuse en mi cabeza sin darme cuenta. Toda mi respuesta debería referirse $$, no $pid.
bahamat
@bahamat: ¿La pregunta es más clara ahora? (Por cierto, no veo tus comentarios a menos que uses "@Gilles", simplemente vi tu edición y vine a verla.)
Gilles
7

Aquí hay un pequeño programa que escribí en C:

Uso:

memdump <pid>
memdump <pid> <ip-address> <port>

El programa usa / proc / $ pid / maps para encontrar todas las regiones de memoria mapeadas del proceso, y luego lee esas regiones de / proc / $ pid / mem, una página a la vez. esas páginas se escriben en stdout o la dirección IP y el puerto TCP que especificó.

Código (probado en Android, requiere permisos de superusuario):

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <sys/ptrace.h>
#include <sys/socket.h>
#include <arpa/inet.h>

void dump_memory_region(FILE* pMemFile, unsigned long start_address, long length, int serverSocket)
{
    unsigned long address;
    int pageLength = 4096;
    unsigned char page[pageLength];
    fseeko(pMemFile, start_address, SEEK_SET);

    for (address=start_address; address < start_address + length; address += pageLength)
    {
        fread(&page, 1, pageLength, pMemFile);
        if (serverSocket == -1)
        {
            // write to stdout
            fwrite(&page, 1, pageLength, stdout);
        }
        else
        {
            send(serverSocket, &page, pageLength, 0);
        }
    }
}

int main(int argc, char **argv) {

    if (argc == 2 || argc == 4)
    {
        int pid = atoi(argv[1]);
        long ptraceResult = ptrace(PTRACE_ATTACH, pid, NULL, NULL);
        if (ptraceResult < 0)
        {
            printf("Unable to attach to the pid specified\n");
            return;
        }
        wait(NULL);

        char mapsFilename[1024];
        sprintf(mapsFilename, "/proc/%s/maps", argv[1]);
        FILE* pMapsFile = fopen(mapsFilename, "r");
        char memFilename[1024];
        sprintf(memFilename, "/proc/%s/mem", argv[1]);
        FILE* pMemFile = fopen(memFilename, "r");
        int serverSocket = -1;
        if (argc == 4)
        {   
            unsigned int port;
            int count = sscanf(argv[3], "%d", &port);
            if (count == 0)
            {
                printf("Invalid port specified\n");
                return;
            }
            serverSocket = socket(AF_INET, SOCK_STREAM, 0);
            if (serverSocket == -1)
            {
                printf("Could not create socket\n");
                return;
            }
            struct sockaddr_in serverSocketAddress;
            serverSocketAddress.sin_addr.s_addr = inet_addr(argv[2]);
            serverSocketAddress.sin_family = AF_INET;
            serverSocketAddress.sin_port = htons(port);
            if (connect(serverSocket, (struct sockaddr *) &serverSocketAddress, sizeof(serverSocketAddress)) < 0)
            {
                printf("Could not connect to server\n");
                return;
            }
        }
        char line[256];
        while (fgets(line, 256, pMapsFile) != NULL)
        {
            unsigned long start_address;
            unsigned long end_address;
            sscanf(line, "%08lx-%08lx\n", &start_address, &end_address);
            dump_memory_region(pMemFile, start_address, end_address - start_address, serverSocket);
        }
        fclose(pMapsFile);
        fclose(pMemFile);
        if (serverSocket != -1)
        {
            close(serverSocket);
        }

        ptrace(PTRACE_CONT, pid, NULL, NULL);
        ptrace(PTRACE_DETACH, pid, NULL, NULL);
    }
    else
    {
        printf("%s <pid>\n", argv[0]);
        printf("%s <pid> <ip-address> <port>\n", argv[0]);
        exit(0);
    }
}
Tal Aloni
fuente
55
Agregue alguna explicación de su código. Su único comentario no tiene sentido: write to stdoutinmediatamente arriba fwrite(..., stdout). Ver programmers.stackexchange.com/questions/119600/…
muru
Dijiste que sólo se probó en Android, así que sólo quería confirmar, funciona bien en Linux 4.4.0-28 x86_64, como era de esperar
albaricoque niño
obtengo un montón de datos como / @ 8 l / @ l en stdout que nunca termina la idea de por qué. compilado en Linux 4.9.0-3-amd64 # 1 SMP Debian 4.9.25-1 (2017-05-02) x86_64 Modelo de hilo GNU / Linux: posix gcc versión 6.3.0 20170516 (Debian 6.3.0-18)
ceph3us
ceph3us, el uso común es canalizar los datos a un archivo (por ejemplo, memdump <pid>> /sdcard/memdump.bin)
Tal Aloni