Malware Family Analysis • Windows

Reversing a Packed AutoIt Malware Sample: Script Decompilation & ASLR Analysis

Decompile an obfuscated AutoIt script, analyze runtime RWX memory allocations in WinDbg, and isolate injected shellcode bypassing ASLR.

Executive Summary

This analysis focused on reversing a packed AutoIt malware sample that attempted to hide its real behavior using anti-debugging checks and runtime shellcode execution. During debugging in WinDbg, the malware was observed allocating executable RWX memory regions through VirtualAlloc(), which were later used to load shellcode into memory.

After bypassing the anti-debugging protection, the unpacked payload was dumped from memory using Process Hacker and analyzed further in Ghidra. The shellcode used dynamic API resolution, direct PEB access, and several low-level Windows APIs to avoid detection and make reverse engineering more difficult.

Overall, the sample demonstrates a typical multi-stage malware execution flow where an AutoIt wrapper is used to unpack and execute the real payload directly in memory.

Meta data

Libraries

DLL NameSuspiciousLoad TypeImportsDescription
WSOCK32.dllxImplicit23Windows Socket 32-Bit Library
VERSION.dll-Implicit3Version Checking and File Installation Library
WINMM.dllxImplicit3Windows Management Library
COMCTL32.dll-Implicit11Common Controls Library
MPR.dllxImplicit4Multiple Provider Router Library
WININET.dllxImplicit14Internet Extensions for Win32 Library
PSAPI.DLLxImplicit1DHCP Server API Stub Library
IPHLPAPI.DLLxImplicit3IP Helper API
USERENV.dll-Implicit4User Environment Library
UxTheme.dll-Implicit1Microsoft UxTheme Library
KERNEL32.dll-Implicit164Windows NT BASE API Client
USER32.dll-Implicit160Multi-User Windows USER API Client Library
GDI32.dll-Implicit35GDI Client Library
COMDLG32.dll-Implicit2Common Dialogs Library
ADVAPI32.dll-Implicit33Advanced Windows 32 Base API
SHELL32.dll-Implicit15Windows Shell Library
ole32.dll-Implicit22Microsoft OLE for Windows
OLEAUT32.dll-Implicit29oleaut32 library

Sections

Property.text.rdata.data.rsrc.reloc
Section Name.text.rdata.data.rsrc.reloc
Unknownn/an/an/a
Entropy6.6766.779
File Ratio (99.92%)47.75%15.52%1.72%32.52%2.40%
Raw Address (Begin)0x000004000x0008E2000x000BC4000x000C16000x00122000
Raw Address (End)0x0008E2000x000BC4000x000C16000x001220000x00129200
Raw Size (1216000 bytes)0x0008DE00 (581120 bytes)0x0002E200 (188928 bytes)0x00005200 (20992 bytes)0x00060A00 (395776 bytes)0x00007200 (29184 bytes)
Virtual Address (Begin)0x000010000x0008F0000x000BE0000x000C70000x00128000
Virtual Address (End)0x0008ECC40x000BD10E0x000C6F740x001279080x0012F11C
Virtual Size (1230698 bytes)0x0008DCC4 (580804 bytes)0x0002E10E (188686 bytes)0x00008F74 (36724 bytes)0x00060908 (395528 bytes)0x0000711C (28956 bytes)

DIE

Screenshot 2026-05-23 at 9.24.39 PM.png
Figure: Screenshot 2026-05-23 at 9.24.39 PM.png Click to zoom ↗

Entropy

Screenshot 2026-05-23 at 9.25.25 PM.png
Figure: Screenshot 2026-05-23 at 9.25.25 PM.png Click to zoom ↗

Description

The sample analyzed so far is a PE32 Win32 executable with a file size of approximately 1.16 MB. Static analysis identifies the binary as being compiled with Microsoft Visual C++ and containing an AutoIt 3.x library. The executable imports multiple Windows networking and system-related libraries such as WSOCK32.dll, WININET.dll, IPHLPAPI.dll, ADVAPI32.dll, and SHELL32.dll, which may indicate capabilities related to network communication, system interaction, and Windows API usage.

Initial PE section analysis shows standard sections including .text, .rdata, .data, .rsrc, and .reloc. Entropy analysis performed using Detect It Easy (DIE) indicates that the .text, .rsrc, and .reloc sections are flagged as packed or compressed due to relatively high entropy values, especially the .rsrc section with an entropy of 7.90. This may suggest the presence of obfuscated, compressed, or embedded data within the executable. At this stage, the analysis is limited to static metadata, import analysis, and PE section inspection; behavioral and dynamic analysis have not yet been performed.

CategoryAnalysis Summary
File TypePE32 Win32 executable
File SizeApproximately 1.16 MB
Compiler / FrameworkCompiled using Microsoft Visual C++ with AutoIt 3.x library detected
Imported LibrariesIncludes networking and system-related DLLs such as WSOCK32.dll, WININET.dll, IPHLPAPI.dll, ADVAPI32.dll, and SHELL32.dll
Possible CapabilitiesMay support network communication, Windows API interaction, and system-level operations
PE Sections Identified.text, .rdata, .data, .rsrc, .reloc
Entropy Analysis.text, .rsrc, and .reloc sections show relatively high entropy values
Packed / Obfuscated SectionsDetect It Easy (DIE) flagged .text, .rsrc, and .reloc sections as packed
Notable Observation.rsrc section has very high entropy (7.90), which may indicate compressed, encrypted, or embedded data

GHIDRA Analysis

Entry point

Screenshot 2026-05-23 at 9.32.05 PM.png
Figure: Screenshot 2026-05-23 at 9.32.05 PM.png Click to zoom ↗

The executable entry point begins at address 0x00427DCD. Upon execution, the binary first calls ___security_init_cookie(), which is a standard Microsoft Visual C++ runtime function responsible for initializing security cookies used for stack protection and buffer overflow mitigation. After this initialization step, execution immediately jumps to the function FUN_00427C56. Since the entry point itself only performs runtime setup and transfers control, the actual program logic is likely located inside FUN_00427C56 or within functions called from it. To identify the real main() or WinMain() function, the analysis should continue by tracing execution flow from FUN_00427C56, as Microsoft Visual C++ compiled binaries commonly route execution through CRT (C Runtime) startup routines before reaching the main application logic.

Finding MAIN

After following execution into FUN_00427C56, scrolling further down reveals an important pattern commonly used to identify the real program entry logic in Microsoft Visual C++ binaries:

Screenshot 2026-05-23 at 9.37.37 PM.png
Figure: Screenshot 2026-05-23 at 9.37.37 PM.png Click to zoom ↗
CPP
LAB_00427d40
00427d40  CALL    __wwincmdln
00427d45  PUSH    ESI
00427d46  PUSH    EAX
00427d47  PUSH    0x0
00427d49  PUSH    IMAGE_DOS_HEADER_00400000
00427d4e  CALL    FUN_004047d0

The first instruction calls __wwincmdln(), which retrieves the command-line arguments passed to the program. The returned value is stored in the EAX register.

Right after that, the binary prepares several arguments using PUSH instructions before calling FUN_004047D0. These pushed values closely resemble the standard parameters normally passed to a program’s main() function.

▪PUSH ESI likely contains the environment pointer (envp) or other runtime startup data prepared earlier by the CRT.
▪PUSH EAX pushes the command-line pointer returned by __wwincmdln(), which corresponds to the program arguments (argv).
▪PUSH 0x0 acts as a placeholder value and is commonly associated with argument count (argc) or initialization-related data depending on the compiler’s startup routine.
▪PUSH IMAGE_DOS_HEADER_00400000 pushes the executable’s image base address (0x00400000), which is commonly used as the application instance handle (HINSTANCE) in Windows applications.

This sequence is important because these grouped PUSH instructions are commonly seen when CRT startup code prepares arguments before transferring execution to the real application logic. By following the call into FUN_004047D0, we move closer to identifying the actual main() or WinMain() function where the program’s core behavior begins.

THREE pushes

InstructionParameterDescription
PUSH ESIenvpPushes environment-related data.
PUSH EAXargvPushes the command-line arguments pointer.
PUSH 0x0argcPushes a value related to argument count/startup data.

FUN004047D0

FUN_004047D0 appears to be a startup initialization routine rather than the actual main() function. The function performs several runtime setup operations such as initializing global variables, configuring memory allocation handlers, checking whether Windows themes are enabled using IsThemeActive(), and retrieving system configuration settings through SystemParametersInfoW(). It also processes the program’s command-line arguments via FUN_00403B3A(param_3). Overall, this function mainly prepares the runtime environment and application state before execution continues deeper into the program’s core logic.

C
// Possible startup / initialization wrapper function
// Likely executed before the real main application logic

undefined4 FUN_004047d0(undefined4 param_1, undefined4 param_2, wchar_t *param_3)

{
    undefined4 uVar1;

    // Local structures used during initialization
    int *local_28[2];
    undefined4 local_20;
    undefined4 local_1c;

    int local_18[2];
    undefined4 local_10;
    undefined4 local_c;

    // Check a global startup flag
    // If the flag is not enabled, return immediately
    if (*(char *)(DAT_004c6428 + 0x1d) == '\0') {

        uVar1 = 1;
    }

    else {

        // Store startup parameter globally
        DAT_004c5278 = param_1;

        // Initialize global state variables
        DAT_004c527c = 0;
        DAT_004c5270 = 0;

        // Initialize local structures
        local_28[0] = (int *)0x0;
        local_20 = 0;
        local_1c = 1;

        local_18[0] = 0;
        local_10 = 0;
        local_c = 1;

        // Perform memory/object initialization
        FUN_004098c0((int *)local_28);

        // Reassign structure pointer
        local_28[0] = local_18;

        // Update structure state/configuration
        local_1c = 6;

        // Check whether Windows themes are enabled
        DAT_004c52b0 = IsThemeActive();

        // Configure C++ runtime memory allocation handler
        _set_new_handler((_func_int_uint *)&LAB_00457c74);

        // Enable new-mode memory behavior
        _set_new_mode(1);

        // Additional runtime/environment setup
        FUN_004048fd(DAT_004c642c);

        // Process command-line arguments passed to the program
        FUN_00403b3a(param_3);

        // Retrieve Windows system/UI configuration settings
        SystemParametersInfoW(
            0x2001,
            0,
            *(PVOID *)(DAT_004c642c + 4),
            2
        );

        // Retrieve final status/result value
        uVar1 = DAT_004c527c;

        // Cleanup initialized structures
        FUN_004098c0(local_18);
        FUN_004098c0((int *)local_28);
    }

    // Return initialization result
    return uVar1;
}

FUN00403B3A

FUN_00403B3A appears to be one of the main execution functions of the sample. The function starts by retrieving the current working directory and processing the command-line arguments passed to the program. It then performs an anti-debugging check using IsDebuggerPresent(). If a debugger is detected, the program displays a message saying "This is a third-party compiled AutoIt script." and immediately stops execution, which is a common anti-analysis technique.

The function also performs several runtime and environment checks, initializes internal data structures, resolves file paths, and restores the working directory when needed. One important behavior is the use of ShellExecuteW() with the "runas" operation, which attempts to relaunch the executable with administrator privileges through UAC elevation. Depending on certain conditions and internal flags, the function then transfers execution into other routines such as FUN_00403A46() and FUN_004039D5(), which likely contain the main functionality of the program. Overall, this function acts as a controller that handles anti-debugging checks, environment setup, privilege escalation attempts, and execution flow before the core logic begins.

C
/* WARNING: Function: __alloca_probe replaced with injection: alloca_probe */

/*
    FUN_00403B3A

    This function appears to be a major execution/control routine.
    It performs:
    - Environment setup
    - Command-line processing
    - Anti-debugging checks
    - Path handling
    - Privilege elevation attempts
    - Transfers execution into deeper program logic
*/

void FUN_00403b3a(wchar_t *param_1)

{
    bool bVar1;
    BOOL BVar2;
    undefined4 uVar3;
    int iVar4;
    HWND hwnd;

    undefined4 extraout_ECX;

    // Used for ShellExecuteW()
    wchar_t *lpOperation;
    WCHAR *lpDirectory;
    INT nShowCmd;

    // Buffers for file paths/current directory
    WCHAR aWStack_20030[32768];
    WCHAR aWStack_10030[32768];

    // Internal structures/buffers
    LPCWSTR local_30[4];
    LPCWSTR local_20[4];

    LPWSTR local_10;

    // Internal flags
    char local_b;
    undefined1 local_a;
    char local_9;

    undefined4 uStack_8;

    // Stack setup
    uStack_8 = 0x403b47;

    // Initialize local structure
    FUN_00407667(local_30);

    local_10 = (wchar_t *)0x0;
    local_a = 0;
    local_9 = '\0';

    // Get current working directory
    GetCurrentDirectoryW(0x7fff, aWStack_10030);

    // Process command-line arguments
    FUN_00403766(param_1, &local_b);

    // Anti-debugging check
    BVar2 = IsDebuggerPresent();

    // If debugger detected -> show message and exit
    if (BVar2 != 0) {

        MessageBoxA(
            (HWND)0x0,
            "This is a third-party compiled AutoIt script.",
            "",
            0x10
        );

        goto LAB_00403c75;
    }

    // Check internal execution flag/state
    if (DAT_004c52e0 == 0) {

        // Set error state
        DAT_004c527c = 0xffffffff;
    }

    else {

        // Simple initialization path
        if (DAT_004c52e0 == 1) {

            // Initialize internal data
            FUN_00407213(
                &DAT_004c6290,
                1,
                DAT_004c52e8,
                0xffffffff
            );

            DAT_004c6292 = DAT_004c5284;
        }

        else {

            // More complex initialization path
            uVar3 = FUN_00407285(
                &DAT_004c6290,
                &DAT_004c52f8,
                &DAT_004c52e0,
                extraout_ECX,
                &local_a
            );

            // Initialization failed
            if ((char)uVar3 == '\0') {

                DAT_004c527c = 1;

                goto LAB_00403c68;
            }

            // Store internal values
            DAT_004c52e4 = DAT_004c6290;
            local_9 = DAT_004c6291;

            // Resolve full file path
            GetFullPathNameW(
                DAT_004c52f8,
                0x7fff,
                aWStack_20030,
                &local_10
            );

            // Additional path/file processing
            uVar3 = FUN_00407bcc(
                &DAT_004c52d0,
                local_10
            );

            local_10 = (LPWSTR)
                CONCAT31((int3)((uint)uVar3 >> 8), local_a);
        }

        // Additional validation/check
        iVar4 = FUN_0041092d(
            &DAT_004c52f8,
            DAT_004c52e0
        );

        // Validation failed
        if (iVar4 != 0) {

            // Cleanup
            FUN_004072db((undefined2 *)&DAT_004c6290);

            // Restore original working directory
            SetCurrentDirectoryW(aWStack_10030);

            DAT_004c527c = 1;

            goto LAB_00403c75;
        }

        // Check execution flag
        if (local_9 == '\x01') {

            // Internal condition check
            bVar1 = FUN_0045874b();

            // Compare conditions
            if ((bVar1) || ((bool)local_b != bVar1))
                goto LAB_00403c26;

            // Prepare executable path
            FUN_00404706(local_30);

            // Build command-line arguments
            FUN_00407de1(local_20, L"!");

            // If no path available
            if ((char)local_10 == '\0') {

                FUN_00407cab(local_20, param_1);
            }

            else {

                // Build quoted command-line string
                FUN_00407cab(local_20, L"\"");
                FUN_00407b2e(local_20, (int *)&DAT_004c52f8);
                FUN_00407cab(local_20, L"\"");
            }

            // Window display mode
            nShowCmd = 1;

            // Working directory
            lpDirectory = aWStack_10030;

            // "runas" triggers UAC/admin prompt
            lpOperation = L"runas";

            // Get active window handle
            hwnd = GetForegroundWindow();

            // Relaunch executable with elevated privileges
            ShellExecuteW(
                hwnd,
                lpOperation,
                local_30[0],
                local_20[0],
                lpDirectory,
                nShowCmd
            );

            // Cleanup
            FUN_00405904(local_20);
        }

        else {

LAB_00403c26:

            // Continue into deeper program execution

            FUN_00403a46();

            FUN_004039d5();

            // Additional internal logic
            if (DAT_004c52e4 == '\0') {

                FUN_0040434a(&DAT_004c5890);
            }

            // Initialize/execute another component
            FUN_004109d0(&DAT_004c5310, 1);

            // More internal execution
            if (DAT_004c52e4 == '\0') {

                FUN_0040443a(0x4c5890);
            }
        }

        // Cleanup internal structures
        FUN_004072db((undefined2 *)&DAT_004c6290);
    }

LAB_00403c68:

    // Restore original working directory
    SetCurrentDirectoryW(aWStack_10030);

LAB_00403c75:

    // Final cleanup
    FUN_00405904(local_30);

    return;
}

So this is a AutoIt compiled executable.

AutoIt is a Windows scripting language used to automate tasks such as GUI interaction, file operations, and system administration. AutoIt scripts can be compiled into standalone .exe files, making them easy to distribute and execute. Because of this, AutoIt is also commonly abused by malware authors to create packed or obfuscated malware loaders and droppers.

Based on the indicators seen so far, the sample very likely contains an AutoIt compiled executable.

Several observations support this conclusion:

▪Detect It Easy (DIE) identified:
▪Library: AutoIt (3.XX)
▪The binary displays the message:
TELEMETRY / DISASSEMBLY
    "This is a third-party compiled AutoIt script."

when a debugger is detected using IsDebuggerPresent().

▪The function structure and behavior also resemble typical compiled AutoIt loaders:
▪heavy runtime initialization,
▪wrapper functions,
▪environment setup,
▪command-line processing,
▪privilege elevation using ShellExecuteW(..., "runas", ...),
▪and large packed .rsrc sections.
▪The .rsrc section has very high entropy (7.90), which is common in compiled AutoIt executables because AutoIt often stores compressed scripts or embedded resources inside the resource section.

At this stage, it is best to describe the sample as:

“A likely AutoIt-compiled executable with packed resources and anti-debugging behavior.”

Further analysis would involve:

▪extracting the embedded AutoIt script,
▪unpacking the .rsrc section,
▪or tracing execution deeper into functions like FUN_00403A46() and FUN_004039D5() to identify the actual payload or behavior.

Using Exe2Aut

Just drag the sample into Exe2Aut. It will create a file named sample_.au3 open it in your favorite text editor for further analysis.

Screenshot 2026-05-23 at 10.10.14 PM.png
Figure: Screenshot 2026-05-23 at 10.10.14 PM.png Click to zoom ↗

AutoIt script Analysis

One of the behavior you can look for is the inclusion of a large amount of code.

Screenshot 2026-05-23 at 10.20.57 PM.png
Figure: Screenshot 2026-05-23 at 10.20.57 PM.png Click to zoom ↗

We can suspect either this is going to be a shellcode or maybe another executable file. This will likely be staged in memory. So we need to use a debugger to extract this shell code from the memory.

Before that

Identifying the Anti-Debugging Check

Since the script checks IsDebuggerPresent(), the sample is using a simple anti-debugging technique to stop execution when it detects a debugger. Earlier, we saw this behavior inside FUN_00403B3A():

C
BVar2 = IsDebuggerPresent();

if (BVar2 != 0) {
    MessageBoxA(
        (HWND)0x0,
        "This is a third-party compiled AutoIt script.",
        "",
        0x10
    );

    goto LAB_00403c75;
}

To locate the anti-debugging functionality, I opened the binary in Ghidra and navigated to:

Symbol Tree → Imports → KERNEL32.dll → IsDebuggerPresent

This revealed the imported Windows API IsDebuggerPresent(), which is commonly used by malware to detect debuggers during execution.

C
PTR_IsDebuggerPresent_0048f330

0048f330  addr  KERNEL32.DLL::IsDebuggerPresent
Screenshot 2026-05-23 at 10.31.33 PM.png
Figure: Screenshot 2026-05-23 at 10.31.33 PM.png Click to zoom ↗

The cross-references (XREFs) showed all locations where the function was used inside the binary:

C
FUN_00403b3a:00403b7a(R)

Following this reference led directly to the anti-debugging routine inside FUN_00403B3A(), where the sample checks if a debugger is attached. If the check returns TRUE, the program displays the message:

C
"This is a third-party compiled AutoIt script."

and terminates execution.

Screenshot 2026-05-23 at 10.35.00 PM.png
Figure: Screenshot 2026-05-23 at 10.35.00 PM.png Click to zoom ↗

Virtual Address where this instruction is located: 00403b7a . we can set the break point here and change the return value. I will use windbg.

C
00403b7a ff 15 30        CALL       dword ptr [->KERNEL32.DLL::IsDebuggerPresent]    = 000bb326
                 f3 48 00
Screenshot 2026-05-23 at 10.35.55 PM.png
Figure: Screenshot 2026-05-23 at 10.35.55 PM.png Click to zoom ↗

Windbg

Dealing with ASLR During Debugging

While debugging the sample in WinDbg, the module list showed that the executable was loaded at a different base address:

C
0:000> lm

start    end        module name
00bd0000 00d00000   sample
Screenshot 2026-05-23 at 10.49.39 PM.png
Figure: Screenshot 2026-05-23 at 10.49.39 PM.png Click to zoom ↗

Originally, the static analysis in Ghidra showed the anti-debugging call at:

C
00403B7A

However, because the program uses ASLR (Address Space Layout Randomization), the executable is relocated in memory each time it runs. This means the runtime address inside WinDbg will be different from the address seen in Ghidra.

To calculate the correct runtime breakpoint address:

C
Runtime Address =
(New Base Address) + (Original RVA)

The original image base in Ghidra is:

C
00400000

The runtime base in WinDbg is:

C
00BD0000

The RVA of the instruction:

C
00403B7A - 00400000 = 0x3B7A

Adding the RVA to the new base:

C
00BD0000 + 0x3B7A = 00BD3B7A

So the correct breakpoint address in WinDbg becomes:

C
bp 00BD3B7A

This allows us to break exactly at the IsDebuggerPresent() call even with ASLR enabled.

ChatGPT Image May 23, 2026, 11_27_53 PM.png
Figure: ChatGPT Image May 23, 2026, 11_27_53 PM.png Click to zoom ↗

Setting Breakpoints in WinDbg

After calculating the correct runtime address caused by ASLR, I set two important breakpoints in WinDbg:

Screenshot 2026-05-23 at 11.32.51 PM.png
Figure: Screenshot 2026-05-23 at 11.32.51 PM.png Click to zoom ↗
C
0:000> bp 00BD3B7A
0:000> bp kernel32!virtualallocstub
▪bp 00BD3B7A sets a breakpoint at the IsDebuggerPresent() check. Once hit, I changed the EAX register to 0 to bypass the anti-debugging protection.
▪bp kernel32!VirtualAllocStub monitors memory allocation activity. Malware commonly uses VirtualAlloc during unpacking, shellcode loading, or payload decryption, making it useful for tracking runtime behavior.

Now continue execution.

Hit the breakpoint:

C
00bd3b7a ff1530f3c500    call    dword ptr [sample+0x8f330 (00c5f330)] ds:002b:00c5f330={KERNEL32!IsDebuggerPresentStub (75c62770)}
Screenshot 2026-05-23 at 11.36.47 PM.png
Figure: Screenshot 2026-05-23 at 11.36.47 PM.png Click to zoom ↗

There’s no need to step into this function so let’s do step over and look at the value in the EAX register.

Screenshot 2026-05-23 at 11.44.33 PM.png
Figure: Screenshot 2026-05-23 at 11.44.33 PM.png Click to zoom ↗
C
EAX = 1

A value of 1 means the program successfully detected that a debugger was attached. To bypass this anti-debugging check, I manually changed the register value to: 0 .

In command:

C
 r EAX = 0
Screenshot 2026-05-23 at 11.54.23 PM.png
Figure: Screenshot 2026-05-23 at 11.54.23 PM.png Click to zoom ↗

Setting EAX to 0 tricks the program into believing that no debugger is attached, allowing execution to continue normally.

Now resume execution until we hit VirtualAlloc.

Command to resume execution.

C
g

hitting the breakpoint: kernel32!VirtualAllocStub .

Screenshot 2026-05-23 at 11.59.22 PM.png
Figure: Screenshot 2026-05-23 at 11.59.22 PM.png Click to zoom ↗

Once the breakpoint at VirtualAlloc was hit, I stepped into the API call to trace where the memory allocation request originated from, and then stepped out back into the malware code. This is useful because it gives insight into the actual memory address returned by VirtualAlloc(). In Windows x86 binaries, the return value of a function is stored in the EAX register.

Step into.

Screenshot 2026-05-24 at 12.07.02 AM.png
Figure: Screenshot 2026-05-24 at 12.07.02 AM.png Click to zoom ↗

Then Step out.

Screenshot 2026-05-24 at 12.13.20 AM.png
Figure: Screenshot 2026-05-24 at 12.13.20 AM.png Click to zoom ↗

This will give us inside into the actual value the address that was return from the function call. This will be held in EAX.

After stepping out of VirtualAlloc, the newly allocated memory address could be observed directly in EAX. This address is important because malware often uses this allocated memory region to unpack, decrypt, or load its real payload during runtime.

I used the following WinDbg command to inspect the memory protection of the allocated region:

C
0:000> !vprot eax

The output showed:

Screenshot 2026-05-24 at 12.17.41 AM.png
Figure: Screenshot 2026-05-24 at 12.17.41 AM.png Click to zoom ↗
C
BaseAddress:       03ff0000
AllocationBase:    03ff0000
AllocationProtect: 00000040  PAGE_EXECUTE_READWRITE
RegionSize:        00035000
State:             00001000  MEM_COMMIT
Protect:           00000040  PAGE_EXECUTE_READWRITE
Type:              00020000  MEM_PRIVATE

This means the allocated memory region has:

▪read permissions,
▪write permissions,
▪and execute permissions.

Memory regions with PAGE_EXECUTE_READWRITE permissions are highly suspicious because malware commonly uses them for:

▪unpacking payloads,
▪decrypting shellcode,
▪or executing code directly from memory.

The memory was also marked as:

TELEMETRY / DISASSEMBLY
MEM_PRIVATE

which indicates that the region was dynamically allocated during runtime rather than being part of the original executable. This suggests that the malware is likely preparing to unpack or execute additional code in memory.

Observing Additional Memory Allocations

During further debugging, the malware performed another call to VirtualAlloc(), indicating that additional memory was being allocated dynamically at runtime. This behavior is commonly seen in unpacking routines or shellcode loaders.

While analyzing the extracted AutoIt script, two different arrays were identified that appeared to contain encoded or embedded shellcode data. This strongly suggested that the malware was allocating memory in order to copy and execute these payloads dynamically.

To continue the analysis, I followed the same approach as before:

▪break on VirtualAlloc,
▪step into the API call,
▪then step out to return to the malware code.
Screenshot 2026-05-24 at 12.27.24 AM.png
Figure: Screenshot 2026-05-24 at 12.27.24 AM.png Click to zoom ↗

Detecting Another Executable Memory Region

After stepping out, the newly allocated memory address could again be observed in the EAX register, allowing further inspection of the memory region and the payload being written into it.

The malware later performed another VirtualAlloc() call, creating a second dynamically allocated executable memory region. After stepping out of the API call, I again inspected the returned address stored in the EAX register using:

C
0:000> !vprot eax

The output showed:

Screenshot 2026-05-24 at 12.27.58 AM.png
Figure: Screenshot 2026-05-24 at 12.27.58 AM.png Click to zoom ↗
C
BaseAddress:       04030000
AllocationBase:    04030000
AllocationProtect: 00000040  PAGE_EXECUTE_READWRITE
RegionSize:        00035000
State:             00001000  MEM_COMMIT
Protect:           00000040  PAGE_EXECUTE_READWRITE
Type:              00020000  MEM_PRIVATE

Once again, the allocated memory region had PAGE_EXECUTE_READWRITE permissions, meaning the memory could be read from, written to, and executed. Malware commonly uses this type of memory allocation for unpacking payloads, storing shellcode, or executing dynamically generated code.

Since the extracted AutoIt script contained two suspicious arrays that appeared to store shellcode data, this second executable memory region likely corresponds to another stage of payload loading or shellcode execution performed at runtime.

Setting an Execution Breakpoint on the Shellcode

After inspecting the allocated memory region using:

C
0:000> dd eax

the memory contents were completely empty:

C
04030000  00000000 00000000 00000000 00000000
04030010  00000000 00000000 00000000 00000000
04030020  00000000 00000000 00000000 00000000
...

At this point, the malware had only reserved executable memory, but the shellcode had not yet been written into it. What we are really interested in is the moment when the shellcode is copied into memory and execution begins.

By analyzing the extracted AutoIt script, it became clear that execution would begin at the start of the allocated memory region:

C
04030000

To catch the exact moment when the CPU starts executing code from this memory region, I set a hardware execution breakpoint on the first byte of the allocation using WinDbg:

C
0:000> ba e 1 04030000

Where:

▪ba → sets a hardware breakpoint,
▪e → triggers on execution,
▪1 → monitors 1 byte,
▪04030000 → start address of the allocated memory region.

Then resume execution.

Screenshot 2026-05-24 at 12.47.19 AM.png
Figure: Screenshot 2026-05-24 at 12.47.19 AM.png Click to zoom ↗

Inspecting the Allocated Memory with Process Hacker

Now we can use a tool like process hacker to explore the content of memory and extract that.

Here are the two allocations.

C
0x3ff0000  Private  212 kB  RWX
0x4030000  Private  212 kB  RWX
Screenshot 2026-05-24 at 12.53.35 AM.png
Figure: Screenshot 2026-05-24 at 12.53.35 AM.png Click to zoom ↗

These regions are important because:

▪Private memory means the regions were dynamically allocated during runtime,
▪RWX stands for:
▪Read
▪Write
▪Execute

Memory regions with RWX permissions are highly suspicious and are commonly used by malware for:

▪unpacking payloads,
▪storing shellcode,
▪or executing code directly from memory.

These two allocations also match the memory regions previously observed through VirtualAlloc() in WinDbg, confirming that the malware is dynamically preparing executable memory for shellcode execution.

Dumping the Shellcode Memory Region

Actual content of the memory. Go on and save this.

Screenshot 2026-05-24 at 12.54.02 AM.png
Figure: Screenshot 2026-05-24 at 12.54.02 AM.png Click to zoom ↗

Once the dump was saved, it could be imported directly into Ghidra for deeper static analysis. Analyzing the dumped payload in Ghidra is much easier than reversing the original packed AutoIt executable because the shellcode is now already unpacked and present in executable memory.

Reversing the Unpacked Payload

Ghidra

Importing the dumped payload in ghidra.

Choose language: x86:LE:32:default:windows

Screenshot 2026-05-24 at 1.07.55 AM.png
Figure: Screenshot 2026-05-24 at 1.07.55 AM.png Click to zoom ↗

Auto analysis in ghidra is challenging since there’s no entry point to define.

Screenshot 2026-05-24 at 1.14.17 AM.png
Figure: Screenshot 2026-05-24 at 1.14.17 AM.png Click to zoom ↗

So just right click at offset 0 and disassemble.

Screenshot 2026-05-24 at 1.17.02 AM.png
Figure: Screenshot 2026-05-24 at 1.17.02 AM.png Click to zoom ↗

Now it’s disassembled.

Screenshot 2026-05-24 at 1.18.27 AM.png
Figure: Screenshot 2026-05-24 at 1.18.27 AM.png Click to zoom ↗

This is a shellcode, one of the first thing we want identify is how it’s resolving function pointer, once we get that, we have the ability to trace functionality and understand what the shellcode is doing.

let’s look for a function that is resolving function pointer.

There’s two function in: FUN_00001E9C()

C
1. FUN_000001cb(local_180);
2. iVar2 = FUN_00001c09(auStack_288,param_2);

let’s look the first one.

FUN000001cb(local180);

This function is too long, which is actually a good thing when it’s comes to shellcode. This could means that this is a primary function responsible for resolving API’s, and possibly the program is using stack strings and that’s account for some of the length in the function because the data of all of the strings that is using has to be defined here as well as move to the stack. in order create the pointer to the strings.

Screenshot 2026-05-24 at 1.34.10 AM.png
Figure: Screenshot 2026-05-24 at 1.34.10 AM.png Click to zoom ↗

FUN_000001CB() appears to be a large API resolution and initialization function used by the unpacked payload. Instead of importing Windows APIs normally through the Import Address Table (IAT), the shellcode dynamically constructs API and DLL names in memory and resolves them at runtime. This is a very common malware technique used to evade static analysis and detection.

PEB

This instruction is important because it accesses the Windows Process Environment Block (PEB), which is a very common technique used by shellcode and malware.

Screenshot 2026-05-24 at 1.53.09 AM.png
Figure: Screenshot 2026-05-24 at 1.53.09 AM.png Click to zoom ↗
NASM
0000131b  MOV EAX, FS:[0x30]

In 32-bit Windows processes, FS:[0x30] points directly to the PEB (Process Environment Block). The PEB contains useful information about the running process, including:

▪loaded DLLs,
▪process parameters,
▪module lists,
▪and memory information.

Malware and shellcode commonly access the PEB directly to:

▪locate loaded DLLs without using Windows APIs,
▪resolve APIs dynamically,
▪bypass the Import Address Table (IAT),
▪and avoid detection by security tools.

This strongly supports the earlier observation that the payload is dynamically resolving Windows APIs at runtime instead of relying on normal imports.

FUN00000005()

FUN_00000005() is a classic dynamic API resolution function commonly seen in shellcode and malware. Instead of using the normal Windows Import Address Table (IAT), the malware manually searches for API addresses inside loaded DLLs at runtime.

The function first verifies that the module in memory is a valid PE file by checking:

▪0x5A4D (MZ) → DOS header
▪0x4550 (PE) → PE header

It then locates the Export Address Table (EAT) of the DLL and begins iterating through exported function names. The input parameter param_1 contains the API name the malware is searching for.

The function compares each exported API name character-by-character:

C
while (cVar1 == cVar2)

If a match is found, the function retrieves and returns the actual memory address of the API:

C
return API_address;
C
/*
    FUN_00000005()

    Purpose:
    Dynamically resolves API addresses from a loaded DLL by manually
    parsing the Export Address Table (EAT).

    This technique is commonly used in shellcode and malware to avoid
    importing suspicious APIs through the normal Import Address Table (IAT).
*/

int FUN_00000005(char *param_1)

{
    char cVar1;
    char cVar2;

    int iVar3;

    uint uVar4;

    char *pcVar5;

    // Points to the base address of a loaded module/DLL
    short *unaff_EBX;

    char *local_10;

    // Loop counter
    uint local_c;

    /*
        Validate DOS Header

        0x5A4D = "MZ"
    */
    if ((*unaff_EBX == 0x5a4d)

        &&

        /*
            Validate PE Header

            0x4550 = "PE"
        */
        (*(int *)(*(int *)(unaff_EBX + 0x1e)
        + (int)unaff_EBX) == 0x4550))

    {

        /*
            Locate the Export Address Table (EAT)

            PE Optional Header + Export Directory RVA
        */
        iVar3 =
            *(int *)(*(int *)(unaff_EBX + 0x1e)
            + 0x78
            + (int)unaff_EBX);

        // Start from first exported function
        local_c = 0;

        /*
            Number of exported APIs
        */
        uVar4 =
            *(uint *)((int)unaff_EBX
            + iVar3
            + 0x18);

        // Continue only if exports exist
        if (uVar4 != 0) {

            do {

                /*
                    Get current exported API name
                */
                pcVar5 =
                    (char *)(

                        *(int *)(

                            (int)unaff_EBX
                            + local_c * 4

                            + *(int *)(

                                (int)unaff_EBX
                                + iVar3
                                + 0x20
                            )
                        )

                        + (int)unaff_EBX
                    );

                // API name we are searching for
                local_10 = param_1;

                /*
                    Compare strings character-by-character
                */
                do {

                    cVar1 = *local_10;

                    cVar2 = *pcVar5;

                    local_10 = local_10 + 1;

                    /*
                        Stop comparison if NULL terminator reached
                    */
                    if ((cVar1 == '\0')

                        ||

                        (
                            pcVar5 = pcVar5 + 1,
                            cVar2 == '\0'
                        ))

                        break;

                } while (cVar1 == cVar2);

                /*
                    If strings match -> API found
                */
                if (cVar1 == cVar2) {

                    /*
                        Return resolved API address

                        Uses:
                        - Export Name Table
                        - Ordinal Table
                        - Export Address Table
                    */
                    return

                        *(int *)(

                            (int)unaff_EBX

                            +

                            (uint)*(ushort *)(

                                (int)unaff_EBX

                                +

                                local_c * 2

                                +

                                *(int *)(

                                    (int)unaff_EBX
                                    + iVar3
                                    + 0x24
                                )

                            ) * 4

                            +

                            *(int *)(

                                (int)unaff_EBX
                                + iVar3
                                + 0x1c
                            )

                        )

                        + (int)unaff_EBX;
                }

                // Move to next exported API
                local_c = local_c + 1;

            } while (local_c < uVar4);
        }
    }

    // API not found
    return 0;
}
Screenshot 2026-05-24 at 1.59.31 AM.png
Figure: Screenshot 2026-05-24 at 1.59.31 AM.png Click to zoom ↗

The function FUN_00000005() has 83 cross refrences.

This will convey a sense of importance, One of the most important things a shellcode needs to do is to make function calls.

This just continues to support our analysis, responsible for resolving those function pointer.

Screenshot 2026-05-24 at 2.02.18 AM.png
Figure: Screenshot 2026-05-24 at 2.02.18 AM.png Click to zoom ↗

Let’s just change the label from FUN_00000005 to resolve_api

Screenshot 2026-05-24 at 2.07.51 AM.png
Figure: Screenshot 2026-05-24 at 2.07.51 AM.png Click to zoom ↗

First, the program calls resolve_api() and stores the returned API address in the ESI register. It then resolves another API and stores that address in EDI. Later, the malware calls the resolved API indirectly using:

C
CALL ESI
Screenshot 2026-05-24 at 2.13.41 AM.png
Figure: Screenshot 2026-05-24 at 2.13.41 AM.png Click to zoom ↗

The value of the function pointer is returning an EAX

Then moved into ESI, a few instructions later there’s a call to ESI.

ESI is not changed or modify in between. So this confirms that this function is returning a pointer that is later called.

Copied