Rating:

# Nesting Snakes (5 solves, 400 points)
by JaGoTu

## Description

Description
I know turtles go all the way down, but what about snakes?

Attachments
* nesting_snakes

Author

Points
400


## ? Pyinstaller

We are given a pretty big **executable ELF file**:


$file nesting_snakes nesting_snakes: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=277dc5acf4d7b9140218325bbc133b79cd3c998e, for GNU/Linux 2.6.32, stripped$ ls -lah nesting_snakes
-rwxrwxrwx 1 jagotu jagotu 7.2M Jul 24 19:40 nesting_snakes


When we tried to run it, we got the following error:


$./nesting_snakes [394] Error loading Python lib '/tmp/_MEIeDa6bW/libpython3.9.so.1.0': dlopen: /lib/x86_64-linux-gnu/libc.so.6: version GLIBC_2.33' not found (required by /tmp/_MEIeDa6bW/libpython3.9.so.1.0)  We could spend some time trying to figure out the libc problem... But this is rev so technically we don't need to run the binary, right? Given the name of the challenge and the size of the binary (there are probably more scientific methods to figure it out), I expected a pyinstaller binary. To cite from https://pyinstaller.readthedocs.io/: > PyInstaller bundles a Python application and all its dependencies into a single package. The user can run the packaged app without installing a Python interpreter or any modules. There's a package called pyinstxtractor that can be used to extract the installer most of the times, but from my experience it works best on Windows binaries and is more hit-or-miss on Linux ones. The same story here: $ python3 pyinstxtractor.py nesting_snakes
[+] Processing nesting_snakes
[!] Error : Unsupported pyinstaller version or not a pyinstaller archive


Luckily, if you can somehow get the pydata from the binary, pyinstxtractor will extract that for you. To get the pydata, we just use 7z:


$7z x nesting_snakes 7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21 p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,8 CPUs Intel(R) Core(TM) i7-4771 CPU @ 3.50GHz (306C3),ASM,AES-NI) Scanning the drive for archives: 1 file, 7452136 bytes (7278 KiB) Extracting archive: nesting_snakes -- Path = nesting_snakes Type = ELF Physical Size = 7452136 CPU = AMD64 64-bit = + Host OS = None Characteristics = Executable file Headers Size = 2472 Everything is Ok Files: 28 Size: 7442756 Compressed: 7452136$ file pydata
pydata: zlib compressed data

$python3 pyinstxtractor.py pydata [+] Processing pydata [+] Pyinstaller version: 2.1+ [+] Python version: 39 [+] Length of package: 7400207 bytes [+] Found 74 files in CArchive [+] Beginning extraction...please standby [+] Possible entry point: pyiboot01_bootstrap.pyc [+] Possible entry point: pyi_rth_pkgutil.pyc [+] Possible entry point: pyi_rth_multiprocessing.pyc [+] Possible entry point: pyi_rth_inspect.pyc [+] Possible entry point: main.pyc Traceback (most recent call last): File "pyinstxtractor.py", line 390, in <module> main() File "pyinstxtractor.py", line 379, in main arch.extractFiles() File "pyinstxtractor.py", line 279, in extractFiles self._writeRawData(entry.name, data) File "pyinstxtractor.py", line 237, in _writeRawData with open(nm, 'wb') as f: PermissionError: [Errno 13] Permission denied: '/stuff.pye'  Hm, seems it tries to write stuff.pye to the root directory. If you feel particularly YOLO, you could probably just run it with sudo, but we decide to instead patch pyinstxtractor, adding these two lines to _writeRawData: py if nm[0] == '/': nm = '.' + nm  After that we can extract succesfully: $ python3 pyinstxtractor.py pydata
[+] Processing pydata
[+] Pyinstaller version: 2.1+
[+] Python version: 39
[+] Length of package: 7400207 bytes
[+] Found 74 files in CArchive
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: main.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python39 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: pydata

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


## ?? main.pyc

The first idea we get is to **decompile** main.pyc. However, both uncompyle6 and decompyle3 fail on the binary. Apparently, adding support for Python 3.9 is kinda hard. The maintainer of uncompyle6 has the following to say about that:

> Right now I have a full-time job which keeps me very busy.
>
> I'd have to do this on the weekends. Getting a rudimentary 3.9 that works _almost_ as well as 3.8 might be 2 or 3 weekends and would be in the decompyle3 project.
>
> To do a proper job would require a reworking of control flow using https://github.com/rocky/python-control-flow . If that were that done, we'd see a general improvement, and probably would be able to handle future Python bytecode and also use this approach in other decompilers. But even after I start, it will to take a long time.
>
> So, what I propose for now is support me in this project for whatever amount seems right to you. When there are enough people interested and about USD $2,025 is collected, I'll spend the time to do it. > > **Edit**: over time the dollar amount may go up, especially at times when I get duplicate requests of this issue such as in #347. Even as the figure stands now, it is a bit low for the amount of effort to do a decent job. So guess we'll just have to work with the xdis disassembler, which works just fine (I shortened the constants quite a bit): $ pydisasm main.pyc
# pydisasm version 5.0.11
# Python bytecode 3.8 (3413)
# Disassembled from Python 3.8.5 (default, Jan 27 2021, 15:41:15)
# [GCC 9.3.0]
# Timestamp in code: 0 (1970-01-01 01:00:00)
# Source code size mod 2**32: 0 bytes
# Method Name: <module>
# Filename: main.py
# Argument count: 0
# Position-only argument count: 0
# Keyword-only arguments: 0
# Number of locals: 0
# Stack size: 6
# Flags: 0x00000040 (NOFREE)
# First Line: 1
# Constants:
# 0: 0
# 1: None
# 2: 358
# 3: (20, -23488, 85, [...], 61, 204, -220885)
# 4: (137, 1351, [...], 863, 1827)
# 5: ''
# 6: 1
# 7: 'Congratulations'
# Names:
# 0: pyconcrete
# 1: stuff
# 2: sys
# 3: os
# 4: _
# 5: __
# 6: ___
# 7: argv
# 8: f
# 9: print
4 IMPORT_NAME (pyconcrete)
6 STORE_NAME (pyconcrete)

12 IMPORT_NAME (stuff)
14 STORE_NAME (stuff)

20 IMPORT_NAME (sys)
22 STORE_NAME (sys)
28 IMPORT_NAME (os)
30 STORE_NAME (os)

34 STORE_NAME (_)

6: 36 BUILD_LIST 0
38 LOAD_CONST ((20, -23488, 85, [...], 61, 204, -220885))
40 CALL_FINALLY (to 43)
42 STORE_NAME (__)

7: 44 BUILD_LIST 0
46 LOAD_CONST ((137, 1351, [...], 863, 1827))
48 CALL_FINALLY (to 51)
50 STORE_NAME (___)

58 BUILD_LIST 1
64 BINARY_SUBSCR
66 STORE_NAME (f)

80 CALL_METHOD 4
84 CALL_FUNCTION 1
86 POP_JUMP_IF_FALSE (to 96)

92 CALL_FUNCTION 1
94 POP_TOP
98 RETURN_VALUE


When rewritten in python ("manually decompiled"), the code looks something like this:

py
import pyconcrete
import stuff
import sys
import os

_ = 358
__ = [20, -23488, 85, [...], 61, 204, -220885]
___ = [137, 1351, [...], 863, 1827]

f = (sys.argv + [''])[1]

if stuff._(_, __, ___, f)(f):
print("Congratulations")


We see that stuff._ returns a function that must return True. So let's dig into stuff.

## ??? stuff.pye

The stuff is in the form of a pye, which I haven't seen yet. Apparently, it's to do with pyconcrete:

> Protect your python script, encrypt .pyc to .pye and decrypt when import it
>
> Protect python script work flow
> --------------
> * your_script.py import pyconcrete
> * pyconcrete will hook import module
> * when your script do import MODULE, pyconcrete import hook will try to find MODULE.pye first
> and then decrypt MODULE.pye via _pyconcrete.pyd and execute decrypted data (as .pyc content)
> * encrypt & decrypt secret key record in _pyconcrete.pyd (like DLL or SO)
> the secret key would be hide in binary code, can't see it directly in HEX view

Secret key would be hide? Whitebox crypto is notoriously hard (see https://whibox.io/), so I fully expect memecrypto. If we throw the _pyconcrete.cpython-39-x86_64-linux-gnu.so into IDA, we see the following snippet:

C
oaes_key_gen_128(v20);
if ( dword_9034 )
{
dword_9034 = 0;
xmmword_9020 = (__int128)_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_6910), (__m128i)xmmword_9020);
}
oaes_key_import_data(v20, &xmmword_9020, 0x10uLL);


Seems the key is just simply stored in two parts that are xored together :) But I guess if the bar you set for yourself is can't see it directly in HEX view, you don't need a lot.

Besides, we actually don't have to reverse the encryption, at all. We can just call the "hooks" manually like so:

py
import _pyconcrete

with open('stuff.pye', 'rb') as f:

dec = _pyconcrete.decrypt_buffer(data)

with open('stuff.pyc', 'wb') as f:
f.write(dec)


If you run python3.9 and have _pyconcrete.cpython-39-x86_64-linux-gnu.so next to this script, running this script you'll get the decrypted module in regular pyc.

## ???? stuff.pyc

Let's disassemble stuff.pyc. I won't include the full disassembly, but here is the code rewritten in python (I renamed private underscore names to a, b, c, d etc.):

py
import random
import types

_____ = lambda: False

def _(a, b, c, d):
random.seed(sum(d.encode()))
random.shuffle(b)

j = []
for val in c:
f = val
h = a
for g in "{:b}".format(f)[1:]:
if(b[h] >= 0):
return _____

if not int(g):
h = abs(b[h]) // len(b)
else:
h = abs(b[h]) % len(b)
if b[h] < 0:
return _____
j.append(b[h])

return types.FunctionType(types.CodeType(1, 0, 0, 7, 38, 67, bytes(j), (None, 0, True, -1, (1, 0, 0, 1, 0, -1, 1, -1, -1, 0, 0, -1, 0, 0, -1, 1), (1, 1, 1, 0, -1, 0, -1, -1, 1, 1, -1, 1, 0, 0, 0, -1), (0, 0, 1, 0, -1, 1, -1, -1, 0, 0, 1, 1, -1, -1, 0, -1), (0, 1, 0, 1, -1, -1, 1, 0, -1, 0, 1, 1, 1, -1, -1, 0), (0, 0, 0, 0, -1, 1, 0, -1, -1, -1, 1, 1, -1, -1, 0, 0), (0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, -1, 1, -1, 0), (-1, 0, -1, 1, 1, 1, -1, 0, 1, -1, -1, 0, -1, 0, 1, 1), (0, 1, -1, 1, 1, 1, 1, 1, -1, 1, -1, 1, 0, 1, -1, 0), (0, 0, 1, 1, -1, 1, -1, 0, 1, 1, -1, 1, 1, 1, -1, -1), (-1, 1, 0, -1, 1, 1, 1, 0, 1, -1, 0, 0, -1, 1, 0, -1), (0, 0, -1, 1, -1, 0, 0, 0, -1, -1, -1, 1, -1, 1, 1, -1), (0, 0, -1, 1, -1, 1, -1, -1, 1, -1, -1, -1, 0, 1, 0, 0), (0, -1, 0, 0, -1, -1, -1, -1, -1, 0, -1, 0, -1, 0, 0, -1), (0, -1, 0, -1, 1, 1, 0, 1, -1, 1, 1, 0, 0, 1, -1, -1), (0, 0, 1, -1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, -1), (1, 1, 1, 1, -1, 1, 1, 0, -1, 0, 0, -1, 1, 0, -1, 1), (0, -1, 1, -1, 1, 0, 1, -1, -1, 0, -1, -1, 0, 0, -1, -1), (0, -1, -1, -1, 1, -1, 1, 1, -1, -1, 0, 0, 0, -1, -1, 0), (0, 0, -1, -1, -1, -1, -1, 1, 1, 0, -1, -1, 0, -1, -1, 1), 16, 1, False), ('random', 'zip', 'seed', 'range', 'append', 'randint'), ('f', 'random', '_____', '___', '____', '_', '__'), '', '', 0, b'', (), ()), {'__builtins__', __builtins__})


The first three arguments to this function are constant, the fourth one is the command-line argument. If something goes wrong, the failure lambda (_____) is returned, otherwise j is used as bytecode for the returned function. As we can see, from the input only the sum of its bytes is used, and we can probably bruteforce that. Let's use a slightly modified version of this function for this:

py
import random

a = 358
c = [137, 1351, [...] , 863, 1827]

def f_(a,_,c,d):
#random.seed(sum(d.encode()))
b = [20, -23488, [...], 204, -220885]
random.seed(d)
random.shuffle(b)

j = []
for val in c:
f = val
h = a
for g in "{:b}".format(f)[1:]:
if(b[h] >= 0):
return False

if not int(g):
h = abs(b[h]) // len(b)
else:
h = abs(b[h]) % len(b)
if b[h] < 0:
return False
j.append(b[h])

return j

for i in range(10000):
res = f_(a, None, c, i)
if res != False:
print(i)
print(res)



$python3 brutesum.py 3299 [100, 1, 100, 0, 108, 0, 125, 1, 100, 2, 125, 2, 116, 1, 124, 0, 100, 0, 100, 0, 100, 3, 133, 3, 25, 0, 103, 0, 100, 4, 162, 1, 103, 0, 100, 5, 162, 1, 103, 0, 100, 6, 162, 1, 103, 0, 100, 7, 162, 1, 103, 0, 100, 8, 162, 1, 103, 0, 100, 9, 162, 1, 103, 0, 100, 7, 162, 1, 103, 0, 100, 10, 162, 1, 103, 0, 100, 11, 162, 1, 103, 0, 100, 8, 162, 1, 103, 0, 100, 12, 162, 1, 103, 0, 100, 9, 162, 1, 103, 0, 100, 13, 162, 1, 103, 0, 100, 9, 162, 1, 103, 0, 100, 8, 162, 1, 103, 0, 100, 14, 162, 1, 103, 0, 100, 9, 162, 1, 103, 0, 100, 6, 162, 1, 103, 0, 100, 15, 162, 1, 103, 0, 100, 15, 162, 1, 103, 0, 100, 16, 162, 1, 103, 0, 100, 17, 162, 1, 103, 0, 100, 9, 162, 1, 103, 0, 100, 18, 162, 1, 103, 0, 100, 8, 162, 1, 103, 0, 100, 14, 162, 1, 103, 0, 100, 5, 162, 1, 103, 0, 100, 6, 162, 1, 103, 0, 100, 7, 162, 1, 103, 0, 100, 8, 162, 1, 103, 0, 100, 19, 162, 1, 103, 0, 100, 20, 162, 1, 103, 0, 100, 5, 162, 1, 103, 0, 100, 21, 162, 1, 103, 0, 100, 22, 162, 1, 103, 35, 131, 2, 68, 0, 93, 68, 92, 2, 125, 3, 125, 4, 124, 1, 160, 2, 124, 3, 161, 1, 1, 0, 103, 0, 125, 5, 116, 3, 100, 23, 131, 1, 68, 0, 93, 24, 125, 6, 124, 5, 160, 4, 124, 1, 160, 5, 100, 3, 100, 24, 161, 2, 161, 1, 1, 0, 144, 1, 113, 16, 124, 5, 124, 4, 107, 3, 114, 242, 100, 25, 125, 2, 113, 242, 124, 2, 83, 0]  So that gives us the (hopefully) last bytecode to reverse :) ## ????? Decrypted bytecode We can use xdis directly to decompile this bytecode: py import types from xdis.version_info import PYTHON_VERSION import xdis j = [100, 1, 100, 0, 108, 0, 125, 1, 100, 2, 125, 2, 116, 1, 124, 0, 100, 0, 100, 0, 100, 3, 133, 3, 25, 0, 103, 0, 100, 4, 162, 1, 103, 0, 100, 5, 162, 1, 103, 0, 100, 6, 162, 1, 103, 0, 100, 7, 162, 1, 103, 0, 100, 8, 162, 1, 103, 0, 100, 9, 162, 1, 103, 0, 100, 7, 162, 1, 103, 0, 100, 10, 162, 1, 103, 0, 100, 11, 162, 1, 103, 0, 100, 8, 162, 1, 103, 0, 100, 12, 162, 1, 103, 0, 100, 9, 162, 1, 103, 0, 100, 13, 162, 1, 103, 0, 100, 9, 162, 1, 103, 0, 100, 8, 162, 1, 103, 0, 100, 14, 162, 1, 103, 0, 100, 9, 162, 1, 103, 0, 100, 6, 162, 1, 103, 0, 100, 15, 162, 1, 103, 0, 100, 15, 162, 1, 103, 0, 100, 16, 162, 1, 103, 0, 100, 17, 162, 1, 103, 0, 100, 9, 162, 1, 103, 0, 100, 18, 162, 1, 103, 0, 100, 8, 162, 1, 103, 0, 100, 14, 162, 1, 103, 0, 100, 5, 162, 1, 103, 0, 100, 6, 162, 1, 103, 0, 100, 7, 162, 1, 103, 0, 100, 8, 162, 1, 103, 0, 100, 19, 162, 1, 103, 0, 100, 20, 162, 1, 103, 0, 100, 5, 162, 1, 103, 0, 100, 21, 162, 1, 103, 0, 100, 22, 162, 1, 103, 35, 131, 2, 68, 0, 93, 68, 92, 2, 125, 3, 125, 4, 124, 1, 160, 2, 124, 3, 161, 1, 1, 0, 103, 0, 125, 5, 116, 3, 100, 23, 131, 1, 68, 0, 93, 24, 125, 6, 124, 5, 160, 4, 124, 1, 160, 5, 100, 3, 100, 24, 161, 2, 161, 1, 1, 0, 144, 1, 113, 16, 124, 5, 124, 4, 107, 3, 114, 242, 100, 25, 125, 2, 113, 242, 124, 2, 83, 0] co = types.CodeType(1, 0, 0, 7, 38, 67, bytes(j), (None, 0, True, -1, (1, 0, 0, 1, 0, -1, 1, -1, -1, 0, 0, -1, 0, 0, -1, 1), (1, 1, 1, 0, -1, 0, -1, -1, 1, 1, -1, 1, 0, 0, 0, -1), (0, 0, 1, 0, -1, 1, -1, -1, 0, 0, 1, 1, -1, -1, 0, -1), (0, 1, 0, 1, -1, -1, 1, 0, -1, 0, 1, 1, 1, -1, -1, 0), (0, 0, 0, 0, -1, 1, 0, -1, -1, -1, 1, 1, -1, -1, 0, 0), (0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, -1, 1, -1, 0), (-1, 0, -1, 1, 1, 1, -1, 0, 1, -1, -1, 0, -1, 0, 1, 1), (0, 1, -1, 1, 1, 1, 1, 1, -1, 1, -1, 1, 0, 1, -1, 0), (0, 0, 1, 1, -1, 1, -1, 0, 1, 1, -1, 1, 1, 1, -1, -1), (-1, 1, 0, -1, 1, 1, 1, 0, 1, -1, 0, 0, -1, 1, 0, -1), (0, 0, -1, 1, -1, 0, 0, 0, -1, -1, -1, 1, -1, 1, 1, -1), (0, 0, -1, 1, -1, 1, -1, -1, 1, -1, -1, -1, 0, 1, 0, 0), (0, -1, 0, 0, -1, -1, -1, -1, -1, 0, -1, 0, -1, 0, 0, -1), (0, -1, 0, -1, 1, 1, 0, 1, -1, 1, 1, 0, 0, 1, -1, -1), (0, 0, 1, -1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, -1), (1, 1, 1, 1, -1, 1, 1, 0, -1, 0, 0, -1, 1, 0, -1, 1), (0, -1, 1, -1, 1, 0, 1, -1, -1, 0, -1, -1, 0, 0, -1, -1), (0, -1, -1, -1, 1, -1, 1, 1, -1, -1, 0, 0, 0, -1, -1, 0), (0, 0, -1, -1, -1, -1, -1, 1, 1, 0, -1, -1, 0, -1, -1, 1), 16, 1, False), ('random', 'zip', 'seed', 'range', 'append', 'randint'), ('f', 'random', '_____', '___', '____', '_', '__'), '', '', 0, b'', (), ()) xdis.disasm.disco(PYTHON_VERSION, co, 0)  Again, I'll only provide the code rewritten in python for posterity (with some renames and shortened constant): py import random def method(f): superarray = [ (1, 0, 0, 1, 0, -1, 1, -1, -1, 0, 0, -1, 0, 0, -1, 1), [...]] (0, 0, -1, -1, -1, -1, -1, 1, 1, 0, -1, -1, 0, -1, -1, 1) ] for (c, nums) in zip(flag[::-1], superarray): result = True random.seed(c) generated = [] for i in range(16): generated.append(random.randint(-1, 1)) if generated != nums: result = False return result  So, every character of the input is used as a seed, 16 values are generated and then compared to the corresponding superarray tuple. We'll build a dictionary of the resulting tuple for each character and then lookup the superarray values in it and get the flag :) py import random superarray = [ (1, 0, 0, 1, 0, -1, 1, -1, -1, 0, 0, -1, 0, 0, -1, 1), (1, 1, 1, 0, -1, 0, -1, -1, 1, 1, -1, 1, 0, 0, 0, -1), (0, 0, 1, 0, -1, 1, -1, -1, 0, 0, 1, 1, -1, -1, 0, -1), (0, 1, 0, 1, -1, -1, 1, 0, -1, 0, 1, 1, 1, -1, -1, 0), (0, 0, 0, 0, -1, 1, 0, -1, -1, -1, 1, 1, -1, -1, 0, 0), (0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, -1, 1, -1, 0), (0, 1, 0, 1, -1, -1, 1, 0, -1, 0, 1, 1, 1, -1, -1, 0), (-1, 0, -1, 1, 1, 1, -1, 0, 1, -1, -1, 0, -1, 0, 1, 1), (0, 1, -1, 1, 1, 1, 1, 1, -1, 1, -1, 1, 0, 1, -1, 0), (0, 0, 0, 0, -1, 1, 0, -1, -1, -1, 1, 1, -1, -1, 0, 0), (0, 0, 1, 1, -1, 1, -1, 0, 1, 1, -1, 1, 1, 1, -1, -1), (0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, -1, 1, -1, 0), (-1, 1, 0, -1, 1, 1, 1, 0, 1, -1, 0, 0, -1, 1, 0, -1), (0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, -1, 1, -1, 0), (0, 0, 0, 0, -1, 1, 0, -1, -1, -1, 1, 1, -1, -1, 0, 0), (0, 0, -1, 1, -1, 0, 0, 0, -1, -1, -1, 1, -1, 1, 1, -1), (0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, -1, 1, -1, 0), (0, 0, 1, 0, -1, 1, -1, -1, 0, 0, 1, 1, -1, -1, 0, -1), (0, 0, -1, 1, -1, 1, -1, -1, 1, -1, -1, -1, 0, 1, 0, 0), (0, 0, -1, 1, -1, 1, -1, -1, 1, -1, -1, -1, 0, 1, 0, 0), (0, -1, 0, 0, -1, -1, -1, -1, -1, 0, -1, 0, -1, 0, 0, -1), (0, -1, 0, -1, 1, 1, 0, 1, -1, 1, 1, 0, 0, 1, -1, -1), (0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, -1, 1, -1, 0), (0, 0, 1, -1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, -1), (0, 0, 0, 0, -1, 1, 0, -1, -1, -1, 1, 1, -1, -1, 0, 0), (0, 0, -1, 1, -1, 0, 0, 0, -1, -1, -1, 1, -1, 1, 1, -1), (1, 1, 1, 0, -1, 0, -1, -1, 1, 1, -1, 1, 0, 0, 0, -1), (0, 0, 1, 0, -1, 1, -1, -1, 0, 0, 1, 1, -1, -1, 0, -1), (0, 1, 0, 1, -1, -1, 1, 0, -1, 0, 1, 1, 1, -1, -1, 0), (0, 0, 0, 0, -1, 1, 0, -1, -1, -1, 1, 1, -1, -1, 0, 0), (1, 1, 1, 1, -1, 1, 1, 0, -1, 0, 0, -1, 1, 0, -1, 1), (0, -1, 1, -1, 1, 0, 1, -1, -1, 0, -1, -1, 0, 0, -1, -1), (1, 1, 1, 0, -1, 0, -1, -1, 1, 1, -1, 1, 0, 0, 0, -1), (0, -1, -1, -1, 1, -1, 1, 1, -1, -1, 0, 0, 0, -1, -1, 0), (0, 0, -1, -1, -1, -1, -1, 1, 1, 0, -1, -1, 0, -1, -1, 1) ] dicted = {} for i in range(127): random.seed(chr(i)) tmp = [] for j in range(16): tmp.append(random.randint(-1, 1)) dicted[tuple(tmp)] = i out = '' for arr in superarray: out += chr(dicted[arr]) print(out[::-1])  ## ?????? Flag $ python3 inner.py
ictf{n3st1ng_d0lls_1n_4_5nak3_n3st}
`

Original writeup (https://wrecktheline.com/writeups/imaginary-2021/#nesting%20snakes).