在C/C++中调用python代码

最近使用C++实现了一个fuzz。但是发现对应目标有认证流程,我的fuzz代码上也得加上这个加密认证逻辑才能正常工作。经过谷哥的帮助,在网上找到了一段python实现的加密认证逻辑。

让我自己用C++重写?那是不可能的,于是就走上了用C++调用python的踩坑之路。

0x01 执行简单Python代码

如果我们应用的场景并不复杂,比如知识想执行一段简单的python代码。那么你只需要了解以下内容

  1. 在头文件中包含Python.h头文件
  2. 使用Py_Initialize() 初始化python解析器
  3. 使用PyRun_SimpleString执行python代码
  4. Py_Finalize 释放python解析器

以下就是一个实现的demo

1
2
3
4
5
6
7
8
//test.cpp
#include <Python.h>
int main()
{
Py_Initialize(); ##初始化
PyRun_SimpleString("print('hello')");
Py_Finalize(); ##释放资源
}

注意在链接的时候加上对应的库,编译指令如下

1
g++ test.cpp -I/usr/include/python3.8 -l python3.8

0x02 执行简单python脚本中的函数

2.1 无参数与返回值

有的时候我们需要调用python脚本中的函数来实现一些功能,假设这个时候我们有这样一个脚本

1
2
3
#cat script/sayHello.py
def say():
print("hello")

然后我们需要了解一些常用api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//导入函数相关
PyObject* PyModule_GetDict( PyObject *module)
/*
PyModule_GetDict()函数可以获得Python模块中的函数列表。PyModule_GetDict()函数返回一个字典。字典中的关键字为函数名,值为函数的调用地址。
字典里面的值可以通过PyDict_GetItemString()函数来获取,其中p是PyModule_GetDict()的字典,而key则是对应的函数名
*/

PyObject* PyObject_GetAttrString(PyObject *o, char *attr_name)
/*
PyObject_GetAttrString()返回模块对象中的attr_name属性或函数,相当于Python中表达式语句:o.attr_name
*/

//调用函数相关
PyObject* PyObject_CallObject( PyObject *callable_object, PyObject *args)
PyObject* PyObject_CallFunction( PyObject *callable_object, char *format, ...)
/*
使用上面两个函数可以在C程序中调用Python中的函数。callable_object为要调用的函数对象,也就是通过上述导入函数得到的函数对象,
而区别在于前者使用python的tuple来传参,后者则使用类似c语言printf的风格进行传参。
如果不需要参数,那么args可能为NULL。返回成功时调用的结果,或失败时返回NULL。
这相当于Python表达式 apply(callable_object, args) 或 callable_object(*args)
*/

我们可以像下面这样去加载调用模块,并调用指定的函数

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
#include <python2.7/Python.h>
#include <iostream>

using namespace std;

int main(){
Py_Initialize();
if( !Py_IsInitialized()){
cout << "python init fail" << endl;
return 0;
}
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./script')");

PyObject* pModule = PyImport_ImportModule("sayHello");
if( pModule == NULL ){
cout <<"module not found" << endl;
return 1;
}

PyObject* pFunc = PyObject_GetAttrString(pModule, "say");
if( !pFunc || !PyCallable_Check(pFunc)){
cout <<"not found function add_num" << endl;
return 0;
}

PyObject_CallObject(pFunc, NULL );

Py_Finalize();
return 0;
}

2.2有参数与返回值

参数构建

在Python/C API中提供了Py_BuildValue()函数对数字和字符串进行转换处理,使之变成Python中相应的数据类型。其函数原型如下所示

1
2
3
4
5
PyObject* Py_BuildValue( const char *format, ...)
/*
Py_BuildValue()提供了类似c语言printf的参数构造方法,format是要构造的参数的类型列表,函数中剩余的参数即要转换的C语言中的整型、浮点型或者字符串等。
其返回值为PyObject型的指针。
*/

format对应的类型列表如下

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
s(str或None)[char *]
使用'utf-8'编码将以null结尾的C字符串转换为Python str对象。如果C字符串指针为NULL,则表示None。

s#(str或None)[char *,int]
使用'utf-8'编码将C字符串及其长度转换为Python str对象。如果C字符串指针为NULL,则忽略长度返回None。

y(字节)[char *]
这会将C字符串转换为Python字节对象。如果C字符串指针为NULL,则返回None。

y#(字节)[char *,int]
这会将C字符串及其长度转换为Python对象。如果C字符串指针为NULL,则返回None。

z(str或None)[char *]
与s相同。

z#(str或None)[char *,int]
与s#相同。

u(str)[Py_UNICODE *]
将Unicode(UCS-2或UCS-4)数据的以null结尾的缓冲区转换为Python Unicode对象。如果Unicode缓冲区指针为NULL,则返回None。

u#(str)[Py_UNICODE *,int]
将Unicode(UCS-2或UCS-4)数据缓冲区及其长度转换为Python Unicode对象。如果Unicode缓冲区指针为NULL,则忽略长度并返回None。

U(str或None)[char *]
与s相同。

U#(str或None)[char *,int]
与s#相同。

i(int)[int]
将普通的C int转换为Python整数对象。

b(int)[char]
将纯C char转换为Python整数对象。

h(int)[short int]
将普通的C short int转换为Python整数对象。

l(int)[long int]
将C long int转换为Python整数对象。

B(int)[unsigned char]
将C unsigned char转换为Python整数对象。

H(int)[unsigned short int]
将C unsigned short int转换为Python整数对象。

I(int)[unsigned int]
将C unsigned int转换为Python整数对象。

k(int)[unsigned long]
将C unsigned long转换为Python整数对象。

L(int)[long long]
将C long long转换为Python整数对象。

K(int)[unsigned long long]
将C unsigned long long转换为Python整数对象。

n(int)[Py_ssize_t]
将C Py_ssize_t转换为Python整数。

c(长度为1的字节)[char]
将表示字节的C int转换为长度为1的Python字节对象。

C(长度为1的str)[int]
将表示字符的C int转换为长度为1的Python str对象。

d(float) [double]
将C double转换为Python浮点数。

f(float) [float]
将C float转换为Python浮点数。

D(complex) [Py_complex *]
将C Py_complex结构转换为Python复数。

O(object) [PyObject *]
不改变Python对象的传递(引用计数除外,它增加1)。如果传入的对象是NULL指针,则假定这是因为产生参数的调用发现错误并设置了异常。
因此,Py_BuildValue()将返回NULL但不会引发异常。如果尚未引发异常,则设置SystemError。

S(object) [PyObject *]
与O相同

N((object) [PyObject *]
与O相同,但不会增加对象的引用计数。通过调用参数列表中的对象构造函数创建对象时很有用。

O&(object) [converter, anything]
通过转换器函数将任何内容转换为Python对象。该函数被调用任何东西(应与void *兼容)作为其参数,并应返回“新”Python对象,如果发生错误则返回NULL

(items) (tuple) [matching-items]
将一系列C值转换为具有相同项目数的Python元组。

[items](list) [matching-items]
将一系列C值转换为具有相同项目数的Python列表。

{items}(dict) [matching-items]
将一系列C值转换为Python字典。每对连续的C值将一个项添加到字典中,分别用作键和值。
如果格式字符串中存在错误,则设置SystemError异常并返回NULL

返回值

python函数的返回值也是PyObject类型,因此,在python脚本返回到C/C++之后,需要解构Python数据为C的类型,这样C/C++程序中才可以使用Python里的数据。但是,由于python的返回值有多种数据结构类型,因此,我们需要为每个类型进行转换。不过由于篇幅问题,我们只是介绍简单的整形和字符串类型的处理,其他类型的返回见文末的github链接,总体思路都是根据类型逐个从值从PyObject中提取。python提供了下面函数来完成这个功能

1
2
int PyArg_Parse( PyObject *args, char *format, ...)
根据format把args的值转换成c类型的值,format接受的类型和上述Py_BuildValue()的是一样的

释放资源

Python使用引用计数机制对内存进行管理,实现自动垃圾回收。在C/C++中使用Python对象时,应正确地处理引用计数,否则容易导致内存泄漏。在Python/C API中提供了Py_CLEAR()、Py_DECREF()等宏来对引用计数进行操作。
当使用Python/C API中的函数创建列表、元组、字典等后,就在内存中生成了这些对象的引用计数。在对其完成操作后应该使用Py_CLEAR()、Py_DECREF()等宏来销毁这些对象。其原型分别如下所示

1
2
3
4
void Py_CLEAR(PyObject *o)
void Py_DECREF(PyObject *o)
其中,o的含义是要进行操作的对象。
对于Py_CLEAR()其参数可以为NULL指针,此时,Py_CLEAR()不进行任何操作。而对于Py_DECREF()其参数不能为NULL指针,否则将导致错误。

下面是个简单的例子,在例子中会有输出和返回值

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
/*
cat script/Py2Cpp.py

def add_num(a,b):
return a+b

*/

#include <python2.7/Python.h>
#include <iostream>

using namespace std;

int main(){
Py_Initialize();
if( !Py_IsInitialized()){
cout << "python init fail" << endl;
return 0;
}
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./script')");


PyObject* moduleName = PyString_FromString("Py2Cpp");
PyObject* pModule = PyImport_Import(moduleName);
if( pModule == NULL ){
cout <<"module not found" << endl;
return 1;
}

PyObject* pFunc = PyObject_GetAttrString(pModule, "add_num");
if( !pFunc || !PyCallable_Check(pFunc)){
cout <<"not found function add_num" << endl;
return 0;
}

PyObject* args = Py_BuildValue("(ii)", 28, 103);
PyObject* pRet = PyObject_CallObject(pFunc, args );
Py_DECREF(args);

int res = 0;
PyArg_Parse(pRet, "i", &res );
Py_DECREF(pRet);
cout << res << endl;

Py_Finalize();
return 0;
}

2.3 调用类中的函数

大概流程是:

第一步,导入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
Py_Initialize();

if (!Py_IsInitialized()){
printf("Inital failed \n");
exit(-1);
}
PyGC_Collect();
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('../src/decrpt_func/')");

//import testpy.py
std::cerr << "Called" << std::endl;
PyObject *pModule = PyImport_ImportModule("testpy");
if(!pModule) {
std::cerr << " Moudle load worong" << std::endl;
return false;
}
//使用PyObject* pDict来存储导入模块中的方法字典, 调用的方法是PyModule_GetDict(module): PyObject* pDict = PyModule_GetDict(pModule);
PyObject *pDict = PyModule_GetDict(pModule);
if (!pDict) {
std::cerr << " Dict worong" << std::endl;
return false;
}
//使用PyDict_GetItemString可以获得该模块中的方法或类,此处导入了Person类
PyObject *pClass = PyDict_GetItemString(pDict, "Person");
if(!pClass) {
std::cerr << " class worong" << std::endl;
return false;
}
//使用PyInstanceMethod_New获取了类的构造函数方法
PyObject *pConstruct = PyInstanceMethod_New(pClass);
if(! pConstruct) {
std::cerr << " construct woronbg" << std::endl;
return false;
}
//使用PyObject_CallObject调用类的构造函数方法,同时生成示例instance
PyObject *pInstance = PyObject_CallObject(pConstruct, NULL);
this->pInstance = pInstance;
if (!pInstance) {
std::cerr << " Person instance failed" << std::endl;
return false;
}
//使用Person类对象的auth方法
PyObject_CallMethod(this->pInstance, "auth", "(ss)", "admin", "123qwe");
//如果调用时python出现错误,那么输出
PyErr_Print();
//释放
Py_DECREF(pInstance);
Py_DECREF(pClass);
Py_DECREF(pDict);
Py_DECREF(pModule);
// 关闭虚拟机
Py_Finalize();

0x03 遇到的问题

类似于下图这种会出现内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <Python.h>
#include <iostream>

int main()
{
std::cout << "Python version: " << PY_VERSION << std::endl;

while (true)
{
Py_Initialize();
//PyGC_Collect();
Py_Finalize();
}

return 0;
}

[解决方案](c++ - Memory leak when embedding python into my application - Stack Overflow)

0x04 参考

[C++调用python脚本 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/79896193#:~:text=C%2FC%2B%2B中调用,的所有初始化, 并销毁)

C调用python类的正确方法_hnlylyb的博客-CSDN博客_c调用python类的正确方法

[C++/Python] 如何在C++中使用一个Python类? (Use Python-defined class in C++) - Lancelod_Liu - 博客园 (cnblogs.com)

How to embed Python code in C program (xmodulo.com)

-------------本文结束感谢您的阅读-------------
+ +