Monday, May 18, 2020

EXE without runtime, structure of executable files, VB6 applications without external dependencies ! PART 1

PART 1 (by Krivous Anatoly Anatolevich)

The description of PE format. Creation of the simple VB6-EXE loader/packer. The VB6-loader without the runtime (msvbvm60) dependencies. And so on...

Hello everyone! Today i want to show you the quite interesting things. One day i was investigating the PE (portable executable) file format especially EXE. I decided to create a simple loader of the executable files specially for VB6-compiled applications. This loader should load an VB6-compiled exe from the memory without file. THIS IS JUST FOR THE EXPERIMENTAL PURPOSES IN ORDER TO CHECK POSSIBILITIES OF VB6. Due to that the VB6-compiled applications don't used most of the PE features it was quite simple objective. Most of programmer's says that a VB6-application is linked with the VB6 runtime (MSVBVM), a VB6 application doesn't work without the runtime and the runtime is quite slow. Today i'll prove that it is possible to write an application that absolutely doesn't use runtime (although i was already doing that in the driver). These projects i had written quite a long time ago, but these were in the russian language. I think it could be quite interesting for someone who wants to examine the basic principles of work with the PE files.

Before we begin i want to say couple words about the projects. These projects were not tested well enough therefore it can cause problems. The loader doesn't support most of the features of PE files therefore some executables may not work. So...

This overview consists three projects:

Compiler - it is the biggest project of all. It creates an installation based on the loader, user files, commands and manifest;

Loader - it is the simple loader that performs commands, unpacks files and runs an executable from memory;

Patcher - it is the small utility that removes the runtime import from an executable file.
I call an exe that contains the commands, files and executable file the installation. The main idea is to put the information about an installation to the resources of the loader. When the loader is being loaded it reads the information and performs the commands from resources. I decided to use an special storage to save the files and exe, and other storage for commands.

The first storage stores all the files that will be unpacked, and the main executable that will be launched. The second storage stores the commands that will be passed to the ShellExecuteEx function after unpacking process will have been completed. The loader supports the following wildcards (for path):

 - application installed path;
 - system windows directory;
 - System32 directory;
 - system drive;
 - temporary directory;
 - user desktop.

Compiler



This is the application that forms the installation information and puts it to the loader resource. All the information is stored in a project. You can save and load a project from file. The clsProject class in VB project represents the compiler-project. This compiler has 3 sections: storage, execute, manifest. 

The 'storage' section allows to add the files that will be copied when the application is being launched. Each item in the list has flags: 'replace if exists', 'main executable', 'ignore error'. If you select 'replace if exists' flag a file will be copied even if one exists. The 'main executable' flag can be set only for the single executable file. It means that this file will be launched when all the operations have been performed. The 'ignore error' flag makes ignore any errors respectively. The order in the list corresponds the order of extracting the files except the main executable. The main executable is not extracted and is launched after all the operations. The storage section is represented as clsStorage class in the VB project. This class implements the standard collection of the clsStorageItem objects and adds some additional methods.The MainExecutable property determines the index of main executable file in the storage. When this parameter equal -1 executable file is not presented. The clsStoragaItem class represent the single item in the storage list. It has some properties that determine the behavior of item. This section is helpful if you want to copy files to disk before execution of the application.

The next section is the 'execute'. This section allows execute any commands. This commands just pass to ShellExecuteEx function. Thus you can register libraries or do something else. Each item in the execution list has two properties: the executable path and parameters. Both the path and the parameters is passed to ShellExecuteEx function. It is worth noting that all the operations is performed synchronously in the order that set in the list. It also has the 'ignore error' flag that prevents appearance any messages if an error occurs. The execute section is represented as two classes: clsExecute and clsExecuteItem. These classes are similar to the storage classes.

The last section is 'manifest'. It is just the manifest text file that you can add to the final executable. You should check the checkbox 'include manifest' in the 'manifest' tab if you wan to add manifest. It can be helpful for Free-Reg COM components or for visual styles.

All the classes refer to the project object (clsProject) that manages them. Each class that refers to project can be saved or loaded to the PropertyBag object. When a project is being saved it alternately saves each entity to the property bag, same during loading. It looks like a IPersistStream interface behavior. All the links to the storage items in the project is stored with relative paths (like a VB6 .vbp file) hence you can move project folder without issues. In order to translate from/to relative/absolute path i used PathRelativePathTo and PathCanonicalize functions.

So... This was basic information about compiler project. Now i want to talk about compilation procedure. As i said all the information about extracting/executing/launching is stored to the loader resources. At first we should define the format of the data. This information is represented in the following structures:

' // Storage list item
Private Type BinStorageListItem
 ofstFileNameAs Long ' // Offset of file name
 ofstDestPathAs Long ' // Offset of file path
 dwSizeOfFileAs Long ' // Size of file
 ofstBeginOfDataAs Long ' // Offset of beginning data
 dwFlagsAs FileFlags' // Flags
End Type
' // Execute list item
Private Type BinExecListItem
 ofstFileNameAs Long ' // Offset of file name
 ofstParametersAs Long ' // Offset of parameters
 dwFlagsAs ExeFlags' // Flags
End Type
' // Storage descriptor
Private Type BinStorageList
 dwSizeOfStructure As Long ' // Size of structure
 iExecutableIndex As Long ' // Index of main executable
 dwSizeOfItemAs Long ' // Size of BinaryStorageItem structure
 dwNumberOfItemsAs Long ' // Number of files in storage
End Type
' // Execute list descriptor
Private Type BinExecList
 dwSizeOfStructure As Long ' // Size of structure
 dwSizeOfItemAs Long ' // Size of BinaryExecuteItem structure
 dwNumberOfItemsAs Long ' // Number of items
End Type
' // Base information about project
Private Type BinProject
 dwSizeOfStructure As Long ' // Size of structure
 storageDescriptor As BinStorageList ' // Storage descriptor
 execListDescriptor As BinExecList' // Command descriptor
 dwStringsTableLen As Long ' // Size of strings table
 dwFileTableLenAs Long ' // Size of data table
End Type

The 'BinProject' structure is located at beginning of resource entry. Notice that project is stored as RT_RCDATA item with 'PROJECT' name. The dwSizeOfStructure  field defines the size of the BinProject structure, storageDescriptor and execListDescriptor represent the storage and execute descriptors respectively. The dwStringsTableLen field shows the size of strings table. The strings table contains all the names and commands in the unicode format. The dwFileTableLen field shows the size of all data in the storage. Both storage (BinStorageList) and execute list (BinExecList) have dwSizeOfItem and dwSizeOfStructure fields that define the size of a descriptor structure and the size of a list item. These structures also have dwNumberOfItems field that shows how many items is contained in the list. The 'iExecutableIndex' field contains the index of executable file that will be launched. The common structure of a project in the resources is shown in this figure:


An item can refers to the strings table and file table for this purpose it uses the offset from beginning of a table. All the items is located one by one. Okay, you have explored the internal project format now i tell how can you build the loader that contains these data. As i said we store data to resources of the loader. I will tell about the loader a little bit later now i want to note one issue. When you put the project data to resources it doesn't affect to exe information. For example if you launch this exe the information contained in the resources of the internal exe won't be loaded. Same with icons and version information. You should copy the resources from the internal exe to loader in order to avoid this troubles. WinAPI provides the set of the functions for replacing resources. In order to obtain the list of resources you should parse the exe file and extract data. I wrote the 'LoadResources' function that extract all the resources of specified exe data to array.

PE format

In order to obtain resources from an exe file, run EXE from memory and well know the stucture of an exe file we should examine the PE (portable executable) format. The PE format has the quite complex structure. When loader launches a PE file (exe or dll) it does quite many work. Each PE file begins with special structure IMAGE_DOS_HEADER aka. dos stub. Because both DOS and WINDOWS applications have exe extension you can launch an exe file in DOS, but if you try to do it DOS launches this dos stub. Usually it show the message: "This program cannot be run in DOS mode", but you can write any program:


Type IMAGE_DOS_HEADER
 e_magicAs Integer
 e_cblpAs Integer
 e_cpAs Integer
 e_crlcAs Integer
 e_cparhdr As Integer
 e_minalloc As Integer
 e_maxalloc As Integer
 e_ssAs Integer
 e_spAs Integer
 e_csumAs Integer
 e_ipAs Integer
 e_csAs Integer
 e_lfarlc As Integer
 e_ovnoAs Integer
 e_res(0 To 3)As Integer
 e_oemidAs Integer
 e_oeminfo As Integer
 e_res2(0 To 9)As Integer
 e_lfanew As Long
End Type

Since we don't write a dos program it doesn't matter. We wonder only two fields of this structure: e_magic and e_lfanew. The first field should contains the 'MZ' signature aka. IMAGE_DOS_SIGNATURE  and e_lfanew offset to very crucial structure IMAGE_NT_HEADERS described as:

Type IMAGE_NT_HEADERS
 SignatureAs Long
 FileHeaderAs IMAGE_FILE_HEADER
 OptionalHeader As IMAGE_OPTIONAL_HEADER
End Type

The first filed of this structure contains the signature 'PE' (aka. IMAGE_NT_SIGNATURE). The next field describes the executable file:

Type IMAGE_FILE_HEADER
 Machine As Integer
 NumberOfSectionsAs Integer
 TimeDateStamp As Long
 PointerToSymbolTable As Long
 NumberOfSymbols As Long
 SizeOfOptionalHeader As Integer
 Characteristics As Integer
End Type

The 'Machine' field defines the processor architecture and should have the IMAGE_FILE_MACHINE_I386 value in our case. The NumberOfSections filed determines the count of sections contained in the exe file.

An exe file contains the sections anyway. Each section takes a place in the address space and optionally in file. A section can contain the code or data (initialized or not), has the name as well. The most common names: .text, .data, .rsrc. Usually, the .text section contains the code, the .data section initialized data and .rsrc - resources. You can change this behavior using the linker directives. Each section have address called virtual address. Generally there are several types of the addresses. The first is relative virtual address (RVA). Because of a PE file can be loaded to any address all the references inside the PE file use the relative addressing. RVA is the offset from beginning of the base address (the address of the first byte of the PE module in the memory). The sum of the RVA and the base address is the VA (the virtual address). Also, there is the raw offset addressing that shows the location of data in the file relative the RVA. Notice that RVA <> raw offset. When a module is being loaded each secion is placed to its address. For example a module could have the section that has no-initialized data. This section wouldn't take a place in the exe file but would occupy the address space. It is the very crucial aspect because we will work with the raw exe file.

The 'TimeDateStamp' field contains the creation date of the PE module in UTC format. The 'PointerToSymbolTable' and 'NumberOfSymbols' contain the information about symbols in the PE file. Generally this fields contains zero. This fields is always used in object files (*.OBJ, *.LIB) in order to resolve links during linking as well as debugging information for PE modules. The next field 'SizeOfOptionalHeader' contains the size of structure following after IMAGE_FILE_HEADER named IMAGE_OPTIONAL_HEADER that always is presented in PE files (although may be missing in OBJ files). This structure is very essential for loading a PE file to memory. Notice that this structure is different in x64 and x86 executable files. Eventually, the 'Characteristics' field contains the PE attributtes.
The IMAGE_OPTIONAL_HEADER structure has the following format:


Type IMAGE_OPTIONAL_HEADER
 Magic As Integer
 MajorLinkerVersionAs Byte
 MinorLinkerVersionAs Byte
 SizeOfCodeAs Long
 SizeOfInitializedData As Long
 SizeOfUnitializedData As Long
 AddressOfEntryPointAs Long
 BaseOfCodeAs Long
 BaseOfDataAs Long
 ImageBaseAs Long
 SectionAlignmentAs Long
 FileAlignment As Long
 MajorOperatingSystemVersionAs Integer
 MinorOperatingSystemVersionAs Integer
 MajorImageVersionAs Integer
 MinorImageVersionAs Integer
 MajorSubsystemVersion As Integer
 MinorSubsystemVersion As Integer
 W32VersionValue As Long
 SizeOfImageAs Long
 SizeOfHeaders As Long
 CheckSumAs Long
 SubSystemAs Integer
 DllCharacteristicsAs Integer
 SizeOfStackReserveAs Long
 SizeOfStackCommitAs Long
 SizeOfHeapReserveAs Long
 SizeOfHeapCommitAs Long
 LoaderFlagsAs Long
 NumberOfRvaAndSizesAs Long
 DataDirectory(15)As IMAGE_DATA_DIRECTORY
End Type

The first field contains the type of the image (x86, x64 or ROM image). We consider only IMAGE_NT_OPTIONAL_HDR32_MAGIC that represents a common 32-it application. The next two fields is not important (they were used in the old systems) and contain 4. The next group of fields contains the sizes of all the code, initialized data and uninitialized data. These values should be a multiple of 'SectionAlignment' of the structure (see later). The 'AddressOfEntryPoint' is very important RVA-value that sets the start point of a program (look like Sub Main). We will use this field when we have already loaded the PE-image to memory and it is necessary to run the program. The next crucial filds is 'ImageBase' that sets the prefered address of loading the module. When a loader is loading a module it tries to load it to thr prefered address (set in the 'ImageBase' filed). If this address is occupied then the loader checks the 'Characteristics' field in the 'IMAGE_FILE_HEADER' structure. If this field contains the IMAGE_FILE_RELOCS_STRIPPED it means that the module can't be loaded. In order to load such module we should add the reloaction information to PE that allows to set up the addresses if the module can't be loaded to prefered base address. We will use this field in the shellcode with the 'SizeOfImage' fields to reserve a memory region for the unpacked PE. The 'SectionAlignment' and 'FileAlignment' set the align in memory and file respectevely. By changing the file alignment we can reduce the PE file size but a system can not be possible to load this modified PE. The section alignment sets the align of a section (usually it equals to page size). The 'SizeOfHeaders' value sets the size of all headers (DOS header, NT headers, sections headers) aligned to the 'FileAlignment' value. The 'SizeOfStackReserve' and 'SizeOfStackCommit' determine the total and initial stack size. It's the same for 'SizeOfHeapReserve' and 'SizeOfHeapCommit' fields but for the heap. The 'NumberOfRvaAndSizes' fields contains the number of items in 'DataDirectory' array. This field always set to 16. The 'DataDirectory' array is very important because it contains the several data catalogs that contain essential information about import, export, resources, relocations, etc. We use only few items from this catalog that is used by VB6 compiler. I'll say about catalogs little bit later let's look what is behind the catalogs. There is list of the section headers. The number of section, if you remember, we can obtain from the IMAGE_FILE_HEADER structure. Let's consider the section header structure:

Type IMAGE_SECTION_HEADER
 SectionName(7)As Byte
 VirtualSize As Long
 VirtualAddressAs Long
 SizeOfRawDataAs Long
 PointerToRawData As Long
 PointerToRelocationsAs Long
 PointerToLinenumbersAs Long
 NumberOfRelocations As Integer
 NumberOfLinenumbers As Integer
 CharacteristicsAs Long
End Type

The first field contains the name of the section in the UTF-8 format with the NULL-terminated characters. This name is limited by 8 symbols (if a section has the size of the name equals 8 the NULL terminated character is skipped). A COFF file may have the name greater than 8 characters in this case the name begins with '/' symbol followed by the ASCII decimal representation offset in the string table (IMAGE_FILE_HEADER field). A PE file doesn't support the long names. The 'VirtualSize' and 'VirtualAddress' fields contain the size of section in memory (the address is set as RVA). The 'SizeOfRawData' and 'PointerToRawData' contain the data location in the file (if section has an initialized data). This is the key moment because we can calculate the raw offset by the virtual address using the information from the section headers. I wrote function for it that translate a RVA address to the RAW offset in the file:

' // RVA to RAW
Function RVA2RAW( _
 ByVal rva As Long, _
 ByRef sec() As IMAGE_SECTION_HEADER) As Long
 Dim index As Long
 
 For index = 0 To UBound(sec)
If rva >= sec(index).VirtualAddress And _
 rva < sec(index).VirtualAddress + sec(index).VirtualSize Then
 RVA2RAW = sec(index).PointerToRawData + (rva - sec(index).VirtualAddress)
 Exit Function
End If
 Next
 
 RVA2RAW = rva
 
End Function

This function enumerates all the sections and checks if the passed address is within section. The next 5 fields are used in only COFF file and don't matter in a PE file. The 'Characteristics' field contains the attributes of section such as memory permissions and managment. This field is important as well. We will use this information for the memory protection of an exe file in the loader.
Okay, let's return to the data directories. As we saw there is 16 items in this catalog. Usually, a PE file doesn't use all of them. Let's consider the structure of single item:
Private Type IMAGE_DATA_DIRECTORY
 VirtualAddress As Long
 Size As Long
End Type

This structure consist two fields. The first fields contains the offset (RVA) to the data of the catalog, second - size. When an item of data catalog is not required it contains zero in the both fields. An VB6-compiled application generally contains only 4 items: import table , resource table , bound import table and import address table (IAT). Now we consider the resource table that has the IMAGE_DIRECTORY_ENTRY_RESOURCE index, because we work with this imformation in the compiler application.
All the resources in the exe file are represented as the tree with triple depth. The first level defines the resource type (RT_BITMAP, RT_MANIFEST, RT_RCDATA, etc.), the next level defines the resource identifier, the third level defines language. In the standard resource editor you can change the first two levels. All the resources placed in the resources table located in a '.rsrc' section of the exe file. Regarding the resources structure we even can change it after compilation. In oredr to access to resources we should read the IMAGE_DIRECTORY_ENTRY_RESOURCE catalog from the optional header. The 'VirtualAddress' field of this structure contains the RVA of the resource table which has the following structure:

Type IMAGE_RESOURCE_DIRECTORY
 CharacteristicsAs Long
 TimeDateStampAs Long
 MajorVersionAs Integer
 MinorVersionAs Integer
 NumberOfNamedEntriesAs Integer
 NumberOfIdEntries As Integer
End Type

This structure describes all the resources in PE file. The first four fields are not important, the 'NumberOfNamedEntries' and 'NumberOfIdEntries' contain the number of named items and the items with the numerical id respectively. For instance, when you add an image in 'VB Resource Editor' it'll add a numerical item with id = 2 (RT_BITMAP). The items are after this structure and have the following form:

Type IMAGE_RESOURCE_DIRECTORY_ENTRY
 NameIdAs Long
 OffsetToDataAs Long
End Type

The first field of this structure defines either an item name or an item id depending on the most significant bit. If this bit is set the remained bits show the offset from beginning of resources to IMAGE_RESOURCE_DIR_STRING_U structure that has the following format:

Type IMAGE_RESOURCE_DIR_STRING_U
 LengthAs Integer
 NameString As String
End Type

Note that this is not the proper VB structure is shown for descriptive reasons. The first two bytes is the unsigned short (the closest analog is Integer) that show the length of the unicode string (in characters) that follows them. Thus, in order to obtain a string we should read the first two bytes to an integer, allocate memory for the string with the read value size, and read the remaining data to the string variable. Conversely, if the most significant bit of the 'NameId' field is cleared the field containts an identifier (RT_BITMAP in the previous case). The 'OffsetToData' field has two interpretations too. If the MSB is set it is the offset (from beginnig of resources) to the next level of tree i.e. to an IMAGE_RESOURCE_DIRECTORY structure. Otherwise, if the MSB is cleared this is the offset (from beginning of resources too) to a "leave" of the tree, i.e. to structure IMAGE_RESOURCE_DATA_ENTRY:

Type IMAGE_RESOURCE_DATA_ENTRY
 OffsetToDataAs Long
 SizeAs Long
 CodePage As Long
 Reserved As Long
End Type

The most important fields of this structure are 'OffsetToData' and 'Size' that contain the RVA and Size of the raw data of the resource. Now we can get all the resources from a PE file.

Compilation.

So... When you start the compilation it calls the Compile function. Firstly it packs all the storage items and execute items to binary format (BinProject, BinStorageListItem, etc.) and forms the string table and the files table. The string table is saves as the unicode strings with the null-terminating characters. I use special class clsStream for safe working with binary data. This class allows to read/write any data or streams into the binary buffer, compress buffer. I use RtlCompressBuffer function for compression stream that uses LZ-compression method. After packing and compression it check output project format. It is supports two formats: bin (raw project data), and exe (loader). The binary format is not interesting right now, we will consider the exe format. Firstly, it extracts all the resources from the main executable to the three-level catalog. This operation is performed by ExtractResources function. An identifier name is saved as the "#" symbol with the appended string that represents the resource id in the decimal format. Afterwards it clones the loader template to the resulting file, then begins to modify the resources in this file using BeginUpdateResource api. Then it alternately copies all the extracted resources (UpdateResource), binary project and mainfest (if needed) to the resulting file and applies changes with EndUpdateResource function. Again, the binary project is saved with "PROJECT" name and RT_DATA type. Basically that's it.

Loader.


So... I think it the most interesting part. So, we must avoid the usage of the runtime. How to do it? I give some rules:
  1. Set an user function as startup;
  2. Avoid any objects in project;
  3. Avoid immediate arrays. The fixed arrays in a type is not forbidden;
  4. Avoid string variables as well Variant/Object. In some cases Currency/Date;
  5. Avoid the API functions with Declare statement.
  6. Avoid VarPtr and some other standard functions.
  7. ....

It isn't the complete list of restrictions and during the shellcode execution it adds additional restrinctions.
So, begin. In order to avoid the usage of a string variable i keep all the string variables as Long pointer to the string. There is an issue with loading of a string because we can't access to any string to load it. I decide to use resources as the storage of strings and load it by ID. Thus, we can save the pointer to a string into a Long variable without references to runtime. I used a TLB (type library) for all the API functions without usesgetlasterror attribute in order to avoid the Declare statement. In order to set a startup function i use the linker options. The startup function in the loader is 'Main'. Note, if in the IDE you select the startup function 'Main' actually this function is not startup because any EXE application written in VB6 begins with ThunRTMain function, that loads project and initialize runtime and thread. Generally, the loader conist three modules:
  1. modMain - startup function and working with storage/execute items;
  2. modConstants - working with string constants;
  3. modLoader - loader of EXE files.
When loader has been launched it starts the Main function.
' // Startup subroutine
Sub Main()
 ' // Load constants
 If Not LoadConstants Then
MessageBox 0, GetString(MID_ERRORLOADINGCONST), 0, MB_ICONERROR Or MB_SYSTEMMODAL
GoTo EndOfProcess
 End If
 
 ' // Load project
 If Not ReadProject Then
MessageBox 0, GetString(MID_ERRORREADINGPROJECT), 0, MB_ICONERROR Or MB_SYSTEMMODAL
GoTo EndOfProcess
 End If
 
 ' // Copying from storage
 If Not CopyProcess Then GoTo EndOfProcess
 
 ' // Execution process
 If Not ExecuteProcess Then GoTo EndOfProcess
 
 ' // If main executable is not presented exit
 If ProjectDesc.storageDescriptor.iExecutableIndex = -1 Then GoTo EndOfProcess
 
 ' // Run exe from memory
 If Not RunProcess Then
' // Error occrurs
MessageBox 0, GetString(MID_ERRORSTARTUPEXE), 0, MB_ICONERROR Or MB_SYSTEMMODAL
 End If
 
EndOfProcess:
 
 If pProjectData Then
HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData
 End If
 
 ExitProcess 0
 
End Sub

Firstly, it function call LoadConstants function to load all the needed constants from resources:

' // modConstants.bas - main module for loading constants
' // © Krivous Anatoly Anatolevich (The trick), 2016
Option Explicit
Public Enum MessagesID
 MID_ERRORLOADINGCONST = 100' // Errors
 MID_ERRORREADINGPROJECT = 101 '
 MID_ERRORCOPYINGFILE = 102'
 MID_ERRORWIN32 = 103 '
 MID_ERROREXECUTELINE = 104'
 MID_ERRORSTARTUPEXE = 105'
 PROJECT = 200 ' // Project resource ID
 API_LIB_KERNEL32 = 300 ' // Library names
 API_LIB_NTDLL = 350'
 API_LIB_USER32 = 400 '
 MSG_LOADER_ERROR = 500
End Enum
' // Paths
Public pAppPath As Long ' // Path to application
Public pSysPath As Long ' // Path to System32
Public pTmpPath As Long ' // Path to Temp
Public pWinPath As Long ' // Path to Windows
Public pDrvPath As Long ' // Path to system drive
Public pDtpPath As Long ' // Path to desktop
' // Substitution constants
Public pAppRepl As Long
Public pSysRepl As Long
Public pTmpRepl As Long
Public pWinRepl As Long
Public pDrvRepl As Long
Public pDtpRepl As Long
Public pStrNull As Long ' // 
Public hInstance As Long ' // Base address
Public lpCmdLine As Long ' // Command line
Public SI As STARTUPINFO ' // Startup parameters
Public LCID As Long ' // LCID
' // Load constants
Function LoadConstants() As Boolean
 Dim lSize As Long
 Dim pBuf As Long
 Dim index As Long
 Dim ctlAs tagINITCOMMONCONTROLSEX
 
 ' // Load windows classes
 ctl.dwSize = Len(ctl)
 ctl.dwICC = &H3FFF&
 InitCommonControlsEx ctl
 
 ' // Get startup parameters
 GetStartupInfo SI
 
 ' // Get command line
 lpCmdLine = GetCommandLine()
 
 ' // Get base address
 hInstance = GetModuleHandle(ByVal 0&)
 
 ' // Get LCID
 LCID = GetUserDefaultLCID()
 
 ' // Alloc memory for strings
 pBuf = SysAllocStringLen(0, MAX_PATH)
 If pBuf = 0 Then Exit Function
 
 ' // Get path to process file name
 If GetModuleFileName(hInstance, pBuf, MAX_PATH) = 0 Then GoTo CleanUp
 
 ' // Leave only directory
 PathRemoveFileSpec pBuf
 
 ' // Save path
 pAppPath = SysAllocString(pBuf)
 
 ' // Get Windows folder
 If GetWindowsDirectory(pBuf, MAX_PATH) = 0 Then GoTo CleanUp
 pWinPath = SysAllocString(pBuf)
 
 ' // Get System32 folder
 If GetSystemDirectory(pBuf, MAX_PATH) = 0 Then GoTo CleanUp
 pSysPath = SysAllocString(pBuf)
 
 ' // Get Temp directory
 If GetTempPath(MAX_PATH, pBuf) = 0 Then GoTo CleanUp
 pTmpPath = SysAllocString(pBuf)
 
 ' // Get system drive
 PathStripToRoot pBuf
 pDrvPath = SysAllocString(pBuf)
 
 ' // Get desktop path
 If SHGetFolderPath(0, CSIDL_DESKTOPDIRECTORY, 0, SHGFP_TYPE_CURRENT, pBuf) Then GoTo CleanUp
 pDtpPath = SysAllocString(pBuf)
 
 ' // Load wildcards
 For index = 1 To 6
If LoadString(hInstance, index, pBuf, MAX_PATH) = 0 Then GoTo CleanUp
Select Case index
Case 1: pAppRepl = SysAllocString(pBuf)
Case 2: pSysRepl = SysAllocString(pBuf)
Case 3: pTmpRepl = SysAllocString(pBuf)
Case 4: pWinRepl = SysAllocString(pBuf)
Case 5: pDrvRepl = SysAllocString(pBuf)
Case 6: pDtpRepl = SysAllocString(pBuf)
End Select
 Next
 
 ' // vbNullChar
 pStrNull = SysAllocStringLen(0, 0)
 ' // Success
 LoadConstants = True
 
CleanUp:
 
 If pBuf Then SysFreeString pBuf
 
End Function
' // Obtain string from resource (it should be less or equal MAX_PATH)
Public Function GetString( _
ByVal ID As MessagesID) As Long
 GetString = SysAllocStringLen(0, MAX_PATH)
 
 If GetString Then
 
If LoadString(hInstance, ID, GetString, MAX_PATH) = 0 Then SysFreeString GetString: GetString = 0: Exit Function
If SysReAllocString(GetString, GetString) = 0 Then SysFreeString GetString: GetString = 0: Exit Function
 End If
 
End Function

The 'LoadConstants' function loads all the needed variables and string (hInstance, LCID, command line, wildcards, default paths, etc.). All the strings is stored in the BSTR unicode format. The 'GetString' function loads a string from resource by number. The 'MessagesID' contains some string identifiers needed in program (error messages, libraries names, etc.). When all the constants are loaded it calls the ReadProject function that loads the binary project:

' // Load project
Function ReadProject() As Boolean
 Dim hResourceAs Long:Dim hMememoryAs Long
 Dim lResSizeAs Long:Dim pRawDataAs Long
 Dim status As Long:Dim pUncompressed As Long
 Dim lUncompressSize As Long:Dim lResultSizeAs Long
 Dim tmpStorageItem As BinStorageListItem: Dim tmpExecuteItem As BinExecListItem
 Dim pLocalBuffer As Long
 
 ' // Load resource
 hResource = FindResource(hInstance, GetString(PROJECT), RT_RCDATA)
 If hResource = 0 Then GoTo CleanUp
 
 hMememory = LoadResource(hInstance, hResource)
 If hMememory = 0 Then GoTo CleanUp
 
 lResSize = SizeofResource(hInstance, hResource)
 If lResSize = 0 Then GoTo CleanUp
 
 pRawData = LockResource(hMememory)
 If pRawData = 0 Then GoTo CleanUp
 
 pLocalBuffer = HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, lResSize)
 If pLocalBuffer = 0 Then GoTo CleanUp
 
 ' // Copy to local buffer
 CopyMemory ByVal pLocalBuffer, ByVal pRawData, lResSize
 
 ' // Set default size
 lUncompressSize = lResSize * 2
 
 ' // Do decompress...
 Do
If pUncompressed Then
 pUncompressed = HeapReAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, ByVal pUncompressed, lUncompressSize)
Else
 pUncompressed = HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, lUncompressSize)
End If
status = RtlDecompressBuffer(COMPRESSION_FORMAT_LZNT1, _
ByVal pUncompressed, lUncompressSize, _
ByVal pLocalBuffer, lResSize, lResultSize)
lUncompressSize = lUncompressSize * 2
 Loop While status = STATUS_BAD_COMPRESSION_BUFFER
 
 pProjectData = pUncompressed
 
 If status Then GoTo CleanUp
 ' // Validation check
 If lResultSize < LenB(ProjectDesc) Then GoTo CleanUp
 
 ' // Copy descriptor
 CopyMemory ProjectDesc, ByVal pProjectData, LenB(ProjectDesc)
 
 ' // Check all members
 If ProjectDesc.dwSizeOfStructure <> Len(ProjectDesc) Then GoTo CleanUp
 If ProjectDesc.storageDescriptor.dwSizeOfStructure <> Len(ProjectDesc.storageDescriptor) Then GoTo CleanUp
 If ProjectDesc.storageDescriptor.dwSizeOfItem <> Len(tmpStorageItem) Then GoTo CleanUp
 If ProjectDesc.execListDescriptor.dwSizeOfStructure <> Len(ProjectDesc.execListDescriptor) Then GoTo CleanUp
 If ProjectDesc.execListDescriptor.dwSizeOfItem <> Len(tmpExecuteItem) Then GoTo CleanUp
 
 ' // Initialize pointers
 pStoragesTable = pProjectData + ProjectDesc.dwSizeOfStructure
 pExecutesTable = pStoragesTable + ProjectDesc.storageDescriptor.dwSizeOfItem * ProjectDesc.storageDescriptor.dwNumberOfItems
 pFilesTable = pExecutesTable + ProjectDesc.execListDescriptor.dwSizeOfItem * ProjectDesc.execListDescriptor.dwNumberOfItems
 pStringsTable = pFilesTable + ProjectDesc.dwFileTableLen
 
 ' // Check size
 If (pStringsTable + ProjectDesc.dwStringsTableLen - pProjectData) <> lResultSize Then GoTo CleanUp
 
 ' // Success
 ReadProject = True
 
CleanUp:
 
 If pLocalBuffer Then HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pLocalBuffer
 
 If Not ReadProject And pProjectData Then
HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData
 End If
 
End Function

As you can see i use the heap memory instead arrays. Firstly, it loads the 'PROJECT' resource and copies one to heap memory then tries to decompres using the RtlDecompressBuffer function. This function is not returns the sufficient output buffer size therefore we try to do decompress of the buffer increasing the output buffer size. Afterwards it checks all parameters and initializes the global project pointers.
If it is succeeded then it launches the 'CopyProcess' procedure that unpacks all the storage items according project data:
' // Copying process
Function CopyProcess() As Boolean
 Dim bItemAs BinStorageListItem: Dim indexAs Long
 Dim pPathAs Long:Dim dwWritten As Long
 Dim msg As Long:Dim lStepAs Long
 Dim isErrorAs Boolean:Dim pItemAs Long
 Dim pErrMsgAs Long:Dim pTempString As Long
 
 ' // Set pointer
 pItem = pStoragesTable
 
 ' // Go thru file list
 For index = 0 To ProjectDesc.storageDescriptor.dwNumberOfItems - 1
' // Copy file descriptor
CopyMemory bItem, ByVal pItem, Len(bItem)
' // Next item
pItem = pItem + ProjectDesc.storageDescriptor.dwSizeOfItem
' // If it is not main executable
If index <> ProjectDesc.storageDescriptor.iExecutableIndex Then
 ' // Normalize path
 pPath = NormalizePath(pStringsTable + bItem.ofstDestPath, pStringsTable + bItem.ofstFileName)
 
 ' // Error occurs
 If pPath = 0 Then
 
pErrMsg = GetString(MID_ERRORWIN32)
MessageBox 0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL
GoTo CleanUp
 Else
Dim hFile As Long
Dim disp As CREATIONDISPOSITION
' // Set overwrite flags
If bItem.dwFlags And FF_REPLACEONEXIST Then disp = CREATE_ALWAYS Else disp = CREATE_NEW
' // Set number of subroutine
lStep = 0
' // Run subroutines
Do
 ' // Disable error flag
 isError = False
 
 ' // Free string
 If pErrMsg Then SysFreeString pErrMsg: pErrMsg = 0
 
 ' // Choose subroutine
 Select Case lStep
 Case 0 ' // 0. Create folder
 
If Not CreateSubdirectories(pPath) Then isError = True
 Case 1 ' // 1. Create file
 
hFile = CreateFile(pPath, FILE_GENERIC_WRITE, 0, ByVal 0&, disp, FILE_ATTRIBUTE_NORMAL, 0)
If hFile = INVALID_HANDLE_VALUE Then
 If GetLastError = ERROR_FILE_EXISTS Then Exit Do
 isError = True
End If
 Case 2 ' // 2. Copy data to file
 
If WriteFile(hFile, ByVal pFilesTable + bItem.ofstBeginOfData, _
bItem.dwSizeOfFile, dwWritten, ByVal 0&) = 0 Then isError = True
If dwWritten <> bItem.dwSizeOfFile Then
 isError = True
Else
 CloseHandle hFile: hFile = INVALID_HANDLE_VALUE
End If
 End Select
 
 ' // If error occurs show notification (retry, abort, ignore)
 If isError Then
 
' // Ignore error
If bItem.dwFlags And FF_IGNOREERROR Then Exit Do
pTempString = GetString(MID_ERRORCOPYINGFILE)
pErrMsg = StrCat(pTempString, pPath)
' // Cleaning
SysFreeString pTempString: pTempString = 0
Select Case MessageBox(0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL Or MB_CANCELTRYCONTINUE)
Case MESSAGEBOXRETURN.IDCONTINUE: Exit Do
Case MESSAGEBOXRETURN.IDTRYAGAIN
Case Else: GoTo CleanUp
End Select
 Else: lStep = lStep + 1
 End If
 
Loop While lStep <= 2
If hFile <> INVALID_HANDLE_VALUE Then
 CloseHandle hFile: hFile = INVALID_HANDLE_VALUE
End If
' // Cleaning
SysFreeString pPath: pPath = 0
 End If
 
End If
 Next
 
 ' // Success
 CopyProcess = True
 
CleanUp:
 
 If pTempString Then SysFreeString pTempString
 If pErrMsg Then SysFreeString pErrMsg
 If pPath Then SysFreeString pPath
 
 If hFile <> INVALID_HANDLE_VALUE Then
CloseHandle hFile
hFile = INVALID_HANDLE_VALUE
 End If
 
End Function

This procedure goes through all the storage items and unpacks all the items one by one except the main executable file. The 'NormalizePath' function replace the wildcards in the path to the real strings path. There is the 'CreateSubdirectories' function that creates the intermediate directories (if needs) for specified path. Then it calls the CreateFile function and copy data to it through WriteFile. If an error occurs it shows the message box with the standard suggestions: Retry, Abort, Ignore.

' // Create all subdirectories by path
Function CreateSubdirectories( _
ByVal pPath As Long) As Boolean
 Dim pComponent As Long
 Dim tCharAs Integer
 
 ' // Pointer to first char
 pComponent = pPath
 
 ' // Go thru path components
 Do
 
' // Get next component
pComponent = PathFindNextComponent(pComponent)
' // Check if end of line
CopyMemory tChar, ByVal pComponent, 2
If tChar = 0 Then Exit Do
' // Write null-terminator
CopyMemory ByVal pComponent - 2, 0, 2
' // Check if path exists
If PathIsDirectory(pPath) = 0 Then
 ' // Create folder
 If CreateDirectory(pPath, ByVal 0&) = 0 Then
' // Error
CopyMemory ByVal pComponent - 2, &H5C, 2
Exit Function
 End If
 
End If
' // Restore path delimiter
CopyMemory ByVal pComponent - 2, &H5C, 2
 Loop
 
 ' // Success
 CreateSubdirectories = True
 
End Function
' // Get normalize path (replace wildcards, append file name)
Function NormalizePath( _
ByVal pPath As Long, _
ByVal pTitle As Long) As Long
 Dim lPathLen As Long: Dim lRelacerLen As Long
 Dim lTitleLen As Long: Dim pRelacer As Long
 Dim lTotalLen As Long: Dim lPtrAs Long
 Dim pTempString As Long: Dim pRetString As Long
 
 ' // Determine wildcard
 Select Case True
 Case IntlStrEqWorker(0, pPath, pAppRepl, 5): pRelacer = pAppPath
 Case IntlStrEqWorker(0, pPath, pSysRepl, 5): pRelacer = pSysPath
 Case IntlStrEqWorker(0, pPath, pTmpRepl, 5): pRelacer = pTmpPath
 Case IntlStrEqWorker(0, pPath, pWinRepl, 5): pRelacer = pWinPath
 Case IntlStrEqWorker(0, pPath, pDrvRepl, 5): pRelacer = pDrvPath
 Case IntlStrEqWorker(0, pPath, pDtpRepl, 5): pRelacer = pDtpPath
 Case Else: pRelacer = pStrNull
 End Select
 
 ' // Get string size
 lPathLen = lstrlen(ByVal pPath)
 lRelacerLen = lstrlen(ByVal pRelacer)
 
 ' // Skip wildcard
 If lRelacerLen Then
pPath = pPath + 5 * 2
lPathLen = lPathLen - 5
 End If
 
 If pTitle Then lTitleLen = lstrlen(ByVal pTitle)
 
 ' // Get length all strings
 lTotalLen = lPathLen + lRelacerLen + lTitleLen
 
 ' // Check overflow (it should be les or equal MAX_PATH)
 If lTotalLen > MAX_PATH Then Exit Function
 
 ' // Create string
 pTempString = SysAllocStringLen(0, MAX_PATH)
 If pTempString = 0 Then Exit Function
 
 ' // Copy
 lstrcpyn ByVal pTempString, ByVal pRelacer, lRelacerLen + 1
 lstrcat ByVal pTempString, ByVal pPath
 ' // If title is presented append
 If pTitle Then
' // Error
If PathAddBackslash(pTempString) = 0 Then GoTo CleanUp
' // Copy file name
lstrcat ByVal pTempString, ByVal pTitle
 End If
 
 ' // Alloc memory for translation relative path to absolute
 pRetString = SysAllocStringLen(0, MAX_PATH)
 If pRetString = 0 Then GoTo CleanUp
 
 ' // Normalize
 If PathCanonicalize(pRetString, pTempString) = 0 Then GoTo CleanUp
 
 NormalizePath = pRetString
 
CleanUp:
 
 If pTempString Then SysFreeString pTempString
 If pRetString <> 0 And NormalizePath = 0 Then SysFreeString pRetString
 
End Function
' // Concatenation strings
Function StrCat( _
ByVal pStringDest As Long, _
ByVal pStringAppended As Long) As Long
 Dim l1 As Long, l2 As Long
 
 l1 = lstrlen(ByVal pStringDest): l2 = lstrlen(ByVal pStringAppended)
 StrCat = SysAllocStringLen(0, l1 + l2)
 
 If StrCat = 0 Then Exit Function
 
 lstrcpyn ByVal StrCat, ByVal pStringDest, l1 + 1
 lstrcat ByVal StrCat, ByVal pStringAppended
 
End Function

After unpacking it is call the 'ExecuteProcess' function that launches all the commands using ShellExecuteEx function:

' // Execution command process
Function ExecuteProcess() As Boolean
 Dim indexAs Long:Dim bItemAs BinExecListItem
 Dim pPathAs Long:Dim pErrMsgAs Long
 Dim shInfoAs SHELLEXECUTEINFO: Dim pTempString As Long
 Dim pItemAs Long:Dim statusAs Long
 ' // Set pointer and size
 shInfo.cbSize = Len(shInfo)
 pItem = pExecutesTable
 
 ' // Go thru all items
 For index = 0 To ProjectDesc.execListDescriptor.dwNumberOfItems - 1
 
' // Copy item
CopyMemory bItem, ByVal pItem, ProjectDesc.execListDescriptor.dwSizeOfItem
' // Set pointer to next item
pItem = pItem + ProjectDesc.execListDescriptor.dwSizeOfItem
' // Normalize path
pPath = NormalizePath(pStringsTable + bItem.ofstFileName, 0)
' // Fill SHELLEXECUTEINFO
shInfo.lpFile = pPath
shInfo.lpParameters = pStringsTable + bItem.ofstParameters
shInfo.fMask = SEE_MASK_NOCLOSEPROCESS Or SEE_MASK_FLAG_NO_UI
shInfo.nShow = SW_SHOWDEFAULT
' // Performing...
status = ShellExecuteEx(shInfo)
' // If error occurs show notification (retry, abort, ignore)
Do Until status
 
 If pErrMsg Then SysFreeString pErrMsg: pErrMsg = 0
 
 ' // Ignore error
 If bItem.dwFlags And EF_IGNOREERROR Then
Exit Do
 End If
 pTempString = GetString(MID_ERROREXECUTELINE)
 pErrMsg = StrCat(pTempString, pPath)
 
 SysFreeString pTempString: pTempString = 0
 
 Select Case MessageBox(0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL Or MB_CANCELTRYCONTINUE)
 Case MESSAGEBOXRETURN.IDCONTINUE: Exit Do
 Case MESSAGEBOXRETURN.IDTRYAGAIN
 Case Else: GoTo CleanUp
 End Select
 status = ShellExecuteEx(shInfo)
 
Loop
' // Wait for process terminaton
WaitForSingleObject shInfo.hProcess, INFINITE
CloseHandle shInfo.hProcess
 Next
 
 ' // Success
 ExecuteProcess = True
 
CleanUp:
 If pTempString Then SysFreeString pTempString
 If pErrMsg Then SysFreeString pErrMsg
 If pPath Then SysFreeString pPath
 
End Function

As you can see it's like the previous procedure. Here is the same it only uses ShellExecuteEx instead unpacking. Note that each operation performs synchronously, i.e. each calling of ShellExecuteEx waits for operation will have been done.
If it is succeeded then it is call the 'RunProcess' function that prepares the data for executing main executable from memory.
' // Run exe from project in memory
Function RunProcess() As Boolean
 Dim bItemAs BinStorageListItem: Dim LengthAs Long
 Dim pFileData As Long
 
 ' // Get descriptor of executable file
 CopyMemory bItem, ByVal pStoragesTable + ProjectDesc.storageDescriptor.dwSizeOfItem * _
ProjectDesc.storageDescriptor.iExecutableIndex, Len(bItem)
 
 ' // Alloc memory within top memory addresses
 pFileData = VirtualAlloc(ByVal 0&, bItem.dwSizeOfFile, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_READWRITE)
 If pFileData = 0 Then Exit Function
 
 ' // Copy raw exe file to this memory
 CopyMemory ByVal pFileData, ByVal pFilesTable + bItem.ofstBeginOfData, bItem.dwSizeOfFile
 
 ' // Free decompressed project data
 HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData
 pProjectData = 0
 
 ' // Run exe from memory
 RunExeFromMemory pFileData, bItem.dwFlags And FF_IGNOREERROR
 
 ' ----------------------------------------------------
 ' // An error occurs
 ' // Clean memory
 
 VirtualFree ByVal pFileData, 0, MEM_RELEASE
 
 ' // If ignore error then success
 If bItem.dwFlags And FF_IGNOREERROR Then RunProcess = True
 
End Function

It allocates the memory in the top area of virtual addresses because the most of exe files is loaded to quite low addresses (usually 0x00400000). Afterwards it free the memory of the project data because if the process is launched this memory won't release, then it call the 'RunExeFromMemory' function that does next step of loading an exe. If for any reasons loading of an exe file wouldn't done it frees the allocated memory and return the control to the 'Main' function. So, in order to load an exe file we should release the loader memory, i.e. unload us loader. We should leave small piece of code that will load an exe and run it. I decide to use the shellcode, although it is possible to use a dll. The shellcode is the small base-independent code (this code doesn't refer to external data) that allows to do the usefull stuff. Anyway we should ensure the access to API functions from the shellcode. You can't call an api function directly from the shellcode because the main exe is unloaded and any reference to the import table of main exe occurs crash. The second restriction is the 'CALL' instruction can use relative offset (it is most frequently case). Therefore we should initialize some "springboard" that will jump an api function. I decide to do it using splicing method. I just replace the first 5 bytes of a stub function to JMP assembler instruction that refers to the needed API:

' // Run EXE file by memory address
Function RunExeFromMemory( _
ByVal pExeData As Long, _
ByVal IgnoreError As Boolean) As Boolean
 Dim Length As Long: Dim pCodeAs Long
 Dim pszMsg As Long: Dim pMsgTable As Long
 Dim index As Long: Dim pCurMsgAs Long
 
 ' // Get size of shellcode
 Length = GetAddr(AddressOf ENDSHELLLOADER) - GetAddr(AddressOf BEGINSHELLLOADER)
 
 ' // Alloc memory within top addresses
 pCode = VirtualAlloc(ByVal 0&, Length, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_EXECUTE_READWRITE)
 
 ' // Copy shellcode to allocated memory
 CopyMemory ByVal pCode, ByVal GetAddr(AddressOf BEGINSHELLLOADER), Length
 
 ' // Initialization of shellcode
 If Not InitShellLoader(pCode) Then GoTo CleanUp
 
 ' // Splice CallLoader function in order to call shellcode
 Splice AddressOf CallLoader, pCode + GetAddr(AddressOf LoadExeFromMemory) - GetAddr(AddressOf BEGINSHELLLOADER)
 
 ' // Check ignore errors
 If Not IgnoreError Then
' // Alloc memory for messages table
pMsgTable = VirtualAlloc(ByVal 0&, 1024, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_READWRITE)
If pMsgTable = 0 Then GoTo CleanUp
' // Skip pointers
pCurMsg = pMsgTable + EM_END * 4
For index = 0 To EM_END - 1
 ' // Load message string
 pszMsg = GetString(MSG_LOADER_ERROR + index)
 If pszMsg = 0 Then GoTo CleanUp
 
 Length = SysStringLen(pszMsg)
 lstrcpyn ByVal pCurMsg, ByVal pszMsg, Length + 1
 
 ' // Store pointer
 CopyMemory ByVal pMsgTable + index * 4, pCurMsg, Len(pCurMsg)
 
 ' // Next message offset
 pCurMsg = pCurMsg + (Length + 1) * 2
 
 SysFreeString pszMsg
 
Next
 End If
 
 ' // Call shellcode
 CallLoader pExeData, pCode, pMsgTable
 
CleanUp:
 
 If pMsgTable Then
VirtualFree ByVal pMsgTable, 0, MEM_RELEASE
 End If
 
 If pCode Then
VirtualFree ByVal pCode, 0, MEM_RELEASE
 End If
 
End Function

As you can see it calculates the size of shellcode using the difference between the extreme functions ENDSHELLLOADER and BEGINSHELLLOADER. These functions should surround the shellcode and have the different prototypes because VB6 compiler can union identical functions. Then it allocates the memory for the shellcode and copies the shellcode to there. Afterwards it calls the 'InitShellLoader' function, that splaces all the function in shellcode:

' // Shellcode initialization
Function InitShellLoader( _
 ByVal pShellCode As Long) As Boolean
 Dim hLib As Long:Dim sName As Long
 Dim sFunc As Long:Dim lpAddr As Long
 Dim libIdx As Long:Dim fncIdx As Long
 Dim libName As MessagesID: Dim fncName As MessagesID
 Dim fncSpc As Long:Dim splAddr As Long
 
 ' // +----------------------------------------------------------------+
 ' // | Fixing of API addresses|
 ' // +----------------------------------------------------------------+
 ' // | In order to call api function from shellcode i use splicing of |
 ' // | our VB functions and redirect call to corresponding api. |
 ' // |I did same in the code that injects to other process.|
 ' // +----------------------------------------------------------------+
 
 splAddr = GetAddr(AddressOf tVirtualAlloc) - GetAddr(AddressOf BEGINSHELLLOADER) + pShellCode
 
 ' // Get size in bytes between stub functions
 fncSpc = GetAddr(AddressOf tVirtualProtect) - GetAddr(AddressOf tVirtualAlloc)
 ' // Use 3 library: kernel32, ntdll и user32
 For libIdx = 0 To 2
 
' // Get number of imported functions depending on library
Select Case libIdx
Case 0: libName = API_LIB_KERNEL32: fncIdx = 13
Case 1: libName = API_LIB_NTDLL: fncIdx = 1
Case 2: libName = API_LIB_USER32: fncIdx = 1
End Select
' // Get library name from resources
sName = GetString(libName): If sName = 0 Then Exit Function
' // Get module handle
hLib = GetModuleHandle(ByVal sName): If hLib = 0 Then Exit Function
SysFreeString sName
' // Go thru functions
Do While fncIdx
 libName = libName + 1
 ' // Get function name
 sName = GetString(libName): If sName = 0 Then Exit Function
 
 ' // Because of GetProcAddress works with ANSI string translate it to ANSI
 sFunc = ToAnsi(sName): If sFunc = 0 Then Exit Function
 
 ' // Get function address
 lpAddr = GetProcAddress(hLib, sFunc)
 SysFreeString sName: SysFreeString sFunc
 
 ' // Error
 If lpAddr = 0 Then Exit Function
 
 ' // Splice stub
 Splice splAddr, lpAddr
 
 ' // Next stub
 splAddr = splAddr + fncSpc
 fncIdx = fncIdx - 1
 
Loop
 Next
 
 ' // Modify CallByPointer
 lpAddr = GetAddr(AddressOf CallByPointer) - GetAddr(AddressOf BEGINSHELLLOADER) + pShellCode
 
 ' // pop eax - 0x58
 ' // pop ecx - 0x59
 ' // push eax - 0x50
 ' // jmp ecx - 0xFFE1
 
 CopyMemory ByVal lpAddr, &HFF505958, 4
 CopyMemory ByVal lpAddr + 4, &HE1, 1
 ' // Success
 InitShellLoader = True
 
End Function
' // Splice function
Sub Splice( _
 ByVal Func As Long, _
 ByVal NewAddr As Long)
 ' // Set memory permissions
 VirtualProtect ByVal Func, 5, PAGE_EXECUTE_READWRITE, 0
 CopyMemory ByVal Func, &HE9, 1' // JMP
 CopyMemory ByVal Func + 1, NewAddr - Func - 5, 4 ' // Relative address
End Sub

Firstly it calculates the offset of the first "springboard" function (in this case it is tVirtualAlloc function) from beginning of the shellcode and calculates the distance in bytes between "springboard" functions. When VB6-compiler compiles an module it puts all the functions in the same order as in the code. The needed condition is to ensure the uni, que returned value from each function. Then it goes through all the needed libraries (kernel32, ntdll, user32 - in this order) and their functions. The first item in the resource strings is the library name followed by the functions names in this library. When an item is obtained it translates the function name to ANSI format and calls GetProcAddress function. Afterwards it calls the Splice function that makes up the "springboard" to the needed function from the shellcode. Eventually, it modified the CallByPointer function in order to ensure the jump from the shellcode to the loaded exe. Okay, further the RunExeFromMemory function splices the CallLoader in order to ensure the jump to shellcode from the main executable. After this operation is done the function begins to form the error message table (if needs) that is just the set of pointers to the messages strings. Eventually it call the spliced CallLoader function that jumps to the LoadExeFromMemory shellcode function that has already not been placed in main exe.



No comments:

Post a Comment