Python进程注入

前言

上篇文章讨论了Linux进程注入,能够在机器码层面进行代码注入,但如果现在有一个Python进程在运行,如何才让将Python代码注入进去呢?

inject by gdb

目前有两个项目能够对Python虚拟机进行注入,分别是pylane以及pyrasite。简单浏览两个项目,可以发现都是使用GDB在进程中进行函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
class Injector(object):
def generate_gdb_codes(self):
# ...
return [
# use char in case of symbol PyGilState_STATE not found
'call $gil_state = (char) PyGILState_Ensure()',
'call (void) PyRun_SimpleString("%s")' % prepare_code,
'call (void) PyRun_SimpleString("%s")' % run_code,
'call (void) PyRun_SimpleString("%s")' % cleanup_code,
# make sure previous codes are safe.
# gdb exit without GIL release is a disaster for target process.
'call (void) PyGILState_Release($gil_state)',
]

GDB附加到进程后,使用call命令在目标进程中依次调用函数PyGILState_Ensure、PyRun_SimpleString、PyGILState_Release就能在Python虚拟机中运行代码。

1
2
3
4
5
6
7
sudo gdb \
-p $pid \
-ex 'call (int)PyGILState_Ensure()' \
-ex 'call (int)PyRun_SimpleString("print(123)")' \
-ex 'call (int)PyGILState_Release($1)' \
-ex 'set confirm off' \
-ex quit

使用上诉命令,可以直接将”print(123)”注入到Python进程中。

inject by pangolin

既然只需要在进程中调用三个函数,就可以完成Python代码注入,那么我们是否可以使用pangolin将某个程序注入到进程中,完成这三个函数的调用。这也就是项目python-inject的由来,它使用pangolin进行Python代码注入,不依赖于GDB。

符号查找

首先我们需要查找函数在目标进程中的地址,解析Python的ELF符号表,然后加上进程基址,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#include <common/log.h>
#include <common/cmdline.h>
#include <common/utils/process.h>
#include <elfio/elfio.hpp>

typedef int (*PFN_RUN)(const char *command);
typedef int (*PFN_ENSURE)();
typedef void (*PFN_RELEASE)(int);

constexpr auto PYTHON = "bin/python";
constexpr auto PYTHON_LIBRARY = "libpython";
constexpr auto PYTHON_CALLER = "python_caller";

int main(int argc, char ** argv) {
cmdline::parser parse;

parse.add<int>("pid", 'p', "pid", true, 0);
parse.add<std::string>("source", 's', "python source file", true, "");
parse.add<std::string>("pangolin", '\0', "pangolin path", true, "");
parse.add("file", '\0', "pass source by file");

parse.parse_check(argc, argv);

int pid = parse.get<int>("pid");

CProcessMap processMap;

if (
!CProcess::getFileMemoryBase(pid, PYTHON_LIBRARY, processMap) &&
!CProcess::getFileMemoryBase(pid, PYTHON, processMap)
) {
LOG_ERROR("find target failed");
return -1;
}

LOG_INFO("find target: 0x%lx -> %s", processMap.start, processMap.file.c_str());

ELFIO::elfio reader;

if (!reader.load(processMap.file)) {
LOG_ERROR("open elf failed: %s", processMap.file.c_str());
return -1;
}

auto it = std::find_if(
reader.sections.begin(),
reader.sections.end(),
[](const auto& s) {
return s->get_type() == SHT_DYNSYM;
});

if (it == reader.sections.end()) {
LOG_ERROR("can't find symbol section");
return -1;
}

unsigned long baseAddress = 0;

if (reader.get_type() != ET_EXEC) {
auto sit = std::find_if(
reader.segments.begin(),
reader.segments.end(),
[](const auto& s) {
return s->get_type() == PT_LOAD;
});

if (sit == reader.segments.end()) {
LOG_ERROR("can't find load segment");
return -1;
}

baseAddress = processMap.start - (*sit)->get_virtual_address();
}

PFN_ENSURE pfnEnsure = nullptr;
PFN_RUN pfnRun = nullptr;
PFN_RELEASE pfnRelease = nullptr;

ELFIO::symbol_section_accessor symbols(reader, *it);

for (ELFIO::Elf_Xword i = 0; i < symbols.get_symbols_num(); i++) {
std::string name;
ELFIO::Elf64_Addr value = 0;
ELFIO::Elf_Xword size = 0;
unsigned char bind = 0;
unsigned char type = 0;
ELFIO::Elf_Half section = 0;
unsigned char other = 0;

if (!symbols.get_symbol(i, name, value, size, bind, type, section, other)) {
LOG_ERROR("get symbol %lu failed", i);
return -1;
}

if (name == "PyGILState_Ensure")
pfnEnsure = (PFN_ENSURE)(baseAddress + value);
else if (name == "PyRun_SimpleString")
pfnRun = (PFN_RUN)(baseAddress + value);
else if (name == "PyGILState_Release")
pfnRelease = (PFN_RELEASE)(baseAddress + value);
}

if (!pfnEnsure || !pfnRun || !pfnRelease) {
LOG_ERROR("can't find python symbols");
return -1;
}

LOG_INFO("ensure func: %p run func: %p release func: %p", pfnEnsure, pfnRun, pfnRelease);

std::string source = parse.get<std::string>("source");
std::string pangolin = parse.get<std::string>("pangolin");
std::string caller = CPath::join(CPath::getAPPDir(), PYTHON_CALLER);

char callerCommand[1024] = {};

snprintf(
callerCommand, sizeof(callerCommand),
"%s %s %d %p %p %p",
caller.c_str(), source.c_str(), parse.exist("file"),
pfnEnsure, pfnRun, pfnRelease
);

int err = execl(
pangolin.c_str(), pangolin.c_str(),
"-c", callerCommand,
"-p", std::to_string(pid).c_str(),
nullptr
);

if (err < 0) {
LOG_ERROR("exec pangolin failed: %s", strerror(errno));
return -1;
}

return 0;
}

函数调用

我们现在知道了函数在进程中的地址,只需要注入程序并传递地址参数,在进程中完成三个函数的调用,就可以实现Python代码注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <crt_asm.h>
#include <crt_log.h>
#include <crt_utils.h>

typedef int (*PFN_RUN)(const char *command);
typedef int (*PFN_ENSURE)();
typedef void (*PFN_RELEASE)(int);

int main(int argc, char ** argv, char ** env) {
if (argc < 6) {
LOG("usage: ./program source [0|1](source is file) address address address");
return -1;
}

char *source = argv[1];
int is_file = !strcmp(argv[2], "1");

PFN_ENSURE pfn_ensure = (PFN_ENSURE)strtoul(argv[3], NULL, 16);
PFN_RUN pfn_run = (PFN_RUN)strtoul(argv[4], NULL, 16);
PFN_RELEASE pfn_release = (PFN_RELEASE)strtoul(argv[5], NULL, 16);

if (!pfn_ensure || !pfn_run || !pfn_release) {
LOG("func address error");
return -1;
}

LOG("inject python source: %s flag: %d", source, is_file);

char *code = source;

if (is_file) {
char *buffer = NULL;
long length = read_file(source, &buffer);

if (length <= 0) {
LOG("read file failed: %s", source);
return -1;
}

code = malloc(length + 1);

if (!code) {
LOG("malloc code memory failed");
free(buffer);
return -1;
}

memset(code, 0, length + 1);
memcpy(code, buffer, length);

free(buffer);
}

int state = pfn_ensure();
int err = pfn_run(code);
pfn_release(state);

if (is_file) {
free(code);
}

return err;
}

void _main(unsigned long * sp) {
int argc = *sp;
char **argv = (char **)(sp + 1);
char **env = argv + argc + 1;

__exit(main(argc, argv, env));
}

void _start() {
CALL_SP(_main);
}

fs寄存器

也许你有疑问,为什么要将函数计算和调用分开,为什么不直接在目标进程中计算函数地址?实际上,我在项目刚开始的时候也是这样设计的,但是在测试过程中一直crash,经过调试发现这和fs寄存器相关。

glibc

如果你使用C/C++编写了一个依赖于标准库的程序,那么程序加载时会先由glibc进行初始化,而在glibc中,每个线程初始化时都会修改fs寄存器指向一段私有的内存,用来存储线程局部变量等数据。
Python也会设置私有的fs寄存器,如果将一个依赖glibc的程序注入进去,那么程序运行时会修改fs寄存器。而此时调用Python的函数,就会导致crash,因为函数内部会访问fs寄存器,但此时的fs寄存器已经被修改了。
为了避免fs寄存器被修改,只能编写一个不依赖于标准库的程序(-nostdlib),类似于我上面的代码,从程序入口点”_start”开始编写。不使用标准库进行符号解析以及地址计算,开发难度较大,所以权衡之下我选择将两者分离。

arch_prctl

使用arch_prctl系统调用可以修改fs寄存器,所以我尝试过一种解决方法,在函数调用之前临时将fs寄存器修改为原有值。所以我修改了pangolin,在进行注入时先获取当前fs寄存器值存入环境变量,在程序注入后根据环境变量恢复寄存器。
但是结果还是会导致crash,因为glibc的架构复杂,所以我没有继续深究,总之之后涉及fs寄存器的一切我都会敬而远之。