Mystic Stealer
The many variants of this new stealer
Overview
According to Zscaler Mystic is a stealer that has been active since April 2023, sold on underground forum such as XSS. Other than stealing browser credentials and crypto wallets the stealer's main differentiator is the use of a custom obfuscator that is used to protect strings. This obfuscator produces similar output as ADVObfuscator, there as been speculation that it is indeed just ADVObfuscator.
Samples
BF38A3699AB2072DEA806FF2EE3E54FCA4ABFA983BE9CBB207C3AE8E65095364 UnpacMe
References
Analysis
Looking at the C2 encryption routine we can see that there was a progression from the earlier builds.
The earliest build from March 2023 47439044a81b96be0bb34e544da881a393a30f0272616f52f54405b4bf288c7c
has a build path that has not been erased G:\Projects\stealer\oGnSUE7arNOZser\tmp_compiler\output.pdb
. This build does not use the custom encryption algorithm described below but simply uses an encrypted stack string to hide the c2 164.132.200.171
.
Builds from April-May 2023 (example 45D29AFC212F2D0BE4E198759C3C152BB8D0730BA20D46764A08503EAB0B454F
) uses the algorithm with a stack string containing the IP address as a DWORD followed by the port as a WORD and a stack string key.
New builds (example BF38A3699AB2072DEA806FF2EE3E54FCA4ABFA983BE9CBB207C3AE8E65095364
) have used a modified version of the algorithm which embed the key in the algorithm and used a global encrypted string to hide the full URL of the C2, not just an IP and Port.
Config
The following is a modified version of the open source decryptor for Zscaler decrypt_c2s.py
import struct
def uint32(val):
return val & 0xffffffff
def decrypt(data):
out = b''
block_size = 8
num_blocks = len(data) // block_size
blocks = struct.unpack(f"<{2 * num_blocks}L", data)
for i in range(0,len(blocks),2):
out += decrypt_block(blocks[i],blocks[i+1])
return out
def decrypt_block(v0, v1):
sum_value = 0xC6EF3720
delta = 0x61C88647
#key0, key1, key2, key3 = struct.unpack("<4L", key)
key0 = 0x7D935554
key1 = 0x7A3B0639
key2 = 0x4C774985
key3 = 0x8F036C0
for i in range(32):
v1 = v1 - ((v0 + sum_value) ^ (key2 + (v0 << 4)) ^ (key3 + (v0 >> 5)))
v1 = uint32(v1)
# print("v1:", hex(v1))
v7 = uint32(v1 + sum_value)
sum_value = uint32(sum_value + delta)
v8 = v7 ^ uint32(key0 + (v1 << 4)) ^ uint32(key1 + (v1 >> 5))
v0 = uint32(v0 - v8)
return struct.pack("<2L", v0, v1)
decrypt(bytes.fromhex('6f928688f64c75a5c916e8abe4560c29e279d7b750aba83f'))
The encrypted C2 can take two forms, either an IP address in DWORD octet format, followed by a port, or a full URL. The octet format follows.
c21 = 0x91736C72
c22 = 0x17DEE303
c23 = 0x85B9C50
c24 = 0xD8A3FC07
c221 = 0x66EB0028;
c222 = 0x41978887;
c223 = 0x85B9C50;
c224 = 0xD8A3FC07;
def decrypt_block(v0, v1):
sum_value = 0xC6EF3720
delta = 0x61C88647
#key0, key1, key2, key3 = struct.unpack("<4L", key)
key0 = 0x51D067C
key1 = 0x3F113D44
key2 = 0x6AA301C0
key3 = 0x72656277
for i in range(32):
v1 = v1 - ((v0 + sum_value) ^ (key2 + (v0 << 4)) ^ (key3 + (v0 >> 5)))
v1 = uint32(v1)
# print("v1:", hex(v1))
v7 = uint32(v1 + sum_value)
sum_value = uint32(sum_value + delta)
v8 = v7 ^ uint32(key0 + (v1 << 4)) ^ uint32(key1 + (v1 >> 5))
v0 = uint32(v0 - v8)
return struct.pack("<2L", v0, v1)
print(decrypt_block(c221, c222))
print(ord('\x87'))
print(ord('\xb5'))
print(ord('/'))
print(ord('_'))
import struct
print(struct.unpack('>H',b'3\xa3')[0])
Sample ID
The new samples share common bytes related to the decryption algorithm c1 e8 05 05 ?? ?? ?? ??33 c8 8b c3 c1 e0 04 05
which can be used to sig them.
C1 E8 05 shr eax, 5
05 C0 36 F0 08 add eax, key3
33 C8 xor ecx, eax
8B C3 mov eax, ebx
C1 E0 04 shl eax, 4
05 85 49 77 4C add eax, key2
33 C8 xor ecx, eax
2B F1 sub esi, ecx
8B C6 mov eax, esi
C1 E0 04 shl eax, 4
05 54 55 93 7D add eax, key0
8D 0C 37 lea ecx, [edi+esi]
33 C8 xor ecx, eax
8D BF 47 86 C8 61 lea edi, [edi+61C88647h]
8B C6 mov eax, esi
C1 E8 05 shr eax, 5
05 39 06 3B 7A add eax, key1
33 C8 xor ecx, eax
2B D9 sub ebx, ecx
83 ED 01 sub ebp, 1
import re
import struct
file_data = open('/tmp/mystic/mys.bin','rb').read()
egg = rb'\xc1\xe8\x05\x05(....)\x33\xc8\x8b\xc3\xc1\xe0\x04\x05(....)\x33\xc8\x2b\xf1\x8b\xc6\xc1\xe0\x04\x05(....)\x8d\x0c\x37\x33\xc8\x8d\xbf\x47\x86\xc8\x61\x8b\xc6\xc1\xe8\x05\x05(....)\x33\xc8\x2b\xd9\x83\xed\x01'
match = re.search(egg, file_data)
assert match is not None
key0 = struct.unpack('<I', match.group(3))[0]
key1 = struct.unpack('<I', match.group(4))[0]
key2 = struct.unpack('<I', match.group(2))[0]
key3 = struct.unpack('<I', match.group(1))[0]
print(f"{hex(key0)}\n{hex(key1)}\n{hex(key2)}\n{hex(key3)}\n")
def decrypt(data, key0, key1, key2, key3):
out = b''
block_size = 8
num_blocks = len(data) // block_size
blocks = struct.unpack(f"<{2 * num_blocks}L", data)
for i in range(0,len(blocks),2):
out += decrypt_block(blocks[i],blocks[i+1], key0, key1, key2, key3)
return out
def decrypt_block(v0, v1, key0, key1, key2, key3):
sum_value = 0xC6EF3720
delta = 0x61C88647
#key0, key1, key2, key3 = struct.unpack("<4L", key)
for i in range(32):
v1 = v1 - ((v0 + sum_value) ^ (key2 + (v0 << 4)) ^ (key3 + (v0 >> 5)))
v1 = uint32(v1)
# print("v1:", hex(v1))
v7 = uint32(v1 + sum_value)
sum_value = uint32(sum_value + delta)
v8 = v7 ^ uint32(key0 + (v1 << 4)) ^ uint32(key1 + (v1 >> 5))
v0 = uint32(v0 - v8)
return struct.pack("<2L", v0, v1)
import pefile
pe = pefile.PE(data=file_data)
rdata = None
for s in pe.sections:
if s.Name[:6] == b'.rdata':
rdata = s.get_data()
assert rdata is not None
candidates = rdata.split(b'\x00\x00\x00')
found = False
for c in candidates:
if found:
break
c = c.strip(b'\x00').rstrip(b'\x00')
if len(c) < 11:
continue
for i in range(len(c) - 10):
tmp = c[i:]
#print(f'\n\nTesting: {tmp}')
try:
out = decrypt(tmp, key0, key1, key2, key3)
if out[:4] == b'http':
print(out)
found = True
break
except:
pass
import re
import struct
import pefile
def extract(file_path):
file_data = open(file_path,'rb').read()
egg = rb'\xc1\xe8\x05\x05(....)\x33\xc8\x8b\xc3\xc1\xe0\x04\x05(....)\x33\xc8\x2b\xf1\x8b\xc6\xc1\xe0\x04\x05(....)\x8d\x0c\x37\x33\xc8\x8d\xbf\x47\x86\xc8\x61\x8b\xc6\xc1\xe8\x05\x05(....)\x33\xc8\x2b\xd9\x83\xed\x01'
match = re.search(egg, file_data)
assert match is not None
key0 = struct.unpack('<I', match.group(3))[0]
key1 = struct.unpack('<I', match.group(4))[0]
key2 = struct.unpack('<I', match.group(2))[0]
key3 = struct.unpack('<I', match.group(1))[0]
pe = pefile.PE(data=file_data)
rdata = None
for s in pe.sections:
if s.Name[:6] == b'.rdata':
rdata = s.get_data()
assert rdata is not None
candidates = rdata.split(b'\x00\x00\x00')
found = False
for c in candidates:
if found:
break
c = c.strip(b'\x00').rstrip(b'\x00')
if len(c) < 11:
continue
for i in range(len(c) - 10):
tmp = c[i:]
#print(f'\n\nTesting: {tmp}')
try:
out = decrypt(tmp, key0, key1, key2, key3)
if out[:4] == b'http':
print(out)
found = True
break
except:
pass
extract('/tmp/mystic/mys.bin')
import os
# assign directory
directory = '/tmp/test/'
# iterate over files in
# that directory
for filename in os.listdir(directory):
f = os.path.join(directory, filename)
# checking if it is a file
if os.path.isfile(f):
try:
print(f)
extract(f)
except:
pass