Add release wording and remove redundant thing
This commit is contained in:
parent
3e0413f9f7
commit
1f3a146448
104
sunburst.py
104
sunburst.py
|
@ -1,13 +1,37 @@
|
|||
#! /usr/bin/python3
|
||||
|
||||
# Neale Pickett <neale@lanl.gov>
|
||||
# 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,8 +110,9 @@ def DecodeSubst(s: str) -> str:
|
|||
out = []
|
||||
for c in s:
|
||||
if c == '0':
|
||||
# Use alternate alphabet next time
|
||||
alphabet = SubstitutionAlphabet0
|
||||
else:
|
||||
continue
|
||||
try:
|
||||
pos = (SubstitutionAlphabet.index(c) - 4) % len(alphabet)
|
||||
except ValueError:
|
||||
|
@ -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]")
|
||||
|
||||
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])
|
||||
|
||||
if 0 <= sequence < 3:
|
||||
dec, _ = DecodeEsab32(s[:15])
|
||||
eguid = dec.to_bytes(10, 'little')[:9]
|
||||
guid = int.from_bytes(xor(eguid[0:1], eguid[1:]), 'big')
|
||||
|
||||
unknown_a = s[15]
|
||||
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]
|
||||
|
||||
return (guid, unknown_a, decoded)
|
||||
dec2 = xor(dec1[:1], dec1[1:])
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue