0x00 前言

以后不做分析了,怪怪的,不如写感想

借这题学习下的pyinstaller打包的程序的逆向流程

借这题把pyinstaller的逆向流程搞清楚来,记得2024国赛初赛也有一题是python相关的,不过是cython,那个难度就高很多了

0x01 解

拿到是一个exe

运行发现是一个贪吃蛇游戏(谁家贪吃蛇按前进方向的反方向键会直接判定转头然后秒了自己的????)

没去掉命令行窗口,可以看到有个pygame,可以确定是个python的程序

查壳,发现是pyinstaller

使用pyinstxtractor进行提取

PyInstaller Extractor is a Python script to extract the contents of a PyInstaller generated executable file.

1
python pyinstxtractor.py snake.exe

然后使用网站进行反编译:https://www.lddgo.net/string/pyc-compile-decompile

当然也可以直接使用pycdc

1
./pycdc.exe snack.pyc > snack.py

不过本地运行输出的结果内的中文部分是乱码

这题似乎不能使用uncompyle6

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# Visit https://www.lddgo.net/string/pyc-compile-decompile for more information
# Version : Python 3.8

import pygame
import random
import key

def initialize(key):
key_length = len(key)
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % key_length]) % 256
S[i] = S[j]
S[j] = S[i]
return S


def generate_key_stream(S, length):
i = 0
j = 0
key_stream = []
for _ in range(length):
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i] = S[j]
S[j] = S[i]
key_stream.append(S[(S[i] + S[j]) % 256])
return key_stream


def decrypt(data, key):
S = initialize(key)
key_stream = generate_key_stream(S, len(data))
decrypted_data = None((lambda .0 = None: [ i ^ data[i] ^ key_stream[i] for i in .0 ])(range(len(data))))
return decrypted_data

pygame.init()
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 600
SNAKE_SIZE = 20
SNAKE_SPEED = 20
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption('贪吃蛇')
font = pygame.font.Font(None, 36)
snake = [
(200, 200),
(210, 200),
(220, 200)]
snake_direction = (SNAKE_SPEED, 0)
food = ((random.randint(0, WINDOW_WIDTH - SNAKE_SIZE) // SNAKE_SIZE) * SNAKE_SIZE, (random.randint(0, WINDOW_HEIGHT - SNAKE_SIZE) // SNAKE_SIZE) * SNAKE_SIZE)
key_bytes = bytes((lambda .0: [ ord(char) for char in .0 ])(key.xor_key))
data = [
101,
97,
39,
125,
218,
172,
205,
3,
235,
195,
72,
125,
89,
130,
103,
213,
120,
227,
193,
67,
174,
71,
162,
248,
244,
12,
238,
92,
160,
203,
185,
155]
decrypted_data = decrypt(bytes(data), key_bytes)
running = True
if running:
window.fill(BLACK)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN or event.key == pygame.K_UP:
snake_direction = (0, -SNAKE_SPEED)
elif event.key == pygame.K_DOWN:
snake_direction = (0, SNAKE_SPEED)
elif event.key == pygame.K_LEFT:
snake_direction = (-SNAKE_SPEED, 0)
elif event.key == pygame.K_RIGHT:
snake_direction = (SNAKE_SPEED, 0)
continue
snake_head = (snake[0][0] + snake_direction[0], snake[0][1] + snake_direction[1])
snake.insert(0, snake_head)
snake.pop()
if snake[0] == food:
food = ((random.randint(0, WINDOW_WIDTH - SNAKE_SIZE) // SNAKE_SIZE) * SNAKE_SIZE, (random.randint(0, WINDOW_HEIGHT - SNAKE_SIZE) // SNAKE_SIZE) * SNAKE_SIZE)
snake.append(snake[-1])
if snake[0][0] < 0 and snake[0][0] >= WINDOW_WIDTH and snake[0][1] < 0 and snake[0][1] >= WINDOW_HEIGHT or snake[0] in snake[1:]:
running = False
for segment in snake:
pygame.draw.rect(window, WHITE, (segment[0], segment[1], SNAKE_SIZE, SNAKE_SIZE))
pygame.draw.rect(window, RED, (food[0], food[1], SNAKE_SIZE, SNAKE_SIZE))
score_text = font.render(f'''Score: {len(snake)}''', True, WHITE)
speed_text = font.render(f'''Speed: {SNAKE_SPEED}''', True, WHITE)
window.blit(score_text, (10, 10))
window.blit(speed_text, (10, 40))
score = len(snake)
if score >= 9999:
flag_text = font.render('Flag: ' + decrypted_data.decode(), True, WHITE)
window.blit(flag_text, (10, 70))
pygame.display.update()
pygame.time.Clock().tick(10)
continue
pygame.quit()

还挺直观,是RC4,主要是把key整到就能解密了

接下来去找PYZ-00.pyz_extracted文件夹里面

结果发现是空的

搜了之后知道了问题

确保使用工具的环境是和Python version一样,否则此文件夹为空

但是我不想装(懒)

所以这边直接去改pyinstxtractor.py,看看能不能让他强制执行

后面发现可以的

1
2
3
4
5
6
# Skip PYZ extraction if not running under the same python version
if self.pymaj != sys.version_info.major or self.pymin != sys.version_info.minor:
print('[!] Warning: This script is running in a different Python version than the one used to build the executable.')
print('[!] Please run this script in Python {0}.{1} to prevent extraction errors during unmarshalling'.format(self.pymaj, self.pymin))
print('[!] Skipping pyz extraction')
return

这里做了个判断,如果版本不对则不执行

直接注释掉就好(无视风险,继续运行)

之后运行,PYZ-00.pyz_extracted文件夹有东西了

找到个key.pyc,直接按部就班,发现有密钥

1
2
3
4
# Source Generated with Decompyle++
# File: key.pyc (Python 3.8)

xor_key = 'V3rY_v3Ry_Ez'

脚本都不用怎么写,直接抄就行了

不过要注意decrypted_data这个变量有点问题

同时还要注意这个

1
2
S[i] = S[j]
S[j] = S[i]

要改成

1
S[i] , S[j] = S[j] , S[i]

其他照抄就行

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
def initialize(key):
key_length = len(key)
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % key_length]) % 256
S[i] , S[j] = S[j] , S[i]
return S


def generate_key_stream(S, length):
i = 0
j = 0
key_stream = []
for _ in range(length):
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i] , S[j] = S[j] , S[i]
key_stream.append(S[(S[i] + S[j]) % 256])
return key_stream


def decrypt(data, key):
key = [ord(c) for c in key]
S = initialize(key)
key_stream = generate_key_stream(S, len(data))
decrypted_data = [i^data[i] ^ key_stream[i] for i in range(len(data))]
return decrypted_data

data = [101,97,39,125,218,172,205,3,235,195,72,125,89,130,103,213,120,227,193,67,174,71,162,248,244,12,238,92,160,203,185,155]
xor_key = "V3rY_v3Ry_Ez"
decrypted_data = decrypt(data, xor_key)
for i in range(len(decrypted_data)):
print(chr(decrypted_data[i]),end='')
# flag{KMLTz3lT_MePUDa7A_P5LpzCBT}