sunburst-decoder

SUNBURST DGA decoder
git clone https://git.woozle.org/neale/sunburst-decoder.git

commit
1f3a146
parent
3e0413f
author
Neale Pickett
date
2021-05-06 14:51:18 -0600 MDT
Add release wording and remove redundant thing
1 files changed,  +100, -36
M sunburst.py
+100, -36
  1@@ -1,13 +1,37 @@
  2 #! /usr/bin/python3
  3 
  4 # Neale Pickett <neale@lanl.gov>
  5-# Unclassified/FOUO
  6 #
  7 # Created: 2020-12-14 16:49:51
  8-# Last-modified: 2020-12-22 21:42:40
  9+# Last-modified: 2021-05-06 11:09:55
 10 #
 11 # Based on work by @RedDrip7 (twitter),
 12-# who should be getting more credit in the English-speaking world.
 13+# and Prevasio (https://blog.prevasio.com/2020/12/sunburst-backdoor-deeper-look-into.html)
 14+
 15+# This is public domain software. The public may copy, distribute, prepare derivative works and 
 16+# publicly display this software without charge, provided that this Notice, the statement 
 17+# of reserved government right, and any statement of authorship are reproduced on all copies. 
 18+# If software is modified to produce derivative works, such modified software should 
 19+# be clearly marked, so as not to confuse it with the version available from LANL.
 20+
 21+# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. 
 22+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 23+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 24+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 25+# NEITHER THE UNITED STATES NOR THE UNITED STATES DEPARTMENT OF ENERGY/NATIONAL 
 26+# NUCLEAR SECURITY ADMINSTRATION, NOR Triad National Security, LLC.  NOR ANY OF THEIR 
 27+# EMPLOYEES, MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR ASSUMES ANY LEGAL LIABILITY 
 28+# OR RESPONSIBILITY FOR THE ACCURACY, COMPLETENESS, OR USEFULNESS OF ANY INFORMATION, 
 29+# APPARATUS, PRODUCT, OR PROCESS DISCLOSED, OR REPRESENTS THAT ITS USE WOULD NOT 
 30+# INFRINGE PRIVATELY OWNED RIGHTS.
 31+
 32+# IN NO EVENT SHALL THE U.S. GOVERNMENT OR ITS CONTRACTORS BE LIABLE FOR ANY DIRECT, 
 33+# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 34+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 35+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 36+# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 37+# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 38+# OF THE POSSIBILITY OF SUCH DAMAGE.
 39 
 40 import argparse
 41 import base64
 42@@ -16,6 +40,7 @@ import csv
 43 import itertools
 44 import re
 45 import sys
 46+import time
 47 
 48 
 49 knownDomains = [
 50@@ -23,7 +48,6 @@ knownDomains = [
 51     "appsync-api.us-east-2.avsvmcloud.com",
 52     "appsync-api.us-west-2.avsvmcloud.com",
 53     "appsync-api.eu-west-1.avsvmcloud.com",
 54-    "avsvmcloud.com",
 55 ]
 56 
 57 
 58@@ -34,6 +58,18 @@ def xor(key, buf):
 59 Esab32Alphabet = "ph2eifo3n5utg1j8d94qrvbmk0sal76c"
 60 SubstitutionAlphabet = 'rq3gsalt6u1iyfzop572d49bnx8cvmkewhj'
 61 SubstitutionAlphabet0 = '0_-.'
 62+SequenceAlphabet = '0123456789abcdefghijklmnopqrstuvwxyz'
 63+
 64+Apps = [
 65+    "Windows Live OneCare / Windows Defender",
 66+    "Windows Defender Advanced Threat Protection",
 67+    "Microsoft Defender for Identity",
 68+    "Carbon Black",
 69+    "CrowdStrike",
 70+    "FireEye",
 71+    "ESET",
 72+    "F-Secure",
 73+]
 74 
 75 
 76 def DecodeBase32(s: str):
 77@@ -74,15 +110,16 @@ def DecodeSubst(s: str) -> str:
 78     out = []
 79     for c in s:
 80         if c == '0':
 81+            # Use alternate alphabet next time
 82             alphabet = SubstitutionAlphabet0
 83-        else:
 84-            try:
 85-                pos = (SubstitutionAlphabet.index(c) - 4) % len(alphabet)
 86-            except ValueError:
 87-                raise RuntimeError(
 88-                    "Not a subst encoded character: %c in %s" % (c, s))
 89-            out.append(alphabet[pos])
 90-            alphabet = SubstitutionAlphabet
 91+            continue
 92+        try:
 93+            pos = (SubstitutionAlphabet.index(c) - 4) % len(alphabet)
 94+        except ValueError:
 95+            raise RuntimeError(
 96+                "Not a subst encoded character: %c in %s" % (c, s))
 97+        out.append(alphabet[pos])
 98+        alphabet = SubstitutionAlphabet
 99     return "".join(out)
100 
101 
102@@ -153,33 +190,61 @@ def DecodeDomain(domain: str) -> (Guid, int, str):
103             foundDomain = d
104             break
105     if not foundDomain:
106-        raise RuntimeError("Can't find domain for %s" % s)
107+        return (None, None, "[Probably not a Sunburst domain]")
108     s = s[:-len(foundDomain)]
109     if not s:
110         return (None, None, "[no data transmitted]")
111     assert(s[-1] == '.')
112     s = s[:-1]
113 
114-    if foundDomain == "avsvmcloud.com":
115-        return (None, None, "[Probably not a Sunburst domain]")
116     if len(s) < 16:
117         return (None, None, "[too short]")
118 
119-    dec, _ = DecodeEsab32(s[:15])
120-    eguid = dec.to_bytes(10, 'little')[:9]
121-    guid = int.from_bytes(xor(eguid[0:1], eguid[1:]), 'big')
122-
123-    unknown_a = s[15]
124-    payload = s[16:]
125-
126-    decoder = DecodersByGuid.get(guid)
127-    if not decoder:
128-        decoder = DGADecoder(guid)
129-        DecodersByGuid[guid] = decoder
130-
131-    decoded = decoder.decode(payload)
132-
133-    return (guid, unknown_a, decoded)
134+    c0 = s.encode("ASCII")[0]
135+    # https://blog.prevasio.com/2020/12/sunburst-backdoor-part-iii-dga-security.html
136+    sequence = (c0 % 36) - SequenceAlphabet.index(s[15])
137+
138+    if 0 <= sequence < 3:
139+        dec, _ = DecodeEsab32(s[:15])
140+        eguid = dec.to_bytes(10, 'little')[:9]
141+        guid = xor(eguid[0:1], eguid[1:])
142+        payload = s[16:]
143+        decoder = DecodersByGuid.get(guid)
144+        if not decoder:
145+            decoder = DGADecoder(guid)
146+            DecodersByGuid[guid] = decoder
147+        decoded = decoder.decode(payload)
148+    else:
149+        # https://blog.prevasio.com/2020/12/sunburst-backdoor-part-iii-dga-security.html
150+        print(s, c0, sequence)
151+        dec1num, bits = DecodeEsab32(s)
152+        dec1len = bits // 8
153+        dec1 = dec1num.to_bytes(dec1len+1, "little")[:dec1len]
154+
155+        dec2 = xor(dec1[:1], dec1[1:])
156+
157+        guid = xor(dec2[9:11], dec2[0:8])
158+        tslen = int.from_bytes(dec2[8:11], "big")
159+        length = tslen >> (8+8+4)
160+        epoch = 1262329200 + ((tslen & 0xfffff) << 2) # 4s intervals since 2010-01-01
161+        timestamp = time.gmtime(epoch)
162+
163+        print(dec2[8:], tslen, length, timestamp)
164+        state = int.from_bytes(dec2[15:17], "big")
165+        decodedStrings = []
166+        for i in range(len(Apps)):
167+            appState = state >> (i * 2)
168+            app = Apps[i]
169+            appStateString = []
170+            if appState & 0b01:
171+                appStateString.append("running")
172+            if appState & 0b10:
173+                appStateString.append("stopped")
174+            if appStateString:
175+                decodedStrings.append("%s [%s]" % (app, ",".join(appStateString)))
176+        decoded = "\n".join(decodedStrings)
177+
178+    return (guid, sequence, decoded)
179 
180 
181 class TextReader:
182@@ -195,8 +260,7 @@ class TextReader:
183 class CsvReader:
184     def __init__(self, infile):
185         self.reader = csv.DictReader(infile)
186-        self.fieldnames = self.reader.fieldnames + \
187-            ["guid", "unknown a", "decode"]
188+        self.fieldnames = self.reader.fieldnames
189 
190     def __iter__(self):
191         for record in self.reader:
192@@ -230,14 +294,14 @@ def main():
193         parser.print_help()
194         return
195 
196-    fieldnames = reader.fieldnames + ["guid", "unknown a", "decode"]
197+    fieldnames = reader.fieldnames + ["guid", "sequence", "decode"]
198     writer = csv.DictWriter(args.outfile, fieldnames)
199     writer.writeheader()
200     for record in reader:
201         name = record.get("name") or record.get("fqdn")
202-        guid, unknown_a, ptext = DecodeDomain(name)
203-        record["guid"] = guid
204-        record["unknown a"] = unknown_a
205+        guid, sequence, ptext = DecodeDomain(name)
206+        record["guid"] = int.from_bytes(guid, "big")
207+        record["sequence"] = sequence
208         record["decode"] = ptext
209         writer.writerow(record)
210