snack

拿到题目,贪吃蛇

使用DIE工具检查可以得知该小游戏是用Py写的,且使用了PyInstaller打包

于是按照常规流程,使用现成的pyinstxtractor脚本拆包

得到exe反编译的文件后,找到该程序的主函数部分,可以发现snack是没有被反编译为pyc文件的,这是由于pyinstaller在打包过程中会自动将PyProject文件头抹除,所以用010edi添上后即可反编译(该头与Python版本有关)

这里又是经典环节之本地工具不如在线工具,同样的库本地就是跑不出来

使用在线反编译工具可出主函数源代码

#!/usr/bin/env python
# visit https://tool.lu/pyc/ 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()

可以看出主函数里已经给出了加密后的密文以及详细的加密过程/函数,但是还缺异或的Key

从key_bytes可以看出异或的key是从key.xor_key引用来的

在pyc工程文件中找到key.pyc

尝试反编译失败,使用010发现其文件头少了四个字节

添加后即可反编译得到xor_key

exp:

xor_key = 'V3rY_v3Ry_Ez'
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
]
key_bytes = bytes(ord(char) for char in xor_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 = bytes(i ^ data[i] ^ key_stream[i] for i in range(len(data)))
    return decrypted_data
​
decrypted_data = decrypt(bytes(data), key_bytes)
flag = decrypted_data.decode()
print(flag)
​

主函数中还用到了诸如lambda之类意义不明的函数,只能暂且理解为生成游戏中会用到的功能了

不同的 Python 版本会有不同的 PyObject_HEAD,以下是各版本的文件头:

Python 版本

十六进制文件头

Python 2.7

03f30d0a00000000

Python 3.0

3b0c0d0a00000000

Python 3.1

4f0c0d0a00000000

Python 3.2

6c0c0d0a00000000

Python 3.3

9e0c0d0a0000000000000000

Python 3.4

ee0c0d0a0000000000000000

Python 3.5

170d0d0a0000000000000000

Python 3.6

330d0d0a0000000000000000

Python 3.7

420d0d0a000000000000000000000000

Python 3.8

55 0d 0d 0a 00 00 00 00 00 00 00 00 00 00 00 00

Python 3.9

610d0d0a000000000000000000000000

Python 3.10

6f0d0d0a000000000000000000000000

Python 3.11

a70d0d0a000000000000000000000000