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

Analysis

C2

Lol this is in plaintext!

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
C7 44 24 0C 88 13 00 00                 mov     [esp+0A0h+a4], 5000
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())
    
b'77.73.133.88'

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

Build our regexes

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)
77.73.133.88
5000
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())
77.73.133.88

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