[Link to original writeup](https://wrecktheline.com/writeups/imaginary-2021/#nesting%20snakes)
# Nesting Snakes (5 solves, 400 points)by JaGoTu
## Description```DescriptionI know turtles go all the way down, but what about snakes?
Attachments* nesting_snakes
AuthorRobin_Jadoul
Points400```
## ? Pyinstaller
We are given a pretty big **executable ELF file**:
```$ file nesting_snakesnesting_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-21p7zip 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_snakesType = ELFPhysical Size = 7452136CPU = AMD6464-bit = +Host OS = NoneCharacteristics = Executable fileHeaders Size = 2472
Everything is Ok
Files: 28Size:       7442756Compressed: 7452136
$ file pydatapydata: 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.pycTraceback (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:
```pyimport pyconcreteimport stuffimport sysimport 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:
```pyimport _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.):
```pyimport randomimport 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:
```pyimport random
a = 358c = [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.py3299[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:
```pyimport typesfrom xdis.version_info import PYTHON_VERSIONimport 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):
```pyimport 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 :)
```pyimport 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.pyictf{n3st1ng_d0lls_1n_4_5nak3_n3st}```