Reversing Golang Developed Ransomware: SNAKE
Introduction
Snake Ransomware (or EKANS Ransomware) is a Golang ransomware which in the past has affected several companies such as Enel and Honda. The MD5 hashing of the analyzed sample is ED3C05BDE9F0EA0F1321355B03AC42D0. This sample in particular is obfuscated with Gobfuscate, an open source obfuscation project available on Github.
Let’s start by quickly summarizing the functionality of the malware:
- First, the sample checks the domain to which the infected host belongs to, before continuing execution
- Next, it checks whether the computer is a Backup Domain Controller or Primary Domain Controller, and if so will only drop the ransom note, rather than encrypting the machine
- SNAKE will then isolate the host machine from the network by leveraging the netsh tool
- The shadow copies on the system are deleted using WMI
- SNAKE attempts to terminate any running AV, EDR, and SIEM components
- Finally, local files on the system are encrypted
- For each file, a unique AES encryption key is generated, which is later encrypted with an RSA-2048 public key and stored within the encrypted file.
Let’s start reversing this sample with IDA!
Static Analysis
There are some differences of Go from other languages to keep in mind:
- Functions can return multiple values.
- Function parameters are passed on the stack.
- Strings are typically a sequence of bytes with a fixed length, that are not null terminated; the string is represented by a structure formed by a pointer to a byte array, and the length of the string.
- The constants are stored in one large buffer and sorted by length.
- Many stack manipulations are present within the binaries, that can make the analysis complex.
Obfuscation
Opening the sample with IDA we immediately notice that the function names are very obfuscated:
Performing a quick search, I found that the malware is obfuscated with Gobfuscate. This project performs obfuscation of several components: Global names, Package names, Struct methods, and Strings.
Each string in the binary is replaced by a function call. Each function contains two arrays that are xored to get the original string. When implementing the decryption function, keep in mind that there are different ways in which these arrays are passed to the function runtime.stringtoslicebyte – either via a variable, a pointer or a hardcoded value).
Analyzing the sample, you will usually find the call to a main function that contains a large number of other calls within it, where each subroutine performs decryption of only one string. Sometimes only the decryption operations are found within these subroutines, other times additional operations are performed on the decrypted string.
As mentioned, the string encryption is fairly basic, XORing the contents of two arrays together to retrieve the final string.
Due to the simplicity of the algorithm, we can develop a simple Python script utilising some regular expressions to locate and decrypt 90% of the encrypted strings within the binary! The script can be found at the end of this post.
startDecryptFunction = b"\x64\x8B\x0D\x14\x00\x00\x00"
sliceStr = b"(" + b"\x8D.....\x89.\$\x04\xC7\x44\$\b...." + b")"
xorLoop = b"\x0F\xB6<\)1\xFE(\x83|\x81)\xFD"
With the majority of the strings decrypted, let’s continue the analysis!
Check Environment
One of the first operations I perform when analyzing malware in GO is to jump to the main.init function.
The main.init is generated for each package by the compiler to initialise all other packages that this sample relies on, as well as the global variables; analysing this function is very important because it allows us to understand a large amount of the malware’s functionality and speed up subsequent analysis.
In the main.init function we can find, for example, references to encryption: AES, RSA, SHA1, X509. In addition, there are several functions for decryption of strings and function names.
Now we move on to analyze the main.main function. One of the first activities the malware performs is to check the environment before continuing with encryption.
The function CheckEnvironment starts by attempting to resolve the hostname mds.honda.com and compare the returned value with 172[.]108[.]71[.]153. This check is used to confirm that the infected machine is part of the correct domain. In fact, it is important to remember that this ransomware is deployed at the end of the infection chain by other loaders, and thus it is likely custom-built for the victim.
After the first environment check, the malware executes the API calls CoInitializeEx, CoInitializeSecurity and CoCreateInstance to instantiate an object of the SWbemLocator interface. Using the SWbemLocator object, SNAKE then invokes the method SWbemLocator::ConnectServer and obtains a pointer to an SWbemServices object. Finally, with this object, it will execute ExecQuery with the following query:
select DomainRole from Win32_ComputerSystem
In an attempt to determine whether the infected computer is a server or a workstation.
After making the query, the malware only continues execution if the DomainRole value is equal to or less than 3.
According to Microsoft documentation, the integers returned by the call correspond to different values:
VALUE | MEANING |
0 | Standalone Workstation |
1 | Member Workstation |
2 | Standalone Server |
3 | Member Server |
4 | Backup Domain Controller |
5 | Primary Domain Controller |
Therefore, the malware performs the infection only if the role obtained of the computer is Standalone Workstation, Member Workstation, Standalone Server, or Member Server.
If this check is successful, the mutex Global\EKANS is created, and presuming the mutex is created successfully, the sample continues executing.
If, on the other hand, the computer role is either a backup domain controller or primary domain controller, a ransom note is dropped to C:\Users\Public\Desktop, and files are not encrypted. Within the ransom note is an email on how to contact the threat actors, with the email used in this sample being [email protected].
When analyzing Go developed programs, two functions to pay attention to are NewLazyDll (essentially LoadLibrary), and NewProc (as you may have guessed, basically GetProcAddress). With the use of Gobfuscate to obfuscate this sample, the names of the libraries and API functions to be passed to the described functions. Pointers to the loaded libraries/functions are stored within DWORDs for later reference.
For example, we can see in the sample we have the function that performs the decryption of a function name before calling LazyDLL.NewProc:
A pointer to the function is saved in a DWORD, so that we can trace to see where the function is called within the binary.
Endpoint Isolation
After the function CheckEnvironment has finished, the strings “netsh advfirewall set allprofiles state on” and “netsh advfirewall set allprofiles firewallpolicy blockinbound,blockoutbound” are decrypted and executed via cmd.run. The first command enables Windows Firewall for all network profiles, while the second blocks all incoming and outgoing connections. This is fairly unusual behaviour for ransomware, which typically performs lateral movement across a network to infect additional machines.
Terminate Process And Services
Prior to encryption, the ransomware terminates a number of processes, to reduce the amount of interference with the encryption (for example any open file handles), as well as disable any running EDR/SIEM software on the machine.
Processes are terminated using syscall.OpenProcess and syscall.TerminateProcess calls. In order for SNAKE to retrieve the PIDs of the target processes, the usual calls of CreateToolhelp32Snapshot, Process32FirstW, and Process32NextW are performed.
In addition to terminating processes, the ransomware stops more than 200 services related to EDR, SIEM, AV, etc.
In order to terminate services, the following API function calls are made:
- OpenSCManagerA: gets a service control manager handle for subsequent calls.
- EnumServicesStatusEx: enumeration of services.
- OpenServiceW: gets a service handle for subsequent calls.
- QueryServiceStatusEx: check the status of services.
- ControlService: used to stop the service (flag SERVICE_CONTROL_STOP).
Shadow Copy Deletion
The ransomware executes the WMI query “SELECT * FROM Win32_ShadowCopy” to get the IDs of Shadow Copies and will always use WMI for deletion (remember that there are many ways to perform shadow copy deletion).
In addition to the Wbemscripting.SWbemLocator object, WbemScripting.SWbemNamedValueSet is also created.
For deletion, SNAKE uses the DeleteInstance method by passing the ID of previously obtained Shadow Copies.
Encryption Process
SNAKE first encrypts all the various files by initializing 8 go-routines (runtime.newproc), before beginning to rename the files.
The offset of the function that does the encryption is passed to runtime.newproc (OffsetStartEncryption).
Before beginning encryption of the file, it’s checked that it has not already been encrypted by checking for the presence of the string EKANS at the end of the file.
If the file hasn’t yet been encrypted and the files are among those to be encrypted (there is an allowlist and a denylist), encryption is initiated, which takes care of:
- Generating AES key for each file; this key is encrypted with an RSA public key in OAEP Mode.
- Encryption of file via AES in CTR mode, with Random Key (32 bytes) and Random IV (16 bytes).
- A random 5 character is appended to the file extension of encrypted files.
- Adds data to the end of the file: encrypted AES Key, IV and EKANS string.
After instantiating the CTR cipher with cipher.NewCTR, encryption is performed with the XORKeyStream method of that class.
The function reads 0x19000 bytes at a time and after encryption the file is rewritten using WriteAt.
After finishing the encryption, three more writes are performed on the file:
It’s easy to see that in the last one the string EKANS is written (which is used to determine if the file has already been encrypted), while it is much more complex to figure out what is written in the first two. As a result, let’s jump over to a debugger.
The first write adds the following to the file:
- The encrypted AES Key
- The random IV
- The path of encrypted file
The AES Key for each file is encrypted with a public RSA key. After decryption, the public key is parsed with pem.decode and x509.ParsePKCS1PublicKey.
The first parameter of the EncryptOAEP function must be the hash function, which in this case is sha1:
Various extensions, file and folders are excluded for file encryption, also using a regex.
Partially excluded Files:
Iconcache.db | Ntuser.dat | Desktop.ini |
Ntuser.ini | Usrclass.dat | Usrclass.dat.log1 |
Usrclass.dat.log2 | Bootmgr | Bootnxt |
Ntuser.dat.log1 | Ntuser.dat.log2 | Boot.ini |
ctfmon.exe | bootsect.bak | ntdlr |
Partially excluded extensions:
Exe | Dll | Sys |
Mui | Tmp | Lnk |
config | settingcontent-ms | Tlb |
Olb | Bfl | ico |
regtrans-ms | devicemetadata-ms | Bat |
Cmd | Ps1 |
Excluded Paths:
\ProgramData |
\Users\All Users |
\Temp\ |
\AppData\ |
\Boot |
\Local Settings |
\Recovery |
\Program Files |
\System Volume Information |
\$Recycle.Bin |
.+\\Microsoft\\(User Account Pictures|Windows\\(Explorer|Caches)|Device |
And that just about wraps up this post on the SNAKE Ransomware!
Decryption Script
#!/usr/bin/env python3
import re, struct, pefile, sys
pe = None
imageBase = None
def GetRVA(va):
return pe.get_offset_from_rva(va - imageBase)
def GetVA(raw):
return imageBase + pe.get_rva_from_offset(raw)
def main():
global pe, imageBase
filename = sys.argv[1]
with open(filename, 'rb') as sample:
data = bytearray(sample.read())
pe = pefile.PE(filename)
imageBase = pe.OPTIONAL_HEADER.ImageBase
startDecryptFunction = b"\x64\x8B\x0D\x14\x00\x00\x00"
sliceStr = b"(" + b"\x8D.....\x89.\$\x04\xC7\x44\$\b...." + b")"
xorLoop = b"\x0F\xB6<\)1\xFE(\x83|\x81)\xFD"
regex = startDecryptFunction + b".{10,100}" + sliceStr + b".{10,100}" + sliceStr + b".{10,100}" + xorLoop
pattern = re.compile(regex, re.MULTILINE|re.DOTALL)
found = pattern.finditer(bytes(data))
for m in found:
va = GetVA(m.start())
funcVA = GetVA(m.start())
str1VA = struct.unpack("<L", data[m.start(1) + 2 : m.start(1) + 2 + 4])[0]
str1Len = struct.unpack("<L", data[m.start(1) + 0xE : m.start(1) + 0xE + 4])[0]
str2VA = struct.unpack("<L", data[m.start(2) + 2 : m.start(2) + 2 + 4])[0]
str1RVA = GetRVA(str1VA)
str2RVA = GetRVA(str2VA)
decrypted = ""
for i in range(str1Len):
decrypted += chr ( ( data[str2RVA+i] ^ (data[str1RVA+i] + i * 2)) & 0xFF)
print(f"## (hex(funcVA)) - {decrypted}")
if __name__ == "__main__":
main()