# 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)

## 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:

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:

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?