Malware Analysis Reverse Engineering

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:

Gobfuscate obfuscates almost all function names within the binary

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.

String Decryption Functions
String Decryption Functions

As mentioned, the string encryption is fairly basic, XORing the contents of two arrays together to retrieve the final string.

Loading the two arrays for decryption
XORing the arrays together to get the decrypted 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.

Initialisations performed within main.init function
API function decryption and loading through NewProc

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.

Call to CheckEnvironment within main.main

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.

Resolving hostname via DNS
Comparing resolved address to hardcoded address

After the first environment check, the malware executes the API calls CoInitializeExCoInitializeSecurity 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.

Decryption of object used to perform WMI query
Decryption of WMI query to retrieve DomainRole

After making the query, the malware only continues execution if the DomainRole value is equal to or less than 3.

Execution of WMI query and checking of result

According to Microsoft documentation, the integers returned by the call correspond to different values:

VALUEMEANING
0Standalone Workstation
1Member Workstation
2Standalone Server
3Member Server
4Backup Domain Controller
5Primary Domain Controller

Therefore, the malware performs the infection only if the role obtained of the computer is Standalone WorkstationMember WorkstationStandalone 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.

Execution flow depending on the result of the DomainRole

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 CarrolBidell@tutanota.com.

Ransom note

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:

API function decryption and loading via 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.

Cross references for API functions
Execution of previously loaded API function

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.

Decryption of netsh commands to alter the firewall
Execution of above commands through os.exec

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.

Decryption of target processes to be terminated

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.

Terminating processes with OpenProcess and TerminateProcess

In addition to terminating processes, the ransomware stops more than 200 services related to EDR, SIEM, AV, etc.

Decryption of target service names to be terminated

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).
Termination of services using OpenService and ControlService

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.

Decryption of WbemScripting.SWbemNamedValueSet
Decryption of WMI Query
Decryption of WMI namespace

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).

Initialisation of go-routines prior to file renaming function

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.

Checking if file is already encrypted

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.
Key generation, encryption, and metadata being added to file

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.

Generating the buffer to hold bytes read from the file
Encrypting buffer data and overwriting file

After finishing the encryption, three more writes are performed on the file:

Adding metadata to 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.

Observing metadata being written to end of file using 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.

Decryption of RSA public key
Parsing of the RSA key

The first parameter of the EncryptOAEP function must be the hash function, which in this case is sha1:

EncryptOAEP Function
Call to EncryptOAEP function

Various extensions, file and folders are excluded for file encryption, also using a regex.

Partially excluded Files:

Iconcache.dbNtuser.datDesktop.ini
Ntuser.iniUsrclass.datUsrclass.dat.log1
Usrclass.dat.log2BootmgrBootnxt
Ntuser.dat.log1Ntuser.dat.log2Boot.ini
ctfmon.exebootsect.bakntdlr

Partially excluded extensions:

ExeDllSys
MuiTmpLnk
configsettingcontent-msTlb
OlbBflico
regtrans-msdevicemetadata-msBat
CmdPs1 

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()

Author

Gabriele Orini

The Remastered
Beginner Malware Analysis Course

Pre-registration is now open

Don’t miss out! Add your email to get notified of course updates, and grab a 15% discount as well as 1-week early access!