Rating:

[Link to original writeup](https://wrecktheline.com/writeups/imaginary-2021/#nesting%20snakes)

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

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
[+] 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
[!] 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.

(see https://github.com/rocky/python-uncompyle6/issues/331#issuecomment-794736502)

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
1: 0 LOAD_CONST (0)
2 LOAD_CONST (None)
4 IMPORT_NAME (pyconcrete)
6 STORE_NAME (pyconcrete)

2: 8 LOAD_CONST (0)
10 LOAD_CONST (None)
12 IMPORT_NAME (stuff)
14 STORE_NAME (stuff)

3: 16 LOAD_CONST (0)
18 LOAD_CONST (None)
20 IMPORT_NAME (sys)
22 STORE_NAME (sys)
24 LOAD_CONST (0)
26 LOAD_CONST (None)
28 IMPORT_NAME (os)
30 STORE_NAME (os)

5: 32 LOAD_CONST (358)
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 (___)

9: 52 LOAD_NAME (sys)
54 LOAD_ATTR (argv)
56 LOAD_CONST ('')
58 BUILD_LIST 1
60 BINARY_ADD
62 LOAD_CONST (1)
64 BINARY_SUBSCR
66 STORE_NAME (f)

11: 68 LOAD_NAME (stuff)
70 LOAD_METHOD (_)
72 LOAD_NAME (_)
74 LOAD_NAME (__)
76 LOAD_NAME (___)
78 LOAD_NAME (f)
80 CALL_METHOD 4
82 LOAD_NAME (f)
84 CALL_FUNCTION 1
86 POP_JUMP_IF_FALSE (to 96)

12: 88 LOAD_NAME (print)
90 LOAD_CONST ('Congratulations')
92 CALL_FUNCTION 1
94 POP_TOP
>> 96 LOAD_CONST (None)
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

(see https://github.com/Falldog/pyconcrete)

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:
data = f.read()

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