├── LICENSE ├── README.md ├── extract_uid.bat ├── extract_uid.sh ├── flames_pub.c └── flames_pub.exe /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, igosha 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SUNBURST DGA decoder 2 | 3 | Details on Securelist: [https://securelist.com/sunburst-connecting-the-dots-in-the-dns-requests/99862/](https://securelist.com/sunburst-connecting-the-dots-in-the-dns-requests/99862/) 4 | 5 | ## Compiling 6 | 7 | Windows: CL.EXE flames\_pub.c (start a Visual Studio Command Prompt) 8 | *binary for Windows included* 9 | 10 | Linux/Unix: gcc -std=c99 -o flames\_pub flames\_pub.c 11 | 12 | ## Match the domain requests published by FireEye with target names 13 | 14 | 1. Compile the binary. 15 | 16 | 2. Download the datasets. 17 | 18 | ``` 19 | https://raw.githubusercontent.com/fireeye/sunburst_countermeasures/main/indicator_release/Indicator_Release_NBIs.csv 20 | https://raw.githubusercontent.com/bambenek/research/main/sunburst/uniq-hostnames.txt 21 | ``` 22 | 23 | 3. Execute in the same directory as datasets: 24 | 25 | Windows: extract\_uids.bat 26 | 27 | Linux/Unix: extract\_uids.sh 28 | 29 | -------------------------------------------------------------------------------- /extract_uid.bat: -------------------------------------------------------------------------------- 1 | REM Required files: 2 | REM https://raw.githubusercontent.com/fireeye/sunburst_countermeasures/main/indicator_release/Indicator_Release_NBIs.csv 3 | REM https://raw.githubusercontent.com/bambenek/research/main/sunburst/uniq-hostnames.txt 4 | @echo off 5 | REM Extract the domain names 6 | del %TEMP%\domain_names_f.txt 2>NUL 7 | for /f "tokens=3 delims=," %%a in ('findstr CNAME Indicator_Release_NBIs.csv') do echo %%a >> %TEMP%\domain_names_f.txt 8 | %~dp0\flames_pub.exe < %TEMP%\domain_names_f.txt > %TEMP%\flames_pub_f.txt 9 | %~dp0\flames_pub.exe < uniq-hostnames.txt | findstr domain > %TEMP%\flames_all.txt 10 | for /f "tokens=4 delims= " %%a in ('findstr NextString %TEMP%\flames_pub_f.txt') do findstr %%a %TEMP%\flames_all.txt 11 | del %TEMP%\domain_names_f.txt 12 | del %TEMP%\flames_pub_f.txt 13 | del %TEMP%\flames_all.txt 14 | -------------------------------------------------------------------------------- /extract_uid.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mydir=$(dirname $0) 4 | flames_all_file=flames_all.out 5 | 6 | if [ ! -f Indicator_Release_NBIs.csv ]; then 7 | wget --quiet https://raw.githubusercontent.com/fireeye/sunburst_countermeasures/main/indicator_release/Indicator_Release_NBIs.csv 8 | else 9 | >&2 echo "- Required file already exists: Indicator_Release_NBIs.csv" 10 | fi 11 | 12 | if [ ! -f uniq-hostnames.txt ]; then 13 | wget --quiet https://raw.githubusercontent.com/bambenek/research/main/sunburst/uniq-hostnames.txt 14 | else 15 | >&2 echo "- Required file already exists: uniq-hostnames.txt" 16 | fi 17 | 18 | if [ ! -x $mydir/flames_pub ]; then 19 | >&2 echo "- Binary not yet compiled - compiling" 20 | gcc -std=c99 -o flames_pub flames_pub.c 21 | else 22 | >&2 echo "- Binary found" 23 | fi 24 | 25 | if [ ! -x $mydir/flames_pub ]; then 26 | >&2 echo "- Binary still not found - aborting" 27 | exit 28 | fi 29 | 30 | # Get full list of strings. 31 | $mydir/flames_pub < uniq-hostnames.txt | grep domain > $flames_all_file 32 | >&2 echo "- Full list output to ${flames_all_file} - $(cat uniq-hostnames.txt | wc -l) lines" 33 | 34 | >&2 echo "- Strings that can be reassembled:" 35 | # Display string fragments in original order. 36 | for uid in $(cut -d , -f 3 Indicator_Release_NBIs.csv | $mydir/flames_pub | grep NextString | cut -d ' ' -f 4); do 37 | grep $uid $flames_all_file | sort -k 6 -n 38 | done 39 | echo "" 40 | 41 | #rm $flames_all_file 42 | 43 | -------------------------------------------------------------------------------- /flames_pub.c: -------------------------------------------------------------------------------- 1 | /* 2 | * ===================================================================================== 3 | * Filename: flames_pub.c 4 | * Description: Decodes SUNBURST requests and prints the results out. 5 | * Reads names from stdin 6 | * Created: 16.12.2020 22:07:30 7 | * Author: Igor Kuznetsov (igosha), 2igosha@gmail.com 8 | * ===================================================================================== 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | // Inverse for OrionImprovementBusinessLayer.Base64Encode 19 | bool Base32Decode(char* str, int len, uint8_t** strOut, int* lenOut){ 20 | const char text[] = "ph2eifo3n5utg1j8d94qrvbmk0sal76c"; 21 | if ( strlen(str) < len ) { 22 | return false; 23 | } 24 | 25 | *lenOut = len*5/8; 26 | *strOut = malloc(*lenOut + 1); 27 | memset(*strOut, 0, *lenOut + 1); 28 | if (*strOut == NULL){ 29 | return false; 30 | } 31 | for (int i = 0; i < len; i++){ 32 | char* ptr = strchr(text, str[i]); 33 | if ( ptr == NULL ) { 34 | free(*strOut); 35 | return false; 36 | } 37 | uint8_t b = ptr - text; 38 | for (int j = 0; j < 5; j++){ 39 | int bit_idx = i*5 + j; 40 | uint8_t bit = (b >> j) & 1; 41 | (*strOut)[bit_idx / 8] |= (bit << ( bit_idx % 8 ) ); 42 | } 43 | } 44 | return true; 45 | } 46 | 47 | // Inverse for OrionImprovementBusinessLayer.Base64Decode 48 | char* decode(char* str){ 49 | const char text[] = "rq3gsalt6u1iyfzop572d49bnx8cvmkewhj"; 50 | const char text2[] = "0_-."; 51 | 52 | if ( str[0] == '0' && str[1] == '0' ) { 53 | uint8_t* result; 54 | int resultLen; 55 | // Special case for non-ascii names 56 | if ( !Base32Decode(str + 2, strlen(str) - 2, &result, &resultLen) ) { 57 | return false; 58 | } 59 | return result; 60 | } 61 | int outCnt = 0; 62 | char* result = strdup(str); 63 | 64 | for (char* ptr = str ; *ptr != 0; ptr++){ 65 | const char* found = strchr(text2, *ptr); 66 | if ( found ) { 67 | ptr++; 68 | if ( *ptr == 0 ) { 69 | free(result); 70 | return NULL; 71 | } 72 | found = strchr(text, *ptr); 73 | if ( found == NULL ) { 74 | free(result); 75 | return NULL; 76 | } 77 | result[outCnt++] = text2[(found-text)%4]; 78 | } else { 79 | found = strchr(text, *ptr); 80 | if ( found == NULL ) { 81 | free(result); 82 | return NULL; 83 | } 84 | int idx = found - text; 85 | result[outCnt++] = text[(sizeof(text)-1+idx-4)%((int)sizeof(text)-1)]; 86 | } 87 | } 88 | result[outCnt]=0; 89 | return result; 90 | } 91 | 92 | bool DecodeSecureString(char* str, int len, uint8_t** strOut, int* lenOut){ 93 | if ( !Base32Decode(str, len, strOut, lenOut) ) { 94 | return false; 95 | } 96 | for (int i = 0; i < *lenOut-1; i++){ 97 | (*strOut)[i+1] ^= (*strOut)[0]; 98 | } 99 | *lenOut -= 1; 100 | memmove(*strOut, *strOut+1, *lenOut); 101 | return true; 102 | } 103 | 104 | // GetCurrentString: 105 | // GUID - 8 bytes, encrypted into 9, encoded by base32 into 15 bytes 106 | // + one char (seq num) 107 | // + encrypted dnstr 108 | // 109 | // GetPreviousString: 110 | // GUID - 8 bytes, encrypted into 9, encoded by base32 into 15 bytes 111 | // + one char (seq num) 112 | // + part of dnstr 113 | // 114 | bool DecodeCurrentString(char* str){ 115 | const char text[] = "ph2eifo3n5utg1j8d94qrvbmk0sal76c"; 116 | if ( strlen(str) <= 16 ) { 117 | return false; 118 | } 119 | uint8_t* uidBuf; 120 | int uidBufLen; 121 | if (!DecodeSecureString(str, 15, &uidBuf, &uidBufLen) ) { 122 | return false; 123 | } 124 | uint64_t uid = *(uint64_t*)(uidBuf); 125 | free(uidBuf); 126 | uidBuf = NULL; 127 | 128 | char c = str[15]; 129 | int val = 0; 130 | if ( c >= '0' && c <= '9' ) { 131 | val = c - '0'; 132 | } else if ( c >= 'a' && c <= 'z' ) { 133 | val = c - 87; 134 | } else { 135 | return false; 136 | } 137 | if ( val > 36 ) { 138 | printf("invalid val!\n"); 139 | return false; 140 | } 141 | int posval = -1; 142 | for (int i = 0; i < 36; i++){ 143 | if ( val == ( i + str[0] ) % 36 ) { 144 | if ( posval != -1 ) { 145 | return -1; 146 | } 147 | posval = i; 148 | } 149 | } 150 | if ( posval == -1 ) { 151 | return false; 152 | } 153 | val = posval; 154 | printf("domain name part UID (0x%" PRIX64 ") offset %i = ", uid, val); 155 | char* dn = decode(str+16); 156 | if ( dn == NULL ) { 157 | return false; 158 | } 159 | for (char* ptr = dn; *ptr; ptr++){ 160 | printf("%c", *ptr); 161 | } 162 | printf("\n"); 163 | return true; 164 | } 165 | 166 | // GetNextString/Ex: 167 | // encrypted base32 of guid + string hash (3), encrypted on xor 168 | bool DecodeNextString(char* str){ 169 | uint8_t* decoded; 170 | int decodedLen; 171 | if ( !DecodeSecureString(str, strlen(str), &decoded, &decodedLen) ) { 172 | return false; 173 | } 174 | if ( decodedLen < 11 ) { 175 | printf("decoded len mismatch, need at least 11\n"); 176 | free(decoded); 177 | return false; 178 | } 179 | for (int i = 0; i < 8; i++){ 180 | decoded[i] ^= decoded[8 + 2 - ( i % 2 )]; 181 | } 182 | uint64_t uid = *(uint64_t*)decoded; 183 | printf("NextString UID = 0x%" PRIX64 "\n", uid); 184 | free(decoded); 185 | return true; 186 | } 187 | 188 | int main(){ 189 | char buf[256]; 190 | while (fgets(buf, sizeof(buf), stdin)) { 191 | char* ptr; 192 | if ((ptr = strchr(buf, '\n')) ){ 193 | *ptr = 0; 194 | } 195 | if ((ptr = strchr(buf, '\r') )){ 196 | *ptr = 0; 197 | } 198 | if ((ptr = strchr(buf, '.') )){ 199 | *ptr = 0; 200 | } 201 | if ( !DecodeCurrentString(buf) ) { 202 | printf("bad str: %s\n", buf); 203 | } 204 | DecodeNextString(buf); 205 | } 206 | } 207 | 208 | 209 | -------------------------------------------------------------------------------- /flames_pub.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2igosha/sunburst_dga/1bf4d93287d0da26251655692106331a33d0abda/flames_pub.exe --------------------------------------------------------------------------------