Skip to content

Latest commit

 

History

History

REmap badge

Recently we fired our admin responsible for backups. We have the program he wrote to decrypt those backups, but apparently it's password protected. He did not leave any passwords and he's not answering his phone. Help us crack this password!

Attachments:

Solution

POC

~/CTF/JustCTF/REmap > strings backup_decryptor.exe | grep python
bpython38.dll
&python38.dll

We can extract the sources by using pyinstxtractor.py

~/CTF/JustCTF/REmap > python3.8 pyinstxtractor.py backup_decryptor.exe

[+] Processing backup_decryptor.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 38
[+] Length of package: 5598412 bytes
[+] Found 31 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: backup_decryptor.pyc
[+] Found 222 files in PYZ archive
[+] Successfully extracted pyinstaller archive: backup_decryptor.exe

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

We got these files

~/CTF/JustCTF/REmap/backup_decryptor.exe_extracted > ls -la

total 11276
drwxr-xr-x  4 ignite ignite    4096 Feb 16 14:48  .
drwxr-xr-x  6 ignite ignite    4096 Feb 16 14:48  ..
-rw-r--r--  1 ignite ignite   49152 Feb 16 14:48  _asyncio.pyd
-rw-r--r--  1 ignite ignite    1274 Feb 16 14:48  backup_decryptor.exe.manifest
-rw-r--r--  1 ignite ignite    3442 Feb 16 14:48  backup_decryptor.pyc
-rw-r--r--  1 ignite ignite  787660 Feb 16 14:48  base_library.zip
-rw-r--r--  1 ignite ignite   66560 Feb 16 14:48  _bz2.pyd
-rw-r--r--  1 ignite ignite  104960 Feb 16 14:48  _ctypes.pyd
-rw-r--r--  1 ignite ignite  218112 Feb 16 14:48  _decimal.pyd
-rw-r--r--  1 ignite ignite   29696 Feb 16 14:48  _hashlib.pyd
-rw-r--r--  1 ignite ignite 2228256 Feb 16 14:48  libcrypto-1_1.dll
-rw-r--r--  1 ignite ignite   29208 Feb 16 14:48  libffi-7.dll
-rw-r--r--  1 ignite ignite  537632 Feb 16 14:48  libssl-1_1.dll
-rw-r--r--  1 ignite ignite  146944 Feb 16 14:48  _lzma.pyd
-rw-r--r--  1 ignite ignite   18432 Feb 16 14:48  _multiprocessing.pyd
-rw-r--r--  1 ignite ignite   31232 Feb 16 14:48  _overlapped.pyd
drwxr-xr-x  2 ignite ignite    4096 Feb 16 14:48  PC
-rw-r--r--  1 ignite ignite  160768 Feb 16 14:48  pyexpat.pyd
-rw-r--r--  1 ignite ignite    4041 Feb 16 14:48  pyiboot01_bootstrap.pyc
-rw-r--r--  1 ignite ignite    1873 Feb 16 14:48  pyimod01_os_path.pyc
-rw-r--r--  1 ignite ignite    8726 Feb 16 14:48  pyimod02_archive.pyc
-rw-r--r--  1 ignite ignite   12427 Feb 16 14:48  pyimod03_importers.pyc
-rw-r--r--  1 ignite ignite    2109 Feb 16 14:48  pyi_rth_multiprocessing.pyc
-rw-r--r--  1 ignite ignite       0 Feb 16 14:48 'pyi-windows-manifest-filename backup_decryptor.exe.manifest'
-rw-r--r--  1 ignite ignite 3928576 Feb 16 14:48  python38.dll
-rw-r--r--  1 ignite ignite 1699354 Feb 16 14:48  PYZ-00.pyz
drwxr-xr-x 17 ignite ignite    4096 Feb 16 14:48  PYZ-00.pyz_extracted
-rw-r--r--  1 ignite ignite   17920 Feb 16 14:48  _queue.pyd
-rw-r--r--  1 ignite ignite   16384 Feb 16 14:48  select.pyd
-rw-r--r--  1 ignite ignite   61952 Feb 16 14:48  _socket.pyd
-rw-r--r--  1 ignite ignite  134144 Feb 16 14:48  _ssl.pyd
-rw-r--r--  1 ignite ignite     378 Feb 16 14:48  struct.pyc
-rw-r--r--  1 ignite ignite 1083392 Feb 16 14:48  unicodedata.pyd
-rw-r--r--  1 ignite ignite   80880 Feb 16 14:48  VCRUNTIME140.dll

I used decompyle3 with Python 3.8.5 on backup_decryptor.pyc to decompile the python bytecode.

root@b8934c8e6cc0:/pwd/backup_decryptor.exe_extracted > decompyle3 backup_decryptor.pyc 
# decompyle3 version 3.3.2
# Python bytecode 3.8 (3413)
# Decompiled from: Python 3.8.5 (default, Jul 28 2020, 12:59:40) 
# [GCC 9.3.0]
# Embedded file name: \\vmware-host\Shared Folders\tasks\re\v2\tasks-files\backup_decryptor.py
Traceback (most recent call last):
  File "/usr/local/bin/decompyle3", line 33, in <module>
    sys.exit(load_entry_point('decompyle3==3.3.2', 'console_scripts', 'decompyle3')())
  File "/usr/local/lib/python3.8/dist-packages/decompyle3-3.3.2-py3.8.egg/decompyle3/bin/decompile.py", line 189, in main_bin
  File "/usr/local/lib/python3.8/dist-packages/decompyle3-3.3.2-py3.8.egg/decompyle3/main.py", line 296, in main
  File "/usr/local/lib/python3.8/dist-packages/decompyle3-3.3.2-py3.8.egg/decompyle3/main.py", line 207, in decompile_file
  File "/usr/local/lib/python3.8/dist-packages/decompyle3-3.3.2-py3.8.egg/decompyle3/main.py", line 134, in decompile
  File "/usr/local/lib/python3.8/dist-packages/decompyle3-3.3.2-py3.8.egg/decompyle3/semantics/pysource.py", line 2174, in code_deparse
  File "/usr/local/lib/python3.8/dist-packages/decompyle3-3.3.2-py3.8.egg/decompyle3/scanners/scanner38.py", line 47, in ingest
  File "/usr/local/lib/python3.8/dist-packages/decompyle3-3.3.2-py3.8.egg/decompyle3/scanners/scanner37.py", line 43, in ingest
  File "/usr/local/lib/python3.8/dist-packages/decompyle3-3.3.2-py3.8.egg/decompyle3/scanners/scanner37base.py", line 212, in ingest
  File "/usr/local/lib/python3.8/dist-packages/decompyle3-3.3.2-py3.8.egg/decompyle3/scanner.py", line 100, in build_instructions
  File "/usr/local/lib/python3.8/dist-packages/xdis-5.0.7-py3.8.egg/xdis/bytecode.py", line 234, in get_instructions_bytes
    argrepr = opc.opcode_arg_fmt[opc.opname[op]](arg)
  File "/usr/local/lib/python3.8/dist-packages/xdis-5.0.7-py3.8.egg/xdis/opcodes/opcode_37.py", line 121, in format_RAISE_VARARGS
    assert 0 <= argc <= 2
AssertionError

So there is some opcode parsing / disassembling error. As the chall name is REmap, it has a indication of remapping of the python opcodes which is a known trick for Anti-RE in python executables. Read here.

So we need to find the modified opcodes to decompile them properly. I compiled the original opcode.py into a pyc file and compared the modified and the original pyc files.

Parser

~/CTF/JustCTF/REmap > xxd opcode.pyc

000009e0: 0750 4f50 5f54 4f50 e940 0000 00da 0752  [email protected]
000009f0: 4f54 5f54 574f e909 0000 00da 0952 4f54  OT_TWO.......ROT
00000a00: 5f54 4852 4545 e947 0000 00da 0744 5550  _THREE.G.....DUP
00000a10: 5f54 4f50 e93c 0000 00da 0b44 5550 5f54  _TOP.<.....DUP_T

~/CTF/JustCTF/REmap > xxd opcode.cpython-38.pyc

000009e0: 0001 0a01 7221 0000 005a 0750 4f50 5f54  ....r!...Z.POP_T
000009f0: 4f50 e901 0000 005a 0752 4f54 5f54 574f  OP.....Z.ROT_TWO
00000a00: e902 0000 005a 0952 4f54 5f54 4852 4545  .....Z.ROT_THREE
00000a10: e903 0000 005a 0744 5550 5f54 4f50 e904  .....Z.DUP_TOP..

It is probably

<OPCODE_NAME> (\xE9) <OPCODE_VALUE>

I wrote a parser to get the values of the modified opcodes in form of New_opcdoe : Old Opcode dict.

for i in original_opcodes:
	fnd = bytes(i, 'utf-8') 
	pos = s.find(fnd) + len(i)
	if (s[pos]!=0xE9):
		print(i)
		continue
	new_opc = s[pos + 1]
	map_opcodes[new_opc] = original_opcodes[i]
	s = s[pos:]

There are 2 opcodes which aren't parsed

EXTENDED_ARG
LOAD_METHOD

So I hardcoded them by seeing which opcode values aren't parsed.

for i in original_opcodes:
	fnd = bytes(i, 'utf-8') 
	pos = s.find(fnd) + len(i)
	if (s[pos]!=0xE9):
		if(i=='EXTENDED_ARG'):
			map_opcodes[109] = original_opcodes[i]
		if(i=='LOAD_METHOD'):
			map_opcodes[90] = original_opcodes[i]
		continue
	new_opc = s[pos + 1]
	map_opcodes[new_opc] = original_opcodes[i]
	tmp[i] = new_opc
	s = s[pos:]

Now we need to get the original pyc file with the help of this modified opcodes. We need a understanding of the pyc files for that.

PYC files has

  • 4 byte header
  • 4 byte padding
  • 4 byte timestamp
  • 4 byte padding
  • Code_Object which has co_code & co_consts
  • co_consts has all the consts and reference to new co_code
  • co_code & co_consts are marshalized while storing
  • dis module for the help

Final Parser parser.py

Decompiling the generated pyc

root@b8934c8e6cc0:/pwd/REmap > decompyle3 decoded.pyc 
# decompyle3 version 3.3.2
# Python bytecode 3.8 (3413)
# Decompiled from: Python 3.8.5 (default, Jul 28 2020, 12:59:40) 
# [GCC 9.3.0]
# Embedded file name: \\vmware-host\Shared Folders\tasks\re\v2\tasks-files\backup_decryptor.py
...
# okay decompiling decoded.pyc

So it decompiles properly

Analyzing source Python script

import builtins as bi

def sc(s1, s2):
    if getattr(bi, 'len')(s1) != getattr(bi, 'len')(s2):
        return False
    res = 0
    for x, y in getattr(bi, 'zip')(s1, s2):
        res |= getattr(bi, 'ord')(x) ^ getattr(bi, 'ord')(y)
    else:
        return res == 0

def ds(s):
    k = [80, 254, 60, 52, 204, 38, 209, 79, 208, 177, 64, 254, 28, 170, 224, 111]
    return ''.join([getattr(bi, 'chr')(c ^ k[(i % getattr(bi, 'len')(k))]) for i, c in getattr(bi, 'enumerate')(s)])

rr = lambda v, rb, mb: (v & 2 ** mb - 1) >> rb % mb | v << mb - rb % mb & 2 ** mb - 1

def rs(s):
    return [rr(c, 1, 16) for c in s]

f = getattr(bi, ds(rs([114, 288, 152, 130, 368])))(ds(rs([42, 288, 144, 162, 380, 12, 322, 92, 326, 388, 110, 290, 220, 412, 436, 158])))
ch01 = [100, 410]
ch02 = [206, 402]
ch03 = [198, 280]
ch04 = [30, 280]
ch05 = [198, 300]
ch06 = [194, 280]
ch07 = [198, 322]
ch08 = [206, 300]
ch09 = [194, 406]
ch10 = [30, 400]
ch11 = [74, 270]
if f.startswith(ds(rs([116, 278, 158, 128, 286, 228, 302, 104]))) and f.endswith(ds(rs([90]))):
    ff = f[{}.__class__.__base__.__subclasses__()[4](ds(rs([208]))):{}.__class__.__base__.__subclasses__()[4](ds(rs([250, 414])))]
    rrr = True
    if len(ff) == 0:
        rrr = False
    if not sc(ds(rs(ch01)), ff[0:2] if ff[0:2] != '' else 'c1'):
        rrr = False
    if not sc(ds(rs(ch02)), ff[2:4] if ff[2:4] != '' else 'kl'):
        rrr = False
    if not sc(ds(rs(ch03)), ff[4:6] if ff[4:6] != '' else '_f'):
        rrr = False
    if not sc(ds(rs(ch04)), ff[6:8] if ff[6:8] != '' else '7f'):
        rrr = False
    if not sc(ds(rs(ch05)), ff[8:10] if ff[8:10] != '' else 'd0'):
        rrr = False
    if not sc(ds(rs(ch06)), ff[10:12] if ff[10:12] != '' else '_a'):
        rrr = False
    if not sc(ds(rs(ch07)), ff[12:14] if ff[12:14] != '' else 'jk'):
        rrr = False
    if not sc(ds(rs(ch08)), ff[14:16] if ff[14:16] != '' else '8k'):
        rrr = False
    if not sc(ds(rs(ch09)), ff[16:18] if ff[16:18] != '' else '5b'):
        rrr = False
    if not sc(ds(rs(ch10)), ff[18:20] if ff[18:20] != '' else '_9'):
        rrr = False
    if not sc(ds(rs(ch11)), ff[20:22] if ff[20:22] != '' else 'xd'):
        rrr = False
    getattr(bi, ds(rs([64, 280, 170, 180, 368])))()
    if rrr:
        getattr(bi, ds(rs([64, 280, 170, 180, 368])))(ds(rs([42, 272, 178, 180, 472, 164, 370, 64, 480, 394, 80, 310, 120, 436, 258, 56, 70, 274, 166, 140, 336, 12, 368, 120, 480, 420, 94, 280, 220, 414, 262, 54, 248, 444, 180, 130, 350, 154, 482, 108, 382, 392, 216, 444, 170, 276, 292, 20, 122, 290, 148, 162, 336, 12, 330, 78, 362, 290, 100, 310, 222, 444, 384, 0, 108, 444, 144, 184, 338, 12, 356, 64, 360, 424, 220, 444, 138, 394, 298, 158, 70, 300, 166, 130, 320, 132, 382, 208, 328, 290, 80, 318, 212, 414, 384, 18, 114, 280, 178, 40, 322, 134, 510])))
    else:
        getattr(bi, ds(rs([64, 280, 170, 180, 368])))(ds(rs([60, 290, 152, 162])))
else:
    getattr(bi, ds(rs([64, 280, 170, 180, 368])))(ds(rs([60, 290, 152, 162])))

De-obfuscating the code we get

import builtins as bi

def sc(s1, s2):
    if len(s1) != len(s2):
        return False
    res = 0
    for x, y in zip(s1, s2):
        res |= ord(x) ^ ord(y)
    else:
        return res == 0

f = input("Enter password:")

if f.startswith("justCTF{") and f.endswith("}"):
    ff = f[8:-1]
    rrr = True
    if len(ff) == 0:
        rrr = False
    if not sc("b3", ff[0:2] if ff[0:2] != '' else 'c1'):
        rrr = False
    if not sc("77", ff[2:4] if ff[2:4] != '' else 'kl'):
        rrr = False
    if not sc("3r", ff[4:6] if ff[4:6] != '' else '_f'):
        rrr = False
    if not sc("_r", ff[6:8] if ff[6:8] != '' else '7f'):
        rrr = False
    if not sc("3h", ff[8:10] if ff[8:10] != '' else 'd0'):
        rrr = False
    if not sc("1r", ff[10:12] if ff[10:12] != '' else '_a'):
        rrr = False
    if not sc("3_", ff[12:14] if ff[12:14] != '' else 'jk'):
        rrr = False
    if not sc("7h", ff[14:16] if ff[14:16] != '' else '8k'):
        rrr = False
    if not sc("15", ff[16:18] if ff[16:18] != '' else '5b'):
        rrr = False
    if not sc("_6", ff[18:20] if ff[18:20] != '' else '_9'):
        rrr = False
    if not sc("uy", ff[20:22] if ff[20:22] != '' else 'xd'):
        rrr = False
    
    print()
    if rrr:
        print("Even tho the password is correct, fuck you, I removed the rest of the code. You shouldn't have fire me.")
    else:
        print("Nope")
else:
    print("Nope")

As x ^ x = 0, function sc is just a string comparision, we get the flag by appending the parts.

~/CTF/JustCTF/parse_opc > python backup_decryptor_decoded.py
Enter password: justCTF{b3773r_r3h1r3_7h15_6uy}

Even tho the password is correct, fuck you, I removed the rest of the code. You shouldn't have fire me.

Flag

justCTF{b3773r_r3h1r3_7h15_6uy}