article

Loader, shellcode, without runtime (part 2)

Email
Submitted on: 12/26/2019 7:02:47 PM
By: The trick  
Level: Advanced
User Rating: By 1 Users
Compatibility: VB 6.0
Views: 3032
 
     The description of PE format. Creation of the simple VB6-EXE loader/packer. The VB6-loader without the runtime (msvbvm60) dependencies. And so on...

This article has accompanying files
 
				

This is the second part. The first one you can read here



Inside shellcode.

So, i made the several function inside the shellcode:

  1. LoadExeFromMemory - is the main function of the shellcode;
  2. GetImageNtHeaders - returns the IMAGE_NT_HEADERS structure and its address by the passed base address;
  3. GetDataDirectory - returns the IMAGE_DATA_DIRECTORY structure and its address by the passed base address and catalog index;
  4. EndProcess - shows the error message (if any) and ends of the process;
  5. ProcessSectionsAndHeaders - allocates the memory for all headers (DOS, NT, sections) and all the sections. Copies all data to the sections;
  6. ReserveMemory - reserves the sufficient memory for EXE;
  7. ProcessRelocations - adjusts the addresses if an exe has not been loaded to base address;
  8. ProcessImportTable - scans the import table of an exe file, loads the needed libraries and fills the import address table;
  9. SetMemoryPermissions - adjusts the memory permissions for each section;
  10. UpdateNewBaseAddress - refresh the new base address in the system structures PEB and LDR.

Due to the fact i can't use the VarPtr function, i made the similar function using the lstrcpyn function - IntPtr. So, the 'LoadExeFromMemory' function obtain firstly the NT headers and checks the processor architecture, whether the PE file is executable and whether the PE file is 32 bit application. If it is succeeded then the shellcode unload the main exe file from memory using the ZwUnmapViewOfSection function. If function has been succeeded the main exe file isn't in the memory anymore and the memory occupied by exe has been released. Henceforth we can't directly use API function, we should use our "springboards":
' // Parse exe in memory
Function LoadExeFromMemory( _
 ByVal pRawData As Long, _
 ByVal pMyBaseAddress As Long, _
 ByVal pErrMsgTable As Long) As Boolean
Dim NtHdrAs IMAGE_NT_HEADERS
Dim pBaseAs Long
Dim indexAs Long
Dim iError As ERROR_MESSAGES
Dim pszMsg As Long
' // Get IMAGE_NT_HEADERS
If GetImageNtHeaders(pRawData, NtHdr) = 0 Then
iError = EM_UNABLE_TO_GET_NT_HEADERS
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Check flags
If NtHdr.FileHeader.Machine <> IMAGE_FILE_MACHINE_I386 Or _
(NtHdr.FileHeader.Characteristics And IMAGE_FILE_EXECUTABLE_IMAGE) = 0 Or _
(NtHdr.FileHeader.Characteristics And IMAGE_FILE_32BIT_MACHINE) = 0 Then Exit Function
' // Release main EXE memory. After that main exe is unloaded from memory.
ZwUnmapViewOfSection GetCurrentProcess(), GetModuleHandle(ByVal 0&)
' // Reserve memory for EXE
iError = ReserveMemory(pRawData, pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Place data
iError = ProcessSectionsAndHeaders(pRawData, pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Update new base address
iError = UpdateNewBaseAddress(pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Import table processing
iError = ProcessImportTable(pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Relocations processing
iError = ProcessRelocations(pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Set the memory attributes
iError = SetMemoryPermissions(pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Release error message table
If pErrMsgTable Then
tVirtualFree pErrMsgTable, 0, MEM_RELEASE
End If
' // Call entry point
CallByPointer NtHdr.OptionalHeader.AddressOfEntryPoint + pBase
' // End process
EndProcess
End Function

Then shellcode calls the ReserveMemory function shown below. This function extracts the NT header from the loadable exe and tries to reserve the memory at 'ImageBase' address with the 'SizeOfImage' size. If it isn't succeeded the function checks if the exe file contains the relocation information. If so, it tries to reserve memory at any address. The relocation information allows to load an PE file to any address other than 'ImageBase'. It contains all the places where an exe uses the absolute addressing. You can adjust these places using the difference between the real base address and the 'ImageBase' field:
' // Reserve memory for EXE
Function ReserveMemory( _
 ByVal pRawExeData As Long, _
 ByRef pBase As Long) As ERROR_MESSAGES
Dim NtHdrAs IMAGE_NT_HEADERS
Dim pLocBaseAs Long
If GetImageNtHeaders(pRawExeData, NtHdr) = 0 Then
ReserveMemory = EM_UNABLE_TO_GET_NT_HEADERS
Exit Function
End If
' // Reserve memory for EXE
pLocBase = tVirtualAlloc(ByVal NtHdr.OptionalHeader.ImageBase, _
 NtHdr.OptionalHeader.SizeOfImage, _
 MEM_RESERVE, PAGE_EXECUTE_READWRITE)
If pLocBase = 0 Then
' // If relocation information not found error
If NtHdr.FileHeader.Characteristics And IMAGE_FILE_RELOCS_STRIPPED Then
ReserveMemory = EM_UNABLE_TO_ALLOCATE_MEMORY
Exit Function
Else
' // Reserve memory in other region
pLocBase = tVirtualAlloc(ByVal 0&, NtHdr.OptionalHeader.SizeOfImage, _
 MEM_RESERVE, PAGE_EXECUTE_READWRITE)
If pLocBase = 0 Then
ReserveMemory = EM_UNABLE_TO_ALLOCATE_MEMORY
Exit Function
End If
End If
End If
pBase = pLocBase
End Function

Okay, if memory reserving failed it shows the message with error and ends the application. Otherwise it calls the ProcessSectionsAndHeaders function. This function places all the headers to the allocated memory, extracts the information about all the sections and copies all the data to sections. If an section has the uninitialized data it fills this region with zero:
' // Allocate memory for sections and copy them data to there
Function ProcessSectionsAndHeaders( _
 ByVal pRawExeData As Long, _
 ByVal pBase As Long) As ERROR_MESSAGES
Dim iSecAs Long
Dim pNtHdr As Long
Dim NtHdrAs IMAGE_NT_HEADERS
Dim sec As IMAGE_SECTION_HEADER
Dim lpSecAs Long
Dim pDataAs Long
pNtHdr = GetImageNtHeaders(pRawExeData, NtHdr)
If pNtHdr = 0 Then
ProcessSectionsAndHeaders = EM_UNABLE_TO_GET_NT_HEADERS
Exit Function
End If
' // Alloc memory for headers
pData = tVirtualAlloc(ByVal pBase, NtHdr.OptionalHeader.SizeOfHeaders, MEM_COMMIT, PAGE_READWRITE)
If pData = 0 Then
ProcessSectionsAndHeaders = EM_UNABLE_TO_ALLOCATE_MEMORY
Exit Function
End If
' // Copy headers
tCopyMemory pData, pRawExeData, NtHdr.OptionalHeader.SizeOfHeaders
' // Get address of beginnig of sections headers
pData = pNtHdr + Len(NtHdr.Signature) + Len(NtHdr.FileHeader) + NtHdr.FileHeader.SizeOfOptionalHeader
' // Go thru sections
For iSec = 0 To NtHdr.FileHeader.NumberOfSections - 1
' // Copy section descriptor
tCopyMemory IntPtr(sec.SectionName(0)), pData, Len(sec)
' // Alloc memory for section
lpSec = tVirtualAlloc(sec.VirtualAddress + pBase, sec.VirtualSize, MEM_COMMIT, PAGE_READWRITE)
If lpSec = 0 Then
ProcessSectionsAndHeaders = EM_UNABLE_TO_ALLOCATE_MEMORY
Exit Function
End If
' If there is initialized data
If sec.SizeOfRawData Then
' // Take into account file alignment
If sec.SizeOfRawData > sec.VirtualSize Then sec.SizeOfRawData = sec.VirtualSize
' // Copy initialized data to section
tCopyMemory lpSec, pRawExeData + sec.PointerToRawData, sec.SizeOfRawData
lpSec = lpSec + sec.SizeOfRawData
sec.VirtualSize = sec.VirtualSize - sec.SizeOfRawData
End If
' // Fill remain part with zero
tFillMemory lpSec, sec.VirtualSize, 0
' // Next section
pData = pData + Len(sec)
Next
End Function

Then the LoadExeFromMemory function calls the UpdateNewBaseAddress function that update the new base address in the user-mode system structures. Windows creates the special stucture named PEB (Process Environment Block) for each process. This is the very usefull structure that allows to obtain the very many information about the process. Many API functions gets information from this structure. For example GetModuleHandle(NULL) takes the returned value from the PEB.ImageBaseAddress or GetModuleHandle("MyExename") takes the returned value from the PEB.Ldr list of the loaded modules. We should update this information according the new base address in order to API functions retrieve the correct values. The small part of PEB structure is shown below:
Type PEB
NotUsed As Long
Mutant As Long
ImageBaseAddressAs Long
LoaderData As Long ' // Pointer to PEB_LDR_DATA
ProcessParametersAs Long
' // ....
End Type

We are interested only the 'ImageBaseAddress' and 'LoaderData' fields. The first field contains the base address of an exe file. The second field contains the pointer to the PEB_LDR_DATA structure that describes all the loaded modules in the process:
Type PEB_LDR_DATA
Length As Long
Initialized As Long
SsHandleAs Long
InLoadOrderModuleListAs LIST_ENTRY
InMemoryOrderModuleList As LIST_ENTRY
InInitializationOrderModuleList As LIST_ENTRY
End Type

This structure contains the three doubly-linked lists that describe each module. The 'InLoadOrderModuleList' list contains the links to items in the loading oreder item, i.e. the items in this list is placed in loading order (the first module is at beginning). The 'InMemoryOrderModuleList' is same only in order of placing in memory, 'InInitializationOrderModuleList' in initialization order. We should get the first element of 'InLoadOrderModuleList' list that is the pointer to structure LDR_MODULE:
Type LDR_MODULE
InLoadOrderModuleListAs LIST_ENTRY
InMemoryOrderModuleList As LIST_ENTRY
InInitOrderModuleListAs LIST_ENTRY
BaseAddress As Long
EntryPoint As Long
SizeOfImage As Long
FullDllName As UNICODE_STRING
BaseDllName As UNICODE_STRING
FlagsAs Long
LoadCountAs Integer
TlsIndexAs Integer
HashTableEntry As LIST_ENTRY
TimeDateStampAs Long
End Type

This structure describes an module. The first element of 'InLoadOrderModuleList' is the main exe module descriptor. We should change the 'BaseAddress' field to new value and save changes. So, in order to obtain the PEB structure we can use the universal function NtQueryInformationProcess that extract the many useful information about process (read more in 'Windows NT/2000 Native API Reference' by Gary Nebbett). The PEB structure can be obtained from the PROCESS_BASIC_INFORMATION structure that describes the basic information about the process:
Type PROCESS_BASIC_INFORMATION
ExitStatus As Long
PebBaseAddress As Long
AffinityMaskAs Long
BasePriorityAs Long
UniqueProcessId As Long
InheritedFromUniqueProcessIdAs Long
End Type

The 'PebBaseAddress' field contains the address of the PEB structure.

In order to obtain the PROCESS_BASIC_INFORMATION structure we should pass the ProcessBasicInformation as the class information to NtQueryInformationProcess function. Because of structure size may change in various versions of Windows i use the heap memory for extracting the PROCESS_BASIC_INFORMATION structure. If the size doesn't suit it increases the size and repeats again:

Function UpdateNewBaseAddress( _
 ByVal pBase As Long) As ERROR_MESSAGES
Dim pPBIAs Long:Dim PBIlen As Long
Dim PBI As PROCESS_BASIC_INFORMATION:Dim cPEBAs PEB
Dim ntstat As Long
Dim ldrData As PEB_LDR_DATA
Dim ldrMod As LDR_MODULE
ntstat = tNtQueryInformationProcess(tGetCurrentProcess(), ProcessBasicInformation, IntPtr(PBI.ExitStatus), Len(PBI), PBIlen)
Do While ntstat = STATUS_INFO_LENGTH_MISMATCH
PBIlen = PBIlen * 2
If pPBI Then
tHeapFree tGetProcessHeap(), HEAP_NO_SERIALIZE, pPBI
End If
pPBI = tHeapAlloc(tGetProcessHeap(), HEAP_NO_SERIALIZE, PBIlen)
ntstat = tNtQueryInformationProcess(tGetCurrentProcess(), ProcessBasicInformation, pPBI, PBIlen, PBIlen)
Loop
If ntstat <> STATUS_SUCCESS Then
UpdateNewBaseAddress = EM_PROCESS_INFORMATION_NOT_FOUND
GoTo CleanUp
End If
If pPBI Then
' // Copy to PROCESS_BASIC_INFORMATION
tCopyMemory IntPtr(PBI.ExitStatus), pPBI, Len(PBI)
End If
' // Get PEB
tCopyMemory IntPtr(cPEB.NotUsed), PBI.PebBaseAddress, Len(cPEB)
' // Modify image base
cPEB.ImageBaseAddress = pBase
' // Restore PEB
tCopyMemory PBI.PebBaseAddress, IntPtr(cPEB.NotUsed), Len(cPEB)
' // Fix base address in PEB_LDR_DATA list
tCopyMemory IntPtr(ldrData.Length), cPEB.LoaderData, Len(ldrData)
' // Get first element
tCopyMemory IntPtr(ldrMod.InLoadOrderModuleList.Flink), ldrData.InLoadOrderModuleList.Flink, Len(ldrMod)
' // Fix base
ldrMod.BaseAddress = pBase
' // Restore
tCopyMemory ldrData.InLoadOrderModuleList.Flink, IntPtr(ldrMod.InLoadOrderModuleList.Flink), Len(ldrMod)
CleanUp:
' // Free memory
If pPBI Then
tHeapFree tGetProcessHeap(), HEAP_NO_SERIALIZE, pPBI
End If
End Function

After updating of the base address in the system structures the shellcode calls the ProcessImportTable function that loads the needed libraryes for exe file. Firstly it gets the IMAGE_DIRECTORY_ENTRY_IMPORT directory that contains the RVA of the array of the IMAGE_IMPORT_DESCRIPTOR structures:
Type IMAGE_IMPORT_DESCRIPTOR
Characteristics As Long
TimeDateStampAs Long
ForwarderChain As Long
pNameAs Long
FirstThunk As Long
End Type

Each structure describes the single DLL. The 'pName' field contains the RVA to the ASCIIZ library name. The 'Characteristics' field contains the RVA to the table of the imported function names and 'FirstThunk' contains the RVA of the import addresses table. The names table is the array of IMAGE_THUNK_DATA structures that is the 32 bit Long value. If the most significant bit is set the remaining bits represents the ordinal of the function (import by ordinal). Otherwise the remaining bits contains the RVA of the function name prenexed by 'Hint' value. If the IMAGE_THUNK_DATA structure contains zero it means that no more names. If all the fields of the IMAGE_IMPORT_DESCRIPTOR equal zero it means that list of structureas is ended.
' // Process import table
Function ProcessImportTable( _
 ByVal pBase As Long) As ERROR_MESSAGES
Dim NtHdrAs IMAGE_NT_HEADERS:Dim datDirectoryAs IMAGE_DATA_DIRECTORY
Dim dsc As IMAGE_IMPORT_DESCRIPTOR: Dim hLibAs Long
Dim thnkAs Long:Dim AddrAs Long
Dim fnc As Long:Dim pDataAs Long
If GetImageNtHeaders(pBase, NtHdr) = 0 Then
ProcessImportTable = EM_UNABLE_TO_GET_NT_HEADERS
Exit Function
End If
' // Import table processing
If NtHdr.OptionalHeader.NumberOfRvaAndSizes > 1 Then
If GetDataDirectory(pBase, IMAGE_DIRECTORY_ENTRY_IMPORT, datDirectory) = 0 Then
ProcessImportTable = EM_INVALID_DATA_DIRECTORY
Exit Function
End If
' // If import table exists
If datDirectory.Size > 0 And datDirectory.VirtualAddress > 0 Then
' // Copy import descriptor
pData = datDirectory.VirtualAddress + pBase
tCopyMemory IntPtr(dsc.Characteristics), pData, Len(dsc)
' // Go thru all descriptors
Do Until dsc.Characteristics = 0 And _
 dsc.FirstThunk = 0 And _
 dsc.ForwarderChain = 0 And _
 dsc.pName = 0 And _
 dsc.TimeDateStamp = 0
If dsc.pName > 0 Then
' // Load needed library
hLib = tLoadLibrary(dsc.pName + pBase)
If hLib = 0 Then
ProcessImportTable = EM_LOADLIBRARY_FAILED
Exit Function
End If
If dsc.Characteristics Then fnc = dsc.Characteristics + pBase Else fnc = dsc.FirstThunk + pBase
' // Go to names table
tCopyMemory IntPtr(thnk), fnc, 4
' // Go thru names table
Do While thnk
' // Check import type
If thnk < 0 Then
' // By ordinal
Addr = tGetProcAddress(hLib, thnk And &HFFFF&)
Else
' // By name
Addr = tGetProcAddress(hLib, thnk + 2 + pBase)
End If
' // Next function
fnc = fnc + 4
tCopyMemory IntPtr(thnk), fnc, 4
tCopyMemory dsc.FirstThunk + pBase, IntPtr(Addr), 4
dsc.FirstThunk = dsc.FirstThunk + 4
Loop
End If
' // Next descriptor
pData = pData + Len(dsc)
tCopyMemory IntPtr(dsc.Characteristics), pData, Len(dsc)
Loop
End If
End If
 
End Function

The ProcessRelocation function is called then. This functions adjust all the absolute references (if any). It obtains the IMAGE_DIRECTORY_ENTRY_BASERELOC catalog that contains the RVA to the array of IMAGE_BASE_RELOCATION structures. Each item in this list contains the settings within 4KB relative 'VirtualAddress' fields:
Type IMAGE_BASE_RELOCATION
VirtualAddress As Long
SizeOfBlock As Long
End Type

The 'SizeOfBlock' contains the size of item in bytes. The array of 16 bits numbers is placed after the each IMAGE_BASE_RELOCATION structure. You can calculate number of this strucuture as (SizeOfBlock - Len(IMAGE_BASE_RELOCATION)) Len(Integer). Each element of the array of the descriptors has the following structure:
img

The high four bits contains the type of relocation. We are interested the IMAGE_REL_BASED_HIGHLOW type that means we should add the difference (RealBaseAddress - ImageBaseAddress) to a Long that is at the address 'VirtualAddress' + 12 least bits of descriptors. Array of IMAGE_BASE_RELOCATION structures is ended with stucture where all fields is zero:
' // Process relocations
Function ProcessRelocations( _
 ByVal pBase As Long) As ERROR_MESSAGES
Dim NtHdrAs IMAGE_NT_HEADERS:Dim datDirectoryAs IMAGE_DATA_DIRECTORY
Dim relBase As IMAGE_BASE_RELOCATION:Dim entriesCountAs Long
Dim relType As Long:Dim dwAddressAs Long
Dim dwOrig As Long:Dim pRelBaseAs Long
Dim deltaAs Long:Dim pDataAs Long
' // Check if module has not been loaded to image base value
If GetImageNtHeaders(pBase, NtHdr) = 0 Then
ProcessRelocations = EM_UNABLE_TO_GET_NT_HEADERS
Exit Function
End If
delta = pBase - NtHdr.OptionalHeader.ImageBase
' // Process relocations
If delta Then
' // Get address of relocation table
If GetDataDirectory(pBase, IMAGE_DIRECTORY_ENTRY_BASERELOC, datDirectory) = 0 Then
ProcessRelocations = EM_INVALID_DATA_DIRECTORY
Exit Function
End If
If datDirectory.Size > 0 And datDirectory.VirtualAddress > 0 Then
' // Copy relocation base
pRelBase = datDirectory.VirtualAddress + pBase
tCopyMemory IntPtr(relBase.VirtualAddress), pRelBase, Len(relBase)
Do While relBase.VirtualAddress
' // To first reloc chunk
pData = pRelBase + Len(relBase)
entriesCount = (relBase.SizeOfBlock - Len(relBase)) 2
Do While entriesCount > 0
tCopyMemory IntPtr(relType), pData, 2
Select Case (relType 4096) And &HF
Case IMAGE_REL_BASED_HIGHLOW
' // Calculate address
dwAddress = relBase.VirtualAddress + (relType And &HFFF&) + pBase
' // Get original address
tCopyMemory IntPtr(dwOrig), dwAddress, Len(dwOrig)
' // Add delta
dwOrig = dwOrig + delta
' // Save
tCopyMemory dwAddress, IntPtr(dwOrig), Len(dwOrig)
End Select
pData = pData + 2
entriesCount = entriesCount - 1
Loop
' // Next relocation base
pRelBase = pRelBase + relBase.SizeOfBlock
tCopyMemory IntPtr(relBase.VirtualAddress), pRelBase, Len(relBase)
Loop
End If
End If
End Function

After relocations processing shellcode calls the function SetMemoryPermissions that adjusts the memory protection for each section according to the 'Characteristics' field of IMAGE_SECTION_HEADER structure. It just calls the VirtualProtect function with the certain memory attributes:

' // Set memory permissions
Private Function SetMemoryPermissions( _
 ByVal pBase As Long) As ERROR_MESSAGES
Dim iSecAs Long:Dim pNtHdr As Long
Dim NtHdrAs IMAGE_NT_HEADERS:Dim sec As IMAGE_SECTION_HEADER
Dim AttrAs MEMPROTECT: Dim pSecAs Long
Dim ret As Long
pNtHdr = GetImageNtHeaders(pBase, NtHdr)
If pNtHdr = 0 Then
SetMemoryPermissions = EM_UNABLE_TO_GET_NT_HEADERS
Exit Function
End If
' // Get address of first section header
pSec = pNtHdr + 4 + Len(NtHdr.FileHeader) + NtHdr.FileHeader.SizeOfOptionalHeader
' // Go thru section headers
For iSec = 0 To NtHdr.FileHeader.NumberOfSections - 1
' // Copy section descriptor
tCopyMemory IntPtr(sec.SectionName(0)), pSec, Len(sec)
' // Get type
If sec.Characteristics And IMAGE_SCN_MEM_EXECUTE Then
If sec.Characteristics And IMAGE_SCN_MEM_READ Then
If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
Attr = PAGE_EXECUTE_READWRITE
Else
Attr = PAGE_EXECUTE_READ
End If
Else
If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
Attr = PAGE_EXECUTE_WRITECOPY
Else
Attr = PAGE_EXECUTE
End If
End If
Else
If sec.Characteristics And IMAGE_SCN_MEM_READ Then
If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
Attr = PAGE_READWRITE
Else
Attr = PAGE_READONLY
End If
Else
If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
Attr = PAGE_WRITECOPY
Else
Attr = PAGE_NOACCESS
End If
End If
End If
' // Set memory permissions
If tVirtualProtect(sec.VirtualAddress + pBase, sec.VirtualSize, Attr, IntPtr(ret)) = 0 Then
SetMemoryPermissions = EM_UNABLE_TO_PROTECT_MEMORY
Exit Function
End If
' // Next section
pSec = pSec + Len(sec)
Next
End Function

Eventually it frees the message table (if any) and calls the entry point of the loaded exe. In the previous version of the loader i unloaded the shellcode too but some exe doesn't call ExitProcess therefore it can causes the crash. The loader has been done.
Although we write the loader without runtime usage the VB6 compiler adds it because all the OBJ files has references to MSVBVM60 during compilation. We have to remove the runtime from the import table of the loader manually. I made the special utility - Patcher that searches runtime in the import table and the bound import table and removes it. This utility is helpful for the VB kernel drivers too. I won't describe the its work because it uses same concepts of the PE format that we already examined. Overall we get the working exe that doesn't use MSVBVM60 runtime on the target machine.

In order to use the loader you should compile it then you should run the patcher and patch compiled loader. Afterwards you can use the compiler.

I hope you enjoyed it. Thank for attention!
Regards,
The trick.

winzip iconDownload article

Note: Due to the size or complexity of this submission, the author has submitted it as a .zip file to shorten your download time. Afterdownloading it, you will need a program like Winzip to decompress it.Virus note:All files are scanned once-a-day by Planet Source Code for viruses, but new viruses come out every day, so no prevention program can catch 100% of them. For your own safety, please:
  1. Re-scan downloaded files using your personal virus checker before using it.
  2. NEVER, EVER run compiled files (.exe's, .ocx's, .dll's etc.)--only run source code.
  3. Scan the source code with Minnow's Project Scanner

If you don't have a virus scanner, you can get one at many places on the net including:McAfee.com


Other 6 submission(s) by this author

 


Report Bad Submission
Use this form to tell us if this entry should be deleted (i.e contains no code, is a virus, etc.).
This submission should be removed because:

Your Vote

What do you think of this article (in the Advanced category)?
(The article with your highest vote will win this month's coding contest!)
Excellent  Good  Average  Below Average  Poor (See voting log ...)
 

Other User Comments

12/26/2019 7:21:00 PMEddie Bole

Now I've got lots of reading to do. Thanks The Trick for the great upload.
(If this comment was disrespectful, please report it.)

 
12/28/2019 12:18:02 PMVB6

Thank you for all of this !
(If this comment was disrespectful, please report it.)

 

Add Your Feedback
Your feedback will be posted below and an email sent to the author. Please remember that the author was kind enough to share this with you, so any criticisms must be stated politely, or they will be deleted. (For feedback not related to this particular article, please click here instead.)
 

To post feedback, first please login.