type
status
date
slug
summary
tags
category
icon
password
关于python的逆向主要是针对Python字节码和Python编译后的
.pyc
文件进行的。在一般情况下拿到一个pyc文件可以直接使用工具uncompyle6将pyc文件直接转换成py文件。关于uncompyle6的下载可以直接使用pip进行安装:
使用方法也很简单:
下面通过一个实例对基本的pyc反编译的操作进行介绍:
实例一:基本的反汇编操作
使用命令行输入如下命令:

如上图键入命令后,ubcompyle6就反编译出了一个py文件,打开这个文件查看一下反编译后的源代码:

可以看到反编译的效果非常好,顺便解出这个题的flag,难度并不大。

flag为:PCTF{PyC_Cr4ck3r}
看到这里是不是感觉python逆向就这?别急,下面慢慢上难度。
实例二:被打包成可执行程序的py逆向
为了在没有安装 Python 解释器的计算机上运行 Python 程序,诞生了一些工具将python文件打包成可执行程序。经过打包后的可执行程序如果直接使用ida进行分析的话是非常困难的Python本身是一种高级语言,其生成的二进制代码通常比C或C++更复杂,而且打包的程序包含了 Python 解释器和所有必要的库,这使得反编译的结果更加复杂,因为不仅需要理解python脚本中的原代码,还需要理解 Python 解释器的工作原理。所以碰到打包成exe的python程序首先是考虑提取还原出pyc文件再反汇编成py文件。
关于将Python程序打包成可执行文件的最常用工具通常是 PyInstaller、cx_Freeze、Py2exe。其中最常见的打包工具为PyInstaller。下面简单介绍一下PyInstaller的使用。
1.PyInstaller使用:
1.下载Pyinstaller:
2.使用pyinstaller打包程序:
基本命令如下:
-h,--help | 查看该模块的帮助信息 |
-F,-onefile | 产生单个的可执行文件 |
-D,--onedir | 产生一个目录(包含多个文件)作为可执行程序 |
-a,--ascii | 不包含 Unicode 字符集支持 |
-d,--debug | 产生 debug 版本的可执行文件 |
-w,--windowed,--noconsolc | 指定程序运行时不显示命令行窗口(仅对 Windows 有效) |
-c,--nowindowed,--console | 指定使用命令行窗口运行程序(仅对 Windows 有效) |
-o DIR,--out=DIR | 指定 spec 文件的生成目录。如果没有指定,则默认使用当前目录来生成 spec 文件 |
-p DIR,--path=DIR | 设置 Python 导入模块的路径(和设置 PYTHONPATH 环境变量的作用相似)。也可使用路径分隔符(Windows 使用分号,Linux 使用冒号)来分隔多个路径 |
-n NAME,--name=NAME | 指定项目(产生的 spec)名字。如果省略该选项,那么第一个脚本的主文件名将作为 spec 的名字 |
-i ICON.ico, -icon=ICON.ico | 指定生成后程序的图标 |
介绍完了工具接下来就产生了一个问题:如何判断分析的可执行程序是不是经过打包的python程序?
2.判断可执行程序是否是经过打包的py程序
首先前面有说过经过打包的程序包含了 Python 解释器和所有必要的库,程序的体积必然不小基本都是几兆大小。其二是直接看图标,在打包时如果没有指定打包后exe的图标的话,默认打包后的exe图标长下面这个样子:

如果看到这个图标基本就可以确定这其实是打包后的python程序,当然如果指定了生成后的程序图标这个办法就白瞎了。
最后还可以将exe丢到ida中然后shift+f12查看程序的字符串,到包的程序会有很多python相关的字符串。

根据这三点基本就可以判断出是否是打包的python程序了。
下面还是通过实例来讲解打包后的exe如何逆向。
3.相关逆向办法:
附件
将后缀txt修改成exe

看到实例的图标就可以确定这是由PyInstaller打包的python程序了,为了验证这个说法可以拖进ida查看其字符串可以看到确实有很多python相关的字符串:

下面介绍一下如何从exe中提取出pyc文件,这里需要用到的工具为pyinstxtractor,github链接为:
将其中的pyinstxtractor.py下载下来,键入命令:
运行后显示提取成功,cmd内容如下:
上面的回显显示提取出来的东西放在了文件名夹加exetracted的目录下

进入login.exe_extracted文件夹

在这个文件夹中可以看到其中一个pyc文件就是提取出来的login.pyc,得到pyc文件后继续使用uncompyle6反编译成py文件:
打开反汇编后的代码:

可以看到到这里代码就成功反编译出来了,这里顺便贴一下exp:

OK,又搞定一个下面继续加大难度。
实例三:
在前面的两个实例中我们都是使用工具将pyc直接反汇编成py,但设想一下有没有什么办法可以让uncompyle6在pyc→pc的过程中反编译失败呢?
在这个实例中就会出现这个问题,使用uncompyle6先尝试对其进行反编译:

看到命令行的回显最后一行显示反编译失败,然后上面报出了一大堆数据。根据现有的手段到了这里基本就是束手无策了。所以我们需要get一些新知识。
1.dis模块介绍:
在原理篇中有介绍到:Python 代码先被编译为字节码后,再由Python虚拟机来执行字节码, Python的字节码是一种类似汇编指令的中间语言, 一个Python语句会对应若干字节码指令,虚拟机一条一条执行字节码指令, 从而完成程序执行。
而dis模块可以帮助我们查看Python代码的字节码,它是python中内置的一个模块。下面看一下关于这个模块的简单示例:
上面这段代码首先是导入了dis模块,然后随便定义了一个函数,最后的输出dis.dis(函数名)表示输出对应函数的字节码,然后看一下输出:

这里我们可以手动将输出分成三列来看:
- 第一列表示当前字节码在源代码中的行号为第四行
- 第二列是字节码的偏移量和对应的字节码,0表示当前字节码,LOAD_FAST是 Python 虚拟机要执行的操作,(LOAD_FAST表示将一个局部变量加载到栈顶)
- 第三列是字节码指令的参数。这是字节码指令的操作数。第一个
LOAD_FAST
指令的参数是0
,表示它将第 0 个局部变量(即a
)加载到栈顶。
接着我们换个视角加深对字节码的理解,在原来的代码上加一行代码:
最后这段代码将打印出
add
函数的字节码指令的二进制表示形式的整数列表。每个整数对应一个字节。查看一下输出情况:
下面这个列表的[124,0]就是上面第一行的
0 LOAD_FAST 0 (a)
,所以124也就是字节码指令(124对应的字节码为LOAD_FAST
),该字节码指令所在的列表偏移为0。第二个元素0就是字节码指令的参数。上一篇写的原理分析有讲过PyCodeObject
对象中的co_varnames
保存了在当前作用域的变量,以字符串的形式保存了变量名,而现在看到的这个0其实就表示co_varnames
下标0处的变量。这样就相对好理解字节码相关的知识点了。最后还有一个问题就是如何得知字节码指令对应的数字是多少?python源码中的
opcode.h
定义了 Python 的字节码指令集,可以去官网直接查看定义:tips:版本之间关于字节码的定义会有不同,我这里给的是v3.8.10版本的定义
了解完dis模块,我们就可以发现前面在反编译实例时出现的报错好像有点眼熟,这些返回的信息不就是字节码嘛:

到这里起码不是完全云里雾里了,接下来介绍一下python的花指令。
2.python的花指令:
花指令指的是插入到Python字节码中的额外、无意义或误导性的指令,用于干扰或误导反编译工具和分析者。还是用前面第例子:
前面有说到使用【数的字节码为:
根据上面这些字节码我们尝试插入一些花指令,这里可以考虑插入一些无效的跳转和无意义的操作。在Python字节码中,一个常见的花指令是使用
POP_TOP
来移除栈顶项(这不会影响函数的实际行为)和JUMP_FORWARD
来进行无效的跳转。在这里,
JUMP_FORWARD
指令实际上跳过了POP_TOP
指令,这使得实际上没有执行。但是,当尝试反编译这段字节码时,这些额外的指令可能会导致反编译工具产生更为复杂或难以理解的代码。补充知识:
POP_TOP:
- 功能:从堆栈顶部移除一个项并丢弃
举个例子,考虑以下Python代码:对应的字节码大致如下:JUMP_FORWARD:
- 功能:向前跳过指定数量的字节码。
举个例子,考虑以下Python代码:对应的字节码大致如下:在上面的例子中,POP_JUMP_IF_FALSE
是根据x == 0
的结果进行跳转的另一个指令。如果条件为False
,它会跳到标签12。JUMP_FORWARD
指令确保,如果条件为True
,解释器会跳过接下来的指令并直接转到标签12。
接着再看下一个花指令的例子:
正常的字节码如下:
现在,我们插入一些花指令来干扰反编译:
假设我们在
COMPARE_OP
后面插入一些额外的指令,包括一个无效的JUMP_FORWARD
和一个不会被执行的LOAD_CONST
,如下:在这里,我们插入了一个
JUMP_FORWARD
指令来跳过一个LOAD_CONST
指令,该指令尝试加载一个常数值42
(这只是一个随便给的一个数)。因为这个
LOAD_CONST
指令实际上被JUMP_FORWARD
跳过了,所以它从未被执行。然而,对于某些反编译工具来说,这可能会造成困惑,因为它们可能会期望每个LOAD_CONST
后面都有一个相关的操作(例如,一个STORE_FAST
或BINARY_ADD
)。当工具看到这个“悬挂”的LOAD_CONST
时,它可能不知道如何正确地处理,于是就会返回报错。3.实例三反编译报错原因解决:
了解了这些知识点之后再头看实例三的报错:

第一条到第三条很明显就是花指令:
0 JUMP_ABSOLUTE 4 'to 4'
- 这条指令跳转到标号 4 的位置。
2 JUMP_ABSOLUTE 6 'to 6'
- 如果上述跳转不执行,这条指令将跳转到标号 6 的位置。
4 JUMP_BACK 2 'to 2'
- 这条指令跳回标号 2 的位置。
结合以上三条指令,代码会在 2 和 4 之间无限循环后面的代码就永远都不会被执行到。接下里就是将这三条指令删除掉,具体步骤如下:
上面的指令码中有贴到
#define JUMP_ABSOLUTE 113
113转换为十六进制为0x71,也就是说这三条指令转换为二进制就是71 04 71 06 71 02
,将实例文件拖到010editor中。直接搜索二进制数据:
然后将这段数据直接删掉。删完之后还没完,co_code中有一个
ob_size
成员里面保存了co_code的长度,如果co_code的实际长度与ob_size里记录的长度不匹配的话反编译时依然会报错。接下来就是找到ob_size所在的位置将其进行修改,在python3.8版本里ob_size会以s
或 t
的类型标志开始接下来的几个字节会是一个整数,代表co_code的长度。
如上图在这个实例中ob_size的标志为s,后面的EE 07就是代码长度,还有不要忘记这是小端存储,所以最终的代码长度为7EE。除了这个办法还可以利用marshal模块输出co_code的长度。
marshal
模块提供了读写 Python 的内部值到字节流的能力。该模块主要用于支持.pyc
文件的读写。marshal常用的方法为:
- marshal.dumps(value):
将值序列化为一个字节字符串。
- marshal.load(file):
从一个已打开的文件对象中读取一个值。
我们可以利用marshal模块编写一个简单的脚本来输出实例pyc的co_code的长度:
简单解析一下这个脚本首先是导入marshal模块,然后加载目标实例pyc到f中,read函数实际作用是跳过前16个字节因为marshal读取的是字节码,所以要跳过前面的魔数等。之后使用marshal的load函数读取实例pyc的co_code,再对其长度以十六进制的形式输出。
tips:为什么跳过16字节:上一篇原理部分有讲过从python3.7版本后魔数部分增长到了16个字节,我们又是如何知道这个pyc文件的版本的呢,其实很简单可以利用010editor查看pyc的版本号: 版本号为3413,还是去看上一篇里面有每个版本与数字的对应关系就可以了,3413对应的版本为Python 3.8b4。因此知道了我们需要跳过16个字节的魔数定义。
运行脚本后输出的长度为:7EE

知道了长度注意改成小端存储形式,还是再010editor直接搜:

这样也能找到co_code的长度,删完了三条花指令(6个字节),将原长度修改为7ee-6=7e8,将修改保存后再次尝试使用uncompyle6反编译,就可以看到反汇编已经成功了。

这个题目是一个谜宫题,关于迷宫的题目还要介绍BFS和DFS,需要介绍的东西不少,这些内容可以看后面的一篇文章,这里提供这题的解法:使用BFS算法寻找这个地图的最短路径,图中方向键为wasd,数字5为起点,数字7位终点。编写如下脚本:
当然这里可以手动解密,问题也不大。
- 作者:Reveone
- 链接:http://www.reveone.cn/article/9ced4f1b-7f94-430d-9e4d-748d99079a99
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。