本文翻译自:Hack The Virtual Memory: C strings & /proc

Hack The Virtual Memory: C strings & /proc

介绍

hack虚拟内存之第0章:学习C字符串和/proc
这是一系列有关于虚拟内存的文章/教程中的第一篇。目标是以另一种更实际的方式,学习一些CS基础知识。
在这第一篇文章中,我们将使用/proc来查找并修改正在运行的进程的虚拟内存中包含的变量(在此示例中为ASCII字符串),并在此过程中学习一些很酷的东西。

环境

所有脚本和程序都已经在以下系统上进行过测试:

  • Ubuntu 14.04 LTS
    • Linux ubuntu 4.4.0-31-generic #50~14.04.1-Ubuntu SMP Wed Jul 13 01:07:32 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
  • gcc
    • gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4
  • Python 3
    • Python 3.4.3 (default, Nov 17 2016, 01:08:31)
    • [GCC 4.8.4] on linux

前提

为了完全理解本文,你需要知道:

  • C语言的基础知识
  • 了解python
  • Linux文件系统和shell的基础知识

虚拟内存

在计算机中,虚拟内存是一种使用硬件和软件实现的内存管理技术。它将程序使用的内存地址(称为虚拟地址)映射到计算机内存中的物理地址。主存(对于进程或任务来说)是连续的地址空间或连续段的集合。操作系统负责管理虚拟地址空间以及将物理内存分配给虚拟内存。CPU中的地址转换硬件(通常称为存储器管理单元,MMU)自动将虚拟地址转换为物理地址。操作系统可以扩展这些功能以提供超过物理内存容量的虚拟地址空间,从而引用比物理内存更多的虚拟内存。
虚拟内存的主要优点包括:使应用程序不必管理共享内存空间,由于内存隔离而提高的安全性,以及通过使用分页技术在概念上使用比物理可用内存更多的内存。
你可以在Wikipedia上阅读有关虚拟内存的更多信息。
在第2章,我们将详细介绍并说明虚拟内存各个部分的内容。目前,在你继续阅读之前,你应该了解以下几个要点:

  • 每个进程都有自己的虚拟内存
  • 虚拟内存大小取决于操作系统的体系结构
  • 每个操作系统处理虚拟内存的方式不同,但对于大多数现代操作系统,进程的虚拟内存如下所示:

在内存的高地址中你可以找到(这是一个简略的列表,还有更多内容可以找到,但这不是今天的主题):

  • 命令行参数和环境变量
  • 栈,“向下”增长。 这看似违反直觉,但这是在虚拟内存中实现栈的方式

在内存低地址中,你可以找到:

  • 你的可执行文件(它比这复杂一点,但这足以理解本文的其余部分)
  • 堆,“向上”增长

堆是动态分配的部分内存(即,包含使用malloc分配的内存)。
另外,请记住虚拟内存与RAM不同。

C程序

让我们从这个简单的C程序开始:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/**
 * main - uses strdup to create a new string, and prints the
 * address of the new duplcated string
 *
 * Return: EXIT_FAILURE if malloc failed. Otherwise EXIT_SUCCESS
 */
int main(void)
{
    char *s;

    s = strdup("Holberton");
    if (s == NULL)
    {
        fprintf(stderr, "Can't allocate mem with malloc\n");
        return (EXIT_FAILURE);
    }
    printf("%p\n", (void *)s);
    return (EXIT_SUCCESS);
}

strdup

在看下面之前,先思考一下:你认为strdup如何创建字符串“Holberton”的副本?你怎么证实这一点?
.
.
.
strdup需要创建一个新的字符串,所以它首先要为字符串保留空间。strdup很可能在函数实现中使用了malloc。快速浏览其手册可以确认:

描述
        strdup()函数返回一个指向新字符串的指针,该字符串是字符串s的副本。
        使用malloc(3)获得新字符串的内存,可以使用free(3)释放。

在看下面之前,先思考一下:根据我们之前所说的关于虚拟内存的内容,你认为该字符串副本位于何处?在内存高地址还是低地址?
.
.
.
可能在较低的地址(在堆中)。让我们编译并运行我们的简易C程序来验证我们的假设:

julien@holberton:~/holberton/w/hackthevm0$ gcc -Wall -Wextra -pedantic -Werror main.c -o holberton
julien@holberton:~/holberton/w/hackthevm0$ ./holberton
0x1822010
julien@holberton:~/holberton/w/hackthevm0$

我们的字符串副本位于地址0x1822010处。非常好。但这是一个内存低地址还是高地址?

进程的虚拟内存有多大

进程的虚拟内存大小取决于你的系统体系结构。在这个例子中,我使用的是64位机器,因此理论上每个进程的虚拟内存大小为2^64字节。理论上,可能的最大内存地址是0xffffffffffffffff(1.8446744e + 19),最小是0x0。
与0xffffffffffffffff相比,0x1822010较小,因此字符串副本很可能位于较低的内存地址。当我们查看proc文件系统时,我们能够确认这一点。

proc文件系统

来自man proc:

proc文件系统是一个伪文件系统,它为内核数据结构提供接口。它通常挂载在`/proc`。其中大多数是只读的,但有些文件允许更改内核变量。

如果列出/proc目录的内容,你可能会看到很多文件。 我们将重点关注其中两个:

  • /proc/[pid]/mem
  • /proc/[pid]/maps

mem

来自man proc:

/proc/[PID]/MEM
        此文件可用于访问进程内存的页面,通过open(2),read(2)和lseek(2)。

maps

来自man proc:

/proc/[pid]/maps
          A  file containing the currently mapped memory regions and their access permissions.
          See mmap(2) for some further information about memory mappings.

              The format of the file is:

       address           perms offset  dev   inode       pathname
       00400000-00452000 r-xp 00000000 08:02 173521      /usr/bin/dbus-daemon
       00651000-00652000 r--p 00051000 08:02 173521      /usr/bin/dbus-daemon
       00652000-00655000 rw-p 00052000 08:02 173521      /usr/bin/dbus-daemon
       00e03000-00e24000 rw-p 00000000 00:00 0           [heap]
       00e24000-011f7000 rw-p 00000000 00:00 0           [heap]
       ...
       35b1800000-35b1820000 r-xp 00000000 08:02 135522  /usr/lib64/ld-2.15.so
       35b1a1f000-35b1a20000 r--p 0001f000 08:02 135522  /usr/lib64/ld-2.15.so
       35b1a20000-35b1a21000 rw-p 00020000 08:02 135522  /usr/lib64/ld-2.15.so
       35b1a21000-35b1a22000 rw-p 00000000 00:00 0
       35b1c00000-35b1dac000 r-xp 00000000 08:02 135870  /usr/lib64/libc-2.15.so
       35b1dac000-35b1fac000 ---p 001ac000 08:02 135870  /usr/lib64/libc-2.15.so
       35b1fac000-35b1fb0000 r--p 001ac000 08:02 135870  /usr/lib64/libc-2.15.so
       35b1fb0000-35b1fb2000 rw-p 001b0000 08:02 135870  /usr/lib64/libc-2.15.so
       ...
       f2c6ff8c000-7f2c7078c000 rw-p 00000000 00:00 0    [stack:986]
       ...
       7fffb2c0d000-7fffb2c2e000 rw-p 00000000 00:00 0   [stack]
       7fffb2d48000-7fffb2d49000 r-xp 00000000 00:00 0   [vdso]

              The address field is the address space in the process that the mapping occupies.
          The perms field is a set of permissions:

                   r = read
                   w = write
                   x = execute
                   s = shared
                   p = private (copy on write)

              The offset field is the offset into the file/whatever;
          dev is the device (major:minor); inode is the inode on that device.   0  indicates
              that no inode is associated with the memory region,
          as would be the case with BSS (uninitialized data).

              The  pathname field will usually be the file that is backing the mapping.
          For ELF files, you can easily coordinate with the offset field
              by looking at the Offset field in the ELF program headers (readelf -l).

              There are additional helpful pseudo-paths:

                   [stack]
                          The initial process's (also known as the main thread's) stack.

                   [stack:<tid/>] (since Linux 3.4)
                          A thread's stack (where the <tid/> is a thread ID).
              It corresponds to the /proc/[pid]/task/[tid]/ path.

                   [vdso] The virtual dynamically linked shared object.

                   [heap] The process's heap.

              If the pathname field is blank, this is an anonymous mapping as obtained via the mmap(2) function.
          There is no easy  way  to  coordinate
              this back to a process's source, short of running it through gdb(1), strace(1), or similar.

              Under Linux 2.0 there is no field giving pathname.

这意味着我们可以查看/proc/[pid]/mem文件以找到正在运行的进程的堆。如果我们可以从堆读取数据,我们可以找到想要修改的字符串。如果我们可以写入堆,我们可以用任意字符串替换这个字符串。

pid

进程是程序的实例,具有唯一的进程ID。许多函数和系统调用使用此进程ID(PID)来与进程交互并操作进程。

C程序

我们现在拥有编写脚本或程序所需的一切,该脚本或程序在正在运行的进程的堆中查找字符串,然后将其替换为另一个字符串(长度相同或更短)。我们将使用下面的程序无限循环并打印“strduplicated”字符串。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

/**              
 * main - uses strdup to create a new string, loops forever-ever
 *                
 * Return: EXIT_FAILURE if malloc failed. Other never returns
 */
int main(void)
{
     char *s;
     unsigned long int i;

     s = strdup("Holberton");
     if (s == NULL)
     {
          fprintf(stderr, "Can't allocate mem with malloc\n");
          return (EXIT_FAILURE);
     }
     i = 0;
     while (s)
     {
          printf("[%lu] %s (%p)\n", i, s, (void *)s);
          sleep(1);
          i++;
     }
     return (EXIT_SUCCESS);
}

编译并运行上面的源代码应该会输出下面的内容,并无限循环,直到你终止该进程。

julien@holberton:~/holberton/w/hackthevm0$ gcc -Wall -Wextra -pedantic -Werror loop.c -o loop
julien@holberton:~/holberton/w/hackthevm0$ ./loop 
[0] Holberton (0xfbd010)
[1] Holberton (0xfbd010)
[2] Holberton (0xfbd010)
[3] Holberton (0xfbd010)
[4] Holberton (0xfbd010)
[5] Holberton (0xfbd010)
[6] Holberton (0xfbd010)
[7] Holberton (0xfbd010)
...

你可以在进一步阅读前,尝试编写一个脚本或程序,在正在运行的进程的堆中找到一个字符串。
.
.
.

查看/proc

让我们运行我们的循环程序。

julien@holberton:~/holberton/w/hackthevm0$ ./loop 
[0] Holberton (0x10ff010)
[1] Holberton (0x10ff010)
[2] Holberton (0x10ff010)
[3] Holberton (0x10ff010)
...

我们需要找到的第一个东西是进程的PID。

julien@holberton:~/holberton/w/hackthevm0$ ps aux | grep ./loop | grep -v grep
julien     4618  0.0  0.0   4332   732 pts/14   S+   17:06   0:00 ./loop

在上面的示例中,PID为4618(每次运行时它都会不同,如果你在自己的计算机上尝试此操作,很可能是不同的数字)。 因此,我们要查看的map和mem文件位于/proc/4618目录中:

  • /proc/4618/maps
  • /proc/4618/mem

ls -la命令应该会输出如下:

julien@ubuntu:/proc/4618$ ls -la
total 0
dr-xr-xr-x   9 julien julien 0 Mar 15 17:07 .
dr-xr-xr-x 257 root   root   0 Mar 15 10:20 ..
dr-xr-xr-x   2 julien julien 0 Mar 15 17:11 attr
-rw-r--r--   1 julien julien 0 Mar 15 17:11 autogroup
-r--------   1 julien julien 0 Mar 15 17:11 auxv
-r--r--r--   1 julien julien 0 Mar 15 17:11 cgroup
--w-------   1 julien julien 0 Mar 15 17:11 clear_refs
-r--r--r--   1 julien julien 0 Mar 15 17:07 cmdline
-rw-r--r--   1 julien julien 0 Mar 15 17:11 comm
-rw-r--r--   1 julien julien 0 Mar 15 17:11 coredump_filter
-r--r--r--   1 julien julien 0 Mar 15 17:11 cpuset
lrwxrwxrwx   1 julien julien 0 Mar 15 17:11 cwd -> /home/julien/holberton/w/funwthevm
-r--------   1 julien julien 0 Mar 15 17:11 environ
lrwxrwxrwx   1 julien julien 0 Mar 15 17:11 exe -> /home/julien/holberton/w/funwthevm/loop
dr-x------   2 julien julien 0 Mar 15 17:07 fd
dr-x------   2 julien julien 0 Mar 15 17:11 fdinfo
-rw-r--r--   1 julien julien 0 Mar 15 17:11 gid_map
-r--------   1 julien julien 0 Mar 15 17:11 io
-r--r--r--   1 julien julien 0 Mar 15 17:11 limits
-rw-r--r--   1 julien julien 0 Mar 15 17:11 loginuid
dr-x------   2 julien julien 0 Mar 15 17:11 map_files
-r--r--r--   1 julien julien 0 Mar 15 17:11 maps
-rw-------   1 julien julien 0 Mar 15 17:11 mem
-r--r--r--   1 julien julien 0 Mar 15 17:11 mountinfo
-r--r--r--   1 julien julien 0 Mar 15 17:11 mounts
-r--------   1 julien julien 0 Mar 15 17:11 mountstats
dr-xr-xr-x   5 julien julien 0 Mar 15 17:11 net
dr-x--x--x   2 julien julien 0 Mar 15 17:11 ns
-r--r--r--   1 julien julien 0 Mar 15 17:11 numa_maps
-rw-r--r--   1 julien julien 0 Mar 15 17:11 oom_adj
-r--r--r--   1 julien julien 0 Mar 15 17:11 oom_score
-rw-r--r--   1 julien julien 0 Mar 15 17:11 oom_score_adj
-r--------   1 julien julien 0 Mar 15 17:11 pagemap
-r--------   1 julien julien 0 Mar 15 17:11 personality
-rw-r--r--   1 julien julien 0 Mar 15 17:11 projid_map
lrwxrwxrwx   1 julien julien 0 Mar 15 17:11 root -> /
-rw-r--r--   1 julien julien 0 Mar 15 17:11 sched
-r--r--r--   1 julien julien 0 Mar 15 17:11 schedstat
-r--r--r--   1 julien julien 0 Mar 15 17:11 sessionid
-rw-r--r--   1 julien julien 0 Mar 15 17:11 setgroups
-r--r--r--   1 julien julien 0 Mar 15 17:11 smaps
-r--------   1 julien julien 0 Mar 15 17:11 stack
-r--r--r--   1 julien julien 0 Mar 15 17:07 stat
-r--r--r--   1 julien julien 0 Mar 15 17:11 statm
-r--r--r--   1 julien julien 0 Mar 15 17:07 status
-r--------   1 julien julien 0 Mar 15 17:11 syscall
dr-xr-xr-x   3 julien julien 0 Mar 15 17:11 task
-r--r--r--   1 julien julien 0 Mar 15 17:11 timers
-rw-r--r--   1 julien julien 0 Mar 15 17:11 uid_map
-r--r--r--   1 julien julien 0 Mar 15 17:11 wchan

/proc/pid/maps

正如我们之前看到的,/proc/pid/maps文件是一个文本文件,所以我们可以直接读取它。我们的maps文件内容如下所示:

julien@ubuntu:/proc/4618$ cat maps
00400000-00401000 r-xp 00000000 08:01 1070052                            /home/julien/holberton/w/funwthevm/loop
00600000-00601000 r--p 00000000 08:01 1070052                            /home/julien/holberton/w/funwthevm/loop
00601000-00602000 rw-p 00001000 08:01 1070052                            /home/julien/holberton/w/funwthevm/loop
010ff000-01120000 rw-p 00000000 00:00 0                                  [heap]
7f144c052000-7f144c20c000 r-xp 00000000 08:01 136253                     /lib/x86_64-linux-gnu/libc-2.19.so
7f144c20c000-7f144c40c000 ---p 001ba000 08:01 136253                     /lib/x86_64-linux-gnu/libc-2.19.so
7f144c40c000-7f144c410000 r--p 001ba000 08:01 136253                     /lib/x86_64-linux-gnu/libc-2.19.so
7f144c410000-7f144c412000 rw-p 001be000 08:01 136253                     /lib/x86_64-linux-gnu/libc-2.19.so
7f144c412000-7f144c417000 rw-p 00000000 00:00 0 
7f144c417000-7f144c43a000 r-xp 00000000 08:01 136229                     /lib/x86_64-linux-gnu/ld-2.19.so
7f144c61e000-7f144c621000 rw-p 00000000 00:00 0 
7f144c636000-7f144c639000 rw-p 00000000 00:00 0 
7f144c639000-7f144c63a000 r--p 00022000 08:01 136229                     /lib/x86_64-linux-gnu/ld-2.19.so
7f144c63a000-7f144c63b000 rw-p 00023000 08:01 136229                     /lib/x86_64-linux-gnu/ld-2.19.so
7f144c63b000-7f144c63c000 rw-p 00000000 00:00 0 
7ffc94272000-7ffc94293000 rw-p 00000000 00:00 0                          [stack]
7ffc9435e000-7ffc94360000 r--p 00000000 00:00 0                          [vvar]
7ffc94360000-7ffc94362000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

回到我们之前所说的,我们可以看到栈([stack])位于内存高地址中,堆([heap])位于内存较低地址中。

[heap]

使用maps文件,我们可以找到查找字符串所需的所有信息:

010ff000-01120000 rw-p 00000000 00:00 0                                  [heap]

堆:

  • 开始于进程的虚拟内存地址0x010ff000处
  • 结束于虚拟内存地址:0x01120000
  • 可读写(rw)

快速回顾一下我们(仍在运行)的循环程序:

...
[1024] Holberton (0x10ff010)
...

-> 0x010ff000 <0x10ff010 <0x01120000。 这证明我们的字符串位于堆中。更确切地说,它位于堆偏移0x10处。 如果我们打开/proc/pid/mem/文件(在这个例子中为/proc/4618/mem)并寻找内存地址0x10ff010,我们可以写入正在运行的进程的堆,覆盖“Holberton”字符串!
让我们编写一个脚本或程序来做到这一点。选择你喜欢的语言,让我们开始!
你可以在进一步阅读前,尝试编写一个脚本或程序,在正在运行的进程的堆中找到一个字符串,然后再继续阅读。下一段将给出这样做的源代码!
.
.
.

Overwriting the string in the virtual memory

我们将使用Python3编写脚本,但你可以用任何语言编写。这是代码:

#!/usr/bin/env python3
'''             
Locates and replaces the first occurrence of a string in the heap
of a process    

Usage: ./read_write_heap.py PID search_string replace_by_string
Where:           
- PID is the pid of the target process
- search_string is the ASCII string you are looking to overwrite
- replace_by_string is the ASCII string you want to replace
  search_string with
'''

import sys

def print_usage_and_exit():
    print('Usage: {} pid search write'.format(sys.argv[0]))
    sys.exit(1)

# check usage  
if len(sys.argv) != 4:
    print_usage_and_exit()

# get the pid from args
pid = int(sys.argv[1])
if pid <= 0:
    print_usage_and_exit()
search_string = str(sys.argv[2])
if search_string  == "":
    print_usage_and_exit()
write_string = str(sys.argv[3])
if search_string  == "":
    print_usage_and_exit()

# open the maps and mem files of the process
maps_filename = "/proc/{}/maps".format(pid)
print("[*] maps: {}".format(maps_filename))
mem_filename = "/proc/{}/mem".format(pid)
print("[*] mem: {}".format(mem_filename))

# try opening the maps file
try:
    maps_file = open('/proc/{}/maps'.format(pid), 'r')
except IOError as e:
    print("[ERROR] Can not open file {}:".format(maps_filename))
    print("        I/O error({}): {}".format(e.errno, e.strerror))
    sys.exit(1)

for line in maps_file:
    sline = line.split(' ')
    # check if we found the heap
    if sline[-1][:-1] != "[heap]":
        continue
    print("[*] Found [heap]:")

    # parse line
    addr = sline[0]
    perm = sline[1]
    offset = sline[2]
    device = sline[3]
    inode = sline[4]
    pathname = sline[-1][:-1]
    print("\tpathname = {}".format(pathname))
    print("\taddresses = {}".format(addr))
    print("\tpermisions = {}".format(perm))
    print("\toffset = {}".format(offset))
    print("\tinode = {}".format(inode))

    # check if there is read and write permission
    if perm[0] != 'r' or perm[1] != 'w':
        print("[*] {} does not have read/write permission".format(pathname))
        maps_file.close()
        exit(0)

    # get start and end of the heap in the virtual memory
    addr = addr.split("-")
    if len(addr) != 2: # never trust anyone, not even your OS :)
        print("[*] Wrong addr format")
        maps_file.close()
        exit(1)
    addr_start = int(addr[0], 16)
    addr_end = int(addr[1], 16)
    print("\tAddr start [{:x}] | end [{:x}]".format(addr_start, addr_end))

    # open and read mem
    try:
        mem_file = open(mem_filename, 'rb+')
    except IOError as e:
        print("[ERROR] Can not open file {}:".format(mem_filename))
        print("        I/O error({}): {}".format(e.errno, e.strerror))
        maps_file.close()
        exit(1)

    # read heap  
    mem_file.seek(addr_start)
    heap = mem_file.read(addr_end - addr_start)

    # find string
    try:
        i = heap.index(bytes(search_string, "ASCII"))
    except Exception:
        print("Can't find '{}'".format(search_string))
        maps_file.close()
        mem_file.close()
        exit(0)
    print("[*] Found '{}' at {:x}".format(search_string, i))

    # write the new string
    print("[*] Writing '{}' at {:x}".format(write_string, addr_start + i))
    mem_file.seek(addr_start + i)
    mem_file.write(bytes(write_string, "ASCII"))

    # close files
    maps_file.close()
    mem_file.close()

    # there is only one heap in our example
    break

注意:你需要以root身份运行此脚本,否则你将无法读取或写入/proc/pid/mem文件,即使你是该进程的所有者也是如此。

运行脚本

julien@holberton:~/holberton/w/hackthevm0$ sudo ./read_write_heap.py 4618 Holberton "Fun w vm!"
[*] maps: /proc/4618/maps
[*] mem: /proc/4618/mem
[*] Found [heap]:
    pathname = [heap]
    addresses = 010ff000-01120000
    permisions = rw-p
    offset = 00000000
    inode = 0
    Addr start [10ff000] | end [1120000]
[*] Found 'Holberton' at 10
[*] Writing 'Fun w vm!' at 10ff010
julien@holberton:~/holberton/w/hackthevm0$

请注意,此地址对应于我们手动找到的地址:

  • 正在运行的进程的堆的虚拟内存地址为0x010ff000到0x01120000
  • 我们的字符串在堆偏移0x10处,因此在内存地址0x10ff010处

如果我们回到我们的循环程序,它现在应该打印“Fun w vm!”

...
[2676] Holberton (0x10ff010)
[2677] Holberton (0x10ff010)
[2678] Holberton (0x10ff010)
[2679] Holberton (0x10ff010)
[2680] Holberton (0x10ff010)
[2681] Holberton (0x10ff010)
[2682] Fun w vm! (0x10ff010)
[2683] Fun w vm! (0x10ff010)
[2684] Fun w vm! (0x10ff010)
[2685] Fun w vm! (0x10ff010)
...

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖