Cython二进制逆向系列(一) 初识Cython
Cython二进制逆向系列(一) 初识Cython
众所周知,Python类题目最难的一种就是使用Cython工具将py源码转换为二进制文件。此类题目相比于直接由Cpython编译而成的类字节码文件更复杂,且目前不存在能够将Cython编译后的二进制文件重新反编译成py源码的工具。Cython作为Python中通用的一个模块,其设计的本意是为了提高Python代码的运行效率。因此,在Cython转换py源代码时,会对源码进行一系列的调整,从而干扰整个文件的逆向。当然,也正是因为他是通用工具,其整体框架和对类似Python在字节码处理上也有一定的规律。本系列将一步步拆解Cython生成的二进制文件/编译中间文件c语言文件,从而手撕Cython逆向。
一、什么是Cython?他与CPython有什么区别?
我们知道Python作为依托于虚拟机的解释型动态语言,代码在运行时逐行解释。这种动态特性增加了运行开销。与编译型语言相比,编译后的代码已优化为机器码,执行效率更高。此外,Python 使用引用计数和垃圾回收机制管理内存。垃圾回收会在不定时触发的清理过程中消耗 CPU 时间,尤其是在大量对象创建和销毁时。高级数据结构(如列表、字典等)的实现灵活性较高,但其底层内存分配和操作效率不及低级语言中的数组和哈希表。
Python的虚拟机由其他编译型语言编写,其中由C语言编写的解释器称为CPython。CPython 是 Python 的官方参考实现。CPython由于扩展性强、稳定、简单的一系列优点,以及他强大的模块社区,使得CPython成为的Python目前应用最为广泛的解释器。

python虚拟机包括python编译器和python解释器
传统的py代码执行需要经历以下步骤:首先交由编译器将py源代码编译成类字节码文件,然后解释器再按照类字节码文件中存储的数据逐行执行。这样的步骤,导致每次py源代码都要经历编译这一步骤。因此,为了提升py源码的运行效率,进而使得Python也能够处理高并发环境下或者高性能要求的问题,Cython由此诞生。类似于编译型语言,Cython会将py源码转换为c语言代码,然后通过c语言编译器将代码编译成二进制文件,这样py源码每次在执行时,就不需要Python编译器编译出类字节码文件,而是直接使用已经由c语言编译好的二进制文件调用Py解释器的相关接口,这大大提高了Python的执行效率。
CPython和Cython的相同点是都能够处理py源代码,但他们是两个截然不同的东西。Cython 是一种工具,主要用于编译和优化 Python 代码,使其更接近 C 的运行效率,并允许调用 C/C++ 函数。
二、使用Cython编译二进制文件
现在假设项目的根目录下有待编译的py源代码文件test.py,我们只写一行代码:
print("hello world")
然后在项目根目录(test.py同级目录)新建setup.py文件
from distutils.core import setup
from Cython.Build import cythonize
setup(ext_modules=cythonize("test.py")) #这里是待编译文件的名字
然后在终端运行命令
python setup.py build_ext --inplace
就会在同级目录下生成.c的C语言代码文件和.pyd的二进制模块库,以及build文件夹(存储了编译过程中的中间文件)。想要使用此模块,于其他模块相同,只需import该模块的模块名即可。
可能会遇到的问题:1.setup文件报错找不到合适的distutils/setup版本。解决方法:切换python版本。笔者用的是3.8.10
2.终端编译时报错找不到vs build。其实是找不到c语言编译器。解决办法:下载visual studio。
到此,我们通过正向的方式得到了Cython产生的二进制文件。本节浅分析一下产生的.c文件代码。
三、初识代码的调用逻辑
打开.c文件,可以看到,一句简单的print,转换后的c语言代码有4165行之多!足以证明其框架代码和处理代码之多。

然而事实上,真正执行了print("hello world")的代码是以下部分
- 位于1781行的常量赋值
static const char __pyx_k_main[] = "__main__";
static const char __pyx_k_name[] = "__name__";
static const char __pyx_k_test[] = "__test__";
static const char __pyx_k_print[] = "print";
static const char __pyx_k_Hello_World[] = "Hello World";
static const char __pyx_k_cline_in_traceback[] = "cline_in_traceback";
- 位于1958行的函数
__Pyx_CreateStringTabAndInitStrings,作用是将字符串和变量/变量名联系在一起
static int __Pyx_CreateStringTabAndInitStrings(void) {
__Pyx_StringTabEntry __pyx_string_tab[] = {
{&__pyx_n_s_, __pyx_k_, sizeof(__pyx_k_), 0, 0, 1, 1},
{&__pyx_kp_s_Hello_World, __pyx_k_Hello_World, sizeof(__pyx_k_Hello_World), 0, 0, 1, 0},
{&__pyx_n_s_cline_in_traceback, __pyx_k_cline_in_traceback, sizeof(__pyx_k_cline_in_traceback), 0, 0, 1, 1},
{&__pyx_n_s_end, __pyx_k_end, sizeof(__pyx_k_end), 0, 0, 1, 1},
{&__pyx_n_s_file, __pyx_k_file, sizeof(__pyx_k_file), 0, 0, 1, 1},
{&__pyx_n_s_main, __pyx_k_main, sizeof(__pyx_k_main), 0, 0, 1, 1},
{&__pyx_n_s_name, __pyx_k_name, sizeof(__pyx_k_name), 0, 0, 1, 1},
{&__pyx_n_s_print, __pyx_k_print, sizeof(__pyx_k_print), 0, 0, 1, 1},
{&__pyx_n_s_test, __pyx_k_test, sizeof(__pyx_k_test), 0, 0, 1, 1},
{0, 0, 0, 0, 0, 0, 0}
};
return __Pyx_InitStrings(__pyx_string_tab);
}
- 位于2916行的
__Pyx_Print,获取print代码对象,并以arg_tuple为参数进行调用
static int __Pyx_Print(PyObject* f, PyObject *arg_tuple, int newline) {
int i;
if (!f) {
if (!(f = __Pyx_GetStdout()))
return -1;
}
Py_INCREF(f);
for (i=0; i < PyTuple_GET_SIZE(arg_tuple); i++) {
PyObject* v;
if (PyFile_SoftSpace(f, 1)) {
if (PyFile_WriteString(" ", f) < 0)
goto error;
}
v = PyTuple_GET_ITEM(arg_tuple, i);
if (PyFile_WriteObject(v, f, Py_PRINT_RAW) < 0)
goto error;
if (PyString_Check(v)) {
char *s = PyString_AsString(v);
Py_ssize_t len = PyString_Size(v);
if (len > 0) {
switch (s[len-1]) {
case ' ': break;
case '\f': case '\r': case '\n': case '\t': case '\v':
PyFile_SoftSpace(f, 0);
break;
default: break;
}
}
}
}
- 位于3015行的
__Pyx_PrintOne,print参数只有一个的情况
static int __Pyx_PrintOne(PyObject* stream, PyObject *o) {
// ...
PyObject* arg_tuple = PyTuple_Pack(1, o);
// ...
res = __Pyx_Print(stream, arg_tuple, 1);
- 位于2345行,调用print
if (__Pyx_PrintOne(0, __pyx_kp_s_Hello_World) < 0) __PYX_ERR(0, 1, __pyx_L1_error)
_pyx_pymod_exec_hello_world把__Pyx_PrintOne展开编进了函数中(都被指定了__attribute__((cold))扩展的函数),这里调用主要是把__pyx_kp_s_Hello_World即字符串"Hello, World!"的 PyObject 打成一个 tuple,然后用PyObject_Call调用PyObject_GetAttr拿到的print函数的 PyCodeObject,完成了对print("Hello, World!")的调用。
这也是普通函数的调用流程,有一个 tuple 存非关键字参数(args)、一个 dict 存关键字参数(kwargs),然后调用PyObject_Call,其三个参数分别是被调用函数的 PyCodeObject、args tuple、kwargs dict,这样就完成了对 Python 函数的调用。
四、Python的内存管理机制
如果单纯考虑print函数的调用,以上代码100行足以。那么为什么整个c文件有长达几千行的代码呢?其中大部分是对Python对象内存的管理。
我们以978行的函数(宏定义函数)__Pyx_PyHeapTypeObject_GC_Del为例:
#define __Pyx_PyHeapTypeObject_GC_Del(obj) {\
PyTypeObject *type = Py_TYPE((PyObject*)obj);\
assert(__Pyx_PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE));\
PyObject_GC_Del(obj);\
Py_DECREF(type);\
}
事实上,这正是python的引用计数内存管理机制。
首先使用 Py_TYPE 宏获取传入对象 obj 的类型(PyTypeObject)。然后使用断言(assert)检查 type 是否具有 Py_TPFLAGS_HEAPTYPE 特性。宏 __Pyx_PyType_HasFeature判断 type 是否为堆分配的类型。调用 PyObject_GC_Del,从 Python 的垃圾回收系统中删除该对象。对类型对象减少引用计数。通常,当一个堆分配的对象被销毁时,其类型的引用计数也需要减少。
引用计数的核心原理为:每个对象都有一个 引用计数器,记录指向它的引用数量。当有新的变量引用该对象时(例如赋值操作),引用计数加 1。当引用被删除或超出作用域时,引用计数减 1。当引用计数变为 0 时,说明该对象不再被使用,系统回收其占用的内存。采用这种方式管理内存,无需复杂的垃圾回收算法。但是同时存在循环引用问题。如果两个对象相互引用,引用计数永远不会降为 0,导致内存泄漏。
Cython二进制逆向系列(一) 初识Cython的更多相关文章
- WCF编程系列(一)初识WCF
WCF编程系列(一)初识WCF Windows Communication Foundation(WCF)是微软为构建面向服务的应用程序所提供的统一编程模型.WCF的基本概念: 地址:定义服务的 ...
- iOS逆向系列-脱壳
概述 通过iOS逆向系列-逆向App中使用class-dump工具导出App的Mach-O文件所有头文件.Hopper工具分析App的Mach-O文件代码大概实现.但是这些前体是App的Mach-O没 ...
- iOS逆向系列-逆向APP思路
界面分析 通过Cycript.Reveal. 对于Reveal安装配置可参考配置iOS逆向系列-Reveal 通过Reveal找到内存中的UI对象 静态分析 开发者编写的所有代码最终编译链接到Mach ...
- Android逆向系列文章— Android基础逆向(6)
本文作者:HAI_ 0×00 前言 不知所以然,请看 Android逆向-Android基础逆向(1) Android逆向-Android基础逆向(2) Android逆向-Android基础逆向(2 ...
- Gradle系列之初识Gradle
原文首发于微信公众号:躬行之(jzman-blog) 学习 Android 有一段时间了,开发中经常使用到 Gradle ,但是不知道 Gradle 构建项目的原理,计划花一点时间学习一下 Gradl ...
- Python零基础学习系列之一--初识计算机!
1-1.计算机概念: Computer: 原指专门负责计算的人,后来演变成特指计算设备,译为"计算机" 计算机的概念: 计算机是能够根据一组指令操作数据的机器. A compute ...
- .net core系列之初识asp.net core
.net core已经发布了2.0版本,相对于1.0的有了很大的完善,最近准备在项目中尝试使用asp.net core,所以就进行了一些简单的研究. 初识asp.net core分为以下几个部分: 1 ...
- SpringMVC 框架系列之初识与入门实例
微信公众号:compassblog 欢迎关注.转发,互相学习,共同进步! 有任何问题,请后台留言联系! 1.SpringMVC 概述 (1). MVC:Model-View-Control Contr ...
- 【安卓网络请求开源框架Volley源码解析系列】初识Volley及其基本用法
在安卓中当涉及到网络请求时,我们通常使用的是HttpUrlConnection与HttpClient这两个类,网络请求一般是比较耗时,因此我们通常会在一个线程中来使用,但是在线程中使用这两个类时就要考 ...
- Node.js实战项目学习系列(1) 初识Node.js
前言 一直想好好学习node.js都是半途而废的状态,这次沉下心来,想好好的学习下node.js.打算写一个系列的文章大概10几篇文章,会一直以实际案例作为贯穿的学习. 什么是node Node.js ...
随机推荐
- ASP.NET Core – Configuration & Options
前言 之前就写过 Asp.net core 学习笔记 ( Configuration 配置 ) 只是有点乱, 这篇作为整理版. 项目中会有许许多多的 Config 要设定. 比较好的管理方式是把它们放 ...
- Flutter 2.5 更新详解
Flutter 2.5 正式版已于上周正式发布!这是一次重要的版本更新,也是 Flutter 发布历史上各项统计数据排名第二的版本.我们关闭了 4600 个 Issue,合并了 3932 个 PR,它 ...
- 三大硬核方式揭秘:Java如何与底层硬件和工业设备轻松通信!
大家好,我是V哥,程序员聊天真是三句不到离不开技术啊,这不前两天跟一个哥们吃饭,他是我好多年前的学员了,一直保持着联系,现在都李总了,在做工业互联网相关的项目,真是只要 Java 学得好,能干一辈子, ...
- 异步解析文件报错 NoSuchFileException
问题描述:同步上传解析文件超时,修改为异步解析后找不到 文件 java.nio.file.NoSuchFileException 原因:异步文件上传,主线程结束后,临时目录的文件会被清理掉,子线程此时 ...
- 能用到“退休”的 600条 Linux 命令,可以解决日常99%的问题~
1.基本命令 uname -m 显示机器的处理器架构 uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 (SMBIOS / DMI) hdparm -i /dev/ ...
- constexpr声明 常量表达式
constexpr 是 C++ 中用于声明 常量表达式 的关键字,表示一个变量或函数的值在编译时就可以确定,而不是在运行时计算. 这对优化非常有用,因为编译器可以直接将结果嵌入到程序中,减少运行时 ...
- /proc/pids/io
rchar:代表自进程启动以来所读取的总字符数,包括通过缓存或直接读取的字符.单位是字节(bytes). wchar:代表自进程启动以来所写入的总字符数,包括通过缓存或直接写入的字符.单位是字节(by ...
- 如何实现高效运维?来谈谈性能优化那些事(含直播回顾 Q&A)
数据库性能问题,常常是困扰DBA高效运维的难题之一.如何多角度地帮助DBA,找到"数据库慢"的原因,保证系统高效.稳定.安全地运行? 2021年10月14日,云和恩墨技术顾问,拥有 ...
- p1ngp0ng
p1ngp0ng 轻量级ICMP C2工具 依赖 Linux环境与GCC套件. 功能: p1ng: p1ng,实现C2服务器正向连接被控端.目前实现了对客户端的命令控制与文件上传下载.明文传输,未加密 ...
- Next.js 深度教程:服务端渲染、静态生成到增量静态再生 | 2024最新版
优化字体和图像 书接上回,我们学习了如何设计Next.js应用程序,让我们继续优化主页和添加自定义字体.图像. 在网站设计中,字体扮演着关键角色,然而,若需获取并加载字体文件,项目中引入自定义字体可能 ...