Comment lire depuis / proc / $ pid / mem sous Linux?

142

La page de manuel Linuxproc(5) me dit que /proc/$pid/mem"peut être utilisé pour accéder aux pages de la mémoire d'un processus". Mais une simple tentative d'utilisation ne me donne que

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

Pourquoi ne catpeut-il pas imprimer sa propre mémoire ( /proc/self/mem)? Et quelle est cette étrange erreur «pas de tel processus» lorsque j'essaie d'imprimer la mémoire du shell ( /proc/$$/mem, évidemment, le processus existe)? Comment puis-je lire /proc/$pid/mem, alors?

Gilles
la source
1
Il existe plusieurs autres méthodes qui montrent comment faire cela sur SF dans cette Q & R intitulée:
Vider
réponse
pizdelect

Réponses:

140

/proc/$pid/maps

/proc/$pid/memaffiche le contenu de la mémoire de $ pid mappée de la même manière que dans le processus, c'est-à-dire que l'octet au décalage x dans le pseudo-fichier est le même que l'octet à l'adresse x dans le processus. Si une adresse n'est pas mappée dans le processus, la lecture de l'offset correspondant dans le fichier est renvoyée EIO(erreur d'entrée / sortie). Par exemple, étant donné que la première page d'un processus n'est jamais mappée (de sorte que la déréférencement d'un NULLpointeur échoue proprement plutôt que d'accéder involontairement à la mémoire réelle), la lecture du premier octet de /proc/$pid/memtoujours génère une erreur d'entrée / sortie.

La manière de savoir quelles parties de la mémoire de processus sont mappées est de lire /proc/$pid/maps. Ce fichier contient une ligne par région mappée, ressemblant à ceci:

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

Les deux premiers chiffres représentent les limites de la région (adresses du premier octet et de l'octet après le dernier, en hexa). La colonne suivante contient les autorisations, puis contient des informations sur le fichier (offset, périphérique, inode et nom) s'il s'agit d'un mappage de fichier. Voir la proc(5)page de manuel ou Comprendre Linux / proc / id / maps pour plus d'informations.

Voici un script de validation de principe qui extrait le contenu de sa propre mémoire.

#! /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 vous essayez de lire le mempseudo-fichier d'un autre processus, cela ne fonctionne pas: vous obtenez une ESRCHerreur (Aucun processus de ce type).

Les autorisations sur /proc/$pid/mem( r--------) sont plus libérales que ce qui devrait être le cas. Par exemple, vous ne devriez pas pouvoir lire la mémoire d'un processus setuid. De plus, essayer de lire la mémoire d’un processus pendant que celui-ci est en train de le modifier pourrait donner au lecteur une vue incohérente de la mémoire, et pire encore, il existait des conditions de concurrence qui pouvaient retracer les anciennes versions du noyau Linux (selon ce fil de discussion lkml , bien que ne connais pas les détails). Des vérifications supplémentaires sont donc nécessaires:

  • Le processus qui veut lire /proc/$pid/memdoit être attaché au processus en utilisant ptraceavec le PTRACE_ATTACHdrapeau. C'est ce que font les débogueurs lorsqu'ils commencent à déboguer un processus. c'est aussi ce qui stracefait aux appels système d'un processus. Une fois que le lecteur a fini de lire /proc/$pid/mem, il devrait se détacher en appelant ptraceavec le PTRACE_DETACHdrapeau.
  • Le processus observé ne doit pas être en cours d'exécution. Normalement, l'appel ptrace(PTRACE_ATTACH, …)arrêtera le processus cible (il enverra un STOPsignal), mais il y a une condition de concurrence critique (la livraison du signal est asynchrone). Le traceur doit donc appeler wait(comme indiqué dans ptrace(2)).

Un processus exécuté en tant que root peut lire la mémoire de n'importe quel processus, sans avoir à appeler ptrace, mais le processus observé doit être arrêté ou la lecture sera toujours renvoyée ESRCH.

Dans la source du noyau Linux, le code fournit des entrées par processus en /procest dans fs/proc/base.c, et la fonction de lire /proc/$pid/memest mem_read. La vérification supplémentaire est effectuée par check_mem_permission.

Voici un exemple de code C à attacher à un processus et à lire un fragment de memfichier (vérification d'erreur omise):

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

J'ai déjà posté un script de validation de principe pour le dumping /proc/$pid/memsur un autre thread .

Gilles
la source
2
@abc Non, la lecture /proc/$pid/memdirecte (avec catou ddou autre chose) ne fonctionne pas. Lire ma réponse.
Gilles
4
@abc il lit de /proc/self/mem. Un processus peut très bien lire son propre espace mémoire. Il lit également l’espace mémoire d’un autre processus PTRACE_ATTACH.
Gilles
2
Notez qu'avec les noyaux Linux récents, vous n'avez pas besoin de PTRACE_ATTACH. Cette modification est fournie avec l' process_vm_readv()appel système (Linux 3.2).
ysdx
2
Hm, avec Linux 4.14.8, cela fonctionne pour moi: démarrez un processus long qui est occupé à écrire la sortie dans / dev / null. Ensuite, un autre processus est capable d’ouvrir, de rechercher et de lire des octets dans / proc / $ otherpid / mem (c’est-à-dire à certains décalages référencés via le vecteur auxiliaire) - sans avoir à ptrace-attach / detach ou à arrêter / démarrer le processus. Fonctionne si le processus s'exécute sous le même utilisateur et pour l'utilisateur root. C'est-à-dire que je ne peux pas générer d' ESRCHerreur dans ce scénario.
maxschlepzig
1
@ maxschlepzig Je suppose que c'est le changement mentionné par ysdx dans le commentaire ci-dessus.
Gilles
28

Cette commande (de gdb) vide la mémoire de manière fiable:

gcore pid

Les dumps peuvent être volumineux, utilisez-les -o outfilesi votre répertoire actuel n’a pas assez de place.

Tobu
la source
12

Lorsque vous exécutez cat /proc/$$/memla variable $$est évaluée par bash qui insère son propre pid. Il exécute ensuite catun pid différent. Vous finissez par catessayer de lire la mémoire de bashson processus parent. Les processus non privilégiés ne pouvant lire que leur propre espace mémoire, le noyau le refuse.

Voici un exemple:

$ echo $$
17823

Notez que la valeur est $$17823. Voyons quel processus il s'agit.

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

C'est ma coquille actuelle.

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

Ici encore $$évalue à 17823, qui est ma coquille. catne peut pas lire l'espace mémoire de mon shell.

bahamat
la source
Vous finissez par essayer de lire la mémoire de ce qui $pidest. Comme je l'explique dans ma réponse, pour lire la mémoire d'un processus différent, vous devez le rechercher.
Gilles le
Ce qui va être bash. Je ne disais pas que votre réponse était fausse. Je répondais simplement en termes plus simples: "pourquoi cela ne fonctionne-t-il pas".
bahamat
@bahamat: Pensez-vous à $$quand vous écrivez (et lisez) $pid?
Gilles le
Oui ... il a commencé à demander de faire référence à $$et à mettre $pidà la fin. Je l'ai transposé dans ma tête sans m'en rendre compte. Toute ma réponse devrait faire référence $$, pas $pid.
bahamat
@bahamat: La question est-elle plus claire maintenant? (BTW, je ne vois pas vos commentaires à moins que vous n'utilisiez “@Gilles”, il m'est arrivé de voir votre montage et de venir le voir.)
Gilles
7

Voici un petit programme que j'ai écrit en C:

Usage:

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

Le programme utilise / proc / $ pid / maps pour rechercher toutes les régions de mémoire mappées du processus, puis lit ces régions dans / proc / $ pid / mem, une page à la fois. ces pages sont écrites sur stdout ou sur l'adresse IP et le port TCP que vous avez spécifiés.

Code (testé sur Android, nécessite des autorisations de superutilisateur):

#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
la source
5
Ajoutez des explications sur votre code. Votre seul commentaire est un peu inutile: write to stdoutimmédiatement au-dessus fwrite(..., stdout). Voir programmers.stackexchange.com/questions/119600/…
muru
Vous avez dit que vous ne l'aviez testé que sur Android, alors je voulais simplement confirmer, cela fonctionne bien sous Linux 4.4.0-28 x86_64, comme on peut s'y attendre
abricot garçon
Je reçois un tas de données comme / @ 8 l / @ l sur stdout qui ne finit jamais aucune idée pourquoi? compilé sous Linux 4.9.0-3-amd64 # 1 SMP Debian 4.9.25-1 (2017-05-02) x86_64 Modèle de fil GNU / Linux: posix gcc version 6.3.0 20170516 (Debian 6.3.0-18)
ceph3us
ceph3us, l'usage courant est de diriger les données vers un fichier (par exemple, memdump <pid>> /sdcard/memdump.bin)
Tal Aloni