This post is also available in: 日本語 (Japanese)
Next Up: “PyXie Lite”
An earlier version of PyXie was previously covered in-depth by BlackBerry Cylance in December 2019. We will be primarily covering an updated variant and some notable changes we have observed.
Some of these changes include:
- Hardened interpreter.
- New remapped opcode table.
- Repurposed as a data theft and reconnaissance tool.
- Exfiltration through internal servers.
We call this variant PyXie Lite because of the significantly smaller code base, but don’t let the name fool you. It still packs a punch.
Loader
The recent variant we analyzed was loaded by Vatet rather than the Goopdate.dll and LMIGuardianDll.dll side loaders seen with earlier versions of PyXie.
The decrypted Vatet payload contains the first stage of PyXie prepended by a shellcode loader responsible for mapping the first stage into memory and executing it.
The shellcode loader utilizes MurmurHash3 hashing to locate APIs needed during this process at runtime.
DLL | Function | API Hash |
Kernel32.dll | GetProcAddress | 0x261C88ED |
Kernel32.dll | VirtualAlloc | 0xC17E7EB2 |
Kernel32.dll | LoadLibraryExA | 0x4B9B30B9 |
Table 2. MurmurHash3 API hashes.
Stage 1
The purpose of the first stage is to decrypt the second stage payload and execute it in memory.
Mutex
A mutex is created to prevent multiple instances from running at the same time. The following logic is used:
- Retrieve computer name with a call to GetComputerNameA. If that fails, fall back to DEFAULTCOMPNAME.
- Compute MD5 Hash of the computer name.
- XOR computed hash with 0x2.
- Convert the result to a string with StringFromGUID2.
- Create mutex using the string with a call to CreateMutexW.
String encryption
Significant strings are encrypted by a routine that increments each byte of the ciphertext by its index, masks the result with 0x7F (highest value in the ASCII character set) and XORs it against a key of equal length.
Table 3. String decryption example.
Decrypted Strings
uiAccess=true"
-q -s {%S} -p %u werfault.exe vsjitdebugger.exe dvdplay.exe onedrivesetup.exe openwith.exe %windir%\syswow64\ %windir%\system32\ kernel32.dll KiUserExceptionDispatcher RtlCreateUser IsWow64Process \StringFileInfo\%04x%04x\ProductName |
Table 4. Decrypted first stage strings.
Payload Decryption
The next stage payload is stored in an encrypted 7z archive located in the .gfids section of the binary. It is decrypted with the modified RC4 algorithm previously discussed in the BlackBerry Cylance write-up using the hard-coded key: 2C01443389BDFC7330A3386981C43E154AE8B60EC6646D916F93D18137A53544
Payload Execution
OpenProcessToken and GetTokenInformation are called to determine if the process is running under the LocalSystem account. This is used to determine how the next stage payload is executed.
If it is determined to be running as LocalSystem, the payload is injected into a newly spawned process chosen from the Windows directory. The command line for this process follows this format and can be used as an indicator:
-q -s {{GUID}} -p NUMBER
If not found to be running as LocalSystem, the payload will execute in the memory space of the current process.
Stage 2
The second stage payload is a custom-compiled Python interpreter very similar to ones seen used with earlier variants of PyXie.
Configuration
The configuration is stored in a zlib compressed json blob and is located in the .gfids section of the interpreter. Unlike previous versions of PyXie, it is not encrypted this time around. The variable sys.builtin_json_cfg is created with a call to PySys_SetObject and the compressed configuration blob is stored in it for later use by the final stage Python component.
Decrypted Strings
The second stage uses the same string encryption that was noted in the previous stage.
kernel32.dll
openwith.exe onedrivesetup.exe dvdplay.exe vsjitdebugger.exe werfault.exe -q -s {%S} -p %u oleout32.dll VariantClear Mozilla\Firefox Mozilla\Firefox\profiles.ini SOFTWARE\Clients\StartMenuInternet\firefox.exe\shell\open\command I_CryptUIProtect cryptui.dll RtlCreateUserThread import core.modules.winapi_stubs as winapi_stubs import core.zip_logs as zip_logs import os zip_logs.send_zip_log(winapi_stubs.get_self_executable_path(), os.getpid(), 'CERTS', r'%s') KiUserExceptionDispatcher uiAccess="true" \StringFileInfo\%04x%04x\ProductName \VarFileInfo\Translation \\?\globalroot\systemroot\system32\drivers\null.sys SystemDrive IsWow64Process core.entry_point zipimporter memzipimport libs_zip_ctx start_bind_port |
Table 5. Decrypted second stage strings.
Final Stage: Libs.zip
The final stage of PyXie bytecode is contained in an encrypted ZIP file embedded within the interpreter binary. As with the earlier version of PyXie, the memzipimport library is used to import the bytecode from memory.
PyXie Lite
The “core” modules in this variant consist of 41 files versus the 79 seen in the previous version of PyXie analyzed by BlackBerry Cylance. The discrepancy is due to a shift in functionality that we will cover in the next section.
Interpreter hardening
Cursory analysis of the bytecode revealed that the headers had been stripped as with previous earlier versions of PyXie. Additionally, we found that the opcode table had once again been modified and the opcodes recovered from previous versions of PyXie could no longer be used to decompile this bytecode.
Knowing this, we attempted to force the interpreter to import DeDrop's all.py with hopes of generating bytecode that could be used for opcode recovery. To our dismay, we found that simply importing a script would no longer cause the interpreter to output bytecode.
A closer look at the interpreter found that the sys.dont_write_bytecode variable had been set to true. This has the effect of preventing bytecode from being written to disk when modules are imported. Likely, this is something the developers intentionally enabled to hinder analysis efforts.
Hijacking the interpreter with a search order vulnerability
During our analysis of the interpreter, we found that it attempted to load a number of modules from the current working directory and was vulnerable to search order hijacking.
We were able use this to our advantage by dropping a simple Python shell into one of the modules it attempted to import. This gave us unfettered access to the interpreter, which enabled us to overwrite the sys.dont_write_bytecode variable, generate opcodes for modules on demand and even dump PyXie’s configuration.
Once we were able to output bytecode for modules of our choosing, it was trivial to recover the remapped opcodes and decompile PyXie. A copy of the opcodes for this variant can be found in the appendix.
Functionality
As we previously mentioned, PyXie Lite has been repurposed to focus on the automated collection and exfiltration of data.
Upon execution, it creates a staging directory whose name is based on the output of the tempfile.NamedTemporaryFile() command.
%temp%\tmp1rjvhglo |
Table 6. Example staging directory name.
Next, it collects data from the system by running a combination of routines that are dictated by the user account PyXie is found to be running under. Table 7 breaks down each of these routines and the types of account they will run under.
Routine | User type | Description |
_mimi_redirector | All | Runs Mimikatz in memory. Injects into a newly spawned process from this list: write.exe, notepad.exe, explorer.exe |
_main_routine | All |
|
_save_sysinfo | All | Collects uninstall list from registry |
_get_passwords | All | Collects passwords with Lazagne |
_find_files | System | Searches for and collects files and directories based on keywords, directories and extensions specified in the configuration |
_scan_network | System | Runs network scans |
_run_shell_cmds | System | Runs a series of commands to gather details about the system |
_get_desktop_files | User | Similar to find_files but only searches the current user’s Desktop |
_take_screenshot | User | Takes a screenshot |
_get_ps_history | User | Collects Powershell history |
Table 7. PyXie routines.
The list of keywords and directories from configuration provides us some insight into the type of data the attackers are interested in:
passw | logins | wallet | private |
confidential | username | wire | access |
treason | vault | operation | bribery |
contraband | censored | instruction | credent |
cardholder | secret | explosive | suspect |
personal | cyber | restricted | balance |
passport | victim | submarine | checking |
saving | routing | esxi | vsphere |
spy | admin | newswire | bitcoin |
ethereum | n-csr | 10-sb | 10-q |
convict | tactical | engeneering | military |
disclosure | attack | infrastruct | marketwired |
agreement | illegal | nda | hidden |
privacy | fraud | statement | finance |
marketwired | clandestine | compromate | concealed |
investigation | security |
Table 8. Keywords from PyXie Lite configuration (including misspellings).
As part of the data gathering routines, a number of commands are executed to collect details about the system.
netstat -an
net user net use net view /all net view /all /domain net share net config workstation net group "Domain Admins" net group "Enterprise Admins" route print net localgroup ipconfig /all tasklist /V wmic process arp -a gpresult /z cmdkey /list net config workstation nslookup -type=any %userdnsdomain% vssadmin List Shadows wmic qfe list klist manage-bde -status nltest /domain_trusts nltest /domain_trusts /all_trusts qwinsta ipconfig /displaydns systeminfo dclist net group "domain admins" /domain net localgroup "administrators" wmic path win32_VideoController get name wmic cpu get name reg.exe save hklm\security %LOCALAPPDATA%\temp\[RANDOM] reg.exe save hklm\system %LOCALAPPDATA%\temp\[RANDOM] reg.exe save hklm\sam %LOCALAPPDATA%\temp\[RANDOM] |
Table 9. Executed commands.
Exfiltration
The staging directory containing the collected data is added to a compressed ZIP archive and encrypted before being sent to the server specified in the gates section of the config. The archive is encrypted with AES in CBC mode and THIS_KEY_IS_FOR_INTERNAL_USE_ONLY is used as the key. A random 16-byte initialization vector (IV) is used and is prepended to the encrypted archive. The exfil servers we have seen in samples to date have typically been compromised internal servers on the victim’s networks listening on ports 31337, 900 and 8443. Although we did not have visibility into how the attackers moved data off the victim network, in at least one incident the exfil server was running Cobalt Strike.
Continue reading: Last, but Not Least: Defray777