diff --git a/sunburst.py b/sunburst.py index c8e7df6..1c11998 100755 --- a/sunburst.py +++ b/sunburst.py @@ -1,13 +1,37 @@ #! /usr/bin/python3 # Neale Pickett -# Unclassified/FOUO # # Created: 2020-12-14 16:49:51 -# Last-modified: 2020-12-22 21:42:40 +# Last-modified: 2021-05-06 11:09:55 # # Based on work by @RedDrip7 (twitter), -# who should be getting more credit in the English-speaking world. +# and Prevasio (https://blog.prevasio.com/2020/12/sunburst-backdoor-deeper-look-into.html) + +# This is public domain software. The public may copy, distribute, prepare derivative works and +# publicly display this software without charge, provided that this Notice, the statement +# of reserved government right, and any statement of authorship are reproduced on all copies. +# If software is modified to produce derivative works, such modified software should +# be clearly marked, so as not to confuse it with the version available from LANL. + +# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# NEITHER THE UNITED STATES NOR THE UNITED STATES DEPARTMENT OF ENERGY/NATIONAL +# NUCLEAR SECURITY ADMINSTRATION, NOR Triad National Security, LLC. NOR ANY OF THEIR +# EMPLOYEES, MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR ASSUMES ANY LEGAL LIABILITY +# OR RESPONSIBILITY FOR THE ACCURACY, COMPLETENESS, OR USEFULNESS OF ANY INFORMATION, +# APPARATUS, PRODUCT, OR PROCESS DISCLOSED, OR REPRESENTS THAT ITS USE WOULD NOT +# INFRINGE PRIVATELY OWNED RIGHTS. + +# IN NO EVENT SHALL THE U.S. GOVERNMENT OR ITS CONTRACTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. import argparse import base64 @@ -16,6 +40,7 @@ import csv import itertools import re import sys +import time knownDomains = [ @@ -23,7 +48,6 @@ knownDomains = [ "appsync-api.us-east-2.avsvmcloud.com", "appsync-api.us-west-2.avsvmcloud.com", "appsync-api.eu-west-1.avsvmcloud.com", - "avsvmcloud.com", ] @@ -34,6 +58,18 @@ def xor(key, buf): Esab32Alphabet = "ph2eifo3n5utg1j8d94qrvbmk0sal76c" SubstitutionAlphabet = 'rq3gsalt6u1iyfzop572d49bnx8cvmkewhj' SubstitutionAlphabet0 = '0_-.' +SequenceAlphabet = '0123456789abcdefghijklmnopqrstuvwxyz' + +Apps = [ + "Windows Live OneCare / Windows Defender", + "Windows Defender Advanced Threat Protection", + "Microsoft Defender for Identity", + "Carbon Black", + "CrowdStrike", + "FireEye", + "ESET", + "F-Secure", +] def DecodeBase32(s: str): @@ -74,15 +110,16 @@ def DecodeSubst(s: str) -> str: out = [] for c in s: if c == '0': + # Use alternate alphabet next time alphabet = SubstitutionAlphabet0 - else: - try: - pos = (SubstitutionAlphabet.index(c) - 4) % len(alphabet) - except ValueError: - raise RuntimeError( - "Not a subst encoded character: %c in %s" % (c, s)) - out.append(alphabet[pos]) - alphabet = SubstitutionAlphabet + continue + try: + pos = (SubstitutionAlphabet.index(c) - 4) % len(alphabet) + except ValueError: + raise RuntimeError( + "Not a subst encoded character: %c in %s" % (c, s)) + out.append(alphabet[pos]) + alphabet = SubstitutionAlphabet return "".join(out) @@ -153,33 +190,61 @@ def DecodeDomain(domain: str) -> (Guid, int, str): foundDomain = d break if not foundDomain: - raise RuntimeError("Can't find domain for %s" % s) + return (None, None, "[Probably not a Sunburst domain]") s = s[:-len(foundDomain)] if not s: return (None, None, "[no data transmitted]") assert(s[-1] == '.') s = s[:-1] - if foundDomain == "avsvmcloud.com": - return (None, None, "[Probably not a Sunburst domain]") if len(s) < 16: return (None, None, "[too short]") - dec, _ = DecodeEsab32(s[:15]) - eguid = dec.to_bytes(10, 'little')[:9] - guid = int.from_bytes(xor(eguid[0:1], eguid[1:]), 'big') + c0 = s.encode("ASCII")[0] + # https://blog.prevasio.com/2020/12/sunburst-backdoor-part-iii-dga-security.html + sequence = (c0 % 36) - SequenceAlphabet.index(s[15]) - unknown_a = s[15] - payload = s[16:] + if 0 <= sequence < 3: + dec, _ = DecodeEsab32(s[:15]) + eguid = dec.to_bytes(10, 'little')[:9] + guid = xor(eguid[0:1], eguid[1:]) + payload = s[16:] + decoder = DecodersByGuid.get(guid) + if not decoder: + decoder = DGADecoder(guid) + DecodersByGuid[guid] = decoder + decoded = decoder.decode(payload) + else: + # https://blog.prevasio.com/2020/12/sunburst-backdoor-part-iii-dga-security.html + print(s, c0, sequence) + dec1num, bits = DecodeEsab32(s) + dec1len = bits // 8 + dec1 = dec1num.to_bytes(dec1len+1, "little")[:dec1len] - decoder = DecodersByGuid.get(guid) - if not decoder: - decoder = DGADecoder(guid) - DecodersByGuid[guid] = decoder + dec2 = xor(dec1[:1], dec1[1:]) - decoded = decoder.decode(payload) + guid = xor(dec2[9:11], dec2[0:8]) + tslen = int.from_bytes(dec2[8:11], "big") + length = tslen >> (8+8+4) + epoch = 1262329200 + ((tslen & 0xfffff) << 2) # 4s intervals since 2010-01-01 + timestamp = time.gmtime(epoch) - return (guid, unknown_a, decoded) + print(dec2[8:], tslen, length, timestamp) + state = int.from_bytes(dec2[15:17], "big") + decodedStrings = [] + for i in range(len(Apps)): + appState = state >> (i * 2) + app = Apps[i] + appStateString = [] + if appState & 0b01: + appStateString.append("running") + if appState & 0b10: + appStateString.append("stopped") + if appStateString: + decodedStrings.append("%s [%s]" % (app, ",".join(appStateString))) + decoded = "\n".join(decodedStrings) + + return (guid, sequence, decoded) class TextReader: @@ -195,8 +260,7 @@ class TextReader: class CsvReader: def __init__(self, infile): self.reader = csv.DictReader(infile) - self.fieldnames = self.reader.fieldnames + \ - ["guid", "unknown a", "decode"] + self.fieldnames = self.reader.fieldnames def __iter__(self): for record in self.reader: @@ -230,14 +294,14 @@ def main(): parser.print_help() return - fieldnames = reader.fieldnames + ["guid", "unknown a", "decode"] + fieldnames = reader.fieldnames + ["guid", "sequence", "decode"] writer = csv.DictWriter(args.outfile, fieldnames) writer.writeheader() for record in reader: name = record.get("name") or record.get("fqdn") - guid, unknown_a, ptext = DecodeDomain(name) - record["guid"] = guid - record["unknown a"] = unknown_a + guid, sequence, ptext = DecodeDomain(name) + record["guid"] = int.from_bytes(guid, "big") + record["sequence"] = sequence record["decode"] = ptext writer.writerow(record)