Python逆向

前言

随着CTF的兴起,有着越来越多样化的逆向题目(各种不会的语言被出出来折磨人),我们可以比较常见的是CC++语言的逆向题目,而Python题目出现也算是较为常见,题目多为pyc或者是exe,今天与大家一起分享一下关于Python逆向的一些东西

前言

随着当下CTF比赛的发展,各种各样的语音逆向逐渐产生,如C、C++、Python等一些常见语言用于出题也是常规题型,而我们此次所初步了解的是Python的语言逆向,文章整体讲解的较为简单,主要涉及到.py /.pyc/.exe(python打包后的程序)三类题型,如有不足还请多多指正。

.py

在这个情况下我们直接拿到了对应的源代码,可以直接通过对源码进行分析,写出对应的逆向算法进行求解

比如我们现在有如下代码:

1
2
3
4
5
6
7
8
9
def check(a):
if a == 123:
print("Yes!")
else:
print("NO!")


a = input("请输入对应flag:")
check(int(a))

我们可以直接观察到程序的源代码,需要我们输入一个flag进行保存在a中,之后便会对a123进行比较,如果相同,那么将会输出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
python code.pyc

.pyc分成两种情况,一种是不带花指令的,另一种是带花指令的

不带花指令

对于不带花指令的.pyc文件,我们可以直接使用uncompyle6对其进行转换,可以直接得到对应的源代码.py文件

如下图:

带花指令

对于带花指令的.pyc文件我们如果直接使用uncompyle6对其进行转换会出现如下状况:

会提示我们数组发生了越界,证明在这个.pyc文件中添加了一些花指令,让我们无法直接进行转换得到.py,那么应该怎么解决呢?

我们通过导入dismarshal两个库来进行查看对应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))

通过上述步骤我们可以看到对应的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
2
3
4
5
6
7
8
9
10
def check(a):
flag = '123'
if flag == a :
print("Yes!")
else:
print("NO!")


a = input("请输入对应flag:")
check(a)

我们通过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))

可以得到:

7.png

可以成功的看到我们的pyc转换为对应的字节码了。那么需要怎么逆向呢?

这个时候就需要借助官方文档了,相对汇编来说Py的字节码相对更好阅读一些,其中python3是每两个字节一个字节码语句,python2是三个字节,之前也提到过

我们查询对应python的手册,可以查看对应的字节码功能,下面给出一部分例子

LOAD_CONST(consti)

co_consts[consti] 推入栈顶。

MAKE_FUNCTION(flags)

将一个新函数对象推入栈顶。

同样我们举出一个例子来尝试逆向(下面为python3字节码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	  0 LOAD_CONST               1 (0)
2 STORE_FAST 1 (num)

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)

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)

首先我们可以看到程序加载了一个常数加载到num中,即可以转换为:

1
num = 0

0 LOAD_CONST 1 (0)
2 STORE_FAST 1 (num)

随后其有设置一个循环(SETUP_LOOP),从 0 到 8

1
for i in range(8):

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
num = (num - 7508399208111569251) % 4294967295

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
2
3
num = 0
for i in range(8):
num = (num - 7508399208111569251) % 4294967295

至此py的字节码逆向就结束了

Python打包EXE

对于这个我们通过一个py脚本(pyinstxtractor.py)将其变成结构体和一个pyc文件,需要注意的是,在得到的对应pyc文件中缺少了时间属性和对应版本的魔术字,需要我们进行手动修复,通过找到struct.pyc文件里面的对应的字节,我们复制到我们题目的.pyc文件前,之后便可以使用uncompyle6进行反编译

假如我们无法直观识别呢?我们直接将其拖入到IDA中进行观察一下:
8.png

我们可以注意到在字符串中存在大量的Py开头的东西,这个是一般程序逆向所遇不到的,至此我们便可以识别出来这个程序是一个python所打包构成的exe文件

我们尝试进行解包,来拿到我们想要的数据:

9.png

通过执行上述代码我们可以得到一个文件夹:

10.png

我们在文件夹里面找到题目名字的pyc文件和struct.pyc文件,将其拖入到010(16进制数据查看器)中

11.png

12.png

通过观察我们可以发现程序缺少了对应红色框部分的魔数字节,我们复制struct.pyc中的红框内容替换掉login.pyc内红框内容,之后保存login.pyc

13.png

之后我们就可以拿uncompyle6进行反编译啦

14.png

可以看到我们成功的将其反编译成.py源码了,之后逆向部分就可以参考前文提到的py源码逆向啦


Python逆向
https://equinox-shame.github.io/2022/07/13/Python逆向/
作者
梓曰
发布于
2022年7月13日
许可协议