Python逆向
前言
随着CTF
的兴起,有着越来越多样化的逆向题目(各种不会的语言被出出来折磨人),我们可以比较常见的是C
、C++
语言的逆向题目,而Python
题目出现也算是较为常见,题目多为pyc
或者是exe
,今天与大家一起分享一下关于Python
逆向的一些东西
前言
随着当下CTF比赛的发展,各种各样的语音逆向逐渐产生,如C、C++、Python等一些常见语言用于出题也是常规题型,而我们此次所初步了解的是Python的语言逆向,文章整体讲解的较为简单,主要涉及到.py
/.pyc
/.exe(python打包后的程序)
三类题型,如有不足还请多多指正。
.py
在这个情况下我们直接拿到了对应的源代码,可以直接通过对源码进行分析,写出对应的逆向算法进行求解
比如我们现在有如下代码:
1 |
|
我们可以直接观察到程序的源代码,需要我们输入一个flag
进行保存在a
中,之后便会对a
和123
进行比较,如果相同,那么将会输出Yes!
如果输入错误那就会输出NO!
,我们可以很直观的观察到程序的逻辑,因此我们只需要对程序的加密过程进行求解,像此题中的比较,进行逆向思维求解即可
.pyc
参考资料
PYC文件是什么
https://www.jianshu.com/p/a2e3ef5b9546
PYC文件结构
https://blog.csdn.net/weixin_35967330/article/details/114390031
如何生成pyc
python -m code.py
生成的pyc
文件是可以运行的
1 |
|
.pyc
分成两种情况,一种是不带花指令的,另一种是带花指令的
不带花指令
对于不带花指令的.pyc
文件,我们可以直接使用uncompyle6
对其进行转换,可以直接得到对应的源代码.py
文件
如下图:
带花指令
对于带花指令的.pyc
文件我们如果直接使用uncompyle6
对其进行转换会出现如下状况:
会提示我们数组发生了越界,证明在这个.pyc
文件中添加了一些花指令,让我们无法直接进行转换得到.py
,那么应该怎么解决呢?
我们通过导入dis
,marshal
两个库来进行查看对应pyc
的字节码
1 |
|
通过上述步骤我们可以看到对应的py
的字节码,我们可以看到如下图:
那么我们应该怎么进行反编译呢?答案是看对应的字节码,我们可以在python库
里找到对应字节码的含义,对其进行翻译,找到对应的花在的地方,之后再到010
中进行修改对应字节码的长度和去除对应字节码(花指令的字节码)
在上图中我们可以看到有一个LOAD_CONST 255
可以看到在此处发生了报错,在这个语句前一句有一个JUMP_ABSOLUTE 18
,对于这条语句就已经强制跳转到LOAD_CONST 255
下一条语句了,因此这个语句被判定为一个花,所以我们需要将其去除。
我们先使用len(code.co_code)
来查看对应的pyc
字节码长度,之后减去 3 (Python3 好像不太一样)
,那么在哪里减呢?我们需要先找到对应的字节码的编号在对应的opcode.h
中进行查找
查找到后我们在010
中进行查找0x71
同时比较后面的数字18
的十六进制0x12
,之后我们可以找到:
之后我们可以直接确定后面的64 FF 00
这三个字节为对应的花位置,我们将这 6 个十六进制进行去除:
我们将0x1B
进行修改为0x15
,同时将对应的 6 个字节进行删除,并再次保存为.pyc
文件,之后使用uncompyle6
进行反编译即可。
txt 里面写有对应的 Python 字节码
对于这种题目就只能看对应字节码进行还原,通过这样的方式手动还原为对应的py
源代码,之后再进行对应的逆向工作。
如我们有源码:
1 |
|
我们通过python
将其编译为.pyc
文件后导入dis
库进行反编译为字节码
与之前转换
pyc
为字节码相同
1
2
3
4
5
import dis,marshal
f = open("[对应pyc名字]","rb").read()
code = marshal.loads(f[8:]) # 前8个为pyc对应的魔数(py2是8,py3是16)
# 我们绕过对应魔数进行加载
print(dis.dis(code))
可以得到:
可以成功的看到我们的pyc
转换为对应的字节码了。那么需要怎么逆向呢?
这个时候就需要借助官方文档了,相对汇编来说Py
的字节码相对更好阅读一些,其中python3
是每两个字节一个字节码语句,python2
是三个字节,之前也提到过
我们查询对应python
的手册,可以查看对应的字节码功能,下面给出一部分例子
LOAD_CONST(consti)
将
co_consts[consti]
推入栈顶。
MAKE_FUNCTION
(flags)将一个新函数对象推入栈顶。
…
同样我们举出一个例子来尝试逆向(下面为python3
字节码):
1 |
|
首先我们可以看到程序加载了一个常数加载到num
中,即可以转换为:
1 |
|
0 LOAD_CONST 1 (0)
2 STORE_FAST 1 (num)
随后其有设置一个循环(SETUP_LOOP
),从 0 到 8
1 |
|
3 4 SETUP_LOOP 42 (to 48)
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 2 (8)
10 CALL_FUNCTION 1
12 GET_ITER
>> 14 FOR_ITER 30 (to 46)
16 STORE_FAST 2 (i)
然后我们将一个数字加载到num
中,再进行减运算,最后存储到num
里
1 |
|
BINARY_MODULO
实现
TOS = TOS1 % TOS
,TOS
为栈顶TOS1
为栈中下一位数据的值
4 18 LOAD_FAST 1 (num)
20 LOAD_CONST 3 (7508399208111569251)
22 BINARY_SUBTRACT
24 LOAD_CONST 4 (4294967295)
26 BINARY_MODULO
28 STORE_FAST 1 (num)
综上我们可以得到对应的代码为:
1 |
|
至此py
的字节码逆向就结束了
Python打包EXE
对于这个我们通过一个py
脚本(pyinstxtractor.py)
将其变成结构体和一个pyc
文件,需要注意的是,在得到的对应pyc
文件中缺少了时间属性和对应版本的魔术字,需要我们进行手动修复,通过找到struct.pyc
文件里面的对应的字节,我们复制到我们题目的.pyc
文件前,之后便可以使用uncompyle6
进行反编译
假如我们无法直观识别呢?我们直接将其拖入到IDA
中进行观察一下:
我们可以注意到在字符串中存在大量的Py
开头的东西,这个是一般程序逆向所遇不到的,至此我们便可以识别出来这个程序是一个python
所打包构成的exe
文件
我们尝试进行解包,来拿到我们想要的数据:
通过执行上述代码我们可以得到一个文件夹:
我们在文件夹里面找到题目名字的pyc
文件和struct.pyc
文件,将其拖入到010
(16进制数据查看器)中
通过观察我们可以发现程序缺少了对应红色框部分的魔数字节,我们复制struct.pyc
中的红框内容替换掉login.pyc
内红框内容,之后保存login.pyc
之后我们就可以拿uncompyle6
进行反编译啦
可以看到我们成功的将其反编译成.py
源码了,之后逆向部分就可以参考前文提到的py
源码逆向啦