【译】Linux 进程内存

发布时间:2026/7/4 23:25:50

【译】Linux 进程内存 【译】Linux 进程内存进程检查/proc/${id}/maps可疑用户memmaps内存分配虚拟内存系统页面进程内存映射提取堆statusfileibsskipcount自动化范围算术处理最终函数总结作者Paula Gearon原文 Linux Process Memory进程检查上周,我和一些工作同事进行了(远程)计算机安全培训。在 Windows 上的一个实验练习中,向我们介绍了一个名为 ProcessHacker 的工具。该工具使用 Windows API,可以比默认提供的工具更精细地控制 Windows 进程。我们查看的功能之一是检查另一个进程的内存的能力。这使用了 ReadProcessMemory API 函数,如果您正在查看怀疑不应该在系统上的进程,这将特别有用。然而,我们的大部分工作都是使用 Linux 系统完成的,因此我的一位同事想知道是否可以以类似的方式检查 Linux 进程的内存。这让我想起了 Linux 上/proc文件系统中可用的信息。许多年前,我在学习编写内核模块时了解了这个文件系统,并且真的对如何使用标准文件隐喻在用户空间中提供如此多的进程检测功能感到着迷。这就是我如此喜欢 *nix 风格系统的原因。鉴于/proc下的可用内容,我想我应该能够给我的同事提供他想要的东西。完成后,我认为将其记录下来可能会是一个有用的练习。/proc/${id}/maps这方面的第一步是选择系统管理员可能想要检查的进程。如果有人试图做一些不正当的事情,那么首先要查找的可能是 shell 的内存。像bash这样的 Shell 会在退出时将其历史记录保存到文件中,但用户总是可以尝试类似:nohupafter5000rm~/.bash_sessions/*就在注销之前。因此,作为管理员,您可能希望查看可疑用户现在正在做什么,而不是在他们注销后。可疑用户为了有一些可以查找的东西,我使用su - testuser进入一个用户帐户,并输入命令:sudo cp /bin/su /tmp/echo这给了我一个属于testuser的 bash 会话,其中包含我不应该在历史记录中看到的内容。使用ps命令可以找到用户的进程。我首先学习这个命令时使用的是 BSD 风格的选项,由于 Linux 支持这些选项,我从未真正查看过其他选项。但是有很多选项和很多输出格式。尝试man ps,你会看到很多细节。默认情况下,ps命令显示附加到当前终端的进程。这通常只是您当前的 shell 和ps命令本身。您可以通过向此命令添加u来请求查看当前用户的所有命令,无论终端如何。您还可以通过添加x来请求查看那些未附加到任何终端的命令。但我们在这里真正需要的选项是查看属于所有其他用户的命令,即a。由于我通常可以使用该信息找到我要找的内容,因此我默认输入ps aux,然后仅在需要其他内容时才会查找手册页。从属于testuser的会话中,我输入了ps并得到:PID TTY TIME CMD10852pts/0 00:00:00bash11874pts/0 00:00:00ps所以我要找的进程 ID (PID) 是 10852。回到 root 提示符,我使用了我的标准ps aux并得到了一个很长的列表,其中包括:root108510.00.5507082944pts/0 S 04:540:00su- testuser testuser108520.01.0233605364pts/0 S 04:540:00-su那是我执行su - testuser来以该用户身份登录,-su是我看到的su为我启动的 bash shell 的标签。用户可以通过各种方式进入 shell(例如通过 ssh),但现在我已经找到了 PID,可以从那里开始工作。那么系统对该进程有什么信息呢?$cd/proc/10852 $lsattr cpuset limits net projid_mapstatautogroup cwd loginuid ns root statm auxv environ map_files numa_maps sched status cgroup exe maps oom_adj schedstat syscall clear_refs fd mem oom_score sessionid task cmdline fdinfo mountinfo oom_score_adj setgroups timerscommgid_map mounts pagemap smaps uid_map coredump_filter io mountstats personality stack wchan这里有很多有趣的东西,我鼓励人们查看 /proc 的文档 以获取更多详细信息。但我要查找的数据在进程内存中。memmem文件实际上表示进程内存。如果我们更仔细地查看该条目,我们会看到:$ls-lmem -rw-------1testuser testuser0May115:25 mem因此,它似乎是一个没有大小的文件,属于拥有该进程的用户。它也只能由进程所有者访问,但作为 root,我们也可以检查它。该文件似乎没有大小,但那是因为它是虚拟的,并将根据需要访问内存。但是,我们不能盲目地访问这个内存,因为它表示进程可能未使用的地址。要找到数据所在的位置,我们需要一个映射。mapsmaps虚拟文件是全局可读的:-r--r--r--1testuser testuser0May115:25 maps该文件提供了关于如何将内存映射到进程的检测信息:$catmaps...lots of data...这里有很多数据,但这一切意味着什么?它与操作系统如何处理进程内存有很大关系。内存分配当 Linux 进程需要内存时,它将使用像malloc(3)这样的库函数。这通常通过使用brk系统调用向操作系统请求更多内存,然后在本地管理它。库函数为用户代码提供所请求的内存,然后在不再需要时回收它以供重用。但是,使用brk调用获得的内存永久与进程相关联,这可能不是所需的,例如在临时处理大型缓冲区时。当malloc被要求提供非常大的数据块时,它会改用mmap(2)函数。要解释这个调用,我们应该看看虚拟内存。虚拟内存在早期个人计算的时代,如果计算机程序引用内存地址,例如 0x800 (2048),那么这就是计算机特定内存芯片中的精确位置。随着计算变得更加强大,应用程序需求更多,程序可能需要比硬件上可用的更多内存,并且计算机同时运行多个程序变得可能。为了管理这一点,创建了虚拟内存。这使用硬件和操作系统的组合将任意内存地址转换为保存所需数据的物理地址。这有几个好处。首先是数据可以从物理 RAM 移动到外部介质(通常是磁盘)。然后,当再次需要数据时,可以将其重新加载回内存,地址转换现在将引用这个新位置。因此,计算机可以使用磁盘的一部分来表现得好像它拥有比物理上存在的更多内存(尽管以速度为代价)。并且由于这种转换是在逐个进程的基础上完成的,这也意味着进程可以使用相同的地址来引用它们的数据,而不会有访问另一个进程的数据的风险。这意味着计算机上的一个进程无法干扰另一个进程的操作…至少,在没有操作系统干预的情况下不会。数据一次移动整个页面。这可以变化,但在 Intel 硬件上默认值是 4KiB。(4096 字节)。因此,进程看到的所有内存地址都经过此转换过程。如果进程引用地址,则可能发生 3 种情况之一:该地址通过表转换为引用位于物理内存中的页面。访问页面中偏移处的数据。该地址通过表转换,发现页面已被换出。一切都暂停片刻,而操作系统从磁盘读取该页面到物理内存中的可用页面,并将该页面的条目放入地址转换表中。然后访问页面中偏移处的数据。在表中找不到该地址。这是一个错误。使用这种机制,进程可以像访问从 0 到 2³² 或 2⁶⁴ 的任何内存一样工作(取决于硬件和操作系统架构)。当然,只有那些已正确设置的地址(例如,通过分配内存)才能工作。其中大部分对用户是隐藏的,但可以在 Linux 上使用mmap或在 Windows 上使用CreateFileMapping或VirtualAlloc与之交互。mmap和CreateFileMapping都允许您指定文件描述符(或 Windows 的文件句柄),这允许操作系统为您分配虚拟内存,其中磁盘上的位置由与句柄关联的文件提供。或者,在 Linux 上,mmap可以指定它不使用文件描述符,这意味着分配的内存将由操作系统的交换文件支持。Windows 通过使用INVALID_HANDLE_VALUE参数调用CreateFileMapping或仅调用VirtualAlloc来提供类似的功能。系统页面使用mmap或CreateFileMapping是进程如何设置大块内存(可能引用文件),但访问包含库函数的内存呢?从进程的角度来看,所有库函数(包括基本 i/o 功能,如打印文本或文件访问)必须出现在其地址空间内。这以与进程调用mmap或CreateFileMapping时非常相似的方式处理。在这种情况下,进程需要的任何库都将映射到内存中并添加到该进程的虚拟表中。大多数中央库已经在系统中,因为它们被其他进程使用,因此这里唯一需要做的工作是确保它们的地址进入该进程的虚拟内存表中。对于许多库,此初始化在进程启动时完成。实际上,进程本身是通过内存映射可执行文件启动的,然后主线程指向这些页面内的初始化地址。进程内存映射一旦进程被映射进来,它的所有库都被映射进来,并且任何额外的内存请求都得到满足,那么各种地址范围将表示所有这些不同的部分。让我们再次查看输出…00400000-004f4000 r-xp 00000000 08:01393218/bin/bash 006f3000-006f4000 r--p 000f3000 08:01393218/bin/bash 006f4000-006fd000 rw-p 000f4000 08:01393218/bin/bash 006fd000-00703000 rw-p 00000000 00:00001b48000-01d16000 rw-p 00000000 00:000[heap]7f40d6af8000-7f40d6b03000 r-xp 00000000 08:01393362/lib/x86_64-linux-gnu/libnss_files-2.21.so 7f40d6b03000-7f40d6d02000 ---p 0000b000 08:01393362/lib/x86_64-linux-gnu/libnss_files-2.21.so 7f40d6d02000-7f40d6d03000 r--p 0000a000 08:01393362/lib/x86_64-linux-gnu/libnss_files-2.21.so 7f40d6d03000-7f40d6d04000 rw-p 0000b000 08:01393362/lib/x86_64-linux-gnu/libnss_files-2.21.so 7f40d6d04000-7f40d6d0e000 r-xp 00000000 08:01393369/lib/x86_64-linux-gnu/libnss_nis-2.21.so 7f40d6d0e000-7f40d6f0e000 ---p 0000a000 08:01393369/lib/x86_64-linux-gnu/libnss_nis-2.21.so 7f40d6f0e000-7f40d6f0f000 r--p 0000a000 08:01393369/lib/x86_64-linux-gnu/libnss_nis-2.21.so 7f40d6f0f000-7f40d6f10000 rw-p 0000b000 08:01393369/lib/x86_64-linux-gnu/libnss_nis-2.21.so 7f40d6f10000-7f40d6f25000 r-xp 00000000 08:01393345/lib/x86_64-linux-gnu/libnsl-2.21.so 7f40d6f25000-7f40d7124000 ---p 00015000 08:01393345/lib/x86_64-linux-gnu/libnsl-2.21.so 7f40d7124000-7f40d7125000 r--p 00014000 08:01393345/lib/x86_64-linux-gnu/libnsl-2.21.so 7f40d7125000-7f40d7126000 rw-p 00015000 08:01393345/lib/x86_64-linux-gnu/libnsl-2.21.so 7f40d7126000-7f40d7128000 rw-p 00000000 00:0007f40d7128000-7f40d712f000 r-xp 00000000 08:01393347/lib/x86_64-linux-gnu/libnss_compat-2.21.so 7f40d712f000-7f40d732e000 ---p 00007000 08:01393347/lib/x86_64-linux-gnu/libnss_compat-2.21.so 7f40d732e000-7f40d732f000 r--p 00006000 08:01393347/lib/x86_64-linux-gnu/libnss_compat-2.21.so 7f40d732f000-7f40d7330000 rw-p 00007000 08:01393347/lib/x86_64-linux-gnu/libnss_compat-2.21.so 7f40d7330000-7f40d74ca000 r-xp 00000000 08:01393300/lib/x86_64-linux-gnu/libc-2.21.so 7f40d74ca000-7f40d76ca000 ---p 0019a000 08:01393300/lib/x86_64-linux-gnu/libc-2.21.so 7f40d76ca000-7f40d76ce000 r--p 0019a000 08:01393300/lib/x86_64-linux-gnu/libc-2.21.so 7f40d76ce000-7f40d76d0000 rw-p 0019e000 08:01393300/lib/x86_64-linux-gnu/libc-2.21.so 7f40d76d0000-7f40d76d4000 rw-p 00000000 00:0007f40d76d4000-7f40d76d6000 r-xp 00000000 08:01393331/lib/x86_64-linux-gnu/libdl-2.21.so 7f40d76d6000-7f40d78d6000 ---p 00002000 08:01393331/lib/x86_64-linux-gnu/libdl-2.21.so 7f40d78d6000-7f40d78d7000 r--p 00002000 08:01393331/lib/x86_64-linux-gnu/libdl-2.21.so 7f40d78d7000-7f40d78d8000 rw-p 00003000 08:01393331/lib/x86_64-linux-gnu/libdl-2.21.so 7f40d78d8000-7f40d78fe000 r-xp 00000000 08:01395088/lib/x86_64-linux-gnu/libtinfo.so.5.9 7f40d78fe000-7f40d7afd000 ---p 00026000 08:01395088/lib/x86_64-linux-gnu/libtinfo.so.5.9 7f40d7afd000-7f40d7b01000 r--p 00025000 08:01395088/lib/x86_64-linux-gnu/libtinfo.so.5.9 7f40d7b01000-7f40d7b02000 rw-p 00029000 08:01395088/lib/x86_64-linux-gnu/libtinfo.so.5.9 7f40d7b02000-7f40d7b24000 r-xp 00000000 08:01395006/lib/x86_64-linux-gnu/libncurses.so.5.9 7f40d7b24000-7f40d7d23000 ---p 00022000 08:01395006/lib/x86_64-linux-gnu/libncurses.so.5.9 7f40d7d23000-7f40d7d24000 r--p 00021000 08:01395006/lib/x86_64-linux-gnu/libncurses.so.5.9 7f40d7d24000-7f40d7d25000 rw-p 00022000 08:01395006/lib/x86_64-linux-gnu/libncurses.so.5.9 7f40d7d25000-7f40d7d47000 r-xp 00000000 08:01393292/lib/x86_64-linux-gnu/ld-2.21.so 7f40d7db5000-7f40d7df4000 r--p 00000000 08:01662823/usr/lib/locale/zu_ZA.utf8/LC_CTYPE 7f40d7df4000-7f40d7f24000 r--p 00000000 08:01662822/usr/lib/locale/zu_ZA.utf8/LC_COLLATE 7f40d7f24000-7f40d7f28000 rw-p 00000000 00:0007f40d7f33000-7f40d7f34000 r--p 00000000 08:01662821/usr/lib/locale/zu_ZA.utf8/LC_NUMERIC7f40d7f34000-7f40d7f35000 r--p 00000000 08:01674657/usr/lib/locale/en_US.utf8/LC_TIME7f40d7f35000-7f40d7f36000 r--p 00000000 08:01674656/usr/lib/locale/en_US.utf8/LC_MONETARY7f40d7f36000-7f40d7f37000 r--p 00000000 08:01673013/usr/lib/locale/ug_CN/LC_MESSAGES/SYS_LC_MESSAGES 7f40d7f37000-7f40d7f38000 r--p 00000000 08:01672854/usr/lib/locale/yi_US.utf8/LC_PAPER7f40d7f38000-7f40d7f39000 r--p 00000000 08:01672855/usr/lib/locale/yi_US.utf8/LC_NAME7f40d7f39000-7f40d7f3a000 r--p 00000000 08:01674655/usr/lib/locale/en_US.utf8/LC_ADDRESS7f40d7f3a000-7f40d7f3b000 r--p 00000000 08:01672853usr/lib/locale/yi_US.utf8/LC_TELEPHONE7f40d7f3b000-7f40d7f3c000 r--p 00000000 08:01672852/usr/lib/locale/yi_US.utf8/LC_MEASUREMENT7f40d7f3c000-7f40d7f43000 r--s 00000000 08:01288093/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache 7f40d7f43000-7f40d7f44000 r--p 00000000 08:01674654/usr/lib/locale/en_US.utf8/LC_IDENTIFICATION7f40d7f44000-7f40d7f46000 rw-p 00000000 00:0007f40d7f46000-7f40d7f47000 r--p 00021000 08:01393292/lib/x86_64-linux-gnu/ld-2.21.so 7f40d7f47000-7f40d7f48000 rw-p 00022000 08:01393292/lib/x86_64-linux-gnu/ld-2.21.so 7f40d7f48000-7f40d7f49000 rw-p 00000000 00:0007ffcd6454000-7ffcd6475000 rw-p 00000000 00:000[stack]7ffcd654f000-7ffcd6551000 r--p 00000000 00:000[vvar]7ffcd6551000-7ffcd6553000 r-xp 00000000 00:000[vdso]ffffffffff600000-ffffffffff601000 r-xp 00000000 00:000[vsyscall]前 3 行都显示十六进制写的地址范围,以及一些其他数据:00400000-004f4000 r-xp 00000000 08:01393218/bin/bash 006f3000-006f4000 r--p 000f3000 08:01393218/bin/bash 006f4000-006fd000 rw-p 000f4000 08:01393218/bin/bash十六进制对于表示二进制数据非常有用,因为每个数字恰好由 4 位表示,而十进制需要计算器才能来回转换。这些地址中的第一个描述了范围 4194304-5193728 中的地址。这是大小为0xf4000或 999424 字节的块。这些数字似乎是任意的,所以让我们以 4k 的页面来考虑它们。4096 是十六进制的0x1000,所以地址范围从页号0x400到页号0x4f4,范围为0xf4页。这使数字看起来更小更规则。下一个块从页面0x6f3开始,一直到0x6f4,但紧接着是从该结束地址开始的下一个块,一直到0x6fd。所以加在一起,这使得 10 个 4k 页面(或0xa页面)。老实说,我真的不关心这些地址的大小或范围。我真正注意到的是映射明确地将这些页面连接到文件/bin/bash。它们还显示了映射开始的文件的偏移量。这些映射之间有一些重叠,当考虑页面属性时可以看到原因。第一个映射(覆盖文件的前0xf4页)被标记为可执行。这意味着它包含可运行的代码。下一个映射被标记为只读,这意味着对这些页面的任何访问都应该是读取数据。我希望在那里找到静态定义,如消息字符串。最后一个块被标记为读/写。这可能包括可以由正在运行的进程修改的静态初始化数据。再往下,我们看到很多对共享库的映射引用:.so文件。例如,libc-2.21.so包含 C 语言的标准库(bash 是用 C 编写的),而libncurses.so.5.9包含用于创建文本界面的ncurses库。然后我们还有另外 2 种类型的页面范围。第一种类型包括 6 个没有文件路径信息的范围。这些可能是在代码请求分配内存时提供给进程的,并且可能包含数据结构。第二种类型包括标签而不是文件路径。特别感兴趣的是这个进程的stack和heap。对于那些不熟悉这些内存布局元素的人来说,stack是计算机用来从一个函数移动到另一个函数的内存结构。每当调用函数时,计算机都会在栈顶添加表示参数的数据,在栈顶添加指向栈的最后一个顶部的指针以及当前执行地址,然后跳转到新函数。该函数现在可以在栈顶附近看到其参数。它需要临时创建的任何变量也可以添加到栈顶。一旦函数完成,就会读取栈以找到栈的前一个顶部位置和前一个执行地址,然后恢复这些位置。这种设置是代码如何调用函数,当函数完成时,它可以返回,调用代码在离开的地方继续。数据结构通常被比作一堆盘子,盘子可以添加到顶部,然后再次移除。现在我们知道栈是什么,我们可以看到它将是快速变化的数据,包含表示函数调用的内部数据结构。另一方面,heap是程序使用malloc请求的内存的位置。这是更长期的工作内存。它通常包含内部数据结构,但如果程序正在存储字符串,那么这就是我们期望找到它们的地方。在这种情况下,我希望找到进程的历史记录,所以我想在这里查看。提取堆堆的相关行是:00f5d000-0111d000 rw-p 00000000 00:000[heap]这意味着我们想要查看从地址0xf5d000开始的内存,并在0x1c0000之后的地址0x111d000结束。除以 4k 页面,这从0xf5d页面开始,延伸0x1c0页面。我们已经讨论了如何通过mem虚拟文件访问内存,而我们恰好有dd工具可以读取文件的精确部分。所以我们只需要为它建立参数。statusdd命令喜欢描述它读取了多少块,写入了多少块,以及诸如此类的事情。我们不想看到这些,所以我们可以将status值设置为none。file要读取的文件将是mem,但要从其他地方访问它,我们想将文件称为/proc/10668/mem。这可以通过以下方式推广以从任意pid变量读取:/proc/${pid}/memibs要求dd一次处理一个字节的数据可能更容易,但我们已经知道数据以 4k 页面的形式出现,因此以 4k 块读取内存文件将更有效率。这意味着我们想将输入块大小或ibs设置为 4096。skip我们还想跳过所有无效地址,直到堆页面的开始。这意味着跳过0xf5d页面。但是,像ibs一样,我们需要为dd命令使用十进制。在这种情况下,即 3933 页面。count要读取的块数是最后一个地址减去第一个地址,除以块大小。那是0x111d000-0xf5d000,得出0x1c0000。除以0x1000(4096) 得出0x1c0,即十进制 448。这为我们提供了最终的命令行:ddstatusnoneif/proc/${pid}/memibs4096skip3933count448输出的数据可以保存或通过管道传输到另一个程序进行处理。自动化这对我的系统来说很好,但需要大量计算才能获得执行此命令所需的值。我不能以那种形式将它传递给我的同事。我决定可以提供一个函数,该函数采用所需进程 ID 的参数进行扫描。如上所述,我将其称为$pid。为了将 PID 放在字符串的中间,我们可以用大括号将其括起来:${pid}范围可以通过使用grep在maps文件中搜索字符串heap来找到堆的地址范围。grepheap /proc/${pid}/maps 00f5d000-0111d000 rw-p 00000000 00:000[heap]这将返回整行,但我们只需要该行的第一部分。我们可以使用cut命令拆分该行,告诉它在空格字符上分隔字段,然后说我们只需要第一个字段。grepheap /proc/${pid}/maps|cut-d -f100f5d000-0111d000我们可以通过将所有内容包装在反引号中来将此结果保存到变量$range:rangegrepheap /proc/${pid}/maps|cut-d -f1算术幸运的是,bash有几个我们现在需要的内置功能:文本替换、十六进制/十进制转换和算术。要获取范围的开始,我们需要范围中的所有内容,直到第一个-字符。我们可以通过说我们想用空字符串替换破折号后跟任何字符来做到这一点。其语法为:${range/-*/}可以通过用空字符串替换一系列字符后跟-来找到范围的第二部分:${range/*-}一旦我们有了数字的字符串表示,我们就可以通过用$(())包装表达式来进行算术。算术需要以十进制完成,但一旦我们在算术表达式内部,我们可以通过在值前面加上16#将任何十六进制(base 16)数字打印为十进制:$((16#1c0))448所以现在我们可以通过提取值、转换为十进制和除以 4096 来为每个dd参数构建一个表达式:# skip$((16#${range/-*/}/4096))# count$(((16#${range/*-/}-16#${range/-*/})/4096))完整的命令是:ddstatusnoneif/proc/${pid}/memibs4096skip$((16#${range/-*/}/4096))count$(((16#${range/*-/}-16#${range/-*/})/4096))处理dd 命令提取堆,但这是活动进程的二进制数据,不应打印。可以使用像hexdump这样的工具检查数据结构,但在这种情况下,我正在寻找用户历史记录中的文本。最好的工具是strings命令,它过滤掉任何不是可打印数据的内容。最终函数这是最终函数:string_heap(){pid$1rangegrepheap /proc/${pid}/maps|cut-d -f1ddstatusnoneif/proc/${pid}/memibs4096skip$((16#${range/-*/}/4096))count$(((16#${range/*-/}-16#${range/-*/})/4096))|strings}我可以针对testuser进程运行它,但我正在寻找使用sudo完成的可疑操作,所以让我们查找:$ string_heap10852|grepsudosudosudocp/bin/su /tmp/echosudocp/bin/su /tmp/echosudocp/bin/su /tmp/echo /usr/share/bash-completion/completions/sudo _sudosudo_sudo sudoedit _sudo _sudo /usr/bin/sudosudocp/bin/su /tmp/echo *sudoedit _sudo /etc/init.d/sudo gksudo kdesudo _sudo *sudoedit _sudo_/usr/bin/sudo果然…输出顶部就有该命令。总结这是对 Linux/proc文件系统中提供的与进程内存相关的一些检测的非常简短的描述。我们还讨论了虚拟内存、其分配以及在 Linux 和 Windows 中的使用。最后,我展示了如何编写一个简短的bash脚本,该脚本可以扫描另一个进程的内存并查找可疑活动。如本文对你有些许帮助欢迎大佬支持我一下点赞收藏关注、关注公众号等您的支持是我持续创作的竭动力支持我的方式

相关新闻