前些天看了关于在密码学应用中使用java.lang.Stringbyte[]的相关讨论,不推荐使用java.lang.String的重点就是其将在JVM中驻留,从而可能被窃取。但是,如何从内存中获取这些内容?JVM当然提供了一些机制,但是个人更喜欢从内核的角度来看看这个问题。

/proc/${pid}/maps

首先当然是确定进程堆栈在物理内存的位置啦。很遗憾,没有找到相关的方案。毕竟进程记录的都是虚拟线性地址,而通过内核分段、分页机制最终映射到物理内存。不过,从/proc虚拟文件系统中,提供了进程虚拟地址映射。

address                   perm offset   dev   inode                      pathname
556566cb5000-556566cb6000 r-xp 00000000 fc:01 2496598                    /root/ffTrace/run
556566eb5000-556566eb6000 r--p 00000000 fc:01 2496598                    /root/ffTrace/run
556566eb6000-556566eb7000 rw-p 00001000 fc:01 2496598                    /root/ffTrace/run
55656814f000-556568170000 rw-p 00000000 00:00 0                          [heap]
7f2a95f91000-7f2a96178000 r-xp 00000000 fc:01 1835434                    /lib/x86_64-linux-gnu/libc-2.27.so
7f2a96178000-7f2a96378000 ---p 001e7000 fc:01 1835434                    /lib/x86_64-linux-gnu/libc-2.27.so
7f2a96378000-7f2a9637c000 r--p 001e7000 fc:01 1835434                    /lib/x86_64-linux-gnu/libc-2.27.so
7f2a9637c000-7f2a9637e000 rw-p 001eb000 fc:01 1835434                    /lib/x86_64-linux-gnu/libc-2.27.so
7f2a9637e000-7f2a96382000 rw-p 00000000 00:00 0
7f2a96382000-7f2a963a9000 r-xp 00000000 fc:01 1835410                    /lib/x86_64-linux-gnu/ld-2.27.so
7f2a965a0000-7f2a965a2000 rw-p 00000000 00:00 0
7f2a965a9000-7f2a965aa000 r--p 00027000 fc:01 1835410                    /lib/x86_64-linux-gnu/ld-2.27.so
7f2a965aa000-7f2a965ab000 rw-p 00028000 fc:01 1835410                    /lib/x86_64-linux-gnu/ld-2.27.so
7f2a965ab000-7f2a965ac000 rw-p 00000000 00:00 0
7ffe2cf5e000-7ffe2cf7f000 rw-p 00000000 00:00 0                          [stack]
7ffe2cfed000-7ffe2cff0000 r--p 00000000 00:00 0                          [vvar]
7ffe2cff0000-7ffe2cff2000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

/proc/${pid}/maps 记录了当前进程虚拟内存区域的分配以及其访问控制。

  • 前三行表述的是当前进程ELF文件在虚拟内存中的地址(这里使用的ELF文件名为 run
    • 第一行 r-xp 表示其将配合Code Segment Register (CS) 作为CPU执行指令的直接依据。
    • 第二三行分别用作可读、可写数据区,将配合 Data Segment Register (DS), ES, FS, GS 等使用
  • 第四行直截了当,就是分配给堆的地址空间。当然,如果不够,可以不断地向上扩张。
  • xxx.so 文件描述的是C共享库在虚拟内存中的地址。
  • 最后才是栈内存,将以倒序的方式下内存低地址扩张。
  • 至于之后的内容,不了解,不表。

ptrace

拿到了进程虚拟内存分布,又如何获取其中的内容。ptrace 总算是派上用场了。之前在阅读内核源码的时候,任务数据结构 struct task 专门为此预留了一些字段来加以描述,但始终找不到其用途。现在总算对其有了初步的了解。

一般来说,进程彼此之间应该相互独立,虽然运行在同一台机器上,但应该是相互间不知道其他进程的存在。那又如何能够通过一个进程的代码来获取另一个进程的堆栈数据呢?ptrace 提供的就是这么一种可能性。通过 PTRACE_ATTACHPTRACE_DETACH,A进程会使得目标进程B陷入Sleeping状态,而等待A继续通过其他命令来获取其数据。至于为什么会是陷入Sleeping呢?一旦B进程的在运行,数据等随时可能改变,显然不适合读取数据啊。

如何读取?PTRACE_PEEKTEXT 就是这样一个实现进程间交互的好工具。

void attach()
{
    if (ptrace(PTRACE_ATTACH, options.pid, NULL, NULL) == -1)
    {
        fprintf(stderr, "ptract attach failed. %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }
    fprintf(stderr, "attach to %d success!\n", options.pid);
    wait(NULL);
}

void peek()
{
    char maps[17];
    sprintf(maps, "/proc/%d/maps", options.pid);
    FILE *fd = fopen(maps, "r");
    if (fd == NULL)
    {
        fprintf(stderr, "open /proc/%d/maps failed. %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    struct map *map = (struct map *) malloc(sizeof(struct map *));

    long word;
    while (fscanf(fd, "%llx-%llx %s %lx %*s %*s%*[^\n]", &map->start_addr, &map->end_addr, map->op_flag, &map->offset) != EOF)
    {
        if (map->op_flag[0] == '-')
            continue;
        fprintf(stderr, "peek from [%llx-%llx]\n", map->start_addr, map->end_addr);
        long mem_len = map->end_addr - map->start_addr;
        char *data = malloc(mem_len + 1);
        for (long cursor = map->start_addr;cursor < map->end_addr;cursor += sizeof(long))
        {
            if ((word = ptrace(PTRACE_PEEKTEXT, options.pid, cursor, NULL)) == -1 && errno)
            {
                fprintf(stderr, "peek failed. %s(errno: %d)\n", strerror(errno), errno);
                free(data);
                exit(0);
            }
            memcpy(data+cursor-map->start_addr, &word, sizeof(word));
        }
        dump(data, mem_len);

        free(data);
    }

    free(map);
}

void detach()
{
    if (ptrace(PTRACE_DETACH, options.pid, NULL, NULL) == -1)
    {
        fprintf("ptract detach failed. %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }
    fprintf(stderr, "detach from %d success!", options.pid);
}

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

    attach();
    peek();
    detach();
}

此处的代码片段就能完成dump堆栈的工作了(当然,由于没有对其它内容进行处理,同时会dump下ELF数据等)。完整代码

小结

当然,之后才发现 GDB 的实现借用的也正是这样一套机制。同时也意味着上面这段代码的实现在 GDB 中有现成的工具了:<

  __                    __                  
 / _| __ _ _ __   __ _ / _| ___ _ __   __ _ 
| |_ / _` | '_ \ / _` | |_ / _ \ '_ \ / _` |
|  _| (_| | | | | (_| |  _|  __/ | | | (_| |
|_|  \__,_|_| |_|\__, |_|  \___|_| |_|\__, |
                 |___/                |___/