Analysis of SunBurst (SolarWinds) embedded backdoor
Scala
Building blocks of NGSAST Policy for sunburst backdoor detection
Create the Code Property Graph
// Derived from https://github.com/Shadow0ps/solorigate_sample_source/
val sunburst_cpg = importCpg(“/XXX/sunburst.bin.zip")
Fetch all hardcoded literals in the trojanized DLL . The system needs to set a threshold to determine if there are one too many hashed literals in the list. To determine the encoding scheme follow the method that is dereferencing each literal to attempt a reversal (the scheme could be base64 encode, SHA-x, FNV-1a). In cases like these, attempt to brute force the decoding process by running this analysis in a separate thread (could be computationally intensive). Skipping the decoding process as FNV-1a was determined to be the encoding scheme (by FireEye and adjunct community) in the GetHash(..) function
val all_literals = cpg.literal.code.l.distinct.map(_.replaceAll("^\"|\"$", "")).toSet
There are many encoded literals in the code .. Let's attempt a base64 decode to detect literals. Other than keeping hardcoded values in hashed form, the malware has used DEFLATE compression to keep strings like WMI queries, registry entries and tokens.
decodeBase64()
For keeping the hardcoded values in the file like the list of processes, services, etc., the malware used a variant of the FNV-1a hashing algorithm by XORing the computed hash of the string with a hardcoded value at the end. In order to determine functions using variant Hashing techniques, ideintiy all code blocks using AND, XOR .. operators
Based on the functions using such operators as indicated above, identify fan-in (all other functions calling this function). This list can determine suspicious encode/decode operations
Spawning threads as a background task could be both benign and suspicious at the same time, especially if the instance spawned is dealing with obfuscated hashes
val all_methods_spawning_threads = cpg.method.fullName(".*Thread.*").caller.fullName.l.distinct
Affinity checks to determine name of current running process is always suspicious
val checking_process_name = cpg.call.name(Operators.equals).where(_.argument.order(2).isLiteral).where(_.argument.order(1).isCall.argument.code(".*Process.GetCurrentProcess.*ProcessName.*")).code.l
Object utility to convert a given string literal to FNV-1a
object FNV {
private val INIT32 = BigInt("811c9dc5", 16);
private val INIT64 = BigInt("cbf29ce484222325", 16);
private val PRIME32 = BigInt("01000193", 16);
private val PRIME64 = BigInt("100000001b3", 16);
private val MOD32 = BigInt("2").pow(32);
private val MOD64 = BigInt("2").pow(64);
private val MASK = 0xff
@inline private final def calc(prime: BigInt, mod: BigInt)(hash: BigInt, b: Byte): BigInt = ((hash * prime) % mod) ^ (b & MASK)
@inline private final def calcA(prime: BigInt, mod: BigInt)(hash: BigInt, b: Byte): BigInt = ((hash ^ (b & MASK)) * prime) % mod
/**
* Calculates 32bit FNV-1 hash
* @param data the data to be hashed
* @return a 32bit hash value
*/
@inline final def hash32(data: Array[Byte]): BigInt = data.foldLeft(INIT32)(calc(PRIME32, MOD32))
/**
* Calculates 32bit FNV-1a hash
* @param data the data to be hashed
* @return a 32bit hash value
*/
@inline final def hash32a(data: Array[Byte]): BigInt = data.foldLeft(INIT32)(calcA(PRIME32, MOD32))
/**
* Calculates 64bit FNV-1 hash
* @param data the data to be hashed
* @return a 64bit hash value
*/
@inline final def hash64(data: Array[Byte]): BigInt = data.foldLeft(INIT64)(calc(PRIME64, MOD64))
/**
* Calculates 64bit FNV-1a hash
* @param data the data to be hashed
* @return a 64bit hash value
*/
@inline final def hash64a(data: Array[Byte]): BigInt = data.foldLeft(INIT64)(calcA(PRIME64, MOD64))
}
Sunburst checks for the following running processes on host (to determine dormancy or execute)
val process_fnv_tuples = process_blacklist.map { i =>
val seed = 6605813339339102567L
val op = FNV.hash64a(i.getBytes) ^ seed
(i, op, op.toString + "L")
}
Verify if obfuscated hashes are encoded in literals
val obfuscated_process_checks = process_fnv_tuples.map(_._3).toSet.intersect(all_literals)
Sunburst checks for the following running services on host (to determine dormancy or execute)
val process_fnv_services = services_blacklist.values.flatten.map { i =>
val seed = 6605813339339102567L
val op = FNV.hash64a(i.getBytes) ^ seed
op.toString + "L"
}
Verify if obfuscated hashes are encoded in literals
val obfuscated_services_checks = process_fnv_services.toSet.intersect(all_literals)
Sunburst checks for the following running drivers on host (to determine dormancy or execute)