获取其他进程的全局变量

profile

Posted by bbkgl on December 2, 2019

微微风簇浪

散作满河星

如何获取存储在bss段上的,未初始化的“其他进程”的全局变量?

首先考虑到的是使用ptrace,但是ptrace获取其他进程的全局变量,需要该变量的地址。

那如何得到对应bss段变量的地址呢?

使用对应程序段的起始地址+变量的地址偏移量,接下来进行验证和说明。

这里先贴出被测试的程序:

#include <cstdio>
#include <iostream>
#include <unistd.h>

struct temp {
    int a;
    int b;
    int c;
};

temp g_test;
int test = 1;


void func() {
    printf("0x%016lx\n", &g_test);
    sleep(100000);
}

int main() {
    printf("%d\n", getpid());
    func();
    return 0;
}

变量的地址以及调试信息

调试符号

可以参考符号表

关于elf中的st_value一定要看,很重要!!!!

关于调试信息,可以自行查资料,目前比较常用的就是dwarf,也就是在编译程序的时候加上-g选项,就能在对应可执行文件中加入符号信息。

linux下查看调试符号可以通过readelfobjdump两个工具。

如果使用readelf -s <exename>会打印出所有符号的信息:

H75098c05a7404b5f868bd18fd4341b38T.jpg

如果需要找到对应变量的信息,可以直接grep,比如我使用readelf -s testo | grep g_test

20200112221738.png

其中value有可能是偏移量,也有可能就是绝对地址。

目前关于st_value的值,因为我们查看的就是elf可执行文件,所以实际上该地址就是内存中的虚拟地址。

特别是对于全局变量来说,无论在.data段还是.bss段,都是在编译后就确定了其虚拟地址,后面会进行验证和分析。

地址范围

程序运行后,代码加载进入内存,并打印结果:

20200112221823.png

第一行是进程id,第二行是未初始化的全局变量g_test的地址,然后再使用gdb attach到目标进程中,可以查找到已经初始化的test全局变量的地址:

Heaf48dd313d0410a812048b7cf168bdaC.jpg

同时会在/proc/<pid>/maps文件中存储下每个段的地址范围:

Hf5f692c25b1646ef980791ffa3427239i.jpg

每一行前面表示的是地址范围和读写权限等信息,最后表示的是文件名。

主要关注前四行和倒数第四行,行数从大到小(内存由低到高)分别表示:

  • 第一行代码段,因为只读+可执行
  • 第二行我也不知道23333
  • 第三行是data+bss段,根据前面打印的初始化以及未初始化的全局变量的地址,可以看到是吻合的
  • 第四行是堆地址
  • 还有倒数第四行是栈地址

说这么多,基本就是想说明,我们可以通过调试信息得到全局变量的地址,无论是bss段还是data段,然后就能使用ptrace读取些变量的值,下面进行实践和验证。

获取进程中全局变量的值

绝对地址验证(gcc较低版本或者clang)

关于ptrace,相关的博客介绍很多,相关使用可以参考我的博客文章如何获取进程函数参数,这里不再过多追说了。

获取变量值需要两个参数,分别是进程id和变量地址,进程id就是之前看到的11976,而变量的地址就是我们在读取elf文件得到的:

20200112222336.png

很简单,贴上代码:

#include <sys/ptrace.h>
#include <iostream>
#include <errno.h>
#include <unistd.h>
#include <wait.h>

int wait4stop(pid_t pid) {
    int status = 99;
    do {
        if (waitpid(pid, &status, 0) == -1 || WIFEXITED(status) || WIFSIGNALED(status))
            return 0;
    } while(!WIFSTOPPED(status));
    return 1;
}

int get_value(int *address, pid_t pid) {
    ptrace(PTRACE_ATTACH, pid, nullptr, nullptr);
    if (!wait4stop(pid))
        std::cerr << "wait SIGSTOP of ptrace failed" << std::endl;
    long ret = ptrace(PTRACE_PEEKDATA, pid, address, nullptr);
    if (errno != 0) {
        std::cerr << "ptrace error!---" << errno << std::endl;
        ret = -1;
    }
    ptrace(PTRACE_DETACH, pid, nullptr, nullptr);
    return static_cast<int>(ret);
}

int main() {
    pid_t pid = 1880;
    int *address1 = reinterpret_cast<int *>(0x601060);
    int *address2 = reinterpret_cast<int *>(0x601070);
    printf("%d\n", get_value(address1, pid));
    printf("%d\n", get_value(address2, pid));
    return 0;
}

输出:

1
0

注:如果报错为3,说明没有权限或者进程id错误,使用sudo或者检查进程id

相对地址验证(gcc6.3较高版本)

需要说明的是,在gcc/g++较高版本,我发现这个问题时,使用的gcc6.3,是相对地址

使用如下待验证程序:

#include <cstdio>
#include <unistd.h>
#include <vector>

struct test {
    int a;
    int b;
    std::vector<int> c;
};

test aaa;

void func(test a, test b, int c, int d) {
    printf("%016lx\n", &aaa);
    sleep(100000);
}

int main() {
    printf("%d\n", getpid());
    printf("%016lx\n", &aaa);
    test a, b;
    func(a, b, 11, 12);
    return 0;
}

首先通过objdump命令查看bss段信息获取aaa的相对地址,假设该地址为X:

20200112222423.png

然后直接运行该程序,打印结果如下:

20200112222536.png

其中第一行打印是进程id,第二三行是aaa的实际地址,设该地址为Z。

接下来查看程序起始地址:

20200112222616.png

也就是563e937b4000,假设为Y。

可以很明显的看到:

563e937b4000 + 2030a0 = 0000563e939b70a0,也就是X + Y = Z。

说明通过这样的方法可以获得变量的实际地址。