Nikhil "Kaido" Hegde

M&M: Malware and Musings

View on GitHub

Getting Rusty and Stringy with Luna Ransomware

Metadata

VirusTotal Sample

Table of Contents

Family Introduction

The Luna ransomware appeared in July 2022. Unlike its competitors, this threat targeted VMware ESXi instances from the day it started operating.

Rust Strings

In my experience as a malware analyst, I’ve been used to seeing ASCII and null-terminated strings in binaries. I was content writing IDAPython scripts where I created strings by searching for ASCII and null characters. And one fine day, I had a Rust binary on my plate which broke my scripts. I interviewed the Rust God about strings. Here’s how it went:

Rust Strings
Fig. 1: Rust Strings

String Slice: &str

String slice is the term for &str type of strings. These kinds of strings may exist in the binary or on the stack or heap. They always reference UTF-8 characters and are immutable. Let’s consider this simple Rust program:

fn main() {
    let str1: &str = "Hello World!\n"; 
}

Fig. 2 shows a snap of the disassembly as seen in IDA Home 7.7:

String Slice
Fig. 2: String Slice

String slices are essentially a data structure containing the address of the slice and its length. Such structures are also called fat pointers because they contain extra data besides just the memory address. Consider the following Rust program which prints the size (in bytes) of the &str type:

use std::mem::size_of;

fn main() {
    println!("A &str size in bytes: {}", size_of::<&str>());
}

On execution, it prints:

A &str size in bytes: 16

My system architecture is x64, so the size of &str, a fat pointer, is 16 bytes. The first 8 bytes is the memory address of the actual string literal and the next 8 bytes represents the length of that string literal. The following structure represents a string slice:

struct string_slice
{
  _QWORD val;
  _QWORD len;
};

IDA detects the above structure as core::fmt::ArgumentV1 and is defined as:

struct core::fmt::ArgumentV1
{
  core::fmt::_extern_0}::Opaque *value;
  core::result::Result<(),core::fmt::Error> (*formatter)(core::fmt::_extern_0}::Opaque *, core::fmt::Formatter *);
};

Although IDA’s structure is of the correct size (16 bytes), it is not particularly readable. So, I replaced it with my structure definition for better readability. Fig. 3 shows it in action.

String Slice IDA Structure
Fig. 3: String Slice IDA Structure

String

The next string type in Rust is String. These kinds of strings are allocated only on the heap and they are mutable.

String is also a data structure. It contains the address of the slice, its length on the heap and also the capacity of the heap region. Consider the following Rust program which prints the size (in bytes) of the String type:

use std::mem::size_of;

fn main() {
    println!("A String size in bytes: {}", size_of::<String>());
}

On execution, it prints:

A String size in bytes: 24

My system architecture is x64, so the size of String is 24 bytes. The first 8 bytes is the memory address of the string slice; the next 8 bytes represents the length of that string literal and the last 8 bytes is the capacity of the memory region in the heap. The capacity signifies the maximum number of bytes that the string can hold. If a longer string is required, then reallocation occurs on the heap. The following structure represents a String:

struct String
{
  _QWORD val;
  _QWORD len;
  _QWORD cap;
};

For example, a String may be allocated on the heap having the following structure field values:

val = "Hello!"
len = 6
cap = 10

IDA detects the above structure as alloc::string::String and is defined as:

struct alloc::string::String
{
  alloc::vec::Vec<u8,alloc::alloc::Global> vec;
};

Let’s consider this simple Rust program:

fn main() {
    let str1: String = String::from("Hello World! 🙏\n");
}

Fig. 4 shows a snap of the disassembly as seen in IDA Home 7.7. Here, v1 is the String variable.

String IDA Structure
Fig. 4: String IDA Structure

Fig 5. shows a snap of the UTF-8 encoding of the string literal:

String UTF-8 Encoding
Fig. 5: String UTF-8 Encoding

Rust Strings Print

It can be seen in Fig. 4 that there is no null character after the Hello World! 🙏\n string. This can make reading strings in IDA decompilation difficult as seen in Fig. 2 where the next string has polluted the decompilation. I wrote an IDAPython script which prints Unicode strings found in a Rust-based binary. I’ve been unable to find an IDAPython function which can create UTF-8 strings.

Luna Strings

Using the IDAPython script, I found interesting strings.

IDAPython Script Output
Fig. 6: String UTF-8 Encoding
Luna
.ini
.exe
.dll
.lnk
Error while writing encrypted data to: 
Error while writing public key to: 
Error while writing extension to: 
Error while renaming file: 
W1dIQVQgSEFQUEVORUQ/XQ0KDQpBbGwgeW91ciBmaWxlcyB3ZXJlIG1vdmVkIHRvIHNlY3VyZSBzdG9yYWdlLg0KTm9ib2R5IGNhbiBoZWxwIHlvdSwgZXhjZXB0IHVzLg0KV2UgaGF2ZSBwcml2YXRlIGtleSwgd2UgaGF2ZSB5b3VyIGJsYWNrIHNoaXQuDQpXZSBhcmUgc3Ryb25nbHkgYWR2aWNlIHlvdSB0byBiZSBpbnRlcmVzdGVkIGluIHNhZmV0eSBvZiB5b3VyIGZpbGVzLCBhcyB3ZSBjYW4gc2hvdyB5b3VyIHJlYWwgZmFjZS4NCg0KW1dIQVQgRE8gV0UgTkVFRD9dDQoNCkFkbWlzc2lvbiwgcmVzcGVjdCBhbmQgbW9uZXkuDQpZb3VyIGluZm9ybWF0aW9uIGNvc3RzIG1vbmV5Lg0KDQpbV0hPIEFSRSBXRT9dDQpBIGxpdHRsZSB0ZWFtIG9mIHBlb3BsZSB3aG8gY2FuIHNob3cgeW91ciBwcm9ibGVtcy4NCg0KW0hPVyBUTyBSRUFDSCBBTiBBR1JFRU1FTlQgV0lUSCBZT1U/XQ0KDQpTZW5kIHVzIGEgbWVzc2FnZSB3aXRoIHRob3NlIGUtbWFpbHM6DQoJZ2l2ZWZpc2h0b2FtYW42NjZAcHJvdG9ubWFpbC5jb20NCglnaXZlaG9va3RvYW1hbjY2NkBwcm90b25tYWlsLmNvbQ0KDQogICA
Error while writing note
AES-NI not supported on this architecture. If you are using the MSVC toolchain, this is because the AES-NI method's have not been ported, yet
Invalid AES key size.
host unreachable
connection reset
/proc/self/exe
openserver
windows
program files
recycle.bin
programdata
appdata
all users
Encrypting file:
How to use:
 -file /home/user/Desktop/file.txt (Encrypts file.txt in /home/user/Desktop directory)
 -dir /home/user/Desktop/ (Encrypts /home/user/Desktop/ directory)

The base64-encoded string decodes to the ransom note:

[WHAT HAPPENED?]

All your files were moved to secure storage.
Nobody can help you, except us.
We have private key, we have your black shit.
We are strongly advice you to be interested in safety of your files, as we can show your real face.

[WHAT DO WE NEED?]

Admission, respect and money.
Your information costs money.

[WHO ARE WE?]
A little team of people who can show your problems.

[HOW TO REACH AN AGREEMENT WITH YOU?]

Send us a message with those e-mails:
	givefishtoaman666@protonmail.com
	givehooktoaman666@protonmail.com

IDA Land

When analyzing Rust binaries, there are some notes to keep in mind:

The previous IDAPython script comes in handy to identify points from where you can start analysis. I could navigate to the string location in the .rodata segment, cross-reference to the source which loads that string and then analyze that piece of code rather than starting at the top. I started my analysis with the code that references the base64-encoded string of the ransom note. I hoped this would position me in the neighborhood of the code that does the encryption.

Writing Ransom Note

As mentioned before, the binary contains the base64-encoded form of the ransom note. It decodes it and then writes it into a file named readme-Luna.txt.

Luna Ransom Note
Fig. 7: Luna Ransom Note

Skips Files and Directories

Luna doesn’t encrypt files which have:

Luna doesn’t encrypt files under directories which contain one of the following substrings:

Encryption Scheme

Luna employs an encryption scheme that is commonly found in the ransomware world. It leverages both asymmetric and symmetric cryptography, i.e., the key for the symmetric cryptography is derived is from asymmetric cryptography. It uses curve25519-dalek package for Elliptic-Curve Cryptography (ECC) and crypto::aes module for AES-256 CTR-mode cryptography.

Luna’s encryption scheme can be summarized as follows:

Both the shared secret and the generated public key are zero’d in memory to prevent data leak. As I was writing this, I remembered Javier Yuste’s Avaddon ransomware decryption tool which relied on key information being available in memory. Perhaps, zero’ing key information in memory is Luna’s safeguard against such decryption tools.

File Encryption

Luna encrypts 50,000 bytes of plaintext file contents at a time. Since AES is in CTR mode, i.e., a stream cipher, the output ciphertext size is equal to the input plaintext size.

For the threat actor’s decryption tool to work, the ransomware binary has to store encryption-related information in the encrypted file. In this case, the threat actor would need two points of information per encrypted file:

Each encrypted file is given the extension, .Luna.

Peculiarities

Capability Peculiarities

When I hear of ransomware targeting VMware ESXi, I usually come across capability in the binary to shut down running VMs. This helps in clean encryption of files. However, Luna doesn’t seem to contain any such capability which may result in encrypted files being corrupted and incapable of being recovered.

Execution Peculiarities

I was wrapping up this article when I noticed a few peculiarities in Luna’s execution.

Execution Peculiarities
Fig. 8: Execution Peculiarities

Summary

In this article, we looked at a sample of Luna ransomware.

In summary, Luna is the typical ransomware but with bugs and no optimizations.

References