Contents

CTE v2 (2026) - Le Vault

CTE v2 (2026) - Le Vault

This challenge begins with some OSINT where we need to find a program protected by password. This program is used by Melanie’s friend, Samir Taleb.

NB. These are fake identities used all along “Capture The Evidence” v2.

Then, we’ll need to provide the author’s name of this challenge as flag.

The tags of the challenge suggest there’s a part with OSINT (first part) and a part with Reverse (second part).

OSINT

We know from previous investigations in other challenges that Samir owns the following accounts:

PlateformeURL
X/Twitterhttps://x.com/samirtaleb75
Blueskyhttps://bsky.app/profile/samirtaleb75.bsky.social
Facebookhttps://www.facebook.com/profile.php?id=61583227872259

We’d typically find a program on a GitHub, GitLab, Gist, or Pastebin. We find a GitHub repo samirtaleb13-ops, but it has a single repository “Rapide” which is HTML/JS code. This does not seem interesting at all and might be a totally different Samir Taleb (not related to CTE).

After a (long) time, we finally spot the GitHub account samir-taleb with a promising repository “The Vault”.

/images/cte2026-vault-github.png

Why did it take us so much time to find samir-taleb? We use sherlock, maigret but it seems we only search for samirtaleb75 as he was using this for all his accounts so far…

Decrypting the ZIP

The ZIP is password protected, but from previous analysis of Samir’s BlueSky posts, we have already decrypted one of his favorite passwords:

/images/cte2026-vault-bluesky.png

1
2
3
import base64
s = "D<YkFDJ<Z=AThX*"
print(base64.a85decode(s).decode("latin-1"))

How did you guess it was a85 decode? To be honest, I did not: my LLM did. The characters are unusual for Base64 alone. Typically, we’d try Base 64, Base 32, Base 85 and Ascii 85.

The password decodes as mélanie4ever, and it decrypts the ZIP.

1
2
3
4
5
unzipped/
├── vault.py
└── pyarmor_runtime_000000/
    ├── __init__.py
    └── pyarmor_runtime.so

Pyarmor

The project is protected by Pyarmor 9.2.4 trial:

1
2
3
4
5
$ cat vault.py
#!/usr/bin/env python3
# Pyarmor 9.2.4 (trial), 000000, non-profits, 2026-05-01T10:12:07.218314
from pyarmor_runtime_000000 import __pyarmor__
__pyarmor__(__name__, __file__, b'PY000000\x00\x03

Pyarmor turns the Python code in binary data. This data is decrypted at runtime by pyarmor_runtime.so.

We use Pyarmor Static Unpack One-Shot Tool to reverse statically the code.

1
python3 oneshot/shot.py /home/axelle/ctf/cte-v2/levault/unzipped -o /home/axelle/ctf/cte-v2/levault/decompiled

It produces 3 files: vault.py.1shot.seq, vault.py.1shot.das and vault.py.1shot.cdc.py. The last one contains the decompiled Python file. It is not perfect, but quite good:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File: vault.py.1shot.seq (Python 3.13)
# Source generated by Pyarmor-Static-Unpack-1shot (v0.4.0), powered by Decompyle++ (pycdc)

# Note: Decompiled code can be incomplete and incorrect.
# Please also check the correct and complete disassembly file: vault.py.1shot.das

'__pyarmor_enter_1082__(...)'
__assert_armored__ = '__pyarmor_assert_1081__'
import sys
import getpass
KEY = b'Sup3rS3cr3tK3y'
SALT = b'S4ltY_V4lu3'
ENC_BANNER = b'\n i3Cc\x17w$f\x14yj|U\x0c{$\nz\x04ZDjyV\x06\x06f.r2 cs/<JX|j|T2Bk{E]\tdwjQtE\tte\x03A+d`j\n'
ENC_PASSWORD = b'0u,p\x19<Uc'
SECRETS = ({
    'id': 1,
    'name': 'Nuclear Launch Code',
    'value': '1234-5678-9012' }, {
    'id': 2,
    'name': 'Swiss Bank Account',
    'value': 'CH93 0000 0000 0000 0000 0' }, {
    'id': 3,
    'name': 'Area 51 Gate Key',
    'value': 'A51-KEY-999' })
	
def xor_string(data, key):
    '''XORs data with a key.'''
    '__pyarmor_enter_1085__(...)'
    __assert_armored__ = '__pyarmor_assert_1084__'
    return None(bytes, (lambda .0: pass# WARNING: Decompyle incomplete
)(None(range, None(len, data))))
    None(None)
    return None
    '__pyarmor_exit_1086__(...)'
    '__pyarmor_exit_1086__(...)'


def decrypt(data, key, salt):
    '''Decrypts data using double XOR (reverse order of encryption).'''
    '__pyarmor_enter_1088__(...)'
    __assert_armored__ = '__pyarmor_assert_1087__'
    return None(None(xor_string, None(xor_string, data, salt), key).decode, 'utf-8')
    None(None)
    return None
    '__pyarmor_exit_1089__(...)'
    '__pyarmor_exit_1089__(...)'


def main():
    '''Main entry point for the vault.'''
    '__pyarmor_enter_1091__(...)'
    __assert_armored__ = '__pyarmor_assert_1090__'
    _var_var_1 = getpass.getpass('Enter password: ')
    None(print, 'Welcome to LeVault Secure Storage.')
# WARNING: Decompyle incomplete

if __name__ == '__main__':
    pass
main()
return None
return None
'__pyarmor_exit_1083__(...)'
'__pyarmor_exit_1083__(...)'

Reconstructing the Python code

In the code above, we have all the important parts:

  • a key
  • a salt
  • an encrypted banner
  • an encrypted password
  • a list of plaintext secrets (maybe useful for a future challenge?)

The source code for xor_string() is incomplete. The source code for decrypt() is incomplete too but from this call return None(None(xor_string, None(xor_string, data, salt), key).decode, 'utf-8'), we clearly understand we have 2 XORs. The first one with the salt, the second one with the key.

Finally, the main() is incomplete too, but it looks like it just reads the password from a user prompt and calls the decrypt function.

Decrypting the password and the banner

It looks like the vault’s password is:

  1. XOR encrypted password with S4ltY_V4lu3
  2. then XOR with Sup3rS3cr3tK3y

Let’s write a Python program for that (and no, this is not AI-generated, it’s manual + copy/paste from the decompiled output we got):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
KEY = b'Sup3rS3cr3tK3y'
SALT = b'S4ltY_V4lu3'
ENC_PASSWORD = b'0u,p\x19<Uc'
ENC_BANNER = b'\n i3Cc\x17w$f\x14yj|U\x0c{$\nz\x04ZDjyV\x06\x06f.r2 cs/<JX|j|T2Bk{E]\tdwjQtE\tte\x03A+d`j\n'

def xor_string(data, key):
    return bytes(data[i] ^ key[i % len(key)] for i in range(len(data)))

def decrypt(data, key, salt):
    '''Decrypts data using double XOR (reverse order of encryption).'''
    return xor_string(xor_string(data, salt), key).decode('utf-8')

def main():
    print(decrypt(ENC_PASSWORD, KEY, SALT))
    print(decrypt(ENC_BANNER, KEY, SALT))
    
if __name__ == '__main__':
    main()

We run that and get the password 04072004 (this is Melanie’s birthday) and author’s sname, Samir (oh surprise :D).

1
2
3
4
04072004

author : Samir TALEB
email contact : samirtaleb75@protonmail.com

Conclusion and flag

The flag is TALEB. This is slightly disappointing because we could have guessed that without doing the reverse engineering: it’s on Samir’s repository and clearly implemented by him.

To force the player to do all the job, I would have encrypted the secrets. For example the Swiss Bank account. To make it even harder, I would have selected a longer/not guessable password, because Melanie’s birth date is known and a lucky player can try it as password and get all the information without a single reverse.

That being said, if you chose to do the challenge to learn and not to flag quickly, it was really interesting. I was happy to look into Pyarmor and defeat it.