Author: Valli-Nayagam Chokkalingam

Instead of walking through shellcode generation, this post explains how shellcode executes, why it is position independent, and what that means in practice.

Contents

Starting With Something Familiar

On Windows, running a program is usually uneventful. There’s a file on disk, you double-click it, and the application shows up. A window opens, something visible happens, and you don’t really think about it any further.

Even when a program does almost nothing—like opening Notepad or Calculator—the flow feels obvious. The executable runs, the OS takes over, and the result appears. Whatever happens in between is mostly invisible, and most of the time that’s fine.

That “normal” experience quietly sets expectations. We get used to the idea that programs start from files, that the operating system handles the heavy lifting, and that execution just works because everything is already in place. For this post, that familiar model is the starting point—not because it’s interesting, but because it’s the set of assumptions everything else will eventually break.

Figure 1. Execution of a custom Windows executable resulting in the launch of notepad.exe as a visible indicator of successful code execution.

A Simple Program That Launches an Application

The executable shown in Figure 1 does one straightforward thing: it launches notepad.exe and exits. There’s no complex logic and no visible setup. The corresponding code, shown in Figure 2, reflects that simplicity.

That simplicity is misleading. By the time this program runs, a lot has already happened. A process exists, memory has been prepared, and system functionality is ready to be used. The program can make a single request and expect it to work because it is running inside an environment that has already been set up.

None of that setup appears in the code. There’s no handling for how the executable is loaded, how dependencies are resolved, or how execution is initialized. Those details are handled elsewhere, allowing the program to stay small and readable. Understanding where that work happens—and what assumptions it creates—is the next step.

Figure 2. C source code of the custom executable responsible for launching notepad.exe.

What Windows Does for an Executable Behind the Scenes

Launching an executable doesn’t mean Windows immediately starts running its code. The file is first treated as something that needs to be understood and prepared.

When you run Launch Notepad.exe (Figure 1), it doesn’t start instantly — behind the scenes, the loader performs several setup steps, illustrated in Figure 3, to prepare the program for execution. Windows recognizes the file as a Portable Executable and maps it into memory in a structured way. Sections are placed where they belong, a process is created, and space is carved out for the program to run. At this point, nothing from the application itself has executed yet.

Windows also deals with anything the program depends on. Required DLLs are loaded, and imported functions are resolved ahead of time. By the time execution reaches the program’s entry point, calls to system APIs already work. The code doesn’t need to locate them or check whether they exist.

Most of this work stays out of sight, which is why it’s easy to forget it’s happening at all. A file on disk is turned into a running process with very little effort from the developer. Those guarantees are what normal Windows programs rely on—and they’re exactly the things that disappear once the loader and executable structure are no longer part of the picture.

Figure 3. High-level view of the steps performed by Windows to load a Portable Executable into memory before execution begins.

What If We Want the Result Without the Executable?

Sometimes dropping an executable just isn’t worth the noise. Writing a payload to disk leaves a trail—something for EDR to watch, something for forensics to recover later. In Figure 4, the contrast is clear: a malware executable dropping Launch Notepad.exe to disk versus achieving the exact same result by executing shellcode injected into an existing process like explorer.exe.

The outcome doesn’t change. Notepad still opens. What changes is how much machinery is involved. With shellcode, there’s no secondary executable, no loader for a new file, and no obvious payload artifact left behind. It’s just raw instructions placed directly into memory, executed where they land.

By reusing a legitimate process and skipping the file drop entirely, execution blends into normal system activity. The functionality is identical, but the footprint is smaller and the signal is quieter. That difference is why shellcode exists—and why attackers keep reaching for it.

Figure 4. Comparison of executable-based and shellcode-based execution paths leading to the same result.

Execution Without a Loader

As shown in Figure 3, normal execution on Windows flows through the loader. A file is read from disk, its structure is understood, memory is prepared, and only then does execution begin. That path is so common it feels invisible.

Shellcode steps outside of it. There is no executable file to load and no loader preparing the ground first. The operating system never treats the code as a program. Execution starts directly from memory, wherever those instructions happen to be placed.

That difference matters. Without the loader, there’s no structured layout waiting in memory and no guarantees about what’s available. The code runs inside an existing process context and works with whatever state already exists there. Nothing is resolved ahead of time.

Figure 5 shows this contrast clearly. On one side, execution depends on the loader to turn a file into something runnable. On the other, execution happens without that preparation step at all. The result can look the same from the outside, but the path taken to get there is very different.

This is the tradeoff shellcode accepts. Less structure. Less comfort. But also less surface area. By avoiding the loader entirely, execution becomes quieter, and that shift is what defines running without one.

Figure 5. Comparison of loader-based execution and direct execution from memory.

Why Shellcode Cannot Rely on Structure or Location

Shellcode doesn’t start life as a program. When execution begins, it’s already inside a process, dropped into memory with none of the usual guarantees in place.

The first point of entry is shown in Figure 6. This is where control reaches the shellcode. There are no imports to lean on and no fixed addresses to trust. The code begins by setting up a small working state and deciding what it needs to find next. One of the first things prepared is a hash identifying the module it wants to locate.

Next comes the module lookup, shown in Figure 7. Here, the shellcode walks the loader structures exposed through the PEB. Each loaded module is checked in turn. A hash match identifies the module and yields its base address. In this case, that module is kernel32.dll. This replaces what the loader normally does when it maps dependent DLLs.

Figure 6. High-level view of the shellcode routine: locating kernel32.dll, resolving WinExec, and invoking it with stack-based arguments.

Figure 7. Shellcode traversing loader-linked module data via the PEB to identify the target DLL using a hashed name comparison.

Once the module base is known, execution still isn’t ready to transfer control anywhere meaningful. A loaded DLL doesn’t provide function addresses on its own. That work normally comes from the import table, and here it has to be reconstructed manually.

Figure 8 shows the first half of that process. Using the base address of kernel32.dll, the shellcode walks the export directory directly. Exported function names are read from memory and processed one by one, each passed through the same hashing logic used earlier. This stage is about identification, not execution — narrowing down which export corresponds to the function the shellcode wants.

Figure 9 picks up from there. Once the correct export name (in this case it’s WinExec) is identified, its ordinal value is used to index into the address table. That final lookup produces the actual virtual address of the WinExec API. The shellcode now holds a real, callable address inside the target module.

By the time Figure 9 completes, the end result mirrors what an import table would normally provide. The address of WinExec is resolved and ready to be called, even though no executable structure was involved and the loader never ran. Figure 10 shows what this would have looked like in a conventional executable (Launch Notepad.exe), where the same API is resolved ahead of time and recorded explicitly in the import table instead of being rebuilt at runtime.

Figure 8. Traversing the export directory of kernel32.dll to identify the target API name (WinExec) using hash comparison.

Figure 9. Resolving the virtual address of WinExec by mapping the matched export name through its ordinal and address tables.

Figure 10. Import table of the corresponding executable (Launch Notepad.exe), showing KERNEL32.dll and the WinExec API resolved by the loader.

Why Shellcode Ends Up Being Assembly

By this point, the form shellcode takes should already feel familiar. In Figures 6 through 9, IDA never shows a program in the usual sense. What’s visible is a direct disassembly of bytes sitting in memory.

That’s not a tooling artifact. It’s the reality of what shellcode is.

Shellcode doesn’t start life as a program Windows recognizes. There’s no file format to parse, no headers to interpret, and no symbols to resolve. A jump lands somewhere in memory, and from that moment on the only meaningful interpretation of those bytes is as CPU instructions.

This is why everything we’ve examined lives at the instruction level. IDA isn’t reconstructing structure or intent in Figures 6, 7, 8, and 9. It’s simply translating raw opcodes into assembly, one instruction at a time. There’s nothing above that layer to rely on.

Higher-level languages assume a loader, a runtime, and a stable execution context. Shellcode gets none of that. It executes where it lands, adapts to the process it’s in, and avoids assumptions about layout or location.

Assembly fits those constraints cleanly. It’s explicit, position-agnostic, and honest about what the code is doing. When shellcode is disassembled, assembly isn’t a side effect — it’s the code in its most direct form.

Proof of Execution Without a Program

At this point, execution isn’t something we infer. It’s something we can see.

The screenshot in this section (Figure 11) show the shellcode bytes resident inside the memory of explorer.exe. There is no executable on disk corresponding to this code. Nothing was launched, mapped, or registered as a program. The bytes exist only in memory, inside a process that was already running.

Yet execution still occurred. The shellcode ran, resolved what it needed, and produced a visible result. That alone is the point being made here.

This is what execution without a program looks like in practice. No file-backed image. No loader activity tied to a new process. Just instructions placed into memory and control transferred to them. From the operating system’s perspective, there is no new application to account for — only behavior happening inside an existing one.

Seeing the bytes in memory closes the loop. It confirms that everything discussed earlier is not theoretical. The code ran where it was injected, using the context it found, without ever becoming a program in the conventional sense.

Figure 11. Shellcode bytes executing from a private, executable memory region inside explorer.exe, without any loaded executable.

That’s where we’ll leave it for now. More to come soon!

Leave a Reply

Discover more from Adversary Craft

Subscribe now to keep reading and get access to the full archive.

Continue reading