Titan Stealer
Another GO stealer
Overview
According to @ViriBack
The ref md5 :82040e02a2c16b12957659e1356a5e19 (a7dfb6bb7ca1c8271570ddcf81bb921cf4f222e6e190e5f420d4e1eda0a0c1f2) for rule 2039778 communicates to same host:port that is mentioned in this blog for Titan panelcallout pattern: /sendlog with a base64 zip
The attribution on this comes from the link to the Titan Stealer panel at http[:]//77.73.133[.]88:5000/login/
Sample
A7DFB6BB7CA1C8271570DDCF81BB921CF4F222E6E190E5F420D4E1EDA0A0C1F2
malware bazaar
References
import re
file_data = open('/tmp/titan.bin','rb').read()
for m in re.finditer(rb"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", file_data):
print(m.group())
GO String Extraction
We can follow the example from JAGS' AlphaGOLang script and just identify where the strings are loaded in the code.
Dynamically Allocated Strings
These are loaded inline in assembly. Usually there is a string ref followed by the string size.
- get the virtual address bounds of the
.rdata
section to validate string refs - create a list of asm blocks used to dynamically load strings
- scan for these and filter the fps using our rdata block
- check if the string is valid
import re
import pefile
file_data = open('/tmp/titan.bin','rb').read()
pe = pefile.PE(data=file_data)
# Rebase PE to 0 and conver addresses into RVAs
# Based!
pe.relocate_image(0)
rdata_start = None
rdata_end = None
for s in pe.sections:
if s.Name.startswith(b'.rdata'):
rdata_start = s.VirtualAddress
rdata_end = rdata_start + s.Misc_VirtualSize
assert rdata_start is not None
text_data = None
for s in pe.sections:
if s.Name.startswith(b'.text'):
text_data = s.get_data()
assert text_data is not None
import struct
def is_ascii(s):
return all(c < 128 or c == 0 for c in s)
def get_ip(s):
m = re.match("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", s)
if m:
return m.group()
return None
# 8D 05 4C B1 50 00 lea eax, a777313388 ; "77.73.133.88"
# 89 44 24 04 mov [esp+0A0h+a2], eax
# C7 44 24 08 0C 00 00 00 mov [esp+0A0h+a3], 0Ch
egg_1 = rb'\x8D.(....)\x89...\xC7...(....)'
strings = []
c2_ip = None
c2_port = None
for m in re.finditer(egg_1, text_data):
str_rva = struct.unpack('<I', m.group(1))[0]
str_len = struct.unpack('<I', m.group(2))[0]
if str_rva < rdata_start or str_rva > rdata_end:
continue
if str_len < 2 or str_len > 100:
continue
tmp_str = pe.get_data(str_rva, str_len)
if is_ascii(tmp_str):
strings.append(tmp_str.decode('utf-8'))
ip = get_ip(tmp_str.decode('utf-8'))
if ip is not None:
c2_ip = ip
c2_port = struct.unpack('<I', text_data[m.end()+4: m.end() + 4 + 4])[0]
print(c2_ip)
print(c2_port)
for s in strings:
m = re.match("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", s)
if m:
print(m.group())
Static Strings
These are located in the .data
section as a struct with the following shape.
struct go_string{
char* str_buff;
size_t str_len;
};
- get the virtual address bounds of the
.rdata
section to validate string refs - scan the
.data
section for the above structure (this will be gross) - for each potential struct found validate the strings