In Part 1 of this series (Swallowing Exceptions in Win32),
I illustrated an efficient x86 shellcode implementation of KERNEL32's IsBadReadPtr() function to encapsulate Access Violation
exceptions when checking arbitrary memory locations in the context of a user-mode process.
In this article (Part 2), we'll build upon this functionality so that we may search memory to discover the locations
for any API in the system, loading the dependent library modules as needed. This is especially useful in modern
versions of Windows where the locations of system API functions are intentionally randomized in memory
for security purposes.
This article also provides insight into how code can be constructed to gain access
to system resources from a process no matter how its associated executable module was built, potentially
reducing any process to a code-launching container. While obviously useful to malware, these techniques can prove
useful for non-invasive patches to alter software where no source code is available.
MEMORY SEARCH: A UNIVERSAL METHOD OF API DISCOVERY:
Let's say we want shellcode to search memory so that we can discover where the MessageBoxA()
function resides allowing us to pop up a message box.
Countless examples always seem to target the MessageBox API
because its one of the simplest methods to indicate a successful proof-of-concept; for that reason, I'm also going to use it.
We also want the method to be portable amongst any x86 (32 or 64-bit) versions of Windows as well as working
under any process which may or may not already import functions from the desired library module. In our case the desired library is
USER32.DLL as this is the executable module that contains MessageBoxA().
While the memory-search method is not the only solution to enable shellcode to reliably locate API functions
from different versions of Windows, it has the highest educational value in my opinion because you get
exposure the following important concepts: exception suppression, Windows memory layout, and
parsing PE files; the only trade-off being code complexity. Luckily, the performance impact of locating the
address of each desired function will be negligible and on par with (if not much less) than the initial delay
when a process first starts: when the Windows loader parses and resolves the process' imports section.
THE KERNEL32.DLL GATEWAY:
Fortunately, we can do everything we need if we can just locate two KERNEL32.DLL functions: LoadLibrary() and GetProcAddress().
From there, we can exclusively rely on the operating system to efficiently load and locate the addresses of any other
And, because ALL processes always have KERNEL32.DLL loaded into their address space, it is always possible to access these
two functions to bootstrap any number of APIs you want to access at runtime.
In summary, we are interested in KERNEL32.DLL because it is:
guaranteed to exist in all processes
contains LoadLibrary() and GetProcAddress() APIs which unlock access to all other API functions
At this point, you may have noticed there is a chicken-and-egg scenario:
Since we don't yet know the location of the LoadLibrary() and GetProcAddress() functions,
how do we get their addresses without first accessing LoadLibrary() and GetProcAddress()?
We'll need to employ one of among a handful of known tricks to discover them.
ADDRESS SPACE LAYOUT RANDOMIZATION (ASLR)
Why can't addresses to API functions be hardcoded? Because system API addresses are no longer predictable on modern (post-XP) versions of Windows.
Prior to Microsoft's release of Vista, Windows loaded system DLLs at hardcoded addresses in all processes.
Because each version of the operating system and service pack level happened to load KERNEL32.DLL at the same base address,
shellcode could hardcode the addresses for the two "holy-grail" functions (LoadLibrary() and GetProcAddress()) for common
versions of Windows and have easy access to the remainder of APIs on the machine. For example, Windows XP Service Pack 3
always loaded KERNEL32 at 0x7C800000, LoadLibrary() at 0x7C801D7B and GetProcAddress() at 0x7C80AE30 system-wide.
Besides shellcode needing a table of different addresses per function per version of Windows,
it was still pretty convenient to access the system once shellcode gained control.
With the public release of Windows Vista in January 2007, Microsoft stepped up their game with a security feature known as
ASLR (Address Space Layout Randomization). ASLR randomizes the base load address for all system DLLs
including KERNEL32.DLL each time the operating system boots. Additionally,
3rd-party modules can "opt-in" to the new security feature by
specifying the IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE flag in the OptionalHeader's DllCharacteristics.
This meant that the days of hardcoding addresses for system DLLs were over and malware must jump through a few
more hoops to bypass ASLR.
This hasn't posed a problem for malware on previous versions of Windows because for any given Windows release, system executable images and DLLs always load at the same location, allowing malware to assume that APIs reside at fixed addresses.
The Windows Vista Address Space Load Randomization (ASLR) feature makes it impossible for malware to know where APIs are located by loading system DLLs and executables at a different location every time the system boots. Early in the boot process, the Memory Manager picks a random DLL image-load bias from one of 256 64KB-aligned addresses in the 16MB region at the top of the user-mode address space. As DLLs that have the new dynamic-relocation flag in their image header load into a process, the Memory Manager packs them into memory starting at the image-load bias address and working its way down.
Executables that have the flag set get a similar treatment, loading at a random 64KB-aligned point within 16MB of the base load address stored in their image header. Further, if a given DLL or executable loads again after being unloaded by all the processes using it, the Memory Manager reselects a random location at which to load it. Figure 7 shows an example address-space layout for a 32-bit Windows Vista system, including the areas from which ASLR picks the image-load bias and executable load address.
Figure 7 - ASLR's effect on executable and DLL load addresses
Only images that have the dynamic-relocation flag, which includes all Windows Vista DLLs and executables, get relocated because moving legacy images could break internal assumptions that developers have made about where their images load. Visual StudioŽ 2005 SP1 adds support for setting the flag so that third-party developers can take full advantage of ASLR.
Randomizing DLL load addresses to one of 256 locations doesn't make it impossible for malware to guess the correct location of an API, but it severely hampers the speed at which a network worm can propagate and it prevents malware that only gets one chance at infecting system from working reliably.
In addition, ASLR's relocation strategy has the secondary benefit that address spaces are more tightly packed than on previous versions of Windows, creating larger regions of free memory for contiguous memory allocations, reducing the number of page tables the Memory Manager allocates to keep track of address-space layout, and minimizing Translation Lookaside Buffer (TLB) misses.
While loading an image that has elected to participate in ASLR, the system uses a random global image
offset. This offset is selected once per reboot, although we've uncovered at least one other way to
cause this offset to be reset without a reboot (see Appendix II). The image offset is selected from a
range of 256 values and is 64 KB aligned. The offset and the other random parameters are generated
pseudo-randomly. All images loaded together into a process - including the main executable and
DLLs - are loaded one after another at this offset. Because image offsets are constant across all
processes, a DLL that is shared between processes can be loaded at the same address in all processes for
When executing a program whose image has been marked for ASLR, the memory layout of the process is
further randomized by placing the thread stack and the process heaps randomly. The stack address is
selected first. The stack region is selected from a range of 32 possible locations, each separated by 64
KB or 256 KB (depending on the STACK_SIZE setting).
Once the stack has been placed, the initial stack pointer is further randomized by a random decremental
amount. The initial offset is selected to be up to half a page (2,048 bytes), but is limited to
naturally aligned addresses (4-byte alignment on IA32 and 16-byte alignment on IA64). The choices result
in an initial stack pointer chosen from one of 16,384 possible values on an IA32 system. Once the stack
address has been selected, the process heaps are selected. Each heap is allocated from a range of 32
different locations, each separated by 64 KB. The location of the first heap must be chosen to avoid the
previously placed stack, and each of the heaps following must be allocated to avoid those that come
The address of an operating system structure known as the Process Environment Block (PEB) is also
selected randomly. The PEB randomization feature was introduced earlier in Windows XP SP2 and Windows
2003 SP1, and is also present in Windows Vista. Although implemented separately, it is also a form of
address space randomization; but unlike the other ASLR features, PEB randomization occurs whether or not
the executable being loaded elected to use the ASLR feature. An important result of the ASLR design in
Windows Vista is that some address space layout parameters, such as PEB, stack, and heap locations, are
selected once per program execution. Other parameters, such as the location of the program code, data
segment, BSS segment, and libraries, change only between reboots.
In general, bypassing ASLR requires searching memory for KERNEL32.DLL or using hint(s) as to its current whereabouts.
A MEMORY SEARCH ALGORITHM:
The basic algorithm we'll be using is the same whether you want to search memory for EXEs or DLLs as both use the same format.
First, pick a start address and test it to see if it accessible; in other words, that memory is mapped into the current process.
If not, swallow the resulting Access Violation exception and loop
back to the beginning to try the next memory location. Otherwise,
see if that memory location begins with the
16-bit DOS-header signature "MZ" (little-endian value 0x5A4D). If this byte-sequence is found, we can proceed to ensure we
have a valid PE image by locating the PE header. From here we can locate the export directory (if any) and walk its function tables
to locate the desired function(s).
The only catch is that testing every user-mode memory location within the normal 2 GB range would take too long and a
a noticeable delay isn't ideal.
Luckily, the operating system imposes some restrictions on how executable modules are aligned in memory:
All executable modules are aligned on a 64 KB (65536 byte) boundary; i.e. a multiple of 0x10000
System modules, such as KERNEL32.DLL, are randomly offset within the upper 16 MB region of user-mode
System modules tend to be placed as high as possible in user-mode memory
Because executable modules are always 64 KB aligned, we can drastically reduce the number of memory locations to test.
As a side note, the 64 KB alignment requirement actually applies to any executable module being loaded anywhere in memory on all versions of Windows
(read about the 64K granularity design decision here).
Additionally, Windows tends to choose high preferred-base addresses for system DLLs. The only thing ASLR does is to force DLLs downwards by a
random offset up to a maximum of 16 MB. This translates to a worst-case of only 256 comparisons to find the DLL, however searching from the top -> backwards
greatly improves the odds.
Now that we know we want to search backwards through memory in 64 KB decrements, at which address should we start?
Since kernel-mode-only addressing begins at 0x80000000, you might guess we start our search at the first 64 KB boundary prior which is 0x7FFF000. This
region of memory is known as the upper user-mode "off-limits" region so there is no point in testing it. Similar to the lower user-mode "off-limits" region between 0x00000000-0x0000FFFF,
the purpose of these regions are to catch bad pointers. Therefore we start our search another 64 KB lower at 0x7FFE0000.
An annotated disassembly of the shellcode search function is illustrated below, with added spacing and indentation for clarity:
004010DE55PUSHEBP;searchMemory()004010DF8BECMOVEBP, ESP;standard stack frame setup004010E183C4 E8ADDESP, -18004010E49CPUSHFD;preserve previous processor state004010E560PUSHAD004010E6FCCLD;ensure default direction is forward004010E7E8 00000000CALL$+5 <004010EC>;CALL-POP technique; get address of next instruction; inline data is then just an offset004010EC58POPEAX;eax = current EIP (as of the beginning of this POP instruction)004010ED83C0 06ADDEAX, 6;adjust eax so it points past these instructions and to string data (below)004010F0EB 1CJMPSHORT searchCodeBegin <0040110E>;jump past string data 004010F24C 6F 61 64 4C 69 62 72 61 72 79 41 00ASCII"LoadLibraryA",0; ASCII "LoadLibraryA"004010FF47 65 74 50 72 6F 63 41 64 64 72 65 73 73 00ASCII"GetProcAddress",0; ASCII "GetProcAddress 0040110E8945 F8MOVDWORD PTR SS:[EBP-8], EAX;searchCodeBegin: init - store address to function strings0040111183C0 0DADDEAX, 0D004011148945 F4MOVDWORD PTR SS:[EBP-0C], EAX00401117C745 FC 00000000MOVDWORD PTR SS:[EBP-4], 00040111EBB 0000FE7FMOVEBX, 7FFE0000;ebx=mem_location, off-limits-start-region minus 64KB (0x7FFF0000-0x10000)0040112333C9XORECX, ECX;module-found count initialized to zero 0040112560PUSHAD;loopMemTest: save the state of register variables prior to possible exceptions0040112653PUSHEBX;address we are testing00401127E8 82FFFFFFCALLisBadReadPtr <004010AE>0040112C8945 F0MOVDWORD PTR SS:[EBP-10], EAX;temporarily store result here (we don't need this variable at this point)0040112F61POPAD;restore our register variables004011308B45 F0MOVEAX, DWORD PTR SS:[EBP-10]0040113385C0TESTEAX, EAX;if memory not readable, try next memory location004011350F85 C5000000JNZsearchNextMemory <00401200>0040113B8B33MOVESI, DWORD PTR DS:[EBX];memory IS readible; see if module has an MZ signature0040113D66:81FE 4D5ACMPSI, 5A4D;is it "MZ" ?004011420F85 B8000000JNEsearchNextMemory <00401200>004011488B73 3CMOVESI, DWORD PTR DS:[EBX+3C];MZ sig FOUND - locate PE sig - esi = DOS header "e_lfanew"0040114B85F6TESTESI, ESI0040114D0F84 AD000000JZsearchNextMemory <00401200>;if (!e_lfanew) skip to next module0040115303F3ADDESI, EBX;esi = PE header004011558B06MOVEAX, DWORD PTR DS:[ESI]004011573D 50450000CMPEAX, 45500040115C0F85 9E000000JNEsearchNextMemory <00401200>;if (pe_hdr != "PE",0,0) goto parsedone0040116266:8B46 18MOVAX, WORD PTR DS:[ESI+18];check esi+18h for PE32 (i.e. 32-bit OptionalHeader magic / IMAGE_NT_OPTIONAL_HDR32_MAGIC)0040116666:3D 0B01CMPAX, 10B;IMAGE_NT_OPTIONAL_HDR32_MAGIC0040116A0F85 90000000JNEsearchNextMemory <00401200>;unsupported OptionalHeader signature, skip this module004011708B7E 78MOVEDI, DWORD PTR DS:[ESI+78];edi = optHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress00401173C745 E8 00000000MOVDWORD PTR SS:[EBP-18], 0;init LoadLibrary and GetProcAddress address locations0040117AC745 EC 00000000MOVDWORD PTR SS:[EBP-14], 00040118185FFTESTEDI, EDI;locate exports table and functions/names array0040118374 7BJZSHORT searchNextMemory <00401200>;if (!optHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress) goto next module0040118503FBADDEDI, EBX;add base to turn RVA into pointer00401187897D F0MOVDWORD PTR SS:[EBP-10], EDI;save off pointer to export table (pExportTable)0040118A8B4F 18MOVECX, DWORD PTR DS:[EDI+18];ecx = NumberOfNames; should always be >= NumberOfFunctions0040118D8B57 20MOVEDX, DWORD PTR DS:[EDI+20]; this is number of names left0040119003D3ADDEDX, EBX;edx = AddressOfNames array + base = pCurNameRVA 0040119251PUSHECX;parseModuleNextName: save NumberOfNamesLeft004011938B32MOVESI, DWORD PTR DS:[EDX];esi = pCurNameRVA0040119583FE 00CMPESI, 00040119874 4FJESHORT parseModuleNextName_cleanup <004011E9>;if (!*pCurNameRVA) goto cleanup0040119A03F3ADDESI, EBX;esi = pszCurName = *pCurNameRVA + base0040119C837D E8 00CMPDWORD PTR SS:[EBP-18], 0;if we already found LoadLibrary's address,004011A075 11JNESHORT parseGpa <004011B3>; skip to look for GetProcAddress instead004011A2B9 0D000000MOVECX, 0D;look for LoadLibraryA export string; length of "LoadLibraryA"004011A78B7D F8MOVEDI, DWORD PTR SS:[EBP-8]004011AAF3:A6REPECMPS BYTE PTR DS:[ESI], BYTE PTR ES:[EDI]004011AC75 05JNESHORT parseGpa <004011B3>;not found, test function name for GPA004011AE8D75 E8LEAESI,[EBP-18];FOUND! translate name location to func address and store at [esi]004011B1EB 13JMPSHORT foundFunction <004011C6>004011B3B9 0F000000MOVECX, 0F;parseGpa: look for GetProcAddress export string; length of "GetProcAddress"004011B88B7D F4MOVEDI, DWORD PTR SS:[EBP-0C]004011BB8B32MOVESI, DWORD PTR DS:[EDX];esi = pointer to current function name004011BD03F3ADDESI, EBX004011BFF3:A6REPECMPS BYTE PTR DS:[ESI], BYTE PTR ES:[EDI]004011C175 26JNESHORT parseModuleNextName_cleanup <004011E9>;not found, this function doesn't match either004011C38D75 ECLEAESI,[EBP-14];FOUND! translate name location to func address and store at [esi]004011C652PUSHEDX;foundFunction: locate and store address of function:004011C756PUSHESI; *savedFunc = arFuncs[arOrdinals[curNamesIdx]]004011C88B7D F0MOVEDI, DWORD PTR SS:[EBP-10];edi = pExportTable004011CB8B77 1CMOVESI, DWORD PTR DS:[EDI+1C]004011CE03F3ADDESI, EBX;esi = pExportTable->AddressOfFunctions array + base004011D08B57 24MOVEDX, DWORD PTR DS:[EDI+24]004011D303D3ADDEDX, EBX;edx = pExportTable->AddressOfNameOrdinals array + base004011D58B4F 18MOVECX, DWORD PTR DS:[EDI+18];ecx = pExportTable->NumberOfNames004011D82B4C24 08SUBECX, DWORD PTR SS:[ESP+8];ecx = NumberOfNames - NumberOfNameseLeft (saved ecx on stack) == base-0 curNamesIdx; a.k.a. hint004011DC0FB70C4AMOVZXECX, WORD PTR DS:[ECX*2+EDX];ecx (curOrd) = arOrdinals[curNamesIdx] current ordinals array index pointer004011E08B0C8EMOVECX, DWORD PTR DS:[ECX*4+ESI];ecx = func_addr RVA = arFuncs[curOrd]004011E303CBADDECX, EBX;ecx = func_addr004011E55EPOPESI004011E6890EMOVDWORD PTR DS:[ESI], ECX;*savedFunc = func_addr004011E85APOPEDX004011E959POPECX;parseModuleNextName_cleanup: ecx = functions left to test004011EA837D E8 00CMPDWORD PTR SS:[EBP-18], 0;do we have addresses for both functions yet?004011EE74 08JESHORT findkernel.004011F8004011F0837D EC 00CMPDWORD PTR SS:[EBP-14], 0004011F474 02JESHORT findkernel.004011F8004011F6EB 17JMPSHORT appDone <0040120F>;pop ecx ;perform outer loop pops, and skip to function end004011F883C2 04ADDEDX, 4;edx = pCurNameRVA++004011FB49DECECX;ecx = NumberOfNameseLeft004011FC85C9TESTECX, ECX;do we have names left?004011FE75 92JNZSHORT parseModuleNextName <00401192>;jump too far for LOOP instruction, so simulate LOOP with dec+test+jnz 0040120085DBTESTEBX, EBX;if somehow we just tested memory location 0, there is nothing more to test0040120274 0BJZSHORT appDone <0040120F>0040120481EB 00000100SUBEBX, 10000;try next lowest 4k memory page as WinNT always loads modules on a 4k boundary0040120AE9 16FFFFFFJMPloopMemTest <00401125> 0040120F61POPAD;cleanup - restore previous processor state004012109DPOPFD004012118B45 ECMOVEAX, DWORD PTR SS:[EBP-14];return LoadLibrary and GetProcAddress addresses through registers004012148B55 E8MOVEDX, DWORD PTR SS:[EBP-18]00401217C9LEAVE;tear-down stack frame00401218C2 0400RETN4;cleanup one argument; this implementation passes stdout handle in DEBUG mode
SOURCE CODE SUMMARY:
The MASM source code can be downloaded at the link at the top of the article. This
package comes with some batch scripts to build an EXE and manipulate the shellcode
embedded within the EXE. The source code embeds ASCII Markers within the
resulting executable so the shellcode can be extracted easily with tools.
This allows the shellcode to be run as an EXE or as an injectable binary.
Once the EXE is built, you can use the shellcode scripts to extract and
launch the shellcode, although you will need to download the
separately as they are needed by these optional scripts. They may be placed
in the same directory as the source or somewhere in your path.
This allows you to launch the shellcode in an "unaware" generic host process
to finalize this proof-of-concept code. With minor modifications, you
can obviously use whatever shellcode extraction and hosting tools are in your toolbox
although I'll illustrate using them as designed below.
To build the source, you'll need to edit the variables at the top of build.bat to point to the directory that contains
your Visual C++ toolset. In other words, where your MASM (ML.EXE) and linker tools are located. You don't need Visual Studio
installed, but you do need a version of those tools just mentioned along with any dependency DLLs - the exact files are
documented in the build.bat file. With that said, the code has been tested and found to build without errors or warnings
on the Visual C++ Toolsets 7.1 (2003) thru 10.0 (2010), but there is no reason it shouldn't work on the latest versions.
Although there may be slight differences in the resulting EXE's due to differing linker versions, the shellcode produced by differing
versions of MASM will be identical.
The source code contains the function isBadReadPtr() that we built
in Part 1 of this series as well as the other boilerplate code to call the
shellcode function from the EXE. To keep things simple, I put everything in a single source file with no includes necessary.
Towards the top are the usual declarations and some debugging macros which can be enabled by setting the DEBUG variable to "1"
in the contained build.bat. This will result in a DEBUG version of the EXE which outputs search progress to the console.
If you do utilize the DEBUG version of the EXE, note that this version is only designed to be run as an EXE from the console.
Any shellcode extracted from the DEBUG version of the EXE won't run properly (i.e. it will crash) because various
debugging strings reference the .data section and I didn't bother trying to build these strings into the shellcode, keeping
it compact and simple for this article.
You'll notice the build script uses editbin.exe (also part of the Visual C++ toolset) as a post-link step to enable ASLR
(/dynamicbase option) for the resulting binary, adding the 0x40 (IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) flag to the OptionalHeader.DllCharacteristics
member of the PE header. This support requires versions of editbin from Visual C++ 8.0 (2005) SP1 or greater.
I included it in the script so you could play with it (by commenting/uncommenting it out). Although this flag only randomizes
the base load address for the EXE itself and doesn't affect the location of KERNEL32.DLL, it is present for completeness and reference.
Another side note to remember is that if a module doesn't contain a .reloc section, the operating system has no choice
but to ignore the IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE flag as it can't load the module anywhere BUT its preferred load address.
This is why modules that support ASLR must be linked with /FIXED:NO, the implied default for DLLs, but not EXEs.
This forces the linker to generate a .reloc section.
USING THE SOURCE CODE:
Building and running the DEBUG version of the target using the build script is shown below:
The program locates the first executable module at the highest
memory location that contains both LoadLibraryA() and GetProcAddress() exported functions (i.e. KERNEL32.DLL).
Then a small bit of code uses the found function pointers to manually load USER32.DLL and subsequently launch MessageBoxA() resulting in the "ASLR defeated" message box.
You can see that running the program multiple times will produce the same addresses, but after a reboot,
the addresses will all change. This is ASLR in action.
pelook (or Microsoft's Depends will also work) to examine the
KERNEL32.DLL exports, we can see that the ordinal and hint numbers match the debug output shown above. This indicates
we parsed the exports table properly:
For the sake of brevity, the output above had the other export lines removed. This was from the 32-bit version of KERNEL32.DLL from Windows 7 SP1 (64-Bit).
LAUNCHING THE SHELLCODE:
There is no question that the code works fine when run from its *nice and safe* EXE file. The real test is whether it will still work when separated from its EXE "shell"
and injected into another process.
The first step is to extract the relevant code from the EXE between the markers explicitly placed there by the source code.
Once the EXE is has been built (and for this article, it must be the default RELEASE build), run the shellcode_extract_from_exe script:
An easy way to test the shellcode fragment in a host-process without writing a
custom launcher is to use the
shelljmp tool. Shelljmp
maps a file into its process and "jumps" into it (start execution at the entrypoint).
Since the shellcode was designed with its entry-point at the first byte, no
offset needs to be specified. The shellcode can be launched with shellcode_run.bat:
Again, the message box appears (and blocks) just as it did when run solely from the EXE.
BENEFITS AND LIMITATIONS:
The algorithm depicted here is one of a few known methods to locate KERNEL32.DLL from shellcode. Unlike most
methods, it is compatible with all versions of NT-based Windows (past and present), with or without ASLR. Its
primary drawback is that it is incompatible with SafeSEH.
Other methods striking a balance between complexity and compatibility include:
Locating the top-most KERNEL32 exception handler from the FS: chain
Walking the PEB's loaded module list
Walking the stack
The code presented here illustrates a "good-enough" solution and is intended for educational purposes only. It performs
only minimal validation of the PE header to achieve its goals. Malformed headers could make it crash.
While code can be easily extracted from a file,
getting it to work regardless of where it might be loaded in memory can be tricky.
Position independent code utilizes techniques that allow data and code to be packed within the same contiguous block.
This shellcode block can only know about itself, at least initially, based on its own fixed offsets. Once it has discovered more
from its surroundings can it do more "useful things" like dynamically load and call functions in separate libraries.
Writing position independent code and making it robust enough to run anywhere on many operating system is truly an art form.
2018-09-18 * added links to top and within sentence in limitations regarding lack of SafeSEH compatibility
2018-02-28 * added excerpt from Symantec's security whitepaper: An Analysis of Address Space Layout Randomization on Windows Vista™