banner
lca

lca

真正的不自由,是在自己的心中设下牢笼。

pyc decompilation example

image

Decompiling pyc files sometimes requires modifying the file header. The problem setter will modify the header, and after decompiling the code, it is necessary to analyze and write the source code to obtain the flag.

File Header#

The format of the Python file header:

MAGIC_1_0 = 0x00999902,
MAGIC_1_1 = 0x00999903,
MAGIC_1_3 = 0x0A0D2E89,
MAGIC_1_4 = 0x0A0D1704,
MAGIC_1_5 = 0x0A0D4E99,
MAGIC_1_6 = 0x0A0DC4FC,

MAGIC_2_0 = 0x0A0DC687,
MAGIC_2_1 = 0x0A0DEB2A,
MAGIC_2_2 = 0x0A0DED2D,
MAGIC_2_3 = 0x0A0DF23B,
MAGIC_2_4 = 0x0A0DF26D,
MAGIC_2_5 = 0x0A0DF2B3,
MAGIC_2_6 = 0x0A0DF2D1,
MAGIC_2_7 = 0x0A0DF303,

MAGIC_3_0 = 0x0A0D0C3A,
MAGIC_3_1 = 0x0A0D0C4E,
MAGIC_3_2 = 0x0A0D0C6C,
MAGIC_3_3 = 0x0A0D0C9E,
MAGIC_3_4 = 0x0A0D0CEE,
MAGIC_3_5 = 0x0A0D0D16,
MAGIC_3_5_3 = 0x0A0D0D17,
MAGIC_3_6 = 0x0A0D0D33,
MAGIC_3_7 = 0x0A0D0D42,
MAGIC_3_8 = 0x0A0D0D55,
MAGIC_3_9 = 0x0A0D0D61,

How to generate a pyc file

The Python code is as follows (filename: pyc.py):

name = input("What is your name? ")
print(f"Hi, {name}!")

Generate it with the following command:

python3 -m py_compile pyc.py

image.png

The file header for version 3.9 is as follows, which is 0x0A0D0D61.

image.png

In CTF challenges involving reversing pyc files, most of the time it requires decompiling the pyc file back to Python code for analysis.

Tools#

There are both online and offline tools for pyc decompilation.

  1. Online pyc and pyo decompilation tools:

http://tools.bugscaner.com/decompyle/

https://tool.lu/pyc/

  1. Offline tools include uncompyle6 and Easy Python Decompiler.

uncompyle6 is a Python library and tool for decompiling compiled Python programs (i.e., .pyc files) back to their original source code, supporting Python versions 1.0-3.8.

Project address: GitHub - rocky/python-uncompyle6: A cross-version Python bytecode decompiler

Easy Python Decompiler is also a Python bytecode decompiler that can decompile pyc and pyo files, supporting Python versions 1.0-3.4.

Download link: Easy Python Decompiler download | SourceForge.net

Example#

The downloaded pyc file name suggests it is likely for Python 3.7.

image.png

Opening it with a hex editor, it is clear that the file header has been modified. A comparison with a normal Python 3.7 pyc file shows the difference.

image.png

The content of a normal Python 3.7 pyc file header is shown below:

image.png

The modified content is as follows:

image.png

Restoring the code

uncompyle6 game.cpython-37.pyc > main.py

The restored Python code is as follows:

# uncompyle6 version 3.9.0
# Python bytecode version base 3.7.0 (3394)
# Decompiled from: Python 3.9.12 (main, Mar 26 2022, 15:44:31) 
# [Clang 13.1.6 (clang-1316.0.21.2)]
# Embedded file name: game.py
# Compiled at: 2020-02-02 19:15:47
# Size of source mod 2**32: 12 bytes
"""Snake Game"""
import random, sys, time, pygame
from pygame.locals import *
from collections import deque
SCREEN_WIDTH = 600
SCREEN_HEIGHT = 480
SIZE = 20
LINE_WIDTH = 1
SCOPE_X = (
 0, SCREEN_WIDTH // SIZE - 1)
SCOPE_Y = (2, SCREEN_HEIGHT // SIZE - 1)
FOOD_STYLE_LIST = [
 (10, (255, 100, 100)), (20, (100, 255, 100)), (30, (100, 100, 255))]
LIGHT = (100, 100, 100)
DARK = (200, 200, 200)
BLACK = (0, 0, 0)
RED = (200, 30, 30)
BGCOLOR = (40, 40, 60)

def print_text(screen, font, x, y, text, fcolor=(255, 255, 255)):
    imgText = font.render(text, True, fcolor)
    screen.blit(imgText, (x, y))


def init_snake():
    snake = deque()
    snake.append((2, SCOPE_Y[0]))
    snake.append((1, SCOPE_Y[0]))
    snake.append((0, SCOPE_Y[0]))
    return snake


def create_food(snake):
    food_x = random.randint(SCOPE_X[0], SCOPE_X[1])
    food_y = random.randint(SCOPE_Y[0], SCOPE_Y[1])
    while (food_x, food_y) in snake:
        food_x = random.randint(SCOPE_X[0], SCOPE_X[1])
        food_y = random.randint(SCOPE_Y[0], SCOPE_Y[1])

    return (
     food_x, food_y)


def get_food_style():
    return FOOD_STYLE_LIST[random.randint(0, 2)]


def main():
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption('Snake Game')
    font1 = pygame.font.SysFont('SimHei', 24)
    font2 = pygame.font.Font(None, 72)
    fwidth, fheight = font2.size('GAME OVER')
    b = True
    snake = init_snake()
    food = create_food(snake)
    food_style = get_food_style()
    pos = (1, 0)
    game_over = True
    start = False
    score = 0
    orispeed = 0.5
    speed = orispeed
    last_move_time = None
    pause = False
    while 1:
        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit()

        screen.fill(BGCOLOR)
        for x in range(SIZE, SCREEN_WIDTH, SIZE):
            pygame.draw.line(screen, BLACK, (x, SCOPE_Y[0] * SIZE), (x, SCREEN_HEIGHT), LINE_WIDTH)

        for y in range(SCOPE_Y[0] * SIZE, SCREEN_HEIGHT, SIZE):
            pygame.draw.line(screen, BLACK, (0, y), (SCREEN_WIDTH, y), LINE_WIDTH)

        curTime = game_over or time.time()
        if curTime - last_move_time > speed and not pause:
            b = True
            last_move_time = curTime
            next_s = (snake[0][0] + pos[0], snake[0][1] + pos[1])
            if next_s == food:
                snake.appendleft(next_s)
                score += food_style[0]
                speed = orispeed - 0.03 * (score // 100)
                food = create_food(snake)
                food_style = get_food_style()
            else:
                if SCOPE_X[0] <= next_s[0] <= SCOPE_X[1]:
                    if SCOPE_Y[0] <= next_s[1] <= SCOPE_Y[1]:
                        if next_s not in snake:
                            snake.appendleft(next_s)
                            snake.pop()
                        else:
                            game_over = True
                    if not game_over:
                        pygame.draw.rect(screen, food_style[1], (food[0] * SIZE, food[1] * SIZE, SIZE, SIZE), 0)
                    for s in snake:
                        pygame.draw.rect(screen, DARK, (s[0] * SIZE + LINE_WIDTH, s[1] * SIZE + LINE_WIDTH,
                         SIZE - LINE_WIDTH * 2, SIZE - LINE_WIDTH * 2), 0)

                    print_text(screen, font1, 30, 7, f"Speed: {score // 100}")
                    print_text(screen, font1, 450, 7, f"Score: {score}")
                    if score > 1000:
                        flag = [
                         30, 196, 
                         52, 252, 49, 220, 7, 243, 
                         3, 241, 24, 224, 40, 230, 
                         25, 251, 28, 233, 40, 237, 
                         4, 225, 4, 215, 40, 231, 
                         22, 237, 14, 251, 10, 169]
                        for i in range(0, len(flag), 2):
                            flag[i], flag[i + 1] = flag[i + 1] ^ 136, flag[i] ^ 119

                        print_text(screen, font2, (SCREEN_WIDTH - fwidth) // 2, (SCREEN_HEIGHT - fheight) // 2, bytes(flag).decode(), RED)
                        pygame.display.update()
                    if game_over:
                        if start:
                            print_text(screen, font2, (SCREEN_WIDTH - fwidth) // 2, (SCREEN_HEIGHT - fheight) // 2, 'GAME OVER', RED)
                pygame.display.update()


if __name__ == '__main__':
    main()
# okay decompiling game.cpython-37.pyc

By analyzing the Python file, this is a game where achieving a score greater than 1000 will yield the flag. The key code is as follows:

if score > 1000:
	flag = [
	 30, 196, 
	 52, 252, 49, 220, 7, 243, 
	 3, 241, 24, 224, 40, 230, 
	 25, 251, 28, 233, 40, 237, 
	 4, 225, 4, 215, 40, 231, 
	 22, 237, 14, 251, 10, 169]
	for i in range(0, len(flag), 2):
		flag[i], flag[i + 1] = flag[i + 1] ^ 136, flag[i] ^ 119

Write code to obtain the flag:

flag = [30, 196, 52, 252, 49, 220, 7, 243, 3, 241, 24, 224, 40, 230, 25, 251, 28, 233, 40, 237, 4, 225, 4, 215, 40, 231, 22, 237, 14, 251, 10, 169]

for i in range(0, len(flag), 2):
    flag[i], flag[i + 1] = flag[i + 1] ^ 136, flag[i] ^ 119
result = ""

for i in range(0, len(flag), 2):
    char = chr(flag[i]) + chr(flag[i + 1])
    result += char

print(result)  # Obtain flag LitCTF{python_snake_is_so_easy!}

The image is from: https://wallhaven.cc/w/1pdg63

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.