Malware sandbox evasion in x64 assembly by checking ram size - Part 2

In the previous post, I explored a sandbox evasion technique that uses GetPhysicallyInstalledSystemMemory to check the size of the RAM of the machine. The idea behind this technique (MBC Technique ID: B0009.014) is that any value that is lower than 4GB may probably be a sandbox (to reduce costs). This information can then be used with other sandbox evasion techniques to confirm.

For part 2 of this series, I'll be talking about an alternative Windows API function called GlobalMemoryStatusEx. This function is as straightforward as the first one, but requires the passing of a pointer to a C struct. This is significant because I'll be converting a working C code to x64 assembly so we can fully understand how it works under the hood.

Using GlobalMemoryStatusEx

Here is an example of an implementation of GlobalMemoryStatusEx in C that we'll later be converting to x64 assembly.

#include <stdio.h>
#include <windows.h>

int main(void)
{
    MEMORYSTATUSEX statex;
    statex.dwLength = sizeof (statex);
    GlobalMemoryStatusEx (&statex);
    printf ("Memory size: %*I64d", 7, statex.ullTotalPhys/1024);
}

You will see that the first parameter for GlobalMemoryStatusEx is expecting a pointer to a MEMORYSTATUSEX object. We need to declare the memory location statex by putting it onto the stack. Before we can do that, however, we first need to know beforehand how much we would need to reserve.

Getting the size of the struct

Finding out the size of a structure in C is easy with the sizeof function. However, we can't really use this in assembly, so we have to determine it manually by adding up the sizes of each member of the struct.

Consider the example struct definition below:

struct TestStruct {
    char member1;
    int member2;
    float member3;
};

If we would look at this table containing the fundamental types and their sizes, we could determine the sizes of each member:

  • member1 is of type char which has a size of 1 byte
  • member2 is of type int which is 4 bytes
  • member3 is of type float which also is 4 bytes

Adding all of these sizes results in TestStruct having a total size of 9 bytes.

Now to apply the same computation to our MEMORYSTATUSEX struct. Here is the definition of the struct according to MSDN:

typedef struct _MEMORYSTATUSEX {
  DWORD     dwLength;
  DWORD     dwMemoryLoad;
  DWORDLONG ullTotalPhys;
  DWORDLONG ullAvailPhys;
  DWORDLONG ullTotalPageFile;
  DWORDLONG ullAvailPageFile;
  DWORDLONG ullTotalVirtual;
  DWORDLONG ullAvailVirtual;
  DWORDLONG ullAvailExtendedVirtual;
} MEMORYSTATUSEX, *LPMEMORYSTATUSEX;

The types that we have are DWORD and DWORDLONG (which is just Window's own version of unsigned long and unsigned int64):

  • DWORD or unsigned long has a size of 4 bytes
  • DWORDLONG or unsigned int64 has a size of 8 bytes

So adding the two DWORDs and seven DWORDLONGs results in MEMORYSTATUSEX having a total size of 64 bytes.

Initializing statex

Now that we know the total size, we can now reserve this amount of space on the stack.

    sub rsp, 0x40   ; Reserve space for struct on stack
                    ; MEMORYSTATUSEX's is 64 bytes (0x40) in size

Before we can call GlobalMemoryStatusEx, however, MSDN states that the dwLength member should be first set. And this can be done by assigning 64 bytes to the corresponding memory location on the stack.

    mov rax, 0x40   
    mov [rsp], rax  ; Assign 0x40 to dwLength
    lea rcx, [rsp]  ; Load the memory location of struct

With this we can finally call our function:

    sub rsp, 32     ; Reserve shadow space
    call    GlobalMemoryStatusEx
    add rsp, 32     ; Release shadow space

Using the result

If successful, the function GlobalMemoryStatusEx populates the memory location we passed to it, as shown below:

malware-sandbox-evasion-in-x64-assembly-by-checking-ram-size-part-2-01

The struct member ullTotalPhys now has the memory size that we need. And because our stack pointer still points to the beginning of the struct, we can get this value by adding an offset to rsp.

    mov rax, [rsp+0x8]  ; Retrive value of ullTotalPhys from stack

We offset by 0x8 because the first 8 bytes is assigned to dwLength and dwMemoryLoad (both at 4 bytes each).

Displaying the result

As seen above, the value returned by GlobalMemoryStatusEx is in bytes. To be consistent with our example from the previous post, we need to convert this value to kilobytes by dividing it by 1024.

    mov rcx, 1024
    xor rdx, rdx    ; Clear rdx; This is required before calling div
    div rcx         ; Divide by 1024 to convert to KB

The result of the above operation is saved to rax which we can then move to rdx so we can pass it as the second argument to printf.

    mov rdx, rax    ; Argument 2; Result of ullTotalPhys / 1024
    lea rcx, [msg_memory_size]  ; Argument 1; Format string
    sub rsp, 32     ; Reserve shadow space
    call    printf
    add rsp, 32     ; Release shadow space

With this, we can now finally display the result on the console:

malware-sandbox-evasion-in-x64-assembly-by-checking-ram-size-part-2-02

Here is the full source code for reference:

    bits 64
    default rel

segment .data
    msg_memory_size db  "Memory size: %lld", 0xd, 0xa, 0

segment .text
    global main
    extern ExitProcess
    extern GlobalMemoryStatusEx
    extern printf

main:
    push    rbp
    mov     rbp, rsp

    sub rsp, 0x40   ; Reserve space for struct on stack
                    ; MEMORYSTATUSEX's is 64 bytes (0x40) in size 

    mov rax, 0x40   
    mov [rsp], rax  ; Assign 0x40 to dwLength
    lea rcx, [rsp]  ; Load the memory location of struct

    sub rsp, 32     ; Reserve shadow space
    call    GlobalMemoryStatusEx
    add rsp, 32     ; Release shadow space

    mov rax, [rsp+0x8]  ; Retrive value of ullTotalPhys from stack
    mov rcx, 1024
    xor     rdx, rdx    ; Clear rdx; This is required before calling div
    div rcx     ; Divide by 1024 to convert to KB

    mov rdx, rax    ; Argument 2; Result of ullTotalPhys / 1024
    lea rcx, [msg_memory_size]  ; Argument 1; Format string
    sub rsp, 32     ; Reserve shadow space
    call    printf
    add rsp, 32     ; Release shadow space

    add rsp, 0x40   ; Release space of struct from stack

    xor     rax, rax
    call    ExitProcess

Conclusion

Over the past two blog posts, we've learned how to use GlobalMemoryStatusEx and GetPhysicallyInstalledSystemMemory to determine the size of the RAM of a machine. We've also learned about using the stack to pass arguments to functions using x64 assembly.

In future posts I plan to continue exploring malware behavior and techniques and at the same time teach x64 assembly so that we can both improve when writing and reverse engineering malware.

Until then, you can view the C and Assembly code along with the build scripts for this evasion technique on this repository here.

Feel free to reach out to me on Twitter or LinkedIn for any questions or comments.

Malware sandbox evasion in x64 assembly by checking ram size - Part 1

During my malware sandbox evasion research, I stumbled upon the Unprotect Project website. It is a community-contributed repository of evasion techniques used by malware. I saw that the the Checking Memory Size technique doesn't have a example snippet yet so I figured this would be a good first contribution to the project.

malware-sandbox-evasion-in-x64-assembly-by-checking-ram-size-part-1-03

What to expect

In this blog post I'll be making a code snippet that showcases how to get the size of a computer's RAM in C. I will then convert this code into x64 assembly, mostly for me to practice writing in it, but also so that we can understand it better.

Checking the memory

The idea behind this evasion technique is simple. Most modern user machines will have at least around 4GB of RAM. Anything lower than that can be an indication that the machine is probably a sandbox (To save costs). While it's not exactly fool-proof, it can be used with other techniques to have a better idea of the machine.

There are two available APIs to get the memory size of a computer on Windows: GetPhysicallyInstalledSystemMemory and GlobalMemoryStatusEx. The former lists the physically installed RAM from the BIOS, while the latter lists the amount available for the operating system to use. Note that the values returned from these two functions will be different but from my tests the difference is only a few hundreds of bytes. Any of these two we can use for our purpose.

Using GetPhysicallyInstalledSystemMemory

Calling GetPhysicallyInstalledSystemMemory in C is simple:

#include <stdio.h>
#include <windows.h>

int main(void)
{
    unsigned long long memory_size = 0;
    GetPhysicallyInstalledSystemMemory(&memory_size);
    printf("Memory size: %lld\n", memory_size);
}

Running the above code shows the following result:

malware-sandbox-evasion-in-x64-assembly-by-checking-ram-size-part-1-01

And this is what my memory settings is set to on VMWare:

malware-sandbox-evasion-in-x64-assembly-by-checking-ram-size-part-1-02

You'll immediately notice that the returned value is not exactly the same as the memory settings. I, too, wondered about this so I did a couple of tests.

Investigating the results

What I found was that the values that are returned by the GetPhysicallyInstalledSystemMemory in hex format always have the last 3 bytes set to zero. To test this I changed the VM settings and noted the values returned by the program. Here's a table of the results:

VM Settings Returned Value In Hex
2000MB 2048000 0x1F4000
3324MB 3403776 0x33F000
4096MB 4194304 0x400000
4338MB 4493312 0x449000
5675MB 5816320 0x58C000

Before you think that this is a VM thing, here is the same behavior with a Windows system that is not on a VM:

Installed RAM Returned Value In Hex
16384MB 16777216 0x1000000

According to the MSDN docs, the value returned is taken from the SMBIOS firmware tables. I tried to dig further and found the SMBIOS standard manual and saw that the value in the memory size field is returned in MB. This still doesn't explain why the last 3 digits are always zero though. I'm guessing that the API just truncates the last 3 values and saves the higher bytes?

EDIT(2022-08-15): Twitter user @Endeavxor pointed out that the returned value of "GetPhysicallyInstalledSystemMemory" is expressed in kibibytes instead of kilobytes. This means the result 4194304 when divided by 1024 is 4096 and is exactly the Memory value set in the VM settings. This means the value returned by the function is correct. It's so simple and I missed it!

Before we get hopelessly trapped in the rabbit hole that is OS internals, let's continue by converting our code above to x64 assembly.

Converting to x64 Assembly

Before we can call the GetPhysicallyInstalledSystemMemory function, we first need to reserve space on the stack that will serve as the memory_size local variable. This is where the result of the function will be placed.

    xor rax, rax    ; Clear rax
    push rax        ; Push rax to the stack
    lea rcx, [rsp]  ; Argument 1; Load the memory location of memory_size to rcx

We then call the GetPhysicallyInstalledSystemMemory function making sure that we reserve and release the shadow space.

    sub rsp, 32         ; Reserve shadow space
    call    GetPhysicallyInstalledSystemMemory
    add rsp, 32         ; Release shadow space

Aside: Shadow space

The concept of "Shadow Space" is important in x64 assembly. I've already discussed it briefly in a previous post but you can read up more about it here and then here.

The result on whether GetPhysicallyInstalledSystemMemory succeeded or not is placed in the ax register. It's good practice to add code to handle if a failure occurs, but we won't be bothering with that for our example.

What we are interested in is the value placed in the memory location pointed to by memory_size. We can confirm this by checking the value on the stack, as shown below where 58C000h converts to 5816320 which is roughly near the 5.5 GB setting we have set in VMWare.

malware-sandbox-evasion-in-x64-assembly-by-checking-ram-size-part-1-04

A much easier way to confirm is that we can also use the printf function to display the value of memory_size on the console. But before we can do that we first need to declare the format string so we can pass it later as the first argument.

segment .data
    msg_memory_size db  "Memory size: %lld", 0xd, 0xa, 0

We then call printf making sure we load the correct argument data to the respective registers.

    mov rdx, [rsp]             ; Argument 2; Result of GetPhysicallyInstalledSystemMemory
    lea rcx, [msg_memory_size] ; Argument 1; Format string
    sub rsp, 32                ; Reserve shadow space
    call    printf
    add rsp, 32                ; Release shadow space

Running that we can now display the value of the memory.

Here's the full assembly code:

    bits 64
    default rel

segment .data
    msg_memory_size db  "Memory size: %lld", 0xd, 0xa, 0

segment .text
    global main
    extern ExitProcess
    extern GetPhysicallyInstalledSystemMemory
    extern printf

main:
    push    rbp
    mov     rbp, rsp

    xor rax, rax    ; Clear rax
    push    rax     ; Push RAX to the stack
    lea rcx, [rsp]  ; Argument 1; Load the memory location of memory_size to rcx

    sub rsp, 32     ; Reserve shadow space
    call    GetPhysicallyInstalledSystemMemory
    add rsp, 32     ; Release shadow space

    mov rdx, [rsp]  ; Argument 2; Result of GetPhysicallyInstalledSystemMemory
    lea rcx, [msg_memory_size]  ; Argument 1; Format string
    sub rsp, 32     ; Reserve shadow space
    call    printf
    add rsp, 32     ; Release shadow space

    add rsp, 0x8    ; Release the space of memory_size local variable
    xor     rax, rax
    call    ExitProcess

Up next

In the next blog post I'll be showing how to get the size RAM size via an alternative method using GlobalMemoryStatusEx. The code is also straightforward but we'll be exploring how it's values differ from GetPhysicallyInstalledSystemMemory and also how to deal with C structures on the stack in x64 assembly.

For now, you can view the C and Assembly code along with the build scripts on the repository here.

Feel free to reach out to me on Twitter or LinkedIn for any questions or comments.

Talking about Mitre's Malware Behavior Catalog

I gave a 10-minute lightning talk at the recently concluded Blackhat Middle East & Africa community meetup. The topic is about Mitre's Malware Behavior Catalog (MBC) framework and the existing tools for it. My reason for selecting this topic is because I feel that more people should know about Mitre's not-so-well-known project.

talking-about-mitres-malware-behavior-catalog-01

A brief overview

MBC is a framework made by Mitre, similar to ATT&CK, but focuses on malware. It lists down the common objectives and behaviors commonly seen in malware. The purpose is to have standardize reporting so that everyone would use the same definitions when writing and talking about malware. This also aids with analysis and correlation with other tools.

It has it's own matrix with malware objectives as headers for columns and an entry for each behavior. Each behavior then has a list of methods that explains how that behavior is achieved, example of malware that uses it, and also IDs of ATT&CK techniques related to the behavior.

talking-about-mitres-malware-behavior-catalog-02

The tools

There are a number of existing tools that make use of MBC. Flare's Capa lists down MBC along with the related ATT&CK techniques and there's also a repository of MBC community rules for the Cuckoo Sandox.

I find MBC to have a lot of potential so I decided to contribute by making my own tool called MBCScan. It's a simple tool that uses Capa which scans a supplied file and lists the MBC behaviors and objectives associated with it. It also allows you to explore the related information and relationships directly from the command line.

talking-about-mitres-malware-behavior-catalog-03

The future

MBC has been around for a number of years already but it still has not risen in popularity. In spite of this, it's still being continuously updated. I hope that by sharing and talking about it it'll help spread awareness and, hopefully, get some adoption.

You can find more information about the project via this video presentation from Mitre. The Github project is here. And the slides are available here.