---
# System prepended metadata

title: 'QnQSec CTF 2025: OneShotRevenge [rev] writeup'

---

# QnQSec CTF 2025: OneShotRevenge [rev] writeup

Writeup author: fsharp

## Introduction

I had some free time during the weekend and played QnQSec CTF 2025 with [`DeadSec`](https://deadsec.team). I solved several reverse engineering challenges, including `OneShotRevenge`, which had just 3 solves when the contest ended. We were the second team to solve it!

## Problem description

Author: .effie

Patch the restrict mode, change some opcode, exploit audit logs, you name it!

(File provided: Revengeee.exe)

![qnqsec_ctf_2025_oneshotrevenge_solved](https://hackmd.io/_uploads/rkBiQ2d0lx.png)

## Initial analysis

I'll try running the program to get a feel of what it does.

```
C:\Users\fsharp\Desktop\qnqsec\OneShotRevenge>Revengeee.exe

Niko is trapped in a puzzle.
Scratches on the walls read:

   THIS IS YOUR ONLY CHANCE.
ONE SHOT, OR STUCK HERE TRYING,
            FOREVER.

"Please... you have to help me. I don't understand this."
Niko looks at you with desperate hope.

"Do you know the way out?"
> idontknow

Niko's hope shatters.
"No... we were so close..."
Everything collapses. The loop seals shut.
Niko is trapped. Forever.
[System shutting down]
```

If I try to run the program after inputting an incorrect flag once:

```
C:\Users\fsharp\Desktop\qnqsec\OneShotRevenge>Revengeee.exe

You return to the same room.
Niko is still there. Waiting.
"You failed last time. We're stuck in a loop now."
"Like Sisyphus pushing the boulder. Like Bill Murray in Groundhog Day."
"We're trapped here. Forever. Rolling the same rock up the same hill."
"There's no way out anymore. You had one chance."
[The program terminates. The loop is sealed.]
```

It doesn't look like the program will accept flags anymore, so I've missed the one shot I had... or have I?

Let's begin reverse engineering it. One of the first things I do when reverse engineering is to figure out what kind of program I'm looking at. My go-to tool for that is [Detect It Easy](https://github.com/horsicq/Detect-It-Easy). Using it on this challenge, I get:

![revengeee_exe_detect_it_easy](https://hackmd.io/_uploads/ry4sMJtAge.png)

The Windows executable is packed by [PyInstaller](https://pyinstaller.org/en/stable/). According to its manual, the packed executable is a Python application and its dependencies all put into one file. This is so that the application's users do not need to install any Python-related software to run it, such as a Python interpreter and the packages the program depends on.

There are ways to unpack the contents of such programs. My favourite way is to use [pyinstxtractor-ng](https://github.com/pyinstxtractor/pyinstxtractor-ng), where you run a Python script on the PyInstaller program you want unpacked and you don't need to worry about the Python version you're using to do so:

```
C:\Users\fsharp\Desktop\qnqsec\OneShotRevenge>python pyinstxtractor_ng.py Revengeee.exe
[+] Processing Revengeee.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.13
[+] Length of package: 7170611 bytes
[+] Found 22 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: revenge.pyc
[+] Found 113 files in PYZ archive
[+] Successfully extracted pyinstaller archive: Revengeee.exe

You can now use a python decompiler on the pyc files within the extracted directory
```

Several files are extracted into a `Revengeee.exe_extracted` folder, including [compiled Python (PYC) files](https://docs.python.org/3/library/py_compile.html). PYCs contain the bytecode of their corresponding Python scripts, and [such files are created and packed by PyInstaller into the program](https://pyinstaller.org/en/stable/advanced-topics.html#python-imports-in-a-bundled-app).

From the pyinstxtractor-ng output above, a possible entry point of the program is `revenge.pyc`, which looks interesting and relevant to the challenge. So, the next step is to decompile that PYC. PYCs are dependent on the Python version used, with new bytecode introduced in newer versions.

The Python version used for this challenge is 3.13, and this can be an issue. Most of the PYC decompilers I've encountered only work reliably for certain Python versions, and they usually do not work from Python 3.13 onwards due to lacking support for newer bytecode. In such cases, I will first try uploading the PYC into the [PyLingual](https://pylingual.io/) website, which [uses natural language processing techniques](https://doi.org/10.1109/SP61157.2025.00052) to perform decompilation:

![revenge_pyc_pylingual_decompilation](https://hackmd.io/_uploads/Bkl2xVh_Agg.png)

PyLingual successfully decompiled the PYC, which means it generated Python code that compiles to the same bytecode as in the PYC file. However, there's a problem: the program has been obfuscated with [Pyarmor](https://github.com/dashingsoft/pyarmor), and it's impossible to tell what it does just by looking at its Python code. So, it needs to be deobfuscated. Deobfuscating Pyarmor programs can be quite involved depending on whether a free trial, basic, or pro version of Pyarmor was used, [with paid versions offering more obfuscation capabilities](https://pyarmor.readthedocs.io/en/latest/licenses.html).

## Deobfuscating the Pyarmored PYC

To proceed further, I need to find a way to deobfuscate the Pyarmored `revenge.pyc`. After searching online for Pyarmor deobfuscation tools, I stumbled upon the [Pyarmor Static Unpack One-Shot Tool](https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot) on GitHub. There are a few interesting tidbits from its project description:

```
This project aims to convert armored data back to bytecode assembly and (experimentally) source code.

Warning: Disassembly results are accurate, but decompiled code can be incomplete and incorrect.

You don't need to execute the encrypted script. We decrypt them using the same algorithm as pyarmor_runtime. This is useful when the scripts cannot be trusted.
```

Sounds promising! The tool claims it can decrypt the armored data without having to run the Pyarmor script, which might be malicious. It also looks like it can disassemble and attempt to decompile the decrypted PYC.

Following the tool's usage instructions, I downloaded [its latest release for Windows](https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot/releases/download/v0.2.1/pyarmor-1shot-v0.2.1-windows-x86_64.zip), extracted the zip archive, and ran the tool as follows:

```
C:\Users\fsharp\Desktop\qnqsec\OneShotRevenge>python pyarmor-1shot-v0.2.1-windows-x86_64\oneshot\shot.py --output-dir output Revengeee.exe_extracted

 ____                                                                     ____
( __ )                                                                   ( __ )
 |  |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|  |
 |  |   ____                                      _ ___  _          _     |  |
 |  |  |  _ \ _  _  __ _ _ __ _ _ __   ___  _ _  / / __|| |_   ___ | |_   |  |
 |  |  | |_) | || |/ _` | '__| ' `  \ / _ \| '_| | \__ \| ' \ / _ \| __|  |  |
 |  |  |  __/| || | (_| | |  | || || | (_) | |   | |__) | || | (_) | |_   |  |
 |  |  |_|    \_, |\__,_|_|  |_||_||_|\___/|_|   |_|___/|_||_|\___/ \__|  |  |
 |  |         |__/                                                        |  |
 |__|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|__|
(____)                                                        v0.2.1     (____)

              For technology exchange only. Use at your own risk.
        GitHub: https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot

INFO     2025-10-24 18:53:12,939      Found data in binary: revenge.pyc
INFO     2025-10-24 18:53:12,987      Found new runtime: 000000 (Revengeee.exe_extracted\pyarmor_runtime_000000\pyarmor_runtime.pyd)
        ========================
        Pyarmor Runtime (Trial) Information:
        Product: non-profits
        AES key: ab738f35ffce23b13ae73d5a2c17a896
        Mix string AES nonce: 692e6e6f6e2d70726f666974
        ========================
INFO     2025-10-24 18:53:13,558      Using executable: pyarmor-1shot.exe
INFO     2025-10-24 18:53:13,558      Decrypting: 000000 (revenge.pyc)
ERROR    2025-10-24 18:53:13,618      PYCDC: Wrong block type 0 for END_FOR (revenge.pyc)
```

The tool's output messages tell me good news and bad news. The good news is that this challenge used a free trial Pyarmor version, which means I only had to deal with the simplest obfuscation techniques Pyarmor uses. Yay! The bad news is that there's an error from [the pycdc decompiler](https://github.com/zrax/pycdc) indicating decompilation failed due to not fully supporting [the `END_FOR` opcode introduced in Python 3.12](https://docs.python.org/3/library/dis.html#opcode-END_FOR). Oh no!

Even though the PYC couldn't be decompiled, the tool should've outputted the deobfuscated PYC's bytecode disassembly, so not all hope is lost. Going into the tool's output directory, I see `revenge.pyc.1shot.cdc.py`, which contains the decompilation results:

```python
# File: revenge.pyc.1shot.seq (Python 3.13)
# Source generated by Pyarmor-Static-Unpack-1shot (v0.2.1), powered by Decompyle++ (pycdc)

# Note: Decompiled code can be incomplete and incorrect.
# Please also check the correct and complete disassembly file: revenge.pyc.1shot.das

'__pyarmor_enter_2273__(...)'
__assert_armored__ = '__pyarmor_assert_2272__'
import winreg
import subprocess
import sys
import os
import random
import string
import json

class RegistryManager:
    __firstlineno__ = 9
    '__pyarmor_enter_2276__(...)'
    __assert_armored__ = '__pyarmor_assert_2275__'
    
    def __init__(self):
        '__pyarmor_enter_2279__(...)'
        __assert_armored__ = '__pyarmor_assert_2278__'
        self.root = winreg.HKEY_CURRENT_USER
        self.base_path = 'Software\\OneShot'
        None(self._ensure_base_exists)
        '__pyarmor_exit_2280__(...)'

    
    def _ensure_base_exists(self):
        '__pyarmor_enter_2282__(...)'
        __assert_armored__ = '__pyarmor_assert_2281__'
        winreg.CreateKey(self.root, self.base_path)
        '__pyarmor_exit_2283__(...)'

    
    def set(self, name, value):
        '__pyarmor_enter_2285__(...)'
        __assert_armored__ = '__pyarmor_assert_2284__'
        _var_var_0 = winreg.CreateKey(self.root, self.base_path)
        if None(isinstance, value, int):
            if  <= -2147483648, value or -2147483648, value <= 2147483647:
                pass
            
        else:
            winreg.SetValueEx(_var_var_0, name, 0, winreg.REG_DWORD, value & 0xFFFFFFFF)
        winreg.SetValueEx(_var_var_0, name, 0, winreg.REG_SZ, None(hex, value))
        winreg.CloseKey(_var_var_0)
        '__pyarmor_exit_2286__(...)'

    
    def u2s(self, v):
        '__pyarmor_enter_2288__(...)'
        __assert_armored__ = '__pyarmor_assert_2287__'
        if v > 2147483647:
            pass
        else:
            None(v - 0x100000000)
            return None
        '__pyarmor_exit_2289__(...)'
        return v - 0x100000000

    
    def get(self, name):
        '__pyarmor_enter_2291__(...)'
        __assert_armored__ = '__pyarmor_assert_2290__'
        _var_var_0 = winreg.OpenKey(self.root, self.base_path, 0, winreg.KEY_READ)
    # WARNING: Decompyle incomplete

    
    def delete(self, name):
        '''Delete a registry value'''
        '__pyarmor_enter_2294__(...)'
        __assert_armored__ = '__pyarmor_assert_2293__'
        _var_var_0 = winreg.OpenKey(self.root, self.base_path, 0, winreg.KEY_WRITE)
        winreg.DeleteValue(_var_var_0, name)
        winreg.CloseKey(_var_var_0)
        '__pyarmor_exit_2295__(...)'

    __static_attributes__ = ('base_path', 'root')
    '__pyarmor_exit_2277__(...)'


def main():
    '__pyarmor_enter_2297__(...)'
    __assert_armored__ = '__pyarmor_assert_2296__'
    _var_var_7 = None(RegistryManager)
    if None(_var_var_7.get, 'r8') == '.effie':
        None(print)
        None(print, "You've done it, my man.")
        None(print, 'The sun now glows a little brighter.')
        None(print, '"Thank you for playing."')
        None(print)
    None(destruction, _var_var_7, False)
    if None(any, (lambda .0: pass# WARNING: Decompyle incomplete
)(None(range, 8))):
        None(print)
        None(print, 'You return to the same room.')
        None(print, 'Niko is still there. Waiting.')
        None(print, '"You failed last time. We\'re stuck in a loop now."')
        None(print, '"Like Sisyphus pushing the boulder. Like Bill Murray in Groundhog Day."')
        None(print, '"We\'re trapped here. Forever. Rolling the same rock up the same hill."')
        None(print, '"There\'s no way out anymore. You had one chance."')
        None(print, '[The program terminates. The loop is sealed.]')
        None(print)
    None(destruction, _var_var_7, False)
    None(print)
    None(print, 'Niko is trapped in a puzzle.')
    None(print, 'Scratches on the walls read:')
    None(print)
    None(print, '   THIS IS YOUR ONLY CHANCE.   ')
    None(print, 'ONE SHOT, OR STUCK HERE TRYING,')
    None(print, '            FOREVER.           ')
    None(print)
    None(print, '"Please... you have to help me. I don\'t understand this."')
    None(print, 'Niko looks at you with desperate hope.')
    None(print)
    None(_var_var_7.set, 'r0', None(None(None(input, '"Do you know the way out?"\n> ').strip).encode))
    if None(len, None(_var_var_7.get, 'r0')) != 162:
        pass
    None(destruction, _var_var_7)
# WARNING: Decompyle incomplete


def child_process(parent_pid, exe_path):
    '''
Child process: wait a bit, then attempt to delete parent exe.
Called as: python script.py --child <parent_pid> <exe_path>
'''
    '__pyarmor_enter_2300__(...)'
    __assert_armored__ = '__pyarmor_assert_2299__'
    import time as _var_var_11
    None(_var_var_11.sleep, 2)
    for _var_var_12 in None(range, 5):
        os.remove(exe_path)
        None(range, 5)
    '__pyarmor_exit_2301__(...)'


def destruction(reg, mes):
    '__pyarmor_enter_2303__(...)'
    __assert_armored__ = '__pyarmor_assert_2302__'
    if mes:
        None(print)
        None(print, "Niko's hope shatters.")
        None(print, '"No... we were so close..."')
        None(print, 'Everything collapses. The loop seals shut.')
        None(print, 'Niko is trapped. Forever.')
        None(print, '[System shutting down]')
    None(print)
    import time as _var_var_11
    None(_var_var_11.sleep, 3)
    _var_var_13 = sys.executable if None(getattr, sys, 'frozen', False) else sys.argv[0]
    subprocess.Popen([
        sys.executable,
        __file__,
        '--child',
        None(str, parent_pid),
        _var_var_13])
    os.remove(_var_var_13)
    sys.exit(1)
    '__pyarmor_exit_2304__(...)'

if __name__ == '__main__':
    if len(sys.argv) > 1 and sys.argv[1] == '--child':
        child_process(int(sys.argv[2]), sys.argv[3])
    else:
        main()
'__pyarmor_exit_2274__(...)'
```

I can see part of the program's logic from the Python script. The `RegistryManager` class looks like it creates the [Windows registry](https://en.wikipedia.org/wiki/Windows_Registry) key `HKEY_CURRENT_USER\Software\OneShot`, then stores different kinds of values under that key, including 32-bit integers (`winreg.REG_DWORD`) and null-terminated strings (`winreg.REG_SZ`). In the `main()` function, if the `r8` registry value is `.effie`, a few congratulatory messages are printed. If some other unknown condition is met, the messages I get from missing my one shot are printed. Otherwise, the messages I get from running the program for the first time are printed, my inputted flag is stored into the `r0` registry value, and there's a check on whether it is 162 characters long. I can't see what happens afterwards though. Aside from that, the `destruction` function can be called in the program's main logic, which  prints the messages I get for inputting an incorrect flag if a boolean is set, sleeps for 3 seconds, and spawns a child process that tries to delete the program.

As the pycdc error message, the note in the Python script, and the various `WARNING: Decompyle incomplete` comments imply, the decompilation is both incomplete and incorrect. The comment below the note says the correct and complete disassembly file can be found in `revenge.pyc.1shot.das`, so let's check that out:

```
# File: revenge.pyc.1shot.seq (Python 3.13)
# Disassembly generated by Pyarmor-Static-Unpack-1shot (v0.2.1), powered by pycdas

# ================================
# Pyarmor notes:
# - Pyarmor bytecode and code objects match standard Python, but special calls to Pyarmor runtime functions exist.
# - Calls on strings are not mistakes but markers, which are processed by Pyarmor at runtime.
#
# Decompilation guidance (without runtime):
# 1. Ignore encrypted bytes after `#`; use only the string before `#`.
# 2. Remove `"__pyarmor_enter_xxx__"(b"<COAddr>...")` and `"__pyarmor_leave_xxx__"(b"<COAddr>...")` (prologue/epilogue).
# 3. `"__pyarmor_assert_xxx__"(A)` is not a real assert statement.
#    - If `A` is a name or readable string: replace with `A`.
#    - If `A` is `(X, "Y")`: replace with `X.Y`.
#    - If `A` is `(X, "Y", Z)`: replace with `X.Y = Z`.
#    - Otherwise: choose the most reasonable replacement.
# 4. `"__pyarmor_bcc_xxx__"(...)` indicates native code; function body is not available. Add a comment.
# ================================

[Code]
    File Name: <frozen revenge>
...
                    [Disassembly]
                        0       NOP                             
                        2       NOP                             
                        4       LOAD_CONST                      2: '__pyarmor_enter_2279__'
                        6       PUSH_NULL                       
                        8       LOAD_CONST                      3: b'<COAddr>\x01\x00\x00\x1an\x00\x00\x00\x00\x00\x00\x00'
                        10      BUILD_TUPLE                     1
                        12      CALL_FUNCTION_EX                0
                        14      POP_TOP                         
                        16      RESUME                          0
                        18      LOAD_CONST                      1: '__pyarmor_assert_2278__'
                        20      STORE_FAST                      1: __assert_armored__
                        22      NOP                             
                        24      NOP                             
                        26      NOP                             
                        28      LOAD_GLOBAL                     0: winreg
                        38      LOAD_ATTR                       2: HKEY_CURRENT_USER
                        58      LOAD_FAST                       0: self
                        60      STORE_ATTR                      2: root
                        70      LOAD_CONST                      4: 'Software\\OneShot'
                        72      LOAD_FAST                       0: self
                        74      STORE_ATTR                      3: base_path
                        84      LOAD_FAST                       0: self
                        86      LOAD_ATTR                       9: _ensure_base_exists
                        106     CALL                            0
                        114     POP_TOP                         
                        116     LOAD_CONST                      0: None
                        118     NOP                             
                        120     NOP                             
                        122     NOP                             
                        124     JUMP_FORWARD                    19 (to 164)
                        132     POP_TOP                         
                        134     RETURN_CONST                    0: None
                        136     PUSH_EXC_INFO                   
                        138     LOAD_CONST                      5: '__pyarmor_exit_2280__'
                        140     NOP                             
                        142     PUSH_NULL                       
                        144     LOAD_CONST                      3: b'<COAddr>\x01\x00\x00\x1an\x00\x00\x00\x00\x00\x00\x00'
                        146     CALL                            1
                        154     POP_TOP                         
                        156     RERAISE                         0
                        158     COPY                            3
                        160     POP_EXCEPT                      
                        162     RERAISE                         1
                        164     LOAD_CONST                      5: '__pyarmor_exit_2280__'
                        166     PUSH_NULL                       
                        168     LOAD_CONST                      3: b'<COAddr>\x01\x00\x00\x1an\x00\x00\x00\x00\x00\x00\x00'
                        170     BUILD_TUPLE                     1
                        172     CALL_FUNCTION_EX                0
                        174     POP_TOP                         
                        176     RETURN_VALUE                    
...
```

I see some strings from the disassembly that are present in the incorrect decompilation. It looks like I can use the disassembly to help me figure out what the program's doing!

## Simplifying the PYC disassembly

The disassembly file I got contains 3336 lines and is somewhat big. However, I've got a good guess on what the `RegistryManager` class does, and I only want to know what other checks on the flag there are after its length check, so I don't actually need to read all 3336 lines.

Let's try to simplify the disassembly. The file contains different sections:
```
[Code]
[Constants]
[Disassembly]
[Exception Table]
[Locals+Names]
[Names]
```

`[Code]` sections describe each part of the Python script the program came from. For example, each function defined (`destruction`, `child_process` etc.) has its own `[Code]` section. The bytecode disassembly is inside the `[Disassembly]` sections. Modules, local variables, constants and the like from the four other types of sections are already included and referenced in the bytecode disassembly.

If I kept just the `[Disassembly]` section of the `main()` function and ignored everything else, only 1344 lines will remain, and that should be enough for me to work off of.

The start of the simplified disassembly looks like this:

```
0       MAKE_CELL                       3: _var_var_7
2       NOP
4       NOP
6       LOAD_CONST                      2: '__pyarmor_enter_2297__'
8       PUSH_NULL
10      LOAD_CONST                      3: b'<COAddr>\x03\x00\x00\x1cD\x1f\x00\x00\x00\x00\x00\x00'
12      BUILD_TUPLE                     1
14      CALL_FUNCTION_EX                0
16      POP_TOP
18      RESUME                          0
20      LOAD_CONST                      1: '__pyarmor_assert_2296__'
22      STORE_FAST                      0: __assert_armored__
24      NOP
26      NOP
28      NOP
30      LOAD_GLOBAL                     1: NULL + RegistryManager
40      CALL                            0
48      STORE_DEREF                     3: _var_var_7
50      LOAD_DEREF                      3: _var_var_7
52      LOAD_ATTR                       3: get
72      LOAD_CONST                      4: 'r8'
74      CALL                            1
82      LOAD_CONST                      5: '.effie'
84      COMPARE_OP                      88 (==)
88      POP_JUMP_IF_FALSE               65 (to 220)
92      LOAD_GLOBAL                     5: NULL + print
102     CALL                            0
110     POP_TOP
112     LOAD_GLOBAL                     5: NULL + print
122     LOAD_CONST                      6: "You've done it, my man."
124     CALL                            1
132     POP_TOP
134     LOAD_GLOBAL                     5: NULL + print
144     LOAD_CONST                      7: 'The sun now glows a little brighter.'
146     CALL                            1
154     POP_TOP
156     LOAD_GLOBAL                     5: NULL + print
166     LOAD_CONST                      8: '"Thank you for playing."'
168     CALL                            1
176     POP_TOP
178     LOAD_GLOBAL                     5: NULL + print
188     CALL                            0
196     POP_TOP
198     LOAD_GLOBAL                     7: NULL + destruction
208     LOAD_DEREF                      3: _var_var_7
210     LOAD_CONST                      9: False
...
746     LOAD_DEREF                      3: _var_var_7
748     LOAD_ATTR                       13: set
768     LOAD_CONST                      26: 'r0'
770     LOAD_GLOBAL                     15: NULL + input
780     LOAD_CONST                      27: '"Do you know the way out?"\n> '
782     CALL                            1
790     LOAD_ATTR                       17: strip
810     CALL                            0
818     LOAD_ATTR                       19: encode
838     CALL                            0
846     CALL                            2
854     POP_TOP
856     LOAD_GLOBAL                     21: NULL + len
866     LOAD_DEREF                      3: _var_var_7
868     LOAD_ATTR                       3: get
888     LOAD_CONST                      26: 'r0'
890     CALL                            1
898     CALL                            1
906     LOAD_CONST                      28: 162
908     COMPARE_OP                      119 (!=)
912     POP_JUMP_IF_FALSE               11 (to 936)
916     LOAD_GLOBAL                     7: NULL + destruction
926     LOAD_DEREF                      3: _var_var_7
928     CALL                            1
936     POP_TOP
938     LOAD_DEREF                      3: _var_var_7
...
```

The first hundred or so lines contain disassembly that match the partial decompilation of the `main()` function I got! The start contains disassembly for initializing a `RegistryManager` instance in the variable `_var_var_7` and printing the congratulatory messages. At opcode positions 746 - 936, the inputted flag is stored in `r0`, and if it isn't 162 characters long, `destruction` is called. For simplicity, I'll replace `_var_var_7` with `reg` so that it's easier to remember that the variable manages registry values.

With the disassembly, I can now look at what happens beyond the flag length check. The [`POP_JUMP_IF_FALSE` opcode](https://docs.python.org/3/library/dis.html#opcode-POP_JUMP_IF_FALSE) at position 912 tells us that if the flag length is 162, then the virtual machine will jump to position 936 and execute opcodes from there. Let's see what happens from that position then:

```
938     LOAD_DEREF                      3: reg
940     LOAD_ATTR                       13: set
960     LOAD_CONST                      29: 'r1'
962     LOAD_GLOBAL                     11: NULL + range
972     LOAD_CONST                      30: 6
974     CALL                            1
982     GET_ITER
984     LOAD_FAST_AND_CLEAR             1: _var_var_8
986     SWAP                            2
988     BUILD_LIST                      0
990     SWAP                            2
992     GET_ITER
994     FOR_ITER                        32 (to 1060)
998     STORE_FAST                      1: _var_var_8
1000    LOAD_DEREF                      3: reg
1002    LOAD_ATTR                       3: get
1022    LOAD_CONST                      26: 'r0'
1024    CALL                            1
1032    LOAD_FAST                       1: _var_var_8
1034    LOAD_CONST                      31: 27
1036    BINARY_OP                       5 (*)
1040    LOAD_FAST                       1: _var_var_8
1042    LOAD_CONST                      32: 1
1044    BINARY_OP                       0 (+)
1048    LOAD_CONST                      31: 27
1050    BINARY_OP                       5 (*)
1054    BINARY_SLICE
1056    LIST_APPEND                     2
1058    JUMP_BACKWARD                   34 (to 992)
1062    END_FOR
1064    POP_TOP
1066    SWAP                            2
1068    STORE_FAST                      1: _var_var_8
1070    CALL                            2
1078    POP_TOP
1080    LOAD_DEREF                      3: reg
1082    LOAD_ATTR                       13: set
1102    LOAD_CONST                      26: 'r0'
1104    LOAD_CONST                      33: 0
1106    CALL                            2
1114    POP_TOP
...
```

Without partial decompilation to refer to, this feels hard to reverse. But if one understands how Python bytecode is executed and how values are stored, then this shouldn't be that bad.

## Understanding PYC disassembly

Python bytecode is executed by the [Python Virtual Machine](https://leanpub.com/insidethepythonvirtualmachine/read#leanpub-auto-the-view-from-30000ft), which is stack-based. This means opcodes can push values onto and pop values from the stack.

The very first opcode after the length check is `LOAD_DEREF reg` (position 938). What does [Python's documentation](https://docs.python.org/3/library/dis.html) say about it?

```
LOAD_DEREF(i)
Loads the cell contained in slot i of the “fast locals” storage.
Pushes a reference to the object the cell contains on the stack.
```

In this case, `LOAD_DEREF reg` pushes a reference of the registry manager onto the stack. If I represented the stack as a Python list (`[]`) and the rightmost element in the list represents the top of the stack, then after executing that opcode, I'd get `[reg]`. I can put that as a comment in the disassembly:

```
938     LOAD_DEREF                      3: reg              # [reg]
```

The next opcode is `LOAD_ATTR set`. In this case, the topmost stack element (`reg`) is replaced by the `set` method from `reg`:

```
940     LOAD_ATTR                       13: set             # [reg.set]
```

After that, `LOAD_CONST 'r1'` pushes the string `'r1'` onto the stack:

```
960     LOAD_CONST                      29: 'r1'            # [reg.set, 'r1']
```

Similarly, `LOAD_GLOBAL NULL + range` pushes `range` (it technically pushes `NULL` too, but that can be ignored), and `LOAD_CONST 6` pushes the integer `6`. `CALL 1` applies a callable function on the topmost argument in the stack. We now have:

```
962     LOAD_GLOBAL                     11: NULL + range    # [reg.set, 'r1', range]
972     LOAD_CONST                      30: 6               # [reg.set, 'r1', range, 6]
974     CALL                            1                   # [reg.set, 'r1', range(6)]
```

Then there's an interesting sequence of opcodes from `GET_ITER` in position 982 to `SWAP` in position 990. A [StackOverflow answer](https://stackoverflow.com/a/77494995) indicates this is related to list comprehension and illustrates that in the following example:

```
>>> import dis
>>> dis.dis("[True for _ in ()]")
  0           0 RESUME                   0

  1           2 LOAD_CONST               0 (())
              4 GET_ITER
              6 LOAD_FAST_AND_CLEAR      0 (_)
              8 SWAP                     2
             10 BUILD_LIST               0
             12 SWAP                     2
        >>   14 FOR_ITER                 4 (to 26)
             18 STORE_FAST               0 (_)
             20 LOAD_CONST               1 (True)
             22 LIST_APPEND              2
             24 JUMP_BACKWARD            6 (to 14)
        >>   26 END_FOR
             28 SWAP                     2
             30 STORE_FAST               0 (_)
             32 RETURN_VALUE
        >>   34 SWAP                     2
             36 POP_TOP
             38 SWAP                     2
             40 STORE_FAST               0 (_)
             42 RERAISE                  0
ExceptionTable:
  10 to 26 -> 34 [2]
```

In our case, each value in the iterable `range(6)` is being used and stored into the variable `_var_var_8`. Let's rename that variable to `i`.

Since there is a `FOR_ITER` and a corresponding `END_ITER`, I'd guess there is a `for` loop iterating over `range(6)`. So, I can indent the disassembly between `FOR_ITER` and `END_FOR` to indicate a `for` loop. Using this intuition, I can annotate more of the disassembly as follows:

```
982     GET_ITER
984     LOAD_FAST_AND_CLEAR             1: i
986     SWAP                            2
988     BUILD_LIST                      0                   # Push an empty list for list comprehension (let's call that some_list)
990     SWAP                            2
992     GET_ITER                                            # for i in range(6):
    994     FOR_ITER                        32 (to 1060)
    998     STORE_FAST                      1: i
    1000    LOAD_DEREF                      3: reg              # [reg.set, 'r1', ..., some_list, reg]
    1002    LOAD_ATTR                       3: get              # [reg.set, 'r1', ..., some_list, reg.get]
    1022    LOAD_CONST                      26: 'r0'            # [reg.set, 'r1', ..., some_list, reg.get, 'r0']
    1024    CALL                            1                   # [reg.set, 'r1', ..., some_list, reg.get('r0') --> r0]
    1032    LOAD_FAST                       1: i                # [reg.set, 'r1', ..., some_list, r0, i]
    1034    LOAD_CONST                      31: 27              # [reg.set, 'r1', ..., some_list, r0, i, 27]
    1036    BINARY_OP                       5 (*)               # [reg.set, 'r1', ..., some_list, r0, i * 27]
    1040    LOAD_FAST                       1: i                # [reg.set, 'r1', ..., some_list, r0, i * 27, i]
    1042    LOAD_CONST                      32: 1               # [reg.set, 'r1', ..., some_list, r0, i * 27, i, 1]
    1044    BINARY_OP                       0 (+)               # [reg.set, 'r1', ..., some_list, r0, i * 27, i + 1]
    1048    LOAD_CONST                      31: 27              # [reg.set, 'r1', ..., some_list, r0, i * 27, i + 1, 27]
    1050    BINARY_OP                       5 (*)               # [reg.set, 'r1', ..., some_list, r0, i * 27, (i + 1) * 27]
    1054    BINARY_SLICE                                        # [reg.set, 'r1', ..., some_list, r0[i * 27 : (i + 1) * 27]]
    1056    LIST_APPEND                     2                   # some_list.append(r0[i * 27 : (i + 1) * 27])
    1058    JUMP_BACKWARD                   34 (to 992)
    1062    END_FOR
1064    POP_TOP
1066    SWAP                            2
1068    STORE_FAST                      1: i                # [reg.set, 'r1', some_list]
1070    CALL                            2                   # [reg.set('r1', some_list) --> r1 = some_list]
1078    POP_TOP                                             # []
```

So the disassembly from position 938 to 1078 is just doing:

```python
r1 = [r0[i * 27 : (i + 1) * 27] for i in range(6)]
```

Remember, from the partial decompilation, that our inputted flag is initially stored in the registry value `r0`. This line of code splits our flag into 6 parts of 27 (`162 // 6`) characters and stores that into `r1`. Very cool!

## Solving for each part of the flag

At this point, it's apparent that the program uses the Windows registry in place of Python variables. So, `reg.get(r<x>)` statements can be simplified into a variable called `r<x>`, and `reg.set(r<x>, <val>)` statements can be turned into `r<x> = <val>`.

Let's continue from where we left off.

```
1080    LOAD_DEREF                      3: reg
1082    LOAD_ATTR                       13: set
1102    LOAD_CONST                      26: 'r0'
1104    LOAD_CONST                      33: 0
1106    CALL                            2               # r0 = 0
1114    POP_TOP
1116    NOP
1118    LOAD_DEREF                      3: reg
1120    LOAD_ATTR                       3: get
1140    LOAD_CONST                      26: 'r0'
1142    CALL                            1
1150    LOAD_CONST                      33: 0           # [r0, 0]
1152    COMPARE_OP                      88 (==)         # [r0 == 0]
1156    POP_JUMP_IF_FALSE               848 (to 2856)   # r0 != 0?
...
```

`r0` is set to `0` and is then checked if it matches `0`. If it doesn't, the VM jumps to position 2856 instead. What's located there?

```
2858    LOAD_DEREF                      3: reg
2860    LOAD_ATTR                       3: get
2880    LOAD_CONST                      26: 'r0'
2882    CALL                            1
2890    LOAD_CONST                      32: 1
2892    COMPARE_OP                      88 (==)
2896    POP_JUMP_IF_FALSE               621 (to 4142)   # r0 != 1?
...
```

This time, if `r0` is not `1`, another jump is taken. Following all the jumps, I see:

```
4144    LOAD_DEREF                      3: reg
4146    LOAD_ATTR                       3: get
4166    LOAD_CONST                      26: 'r0'
4168    CALL                            1
4176    LOAD_CONST                      48: 2
4178    COMPARE_OP                      88 (==)
4182    POP_JUMP_IF_FALSE               529 (to 5244)   # r0 != 2?
...
5246    LOAD_DEREF                      3: reg
5248    LOAD_ATTR                       3: get
5268    LOAD_CONST                      26: 'r0'
5270    CALL                            1
5278    LOAD_CONST                      43: 3
5280    COMPARE_OP                      88 (==)
5284    POP_JUMP_IF_FALSE               342 (to 5972)   # r0 != 3?
...
5974    LOAD_DEREF                      3: reg
5976    LOAD_ATTR                       3: get
5996    LOAD_CONST                      26: 'r0'
5998    CALL                            1
6006    LOAD_CONST                      39: 4
6008    COMPARE_OP                      88 (==)
6012    POP_JUMP_IF_FALSE               477 (to 6970)   # r0 != 4?
...
6972    LOAD_DEREF                      3: reg
6974    LOAD_ATTR                       3: get
6994    LOAD_CONST                      26: 'r0'
6996    CALL                            1
7004    LOAD_CONST                      58: 5
7006    COMPARE_OP                      88 (==)
7010    POP_JUMP_IF_FALSE               330 (to 7674)   # r0 != 5?
...
7674    JUMP_FORWARD                    104 (to 7884)
7676    LOAD_DEREF                      3: reg
7678    LOAD_ATTR                       3: get
7698    LOAD_CONST                      26: 'r0'
7700    CALL                            1
7708    LOAD_CONST                      30: 6
7710    COMPARE_OP                      88 (==)
7714    POP_JUMP_IF_FALSE               72 (to 7860)    # r0 != 6?
7718    LOAD_GLOBAL                     5: NULL + print
7728    CALL                            0
7736    POP_TOP
7738    LOAD_GLOBAL                     5: NULL + print
7748    LOAD_CONST                      62: "Niko's eyes light up. The puzzle dissolves."
7750    CALL                            1
7758    POP_TOP
7760    LOAD_GLOBAL                     5: NULL + print
7770    LOAD_CONST                      63: '"We did it! We actually made it!"'
7772    CALL                            1
7780    POP_TOP
7782    LOAD_GLOBAL                     5: NULL + print
7792    LOAD_CONST                      64: 'The way forward opens. Niko is free.'
7794    CALL                            1
7802    POP_TOP
7804    LOAD_GLOBAL                     5: NULL + print
7814    CALL                            0
7822    POP_TOP
7824    LOAD_DEREF                      3: reg
7826    LOAD_ATTR                       13: set
7846    LOAD_CONST                      4: 'r8'
7848    LOAD_CONST                      5: '.effie'
7850    CALL                            2
7858    POP_TOP
7860    JUMP_FORWARD                    14 (to 7890)
...
```

There are checks for integers `2` to `6` as well. If `r0 == 6`, messages indicating a puzzle is solved get printed. So, the goal seems to be to get `r0` to `6`!

Let's check what gets executed if `r0 == 5`:

```
7016    LOAD_DEREF                      3: reg
7018    LOAD_ATTR                       13: set
7038    LOAD_CONST                      37: 'r4'
7040    BUILD_LIST                      0
7042    LOAD_CONST                      59: (33, 20, 23, 94, 127, 14, 26, 30, 58, 27, 84, 68, 27, 51, 78, 72, 80, 62, 2, 71, 92, 65, 64, 43, 89, 87, 83)
7044    LIST_EXTEND                     1
7046    CALL                            2               # r4 = the list above
7054    POP_TOP
7056    LOAD_DEREF                      3: reg
7058    LOAD_ATTR                       13: set
7078    LOAD_CONST                      34: 'r3'
7080    BUILD_LIST                      0
7082    LOAD_CONST                      60: (73, 32, 97, 109, 32, 104, 111, 112, 101, 108, 101, 115, 115, 108, 121, 32, 99, 97, 112, 116, 105, 118, 97, 116, 101, 100, 46)
7084    LIST_EXTEND                     1
7086    CALL                            2               # r3 = the list above
7094    POP_TOP
7096    LOAD_DEREF                      3: reg
7098    LOAD_ATTR                       13: set
7118    LOAD_CONST                      36: 'r2'
7120    LOAD_CONST                      48: 2
7122    CALL                            2               # r2 = 2
7130    POP_TOP
7132    LOAD_DEREF                      3: reg
7134    LOAD_ATTR                       3: get
7154    LOAD_CONST                      36: 'r2'
7156    CALL                            1
7164    LOAD_CONST                      61: 110
7166    COMPARE_OP                      18 (<)
7170    POP_JUMP_IF_FALSE               214 (to 7600)   # if r2 >= 110, go to position 7600
7174    LOAD_DEREF                      3: reg
7176    LOAD_ATTR                       3: get
7196    LOAD_CONST                      29: 'r1'
7198    CALL                            1               # [r1]
7206    LOAD_DEREF                      3: reg
7208    LOAD_ATTR                       3: get
7228    LOAD_CONST                      26: 'r0'
7230    CALL                            1               # [r1, r0]
7238    BINARY_SUBSCR                                   # [r1[r0]]
7242    LOAD_DEREF                      3: reg
7244    LOAD_ATTR                       3: get
7264    LOAD_CONST                      36: 'r2'
7266    CALL                            1               # [r1[r0], r2]
7274    LOAD_CONST                      48: 2
7276    BINARY_OP                       10 (-)          # [r1[r0], r2 - 2]
7280    LOAD_CONST                      39: 4
7282    BINARY_OP                       2 (//)          # [r1[r0], (r2 - 2) // 4]
7286    BINARY_SUBSCR                                   # [r1[r0][(r2 - 2) // 4]]
7290    LOAD_DEREF                      3: reg
7292    LOAD_ATTR                       3: get
7312    LOAD_CONST                      37: 'r4'
7314    CALL                            1               # [r1[r0][(r2 - 2) // 4], r4]
7322    LOAD_DEREF                      3: reg
7324    LOAD_ATTR                       3: get
7344    LOAD_CONST                      36: 'r2'
7346    CALL                            1               # [r1[r0][(r2 - 2) // 4], r4, r2]
7354    LOAD_CONST                      48: 2
7356    BINARY_OP                       10 (-)          # [r1[r0][(r2 - 2) // 4], r4, r2 - 2]
7360    LOAD_CONST                      39: 4
7362    BINARY_OP                       2 (//)          # [r1[r0][(r2 - 2) // 4], r4, (r2 - 2) // 4]
7366    BINARY_SUBSCR                                   # [r1[r0][(r2 - 2) // 4], r4[(r2 - 2) // 4]]
7370    BINARY_OP                       12 (^)          # [r1[r0][(r2 - 2) // 4] ^ r4[(r2 - 2) // 4]]
7374    LOAD_DEREF                      3: reg
7376    LOAD_ATTR                       3: get
7396    LOAD_CONST                      34: 'r3'
7398    CALL                            1               # [r1[r0][(r2 - 2) // 4] ^ r4[(r2 - 2) // 4], r3]
7406    LOAD_DEREF                      3: reg
7408    LOAD_ATTR                       3: get
7428    LOAD_CONST                      36: 'r2'
7430    CALL                            1               # [r1[r0][(r2 - 2) // 4] ^ r4[(r2 - 2) // 4], r3, r2]
7438    LOAD_CONST                      48: 2
7440    BINARY_OP                       10 (-)          # [r1[r0][(r2 - 2) // 4] ^ r4[(r2 - 2) // 4], r3, r2 - 2]
7444    LOAD_CONST                      39: 4
7446    BINARY_OP                       2 (//)          # [r1[r0][(r2 - 2) // 4] ^ r4[(r2 - 2) // 4], r3, (r2 - 2) // 4]
7450    BINARY_SUBSCR                                   # [r1[r0][(r2 - 2) // 4] ^ r4[(r2 - 2) // 4], r3[(r2 - 2) // 4]]
7454    COMPARE_OP                      119 (!=)
7458    POP_JUMP_IF_FALSE               11 (to 7482)    # if r1[r0][(r2 - 2) // 4] ^ r4[(r2 - 2) // 4] == r3[(r2 - 2) // 4], go to position 7482
7462    LOAD_GLOBAL                     7: NULL + destruction
7472    LOAD_DEREF                      3: reg
7474    CALL                            1               # destruction(reg)
7482    POP_TOP                                         # []
7484    LOAD_DEREF                      3: reg
7486    LOAD_ATTR                       13: set
7506    LOAD_CONST                      36: 'r2'        # [reg.set, 'r2']
7508    LOAD_DEREF                      3: reg
7510    LOAD_ATTR                       3: get
7530    LOAD_CONST                      36: 'r2'
7532    CALL                            1               # [reg.set, 'r2', r2]
7540    LOAD_CONST                      39: 4
7542    BINARY_OP                       0 (+)           # [reg.set, 'r2', r2 + 4]
7546    CALL                            2               # r2 = r2 + 4
7554    POP_TOP
7556    LOAD_DEREF                      3: reg
7558    LOAD_ATTR                       3: get
7578    LOAD_CONST                      36: 'r2'
7580    CALL                            1               # [r2]
7588    LOAD_CONST                      61: 110
7590    COMPARE_OP                      18 (<)          # [r2 < 110]
7594    POP_JUMP_IF_FALSE               2 (to 7600)     # If r2 >= 110, go to position 7600
7598    JUMP_BACKWARD                   214 (to 7172)   # Otherwise, go to position 7172
7602    LOAD_DEREF                      3: reg
7604    LOAD_ATTR                       13: set
7624    LOAD_CONST                      26: 'r0'        # [reg.set, 'r0']
7626    LOAD_DEREF                      3: reg
7628    LOAD_ATTR                       3: get
7648    LOAD_CONST                      26: 'r0'
7650    CALL                            1               # [reg.set, 'r0', r0]
7658    LOAD_CONST                      32: 1
7660    BINARY_OP                       0 (+)           # [reg.set, 'r0', r0 + 1]
7664    CALL                            2               # r0 = r0 + 1
7672    POP_TOP                                         # []
7674    JUMP_FORWARD                    104 (to 7884)   # Go to position 7884
...
7884    JUMP_BACKWARD                   3386 (to 1116)  # Go to position 1116
...
```

Here, `r1[r0]` is being referred to. Since `r0` is `5` in this case, `r1[r0]` represents the last part of the flag. If a condition is met, `destruction(reg)` is called and the challenge is 'failed.' If conditions are met so that the `destruction` function is never called, then `r0` is incremented by `1`, and the VM jumps to position 7884, which contains an opcode that just makes it jump to position 1116:

```
1116    NOP
1118    LOAD_DEREF                      3: reg
1120    LOAD_ATTR                       3: get
1140    LOAD_CONST                      26: 'r0'
1142    CALL                            1
1150    LOAD_CONST                      33: 0           # [r0, 0]
1152    COMPARE_OP                      88 (==)         # [r0 == 0]
1156    POP_JUMP_IF_FALSE               848 (to 2856)   # r0 != 0?
...
```

Since execution goes back to the start of the `r0` value checks, this means each of the 6 flag parts has its own checker and they are checked sequentially! In fact, all 6 checkers essentially have the same structure, with each checking the corresponding flag part differently.

I will now go through the 6 parts in order. For each part, I'll show its Python equivalent and the commented disassembly for part 1 so that you can see how I came up with its Python code.

(No commented disassembly for other parts... darn it, free plan character limits...)

### Part 1

<details>
    <summary>Commented disassembly</summary>

    1162    LOAD_DEREF                      3: reg
    1164    LOAD_ATTR                       13: set
    1184    LOAD_CONST                      34: 'r3'
    1186    BUILD_LIST                      0
    1188    LOAD_CONST                      35: (213, 102, 213, 85, 64, 198, 119, 243, 85, 179, 64, 85, 230, 183, 115, 179, 85, 179, 85, 179, 247, 179, 230, 102, 179, 102, 85)
    1190    LIST_EXTEND                     1
    1192    CALL                            2               # r3 = the list above
    1200    POP_TOP
    1202    LOAD_DEREF                      3: reg
    1204    LOAD_ATTR                       13: set
    1224    LOAD_CONST                      36: 'r2'
    1226    LOAD_CONST                      33: 0
    1228    CALL                            2               # r2 = 0
    1236    POP_TOP
    1238    LOAD_DEREF                      3: reg
    1240    LOAD_ATTR                       3: get
    1260    LOAD_CONST                      36: 'r2'
    1262    CALL                            1               # [r2]
    1270    LOAD_CONST                      31: 27          # [r2, 27]
    1272    COMPARE_OP                      18 (<)          # [r2 < 27]
    1276    POP_JUMP_IF_FALSE               750 (to 2780)   # while r2 < 27:
        1282    LOAD_DEREF                      3: reg
        1284    LOAD_ATTR                       13: set
        1304    LOAD_CONST                      37: 'r4'        # [reg.set, 'r4']
        1306    LOAD_DEREF                      3: reg
        1308    LOAD_ATTR                       3: get
        1328    LOAD_CONST                      29: 'r1'
        1330    CALL                            1               # [reg.set, 'r4', r1]
        1338    LOAD_DEREF                      3: reg
        1340    LOAD_ATTR                       3: get
        1360    LOAD_CONST                      26: 'r0'
        1362    CALL                            1               # [reg.set, 'r4', r1, r0]
        1370    BINARY_SUBSCR                                   # [reg.set, 'r4', r1[r0]]
        1374    LOAD_DEREF                      3: reg
        1376    LOAD_ATTR                       3: get
        1396    LOAD_CONST                      36: 'r2'
        1398    CALL                            1               # [reg.set, 'r4', r1[r0], r2]
        1406    BINARY_SUBSCR                                   # [reg.set, 'r4', r1[r0][r2]]
        1410    CALL                            2               # r4 = r1[r0][r2]
        1418    POP_TOP
        1420    LOAD_DEREF                      3: reg
        1422    LOAD_ATTR                       13: set
        1442    LOAD_CONST                      38: 'r6'
        1444    LOAD_CONST                      33: 0
        1446    CALL                            2               # r6 = 0
        1454    POP_TOP
        1456    LOAD_DEREF                      3: reg
        1458    LOAD_ATTR                       3: get
        1478    LOAD_CONST                      38: 'r6'
        1480    CALL                            1               # [r6]
        1488    LOAD_CONST                      39: 4           # [r6, 4]
        1490    COMPARE_OP                      18 (<)          # [r6 < 4]
        1494    POP_JUMP_IF_FALSE               251 (to 1998)   # while r6 < 4:
            1498    LOAD_DEREF                      3: reg
            1500    LOAD_ATTR                       13: set
            1520    LOAD_CONST                      40: 'r5'        # [reg.set, 'r5']
            1522    LOAD_DEREF                      3: reg
            1524    LOAD_ATTR                       3: get
            1544    LOAD_CONST                      37: 'r4'
            1546    CALL                            1               # [reg.set, 'r5', r4]
            1554    LOAD_CONST                      39: 4
            1556    BINARY_OP                       9 (>>)          # [reg.set, 'r5', r4 >> 4]
            1560    LOAD_CONST                      41: 15
            1562    BINARY_OP                       1 (&)           # [reg.set, 'r5', (r4 >> 4) & 15]
            1566    CALL                            2               # r5 = (r4 >> 4) & 15
            1574    POP_TOP
            1576    LOAD_DEREF                      3: reg
            1578    LOAD_ATTR                       13: set
            1598    LOAD_CONST                      38: 'r6'        # [reg.set, 'r6']
            1600    LOAD_DEREF                      3: reg
            1602    LOAD_ATTR                       3: get
            1622    LOAD_CONST                      37: 'r4'
            1624    CALL                            1               # [reg.set, 'r6', r4]
            1632    LOAD_CONST                      41: 15
            1634    BINARY_OP                       1 (&)           # [reg.set, 'r6', r4 & 15]
            1638    CALL                            2               # r6 = r4 & 15
            1646    POP_TOP
            1648    LOAD_DEREF                      3: reg
            1650    LOAD_ATTR                       13: set
            1670    LOAD_CONST                      38: 'r6'        # [reg.set, 'r6']
            1672    LOAD_DEREF                      3: reg
            1674    LOAD_ATTR                       3: get
            1694    LOAD_CONST                      38: 'r6'
            1696    CALL                            1               # [reg.set, 'r6', r6]
            1704    LOAD_DEREF                      3: reg
            1706    LOAD_ATTR                       3: get
            1726    LOAD_CONST                      40: 'r5'
            1728    CALL                            1               # [reg.set, 'r6', r6, r5]
            1736    LOAD_CONST                      42: 7
            1738    BINARY_OP                       5 (*)           # [reg.set, 'r6', r6, r5 * 7]
            1742    LOAD_DEREF                      3: reg
            1744    LOAD_ATTR                       3: get
            1764    LOAD_CONST                      38: 'r6'
            1766    CALL                            1               # [reg.set, 'r6', r6, r5 * 7, r6]
            1774    LOAD_CONST                      43: 3
            1776    BINARY_OP                       5 (*)           # [reg.set, 'r6', r6, r5 * 7, r6 * 3]
            1780    BINARY_OP                       0 (+)           # [reg.set, 'r6', r6, r5 * 7 + r6 * 3]
            1784    LOAD_DEREF                      3: reg
            1786    LOAD_ATTR                       3: get
            1806    LOAD_CONST                      40: 'r5'
            1808    CALL                            1               # [reg.set, 'r6', r6, r5 * 7 + r6 * 3, r5]
            1816    LOAD_CONST                      32: 1
            1818    BINARY_OP                       3 (<<)          # [reg.set, 'r6', r6, r5 * 7 + r6 * 3, r5 << 1]
            1822    BINARY_OP                       12 (^)          # [reg.set, 'r6', r6, (r5 * 7 + r6 * 3) ^ (r5 << 1)]
            1826    LOAD_CONST                      41: 15
            1828    BINARY_OP                       1 (&)           # [reg.set, 'r6', r6, ((r5 * 7 + r6 * 3) ^ (r5 << 1)) & 15]
            1832    BINARY_OP                       12 (^)          # [reg.set, 'r6', r6 ^ (((r5 * 7 + r6 * 3) ^ (r5 << 1)) & 15)]
            1836    CALL                            2               # r6 ^= ((r5 * 7 + r6 * 3) ^ (r5 << 1)) & 15
            1844    POP_TOP
            1846    LOAD_DEREF                      3: reg
            1848    LOAD_ATTR                       13: set
            1868    LOAD_CONST                      37: 'r4'        # [reg.set, 'r4']
            1870    LOAD_DEREF                      3: reg
            1872    LOAD_ATTR                       3: get
            1892    LOAD_CONST                      38: 'r6'
            1894    CALL                            1               # [reg.set, 'r4', r6]
            1902    LOAD_CONST                      39: 4
            1904    BINARY_OP                       3 (<<)          # [reg.set, 'r4', r6 << 4]
            1908    LOAD_DEREF                      3: reg
            1910    LOAD_ATTR                       3: get
            1930    LOAD_CONST                      40: 'r5'
            1932    CALL                            1               # [reg.set, 'r4', r6 << 4, r5]
            1940    BINARY_OP                       7 (|)           # [reg.set, 'r4', (r6 << 4) | r5]
            1944    CALL                            2               # r4 = (r6 << 4) | r5
            1952    POP_TOP
            1954    LOAD_DEREF                      3: reg
            1956    LOAD_ATTR                       3: get
            1976    LOAD_CONST                      38: 'r6'
            1978    CALL                            1
            1986    LOAD_CONST                      39: 4
            1988    COMPARE_OP                      18 (<)
            1992    POP_JUMP_IF_FALSE               2 (to 1998)
            1996    JUMP_BACKWARD                   251 (to 1496)
        2000    LOAD_DEREF                      3: reg
        2002    LOAD_ATTR                       13: set
        2022    LOAD_CONST                      29: 'r1'        # [reg.set, 'r1']
        2024    LOAD_DEREF                      3: reg
        2026    LOAD_ATTR                       3: get
        2046    LOAD_CONST                      29: 'r1'
        2048    CALL                            1               # [reg.set, 'r1', r1]
        2056    LOAD_CONST                      0: None         # [reg.set, 'r1', r1, None]
        2058    LOAD_DEREF                      3: reg
        2060    LOAD_ATTR                       3: get
        2080    LOAD_CONST                      26: 'r0'
        2082    CALL                            1               # [reg.set, 'r1', r1, None, r0]
        2090    BINARY_SLICE                                    # [reg.set, 'r1', r1[:r0]]
        2092    LOAD_DEREF                      3: reg
        2094    LOAD_ATTR                       3: get
        2114    LOAD_CONST                      29: 'r1'
        2116    CALL                            1               # [reg.set, 'r1', r1[:r0], r1]
        2124    LOAD_DEREF                      3: reg
        2126    LOAD_ATTR                       3: get
        2146    LOAD_CONST                      26: 'r0'
        2148    CALL                            1               # [reg.set, 'r1', r1[:r0], r1, r0]
        2156    BINARY_SUBSCR                                   # [reg.set, 'r1', r1[:r0], r1[r0]]
        2160    LOAD_CONST                      0: None         # [reg.set, 'r1', r1[:r0], r1[r0], None]
        2162    LOAD_DEREF                      3: reg
        2164    LOAD_ATTR                       3: get
        2184    LOAD_CONST                      36: 'r2'
        2186    CALL                            1               # [reg.set, 'r1', r1[:r0], r1[r0], None, r2]
        2194    BINARY_SLICE                                    # [reg.set, 'r1', r1[:r0], r1[r0][:r2]]
        2196    LOAD_GLOBAL                     25: NULL + bytes# [reg.set, 'r1', r1[:r0], r1[r0][:r2], bytes]
        2206    LOAD_DEREF                      3: reg
        2208    LOAD_ATTR                       3: get
        2228    LOAD_CONST                      37: 'r4'
        2230    CALL                            1               # [reg.set, 'r1', r1[:r0], r1[r0][:r2], bytes, r4]
        2238    BUILD_LIST                      1               # [reg.set, 'r1', r1[:r0], r1[r0][:r2], bytes, [r4]]
        2240    CALL                            1               # [reg.set, 'r1', r1[:r0], r1[r0][:r2], bytes([r4])]
        2248    BINARY_OP                       0 (+)           # [reg.set, 'r1', r1[:r0], r1[r0][:r2] + bytes([r4])]
        2252    LOAD_DEREF                      3: reg
        2254    LOAD_ATTR                       3: get
        2274    LOAD_CONST                      29: 'r1'
        2276    CALL                            1               # [reg.set, 'r1', r1[:r0], r1[r0][:r2] + bytes([r4]), r1]
        2284    LOAD_DEREF                      3: reg
        2286    LOAD_ATTR                       3: get
        2306    LOAD_CONST                      26: 'r0'
        2308    CALL                            1               # [reg.set, 'r1', r1[:r0], r1[r0][:r2] + bytes([r4]), r1, r0]
        2316    BINARY_SUBSCR                                   # [reg.set, 'r1', r1[:r0], r1[r0][:r2] + bytes([r4]), r1[r0]]
        2320    LOAD_DEREF                      3: reg
        2322    LOAD_ATTR                       3: get
        2342    LOAD_CONST                      36: 'r2'
        2344    CALL                            1               # [reg.set, 'r1', r1[:r0], r1[r0][:r2] + bytes([r4]), r1[r0], r2]
        2352    LOAD_CONST                      32: 1
        2354    BINARY_OP                       0 (+)           # [reg.set, 'r1', r1[:r0], r1[r0][:r2] + bytes([r4]), r1[r0], r2 + 1]
        2358    LOAD_CONST                      0: None         # [reg.set, 'r1', r1[:r0], r1[r0][:r2] + bytes([r4]), r1[r0], r2 + 1, None]
        2360    BINARY_SLICE                                    # [reg.set, 'r1', r1[:r0], r1[r0][:r2] + bytes([r4]), r1[r0][r2 + 1:]]
        2362    BINARY_OP                       0 (+)           # [reg.set, 'r1', r1[:r0], r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]]
        2366    BUILD_LIST                      1               # [reg.set, 'r1', r1[:r0], [r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]]]
        2368    BINARY_OP                       0 (+)           # [reg.set, 'r1', r1[:r0] + [r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]]]
        2372    LOAD_DEREF                      3: reg
        2374    LOAD_ATTR                       3: get
        2394    LOAD_CONST                      29: 'r1'
        2396    CALL                            1               # [reg.set, 'r1', r1[:r0] + [r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]], r1]
        2404    LOAD_DEREF                      3: reg
        2406    LOAD_ATTR                       3: get
        2426    LOAD_CONST                      26: 'r0'
        2428    CALL                            1               # [reg.set, 'r1', r1[:r0] + [r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]], r1, r0]
        2436    LOAD_CONST                      32: 1
        2438    BINARY_OP                       0 (+)           # [reg.set, 'r1', r1[:r0] + [r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]], r1, r0 + 1]
        2442    LOAD_CONST                      0: None         # [reg.set, 'r1', r1[:r0] + [r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]], r1, r0 + 1, None]
        2444    BINARY_SLICE                                    # [reg.set, 'r1', r1[:r0] + [r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]], r1[r0 + 1:]]
        2446    BINARY_OP                       0 (+)           # [reg.set, 'r1', r1[:r0] + [r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]] + r1[r0 + 1:]]
        2450    CALL                            2               # r1 = r1[:r0] + [r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]] + r1[r0 + 1:]
        2458    POP_TOP
        2460    LOAD_DEREF                      3: reg
        2462    LOAD_ATTR                       3: get
        2482    LOAD_CONST                      29: 'r1'
        2484    CALL                            1               # [r1]
        2492    LOAD_DEREF                      3: reg
        2494    LOAD_ATTR                       3: get
        2514    LOAD_CONST                      26: 'r0'
        2516    CALL                            1               # [r1, r0]
        2524    BINARY_SUBSCR                                   # [r1[r0]]
        2528    LOAD_DEREF                      3: reg
        2530    LOAD_ATTR                       3: get
        2550    LOAD_CONST                      36: 'r2'
        2552    CALL                            1               # [r1[r0], r2]
        2560    BINARY_SUBSCR                                   # [r1[r0][r2]]
        2564    LOAD_DEREF                      3: reg
        2566    LOAD_ATTR                       3: get
        2586    LOAD_CONST                      34: 'r3'
        2588    CALL                            1               # [r1[r0][r2], r3]
        2596    LOAD_DEREF                      3: reg
        2598    LOAD_ATTR                       3: get
        2618    LOAD_CONST                      36: 'r2'
        2620    CALL                            1               # [r1[r0][r2], r3, r2]
        2628    BINARY_SUBSCR                                   # [r1[r0][r2], r3[r2]]
        2632    COMPARE_OP                      119 (!=)
        2636    POP_JUMP_IF_FALSE               11 (to 2660)    # if r1[r0][r2] != r3[r2]:
            2640    LOAD_GLOBAL                     7: NULL + destruction
            2650    LOAD_DEREF                      3: reg
            2652    CALL                            1               # destruction(reg)
        2660    POP_TOP
        2662    LOAD_DEREF                      3: reg
        2664    LOAD_ATTR                       13: set
        2684    LOAD_CONST                      36: 'r2'        # [reg.set, 'r2']
        2686    LOAD_DEREF                      3: reg
        2688    LOAD_ATTR                       3: get
        2708    LOAD_CONST                      36: 'r2'
        2710    CALL                            1               # [reg.set, 'r2', r2]
        2718    LOAD_CONST                      32: 1
        2720    BINARY_OP                       0 (+)           # [reg.set, 'r2', r2 + 1]
        2724    CALL                            2               # r2 += 1
        2732    POP_TOP
        2734    LOAD_DEREF                      3: reg
        2736    LOAD_ATTR                       3: get
        2756    LOAD_CONST                      36: 'r2'
        2758    CALL                            1
        2766    LOAD_CONST                      31: 27
        2768    COMPARE_OP                      18 (<)
        2772    POP_JUMP_IF_FALSE               3 (to 2780)
        2776    JUMP_BACKWARD                   750 (to 1280)
    2782    LOAD_DEREF                      3: reg
    2784    LOAD_ATTR                       13: set
    2804    LOAD_CONST                      26: 'r0'        # [reg.set, 'r0']
    2806    LOAD_DEREF                      3: reg
    2808    LOAD_ATTR                       3: get
    2828    LOAD_CONST                      26: 'r0'
    2830    CALL                            1               # [reg.set, 'r0', r0]
    2838    LOAD_CONST                      32: 1
    2840    BINARY_OP                       0 (+)           # [reg.set, 'r0', r0 + 1]
    2844    CALL                            2               # Increment r0 by 1 if the flag part is correct
    2852    POP_TOP
    2854    JUMP_FORWARD                    2513 (to 7884)
</details>

Python equivalent:

```python
r3 = [213, 102, 213, 85, 64, 198, 119, 243, 85, 179, 64, 85, 230, 183, 115, 179, 85, 179, 85, 179, 247, 179, 230, 102, 179, 102, 85]

for r2 in range(27):
    r4 = r1[r0][r2]
    r6 = 0
    while r6 < 4:
        r5 = (r4 >> 4) & 0xf
        r6 = r4 & 0xf
        r6 ^= ((r5 * 7 + r6 * 3) ^ (r5 << 1)) & 15
        r4 = (r6 << 4) | r5
    r1 = r1[:r0] + [r1[r0][:r2] + bytes([r4]) + r1[r0][r2 + 1:]] + r1[r0 + 1:]
    if r1[r0][r2] != r3[r2]:
        destruction(reg)
```

The `r1 = r1[:r0] + ...` line just replaces `r1[r0][r2]` with `r4`, so this can be simplified further:

```python
r3 = [213, 102, 213, 85, 64, 198, 119, 243, 85, 179, 64, 85, 230, 183, 115, 179, 85, 179, 85, 179, 247, 179, 230, 102, 179, 102, 85]

for r2 in range(27):
    r4 = r1[r0][r2]
    r6 = 0
    while r6 < 4:
        r5 = (r4 >> 4) & 0xf
        r6 = r4 & 0xf
        r6 ^= ((r5 * 7 + r6 * 3) ^ (r5 << 1)) & 15
        r4 = (r6 << 4) | r5
    if r4 != r3[r2]:
        destruction(reg)
```

Let's try to solve for this part!

```python
alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_{'

r3 = [213, 102, 213, 85, 64, 198, 119, 243, 85, 179, 64, 85, 230, 183, 115, 179, 85, 179, 85, 179, 247, 179, 230, 102, 179, 102, 85]
part_1 = ""

for r3_val in r3:
    for c in alphabet:
        r4 = ord(c)
        r6 = 0
        while r6 < 4:
            r5 = (r4 >> 4) & 0xf
            r6 = r4 & 0xf
            r6 ^= ((r5 * 7 + r6 * 3) ^ (r5 << 1)) & 15
            r4 = (r6 << 4) | r5
        if r4 == r3_val:
            part_1 += c
            break

print(part_1) # QfQSacs1S0aSbq50S0S0p0bf0fS
```

Oh dear. Why does it look so weird? Are there multiple accepted characters for each number in `r3`?

```python
alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_{'

r3 = [213, 102, 213, 85, 64, 198, 119, 243, 85, 179, 64, 85, 230, 183, 115, 179, 85, 179, 85, 179, 247, 179, 230, 102, 179, 102, 85]

for r3_val in r3:
    accepted_chars = ""
    for c in alphabet:
        r4 = ord(c)
        r6 = 0
        while r6 < 4:
            r5 = (r4 >> 4) & 0xf
            r6 = r4 & 0xf
            r6 ^= ((r5 * 7 + r6 * 3) ^ (r5 << 1)) & 15
            r4 = (r6 << 4) | r5
        if r4 == r3_val:
            accepted_chars += c
    print(f"{r3_val}: {accepted_chars}")
```

```
213: QUY
102: fhn
213: QUY
85: SW_
64: aeim
198: ck
119: st{
243: 129
85: SW_
179: 03478
64: aeim
85: SW_
230: bdjl
183: qruvyz
115: 56
179: 03478
85: SW_
179: 03478
85: SW_
179: 03478
247: pwx
179: 03478
230: bdjl
102: fhn
179: 03478
102: fhn
85: SW_
```

Unfortunately, there are. I can tell that I'm on the right track though as I can see that the first 7 characters of the expected flag format (`QnQSec{...}`) are included in the first 7 lines. Right now, it's hard for me to guess what part 1 is saying, so I'll come back to it after solving part 2.

### Part 2

Python equivalent:

```python
r4 = [152, 245, 229, 38, 25, 181, 212, 42, 27, 185, 33, 161, 163, 166, 229, 114, 162, 239, 0, 215, 13, 152, 169, 181, 40, 122, 200]
r3 = 66

for r2 in range(27):
    r3 = ((r3 * 17) ^ r2) & 0xff
    r1 = r1[:r0] + [r1[r0][:r2] + bytes([(r1[r0][r2] + r3) & 255]) + r1[r0][r2 + 1:]] + r1[r0 + 1:]
    if r1[r0][r2] != r4[r2]:
        destruction(reg)
```

Solver:

```python
r4 = [152, 245, 229, 38, 25, 181, 212, 42, 27, 185, 33, 161, 163, 166, 229, 114, 162, 239, 0, 215, 13, 152, 169, 181, 40, 122, 200]
r3 = 66
part_2 = ""

for r2 in range(27):
    r3 = ((r3 * 17) ^ r2) & 0xff
    part_2 += chr((r4[r2] - r3) & 0xff)

print(part_2) # 6r4d3r_h16h_5ch00l_57ud3n7_
```

### Part 1 revisited

Since part 2 is `6r4d3r_h16h_5ch00l_57ud3n7_`, there should be a number before `6r4d3r`. After some guessing, I realized the first part is `QnQSec{1_4m_ju57_4_7w3lf7h_`.

### Part 3

Python equivalent:

```python
r3 = []
r2 = int.from_bytes(r1[r0])
while r2 > 0:
    r3 = [r2 % 69] + r3
    r2 //= 69

r2 = 0
for r3_elem in r3:
    r2 = 69 * r2 + r3_elem

r3 = []
while r2 > 0:
    r3 = [r2 % 73] + r3
    r2 //= 73

if r3 != [9, 41, 37, 34, 21, 5, 29, 42, 16, 37, 31, 5, 53, 18, 72, 65, 5, 23, 67, 26, 30, 53, 64, 7, 55, 19, 27, 22, 68, 62, 18, 1, 39, 6, 50]:
    destruction(reg)
```

Solver:

```python
r3 = [9, 41, 37, 34, 21, 5, 29, 42, 16, 37, 31, 5, 53, 18, 72, 65, 5, 23, 67, 26, 30, 53, 64, 7, 55, 19, 27, 22, 68, 62, 18, 1, 39, 6, 50]
r2 = 0
for r3_elem in r3:
    r2 = 73 * r2 + r3_elem

part_3 = r2.to_bytes((r2.bit_length() + 7) // 8, "big").decode()
print(part_3) # 4nd_1_4m_45k3d_70_m4k3_7h35
```

### Part 4

Python equivalent:

```python
r4 = '\n========================\n  THE ENIGMA OF HEAVEN\n=====================†==\n\nPRAISE THE LORD! The air conditioner keeps them away it sings gospels and\nPRA†SE THE LORD! Finding faith in whit† noise.\nPRAISE THE LORD! The messages are coming in loud and clear and\nI hear them and I see them in the sky the towers are sending m†ssages and\nI hear them and I see t†em.\nPRAISE THE LORD! The people in the parking lot can\'t hurt me anymore\nthey can\'t hurt me †ny†ore their words are weak an† the lord is strong.\nPRAIS† THE LORD† The bibl††shows the way and†the way protects me and\nI\'ve seen the messages and I†heart them and † see them and they can\'t\nhurt me anymore†\nPRAISE THE LORD!\n\nCHAPTERS:      †                          ╔═══════════════╗\n—————————                                 ║               ║\nI. The Eni†ma of Hea†en:                  ║†   H†AVEN     ║\n 9†999,999 Channels,†Finding Faith†    †  ╟───────────────╢\n in White Noise... The God Stimulation!   ║               ║\nII. The Hierarchy of Equality:            ║   †ADIATION   ║\n Angelic Voices Echo Through the Halls    ╟───────────────╢\n of Heaven, Under the Railroad Bridge     ║               ║    "And I have\nIII. The Paradox of Faith:                ║     RADIO     ║      told you\n There\'s a Knocking at the Door!          ╟───────────────╢     the TRUTH,\n God is in, God is in!                    ║               ║\nIV. The Senselessness of Endlessness:     ║  TELEVISION   ║      for you\n Returning to an Empty Apartment          ║               ║   are my child,\n with a Grocery Store Guardian Angel      ╚═══════════════╝    and you have\n                                                              seen my face"\n\nAn EP titled "The Enigma of Heaven and Other Daily De†usions" '
r3 = 0

for r2 in range(27):
    r3 += (r1[r0][r2] - 16) * (2 if r2 & 1 == 0 else -1)
    if r4[r3] != '†':
        destruction(reg)
```

Like part 1, this checker accepts multiple flag parts. My solver is based on ensuring that each cross is only visited once:

```python
r4 = '\n========================\n  THE ENIGMA OF HEAVEN\n=====================†==\n\nPRAISE THE LORD! The air conditioner keeps them away it sings gospels and\nPRA†SE THE LORD! Finding faith in whit† noise.\nPRAISE THE LORD! The messages are coming in loud and clear and\nI hear them and I see them in the sky the towers are sending m†ssages and\nI hear them and I see t†em.\nPRAISE THE LORD! The people in the parking lot can\'t hurt me anymore\nthey can\'t hurt me †ny†ore their words are weak an† the lord is strong.\nPRAIS† THE LORD† The bibl††shows the way and†the way protects me and\nI\'ve seen the messages and I†heart them and † see them and they can\'t\nhurt me anymore†\nPRAISE THE LORD!\n\nCHAPTERS:      †                          ╔═══════════════╗\n—————————                                 ║               ║\nI. The Eni†ma of Hea†en:                  ║†   H†AVEN     ║\n 9†999,999 Channels,†Finding Faith†    †  ╟───────────────╢\n in White Noise... The God Stimulation!   ║               ║\nII. The Hierarchy of Equality:            ║   †ADIATION   ║\n Angelic Voices Echo Through the Halls    ╟───────────────╢\n of Heaven, Under the Railroad Bridge     ║               ║    "And I have\nIII. The Paradox of Faith:                ║     RADIO     ║      told you\n There\'s a Knocking at the Door!          ╟───────────────╢     the TRUTH,\n God is in, God is in!                    ║               ║\nIV. The Senselessness of Endlessness:     ║  TELEVISION   ║      for you\n Returning to an Empty Apartment          ║               ║   are my child,\n with a Grocery Store Guardian Angel      ╚═══════════════╝    and you have\n                                                              seen my face"\n\nAn EP titled "The Enigma of Heaven and Other Daily De†usions" '

alphabet = bytes(range(33, 127)).decode()
candidates = [[0, '', []]]

while len(candidates) != 0:
    r3, part_4, visited_crosses = candidates.pop()
    if len(part_4) == 27:
        print(part_4)
        continue
    r2 = len(part_4)
    for c in alphabet:
        r3_new = r3 + (ord(c) - 16) * (2 if r2 & 1 == 0 else -1)
        if (r4[r3_new] != '†') or (r3_new in visited_crosses):
            continue
        candidates.append([r3_new, part_4 + c, visited_crosses + [r3_new]])
```

While the solver prints multiple answers, this time it's a lot easier to find the correct one: `3_r3v3r51n6_ch4ll3n635,_50b`

### Part 5

Python equivalent:

```python
r3 = 0
r4 = 0
r5 = [(9, 5), (14, 8), (19, 6), (29, 4), (33, 8), (43, 3), (54, 5), (65, 3), (70, 4), (75, 6), (80, 9), (86, 0), (95, 5), (105, 5), (110, 3), (119, 8), (130, 8), (135, 6), (141, 1), (150, 6), (161, 5), (166, 4), (177, 4), (187, 4), (191, 8), (201, 3), (204, 6)]

for r2 in range(27):
    r3 = (10 * r3 + r4 + r1[r0][r2]) // 10
    r4 = (r4 + r1[r0][r2]) % 10
    if r5[r2] != (r3, r4):
        destruction(reg)
```

Solver:

```python
r5 = [(9, 5), (14, 8), (19, 6), (29, 4), (33, 8), (43, 3), (54, 5), (65, 3), (70, 4), (75, 6), (80, 9), (86, 0), (95, 5), (105, 5), (110, 3), (119, 8), (130, 8), (135, 6), (141, 1), (150, 6), (161, 5), (166, 4), (177, 4), (187, 4), (191, 8), (201, 3), (204, 6)]
nums = [0] + [10 * a + b for (a, b) in r5]
part_5 = bytes([nums[i + 1] - nums[i] for i in range(len(nums) - 1)]).decode()
print(part_5) # _50b,_pl3453_d0_n07_m1nd,_!
```

### Part 6

Python equivalent:

```python
r4 = [33, 20, 23, 94, 127, 14, 26, 30, 58, 27, 84, 68, 27, 51, 78, 72, 80, 62, 2, 71, 92, 65, 64, 43, 89, 87, 83]
r3 = [73, 32, 97, 109, 32, 104, 111, 112, 101, 108, 101, 115, 115, 108, 121, 32, 99, 97, 112, 116, 105, 118, 97, 116, 101, 100, 46]
for r2 in range(2, 110, 4):
    if r1[r0][(r2 - 2) // 4] ^ r4[(r2 - 2) // 4] != r3[(r2 - 2) // 4]:
        destruction(reg)
```

Solver:

```python
r4 = [33, 20, 23, 94, 127, 14, 26, 30, 58, 27, 84, 68, 27, 51, 78, 72, 80, 62, 2, 71, 92, 65, 64, 43, 89, 87, 83]
r3 = [73, 32, 97, 109, 32, 104, 111, 112, 101, 108, 101, 115, 115, 108, 121, 32, 99, 97, 112, 116, 105, 118, 97, 116, 101, 100, 46]
part_6 = bytes([a ^ b for (a, b) in zip(r4, r3)]).decode()
print(part_6) # h4v3_fun_w17h_7h3_r357!_<3}
```

## Solving the challenge

Piecing all 6 parts together, the flag should be `QnQSec{1_4m_ju57_4_7w3lf7h_6r4d3r_h16h_5ch00l_57ud3n7_4nd_1_4m_45k3d_70_m4k3_7h353_r3v3r51n6_ch4ll3n635,_50b_50b,_pl3453_d0_n07_m1nd,_!h4v3_fun_w17h_7h3_r357!_<3}`. But how can I check whether this flag is accepted by the program if I've missed my one shot at it already?

Going back to the disassembly, I see this after the `r8 == '.effie'` check:

```
222     LOAD_GLOBAL                     9: NULL + any
232     LOAD_FAST                       3: reg
234     BUILD_TUPLE                     1
236     LOAD_CONST                      10: <CODE> <genexpr>
238     MAKE_FUNCTION
240     SET_FUNCTION_ATTRIBUTE          8 (MAKE_FUNCTION_CLOSURE)
242     LOAD_GLOBAL                     11: NULL + range
252     LOAD_CONST                      11: 8
254     CALL                            1
262     GET_ITER
264     CALL                            0
272     CALL                            1
280     TO_BOOL
288     POP_JUMP_IF_FALSE               109 (to 508)
292     LOAD_GLOBAL                     5: NULL + print
302     CALL                            0
310     POP_TOP
312     LOAD_GLOBAL                     5: NULL + print
322     LOAD_CONST                      12: 'You return to the same room.'
324     CALL                            1
...
```

There is a generator object used here that determines if I've taken my one shot. Its disassembly is excluded from my simplified disassembly and it looks like this:

```
0       COPY_FREE_VARS                  1
2       RETURN_GENERATOR                
4       POP_TOP                         
6       RESUME                          0
8       LOAD_FAST                       0: .0
10      GET_ITER                        
12      FOR_ITER                        36 (to 86)
16      STORE_FAST                      1: _var_var_10
18      LOAD_DEREF                      2: _var_var_7
20      LOAD_ATTR                       1: get
40      LOAD_CONST                      0: 'r'
42      LOAD_GLOBAL                     3: NULL + str
52      LOAD_FAST                       1: _var_var_10
54      CALL                            1
62      BINARY_OP                       0 (+)
66      CALL                            1
74      LOAD_CONST                      1: None
76      IS_OP                           1 (is not)
78      YIELD_VALUE                     0
80      RESUME                          5
82      POP_TOP                         
84      JUMP_BACKWARD                   38 (to 10)
88      END_FOR                         
90      POP_TOP                         
92      RETURN_CONST                    1: None
94      CALL_INTRINSIC_1                3 (INTRINSIC_STOPITERATION_ERROR)
96      RERAISE                         1
```

The check looks like:

```python
if any((reg.get('r' + str(i)) is not None for i in range(8))):
    print()
    print('You return to the same room.')
    ...
```

So all it's doing is checking if any of the registry values `r0` to `r7` exist! I can just delete the `HKEY_CURRENT_USER\Software\OneShot` registry key to bypass this check. Let's give this challenge another shot:

```
C:\Users\fsharp\Desktop\qnqsec\OneShotRevenge>Revengeee.exe

Niko is trapped in a puzzle.
Scratches on the walls read:

   THIS IS YOUR ONLY CHANCE.
ONE SHOT, OR STUCK HERE TRYING,
            FOREVER.

"Please... you have to help me. I don't understand this."
Niko looks at you with desperate hope.

"Do you know the way out?"
> QnQSec{1_4m_ju57_4_7w3lf7h_6r4d3r_h16h_5ch00l_57ud3n7_4nd_1_4m_45k3d_70_m4k3_7h353_r3v3r51n6_ch4ll3n635,_50b_50b,_pl3453_d0_n07_m1nd,_!h4v3_fun_w17h_7h3_r357!_<3}

Niko's eyes light up. The puzzle dissolves.
"We did it! We actually made it!"
The way forward opens. Niko is free.
```

It's over! Since I've already solved the challenge, if I run the program again without deleting the registry key, I'll get:

```
C:\Users\fsharp\Desktop\qnqsec\OneShotRevenge>Revengeee.exe

You've done it, my man.
The sun now glows a little brighter.
"Thank you for playing."
```

## Bonus: A few CTF post-mortem thoughts

Only when creating this writeup did I notice the Pyarmor deobfuscation tool I used has '1Shot' in its name. '1Shot'... 'OneShot'... is this merely a coincidence? Is this tool what the challenge author would use to deobfuscate the flag checker?

I also find it weird how the program didn't delete itself when `destruction` is called. Maybe it's got something to do with file permissions?