Created
December 7, 2016 10:20
-
-
Save msm-code/5c98c80ed1694e2d853c7a640c8daf3d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import socket, struct, binascii, random, hashlib | |
import time, os, errno, glob | |
import pprint | |
import libs.tofsee.parse as parse | |
import libs.tofsee.decompress as decompress | |
import libs.tofsee.parse_mail as parse_mail | |
import libs.log as log | |
from libs.remote import HTTPBot | |
log = log.get_logger(__name__) | |
# This is the main file implementing communication with Tofsee. | |
def recv_exactly(s, n): | |
r = "" | |
while len(r) < n: | |
resp = s.recv(n - len(r)) | |
if resp == "": | |
raise EOFError("EOF...") | |
r += resp | |
return r | |
def receive_greet(s): | |
return parse.parse_greet(recv_exactly(s, 200)) | |
def send_encrypted(s, hdr, payload, key): | |
hdr["crc"] = binascii.crc32(payload) | |
hdr["size"] = len(payload) | |
hdr["size_decomp"] = len(payload) | |
hdr["flag_compr_2"] = 1 # No compression. | |
hdrstr = "" | |
hdrstr += struct.pack("<I", hdr["size"]) | |
hdrstr += struct.pack("<I", hdr["size_decomp"]) | |
hdrstr += struct.pack("<i", hdr["crc"]) | |
hdrstr += struct.pack("<B", hdr["flag_compr_2"]) | |
hdrstr += struct.pack("<I", hdr["op"]) | |
hdrstr += struct.pack("<I", hdr["subop1"]) | |
hdrstr += struct.pack("<I", hdr["subop2"]) | |
msg = hdrstr + payload | |
msg = parse.xor_deencrypt(msg, key) | |
s.sendall(msg) | |
def receive_encrypted(s, key): | |
hdr = parse.xor_deencrypt(recv_exactly(s, 25), key) | |
hdr = parse.parse_header(hdr) | |
payload = parse.xor_deencrypt(recv_exactly(s, hdr["size"]), key) | |
if hdr["flag_compr_2"] & 2: | |
payload = decompress.deco(payload, hdr["size_decomp"]) | |
return hdr, payload | |
def parse_message2(payload): | |
type = struct.unpack("<I", payload[:4])[0] | |
name = payload[4:20].strip("\x00").replace("\x00", "_") | |
length = struct.unpack("<I", payload[0x18:0x1c])[0] | |
buf = payload[0x24:] | |
assert len(buf) == length | |
return type, name, buf | |
def parse_ips_list(payload): | |
assert len(payload) % 0x45 == 0 | |
ips = [] | |
for i in range(len(payload) / 0x45): | |
part = payload[i * 0x45:(i + 1) * 0x45] | |
ip = part[1:0x41].strip("\x00") | |
port = struct.unpack("<I", part[0x41:])[0] | |
ips.append((ip, port)) | |
return ips | |
def send_list_of_resources(s, key, hdr, resources): | |
# Send resources - commented out to simulate lack of resources. | |
# hdr["op"]=2 | |
# payload="" | |
# for res in resources: | |
# payload+=struct.pack("<I", res[0]) # Type | |
# name=res[1] | |
# payload+=name+"\x00"*(16-len(name)) | |
# payload+="\x00"*16 | |
# sendEncrypted(s, hdr, payload, key) | |
# Finish transmission. | |
hdr["op"] = 0 | |
send_encrypted(s, hdr, "", key) | |
def save_resource(type, name, content): | |
return # NO SAVING. Comment this line out to save all resources to local folders. | |
if name.startswith("work_srv"): | |
ips = parse_ips_list(content) | |
for ip in ips: | |
save_resource(type, "work_srv/" + ip[0] + ":" + str(ip[1]), "") | |
return | |
elif type == 2 and name.startswith("ID"): | |
return # Don't save IDs, that's spammy. | |
elif type == 7 and (name.startswith("%EVA_AUTOURL") or name.startswith("#EVA_URL")): | |
return | |
directory = "resources/" + str(type) | |
try: | |
os.makedirs(directory) | |
except OSError as e: | |
if e.errno == errno.EEXIST: | |
pass | |
else: | |
raise | |
files = glob.glob(directory + "/*") | |
name = directory + "/" + name + (16 - len(name)) * "_" | |
if content != "": | |
name += "." + hashlib.md5(content).hexdigest() | |
if name not in files: | |
print repr(name) | |
open(name, "wb").write(content) | |
class tofsee(HTTPBot): | |
@classmethod | |
def create(cls, el): | |
yield cls(el) | |
def init_bot(self, el): | |
self.start_ips = [] | |
self.resources = {} | |
self.url = 'my legs are dangling off the edge' | |
self._raw_cfg = None | |
for ipport in el["urls"]: | |
self.start_ips.append((ipport["ip"], ipport["port"])) | |
def parse(self): | |
return True | |
def ppr(self): | |
return self._raw_cfg | |
def get_cfg(self): | |
ips = self.start_ips | |
for tries in range(10): | |
try: | |
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
s.settimeout(30) | |
# Choose random IP from C&C list (initially from static config, | |
# later from the initial C&C). | |
ip = random.choice(ips) | |
log.info("Trying IP: " + str(ip) + "...") | |
s.connect(ip) | |
log.info("Connected.") | |
greet = receive_greet(s) | |
log.info("Greeting received.") | |
botID = 0xdeadcafebabe1337 | |
botinfo_struct = { | |
"flags_upd": struct.pack("<I", 0), | |
"dl": struct.pack("<I", botID >> 32), | |
"id": struct.pack("<I", botID & 0xffffFFFF), | |
"unk0": struct.pack("<I", 72), | |
"unk1": struct.pack("<I", 0), | |
"net_type_flags": struct.pack("<I", 0x1f), | |
"VM": struct.pack("<I", 0), | |
"unk2": struct.pack("<I", 10), | |
"unk3": struct.pack("<I", 0), | |
"unk4": struct.pack("<I", 0), | |
"lid_file_upd": struct.pack("<I", 0), | |
"tickcount": struct.pack("<I", 0x1000000), | |
"tickcount_delta": struct.pack("<I", 84), | |
"born_timestamp": struct.pack("<I", int(time.time())), | |
"localIP": struct.pack("<I", 0x0f02000a), | |
"host_flags": struct.pack("<I", 0), | |
"unk5": struct.pack("<I", 0), | |
"unk6": struct.pack("<B", 0), | |
"os": struct.pack("<B", 0x61), | |
"unkrest": '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' | |
} | |
botinfo = "" | |
botinfo += botinfo_struct["flags_upd"] | |
botinfo += botinfo_struct["dl"] | |
botinfo += botinfo_struct["id"] | |
botinfo += botinfo_struct["unk0"] | |
botinfo += botinfo_struct["unk1"] | |
botinfo += botinfo_struct["net_type_flags"] | |
botinfo += botinfo_struct["VM"] | |
botinfo += botinfo_struct["unk2"] | |
botinfo += botinfo_struct["unk3"] | |
botinfo += botinfo_struct["unk4"] | |
botinfo += botinfo_struct["lid_file_upd"] | |
botinfo += botinfo_struct["tickcount"] | |
botinfo += botinfo_struct["tickcount_delta"] | |
botinfo += botinfo_struct["born_timestamp"] | |
botinfo += botinfo_struct["localIP"] | |
botinfo += botinfo_struct["host_flags"] | |
botinfo += botinfo_struct["unk5"] | |
botinfo += botinfo_struct["unk6"] | |
botinfo += botinfo_struct["os"] | |
botinfo += botinfo_struct["unkrest"] | |
hdr = {'subop2': 0, 'subop1': 0, 'op': 1} | |
# Send the first message (general bot information). | |
send_encrypted(s, hdr, botinfo, greet["skey"]) | |
log.info("Botinfo sent.") | |
# Now, as long as server sends anything, try to keep up conversation with it. | |
while True: | |
hdr, payload = receive_encrypted(s, greet["rkey"]) | |
if hdr["op"] == 1: | |
# Server wants to know which resources we already have. | |
flags = struct.unpack("<I", payload[:4])[0] | |
if flags == 4 and (hdr["subop2"] & 1) == 0: | |
log.info("Sending list of resources...") | |
# We answer "we have no resources yet". | |
send_list_of_resources(s, greet["skey"], hdr, self.resources) | |
elif flags == 0x1bb and hdr["subop2"] == 0: | |
# No idea what this message means, but VM sent zeroes | |
# in response. | |
hdr["op"] = 3 | |
send_encrypted(s, hdr, "\x00" * 40, greet["skey"]) | |
hdr["op"] = 0 | |
send_encrypted(s, hdr, "", greet["skey"]) | |
log.info("Sent some zeroes, meaning unknown.") | |
else: | |
log.warning("Unknown operation " + hex(flags) + ".") | |
log.debug(repr(payload)) | |
elif hdr["op"] == 2: | |
# New resource has been received - add it to result. | |
type, name, content = parse_message2(payload) | |
self.resources[(type, name)] = content | |
save_resource(type, name, content) | |
if type == 1 and name == "work_srv": | |
ips = parse_ips_list(content) | |
log.info("New IPs: " + repr(ips)) | |
else: | |
log.info("New resource (type=" + str(type) + ") received: " + name) | |
else: | |
log.warning("Unknown operation.") | |
log.info(repr(hdr)) | |
log.debug(repr(payload)) | |
except EOFError as e: | |
log.info("EOF received.") | |
# We already have all the resources server wanted to send us. | |
# Let's parse some emails then. | |
vars11 = {} # Type 11: email templates. | |
vars7 = {} # Type 7 : global substitutions. | |
vars8 = {} # Type 8 : local substitutions. | |
for var in self.resources: | |
if var[0] == 11: | |
vars11[var[1]] = self.resources[var] | |
elif var[0] == 7: | |
vars7[var[1]] = self.resources[var] | |
elif var[0] == 8: | |
vars8[var[1]] = self.resources[var] | |
result = {"sample_mails": {}} | |
for num in vars11: | |
# For each template, create a sample email using global | |
# variables and those local to this particular script. | |
variables = vars7.copy() | |
for var in vars8: | |
if var.startswith(num + "%"): | |
newvarname = var.lstrip(num) | |
variables[newvarname] = vars8[var] | |
template = vars11[num] | |
parsed = parse_mail.parse(template, variables) | |
result["sample_mails"][num] = parsed | |
# This "if" is necessary, because the first server doesn't return | |
# any resources, so we need to retry communication - this time | |
# with new C&Cs. | |
if len(self.resources) > 10: | |
s.close() | |
rsrc = {} | |
# Make keys strings (so far, they were tuples (type, name)). | |
for key in self.resources: | |
s = str(key[0]) | |
if s not in rsrc: | |
rsrc[s] = {} | |
rsrc[s][key[1]] = self.resources[key] | |
result["resources"] = rsrc | |
self._raw_cfg = pprint.pformat(result) | |
return result | |
if ip not in ips: | |
s.close() | |
ip = random.choice(ips) | |
except Exception as e: | |
log.warning("Exception caught: " + repr(e)) | |
self._raw_cfg = 'no config' | |
return self._raw_cfg | |
# Returned dict has the following form (obviously edited for brevity): | |
# { | |
# 'resources': { | |
# '5': { | |
# '1': 'MZ\x90\x00...', | |
# '10': 'MZ\x90\x00...', | |
# '11': 'MZ\x90\x00...', | |
# '12': 'MZ\x90\x00...' | |
# }, | |
# '7': { | |
# '%ACCFN': 'substitutions', | |
# '%ACCLN': 'substitutions', | |
# '%ACCNAME': 'substitutions' | |
# }, | |
# '8': { | |
# '1835%FROM_EMAIL': 'S%FROM_USE', | |
# '1835%RNDRFONT': 'SVerdana', | |
# '1835%RNDRSIZE': 'S2\n3\n4' | |
# }, | |
# }, | |
# 'sample_mails': { | |
# '1835': 'parsed email', | |
# '1906': 'parsed email' | |
# } | |
# } | |
# Some of the more interesting (from our perspective) resources: | |
# js["resources"]["7"]["%JS_EXPL_00"] - contains base64-encoded | |
# zip attachment (also JS_EXPL_01, 02 and so on) | |
# js["sample_mails"] - contains parsed mails | |
# js["resources"]["1"]["work_srv"] - contains C&C addresses | |
# js["resources"]["1"]["start_srv"] - some more C&C | |
# js["resources"]["5"] - all the submodules of Tofsee (DDoS etc.) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment