Pure WIN32 Self-Extract EXE Builder

Pure WIN32 Self-Extract EXE Builder
By hackzai

Sample Image - selfextract70.jpg

Why I developed it

Sometimes, those off-the-shelf installation tools might require you to write some script in order to properly copy those distributed files into the defined folder. Yet, it may not best fit your requirement as in our real software development community, there is always some demanding boss keeping on asking more but paying less for the development tools. That's the reason why I have started to design & develop my first self-extract executable file to support the new LiveUpdate module in my application.

After I developed it, I found I can make it even better by reusing the same code (EXE) more efficiently in the future. So finally, I have come-out with my own Self-Extract EXE builder which will scan the specified folder and accumulate each file (binary data) into a single binary file, followed by compressed it using the Zlib* algorithm and subsequently injecting it into the embedded self-extract executable file by the UpdateResource API.

Note: This API is only supported on Windows 2000/XP/NT but not Windows 95/98/ME.

*Zlib compression algorithm was Copyright © 1995-2003 Jean-loup Gailly and Mark Adler. Please visit Zlib to learn more about the Zlib compression algorithm.

Methodology behind the scene

The methodology applied in this project is simple and straight forward. As the entire project was split into 3 separate modules, each module takes place at different stages in order to achieve the final goal (distribute any files via a self-extract executable file)!

ItemModuleDescription
1.SelfExtract.exe

Core module #1, which will go through the following (pre-distribute) stages:

  1. Provide GUI which enables the user to specify the source folder and output executable filename.
  2. Scan the source file from the user specified folder.
  3. Build the source file information block and combine individual source files data into a binary file.
  4. Merge the source file information block and binary data into a single file.
  5. Compress the merged binary file.
  6. Spawn the SetupEx.exe from the embedded custom resource table (EXTRACTOR).
  7. Inject compressed binary data into the SetupEx.exe custom resource table (SETUP).
  8. Copy the final file into the user specified file path and name.
  9. Delete all the created temporary files and release all the allocated memory.
  10. Notify user about the process completed.
2.SetupEx.exe

Core module #2, which will go through the following (post-distribute) stages:

  1. Provide GUI which enables the user to specify the output folder.
  2. Read the compressed binary data from custom resource table (SETUP) and write into a temporary file.
  3. Uncompress the binary data.
  4. Read the source file information block from the temporary file.
  5. Read individual block of binary data based on the file size specified in the source file information block.
  6. Write the data into the temporary file using the filename stored under the source file information block.
  7. Update the temporary file's file time with the value stored under the source file information block.
  8. Copy the temporary file from temporary folder into the user specified output folder.
  9. Delete all the created temporary files and release all the allocated memory.
  10. Notify user about the process completed.
3.Zlib10.lib

Core module #3, which is used to compress and uncompress the distributed data at both (pre/post-distribute) stages:

Although the code of this module is free to distribute, it is Copyright © 1995-2003 Jean-loup Gailly and Mark Adler. I just recompiled it onto a static library which will make the first 2 module code look more clean. You can always refer to the Zlib for detail information about how the compress and uncompress algorithm work, because I am not going to explain this complicated algorithm code and it is beyond my capability. :)

I also included the sample source code, which will offer you a better understanding on the process involved in the above first 2 modules (SelfExtract.exe and SetupEx.exe).

The format I adopted

The format I used to merge multiple files into a single file is simple. In general, it will split into 3 parts:

SectionDescription
Setup InfoInclude the first 4 bytes (DWORD) value which indicate how many files are available in the following Header and Data section. The following 260 bytes will be stored the Auto-Exec filename, which will eventually be launched by the Self-Extract module right after successfully extracting the embedded data into the selected destination folder.
HeaderN block of EXTRACTFILEINFO, where N will be the value (dwFileCount) stored in the Setup Info section.
DataN block of file content data, where N will be the value (dwFileCount) stored in the Setup Info section.

First part will indicate how many files are available in the merge data file. Second and third parts will then carry a multiple block of information, as this will vary with how many files (0, 1, 2, ...N) you are going to distribute using the self-extract executable file. In summary, the overview of the merge data file format will be like the figure shown below:

Merge Data File Format.

Reason I adopted this format

The thousand and one reason I adopted this format is because it was so easy to build (write) and extract (read). All it needs is refer to individual file information stored under the header block. So, you can say the header block is the heart of the entire merge data file in this project. Hence, a single silly mistake made in the header block will cause the SetupEx.exe to fail to extract each individual data and restore with its original file property.

Information stored in the setup info block

In this project, information available in this section will be used by the spawned SetupEx.exe module to identify how many files' information are available in the Header section, as well as the auto-exec filename also stored under this section. No doubt, you can add in more specified information that fits into your own application needs. Because, this project will act as the framework which will speed up your development.

typedef struct tagSETUPINFO
{
DWORD dwFileCount;
char szAutoExecFile[MAX_PATH];
} SETUPINFO, FAR * LPSETUPINFO;

What should be stored inside the header block

The original file information will be stored under this header block. As this information is subject to change for different application needs, some might need full range of original file information, and some may not. For this project, I stored the information which is just nice for distributing those files that do not require extra process (like DLL Register) after deployment.

Again, all this information must be stored in a pre-defined structure which will be fully understood between both the SelfExtract.exe and SetupEx.exe. Other wise, you'll have no problem in building the merged data file. But later, you'll have problem in extracting it. As the data location pointer (memory location) was running out, it will cause the SetupEx.exe over-read or read less data as compared with the original file. So, this structure is very important in the entire merge data file.

Below is the pre-defined EXTRACTFILEINFO structure, which will be stored under the header block and used throughout this project.

typedef struct tagEXTRACTFILEINFO
{
// Running index value of the current file out of the
// total distributed file count.
DWORD dwIndex;
// Original file created time.
FILETIME CreateTime;
// Original file last read/written time.
FILETIME LastAcessTime;
// Original file last written time.
FILETIME LastWriteTime;
// Specifies the high-order DWORD value
// of the file size, in bytes.
DWORD dwFileSizeHigh;
// Specifies the low-order DWORD
// value of the file size, in bytes.
DWORD dwFileSizeLow;
// A null-terminated string that
// is the name of the original file.
char szBinFileName[MAX_PATH];
} EXTRACTFILEINFO, FAR * LPEXTRACTFILEINFO;

The source of all this information comes from the WIN32_FIND_DATA structure as shown below:

typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
TCHAR cFileName[ MAX_PATH ];
TCHAR cAlternateFileName[ 14 ];
} WIN32_FIND_DATA, *PWIN32_FIND_DATA;

Other file information you can include into EXTRACTFILEINFO structure (to enhance the current project) are like those 2 shown below. But like I say, this is subject to your application needs.

For instance, if you wish to distribute an ActiveX, then most likely you cannot skip the DLL registration (by executing the regsvr32.exe) process right after deployment to the target machine. Therefore, based on the bDllSelfRegister value, the self-extract executable will automate the registering of the ActiveX DLL for you.

If you wish to include sub-folder into your distribution package, you will require to include the dwAttributes information. This indication will instruct the self-extract executable file to create the respective sub-folder before it proceeds to deploy those files which sits inside this sub-folder. Else, the entire process will fail.

typedef struct tagEXTRACTFILEINFO
// Original file attributes.
DWORD dwFileAttributes;
// Indicate the file require
// (most likely is *.DLL)
// is require register or not.
BOOL bDllSelfRegister;
} EXTRACTFILEINFO, FAR * LPEXTRACTFILEINFO;

Retrieve file information

Before proceeding to create the merge data file, it is important to know how to get the respective file information. If we fail to achieve this, no point for us to proceed further from here. But to achieve this simply, the WIN32 SDK does provide us the FindFirstFile or GetFileAttributesEx API. These APIs will return all the information we need.

But, this project was scanning the specified folder without knowing the filename at the first place. So, GetFileAttributesEx will be returning duplicate file information. The FindFirstFile does return those information which is returned by GetFileAttributesEx. Below is the code snippet:

HANDLE hFile               = NULL;
WIN32_FIND_DATA wfs = {NULL};

// Get the specified file information by using
// the FindFirstFile instead of GetFileAttributesEx
hFile = FindFirstFile("C://SelfExtract.exe", &wfs);

// Varify the reutn find file handle
if (NULL != hFile || INVALID_HANDLE_VALUE != hFile)
{
// Copy those file information you need
// into your define local/global variable here.
}

// Close the search file handle
if (NULL != hFile) {FindClose(hFile);}
hFile = NULL;

The way I scan files

Since the current GUI of the SelfExtract.exe module only supports user to specify the folder (full path) whereby those source files are saved, there is a need that the SelfExtract.exe module be able to scan through the specified folder and retrieve each available file information as well as its content before the merge data file can be successfully built.

The code I use to scan a folder is exactly the same as the code I use to retrieve the file information. The only difference is now, it requires a do...while loop instead of just calling the API once, and the code snippet is shown below:

HANDLE hFile               = NULL;
WIN32_FIND_DATA wfs = {NULL};

// Start scaning the directory
hFile = FindFirstFile("C://*.*", &wfs);

// Check the return handle value
do
{
// Check is the current found file is directory?
if (!(FILE_ATTRIBUTE_DIRECTORY & wfs.dwFileAttributes))
{
// Put your code to read the current file information here.
}

// Scan the next match item in the directory
if (!FindNextFile(hFile, &wfs))
{
if (ERROR_NO_MORE_FILES == GetLastError()) {break;}
}

} while (NULL != hFile || INVALID_HANDLE_VALUE != hFile);

// Close the search file handle
if (NULL != hFile) {FindClose(hFile);}
hFile = NULL;

Basic file access

Now, you have no problem in scanning any specified folder. You still need to know how to open the file and read its content into a local variable. Without this, this project will not be complete, as it fails to create the merge data file. But, life is easy. Since the WIN32 SDK does provide us the necessary file I/O API, all you need to understand and remember are the following few APIs:

WIN32 APIdescription
CreateFileOpen or create a file on the disk drive.
SetFilePointerMove the current open or create file pointer.
ReadFileRead partial or full of the current open file content.
WriteFileWrite data into an open file.
CloseHandleClose the current open file when it is no longer used.
// Create new file and write data into it.

HANDLE hFile1 = NULL;
DWORD dwByteWrite = 0;

// Create a new text file
hFile1 = CreateFile("C://sample.txt",
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);

// Check the return handle value
if (NULL != hFile1 && INVALID_HANDLE_VALUE != hFile1)
{
// Move the file pointer to the begin of the file
SetFilePointer(hFile1, 0, 0, FILE_BEGIN);
// Write the data into newly create file
WriteFile(hFile1,
"Hello world",
strlen("Hello world"),
&dwByteWrite,
NULL);
}
else
{
// Notify user about the error
MessageBox(hWnd, "Fail to create file!",
APP_TITLE, MB_OK | MB_ICONSTOP);
}

// Close the current open data file
if (NULL != hFile1) {CloseHandle(hFile1);}
hFile1 = NULL;
// Open existing file and read the contents.

HANDLE hFile = NULL;
DWORD dwByteRead = 0;
DWORD dwFileSize = 0;
LPBYTE lpData = NULL;

// Open the existing file
hFile = CreateFile("C://sample.txt",
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);

// Check the return handle value
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
// Get the current file size
dwFileSize = GetFileSize(hFile, NULL);

// Allocate local data buffer
lpData = (LPBYTE)LocalAlloc(LPTR, dwFileSize);
// Reset local data buffer
ZeroMemory(lpData, dwFileSize);

// Move the file pointer to the begining
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
// Read the data from the current open file
ReadFile(hFile, lpData, dwFileSize, &dwByteRead, NULL);
}
else
{
// Notify user about the error
MessageBox(hWnd, "Fail to open file!",
APP_TITLE, MB_OK | MB_ICONSTOP);
}

// Close the open file handle
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;

How to build the merge data file

At this stage, you have gain all the minimum knowledge on accessing the file I/O. Also, you get the idea how the final merge data file is being structured. So, it is the big time for the core task of the entire project... building the merge data file. Before I start explaining the most interesting part (program code) step by step, let's recap some of the important steps to build the merge data file.

StepAction
1.Scan the specified folder to retrieve the individual file information.
2.Save these read file information into a temporary file (let's say, temp1.tmp, note that the data writing sequence will be FIFO).
3.Save these content into another temporary file (let's say, temp2.tmp, note that the data writing sequence will be FIFO).
4.Repeat step #2-#3 until there is no more file available from the specified folder.
5.Append the content in the second temporary file (temp2.tmp) into the end of the first temporary file (temp1.tmp).
6.Compress the merge data file (temp1.tmp) into a new temporary file (temp3.tmp).
7.Congratulations, you have successfully built your first merge data file.

Note: In the following code snippet, I was filtering out those code related to GUI update. This GUI update is mainly to keep the user informed about the status of the entire building of the self-extract executable file process. If you wish to get more about this GUI update code, you can always refer to the full source code enclosed together with this article.

Basically, the following code snippet will cover step #1 and #4, which is a do...while loop to scan through the folder:

// Get the current user define source folder
GetDlgItemText(hWnd, IDC_EDIT1, szBuffer1, sizeof(szBuffer1));

// Format the full path for the source folder
sprintf(szBuffer2,
"%s//*.*",
szBuffer1);

// Start scaning the directory
hFile = FindFirstFile(szBuffer2, &wfs);

// Check the return handle value
do
{
// Check is the current found file is directory?
if (!(FILE_ATTRIBUTE_DIRECTORY & wfs.dwFileAttributes))
{
// Save the information into data file
WriteSelfExtractHeader (hWnd, &wfs);
// Save the information into data file
WriteSelfExtractBinData (hWnd, &wfs);
}

// Scan the next match item in the directory
if (!FindNextFile(hFile, &wfs))
{
if (ERROR_NO_MORE_FILES == GetLastError()) {break;}
}

} while (NULL != hFile || INVALID_HANDLE_VALUE != hFile);

// Close the search handle
if (NULL != hFile) {FindClose(hFile);}
hFile = NULL;

Next block of code snippet will cover the reading & writing of the file information into the header block, as well as the code for reading & writing the content of the current file.

Note: The file size information is very important in the (SetupEx.exe) module, because this will be the key information that instructs the SetupEx.exe how much it should move the file pointer in order to get back to the original staring point of each individual distributed file.

void WriteSelfExtractHeader (HWND hWnd, LPWIN32_FIND_DATA lpFindFileData)
{
EXTRACTFILEINFO efi = {NULL};

HANDLE hFile = NULL;
DWORD dwByteWrite = 0;

__try
{
// Open the existing temporary data file
hFile = CreateFile(szTmpBinFile1,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);

// Check the return handle value
if (NULL == hFile || INVALID_HANDLE_VALUE == hFile)
{
// Open the existing temporary data file
hFile = CreateFile(szTmpBinFile1,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
NULL);
}

// Check the return handle value
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
// Reset the local EXTRACTFILEINFO structure
ZeroMemory(&efi, sizeof(EXTRACTFILEINFO));

// Initialize the EXTRACTFILEINFO structure
efi.dwIndex = dwFileCount;
CopyMemory(&efi.CreateTime,
&lpFindFileData->ftCreationTime, sizeof(FILETIME));
CopyMemory(&efi.LastAcessTime,
&lpFindFileData->ftLastAccessTime, sizeof(FILETIME));
CopyMemory(&efi.LastWriteTime,
&lpFindFileData->ftLastWriteTime, sizeof(FILETIME));
CopyMemory(&efi.dwFileSizeHigh,
&lpFindFileData->nFileSizeHigh , sizeof(DWORD));
CopyMemory(&efi.dwFileSizeLow,
&lpFindFileData->nFileSizeLow, sizeof(DWORD));
CopyMemory(&efi.szBinFileName,
&lpFindFileData->cFileName,
strlen(lpFindFileData->cFileName));

// Check current file count & move the file pointer
if (0 == dwFileCount)
SetFilePointer(hFile, sizeof(UPDATEINFO), 0, FILE_BEGIN);
else
SetFilePointer(hFile, 0, 0, FILE_END);

// Write the data into setup list file
WriteFile(hFile,
&efi,
sizeof(EXTRACTFILEINFO),
&dwByteWrite,
NULL);

// Increate the counter
dwFileCount++;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
// PUT YOUR ERROR HANDLING CODE HERE

}

// Close the open file handle
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;

}

void WriteSelfExtractBinData (HWND hWnd, LPWIN32_FIND_DATA lpFindFileData)
{
HANDLE hFile = NULL;
LPBYTE lpData = NULL;
DWORD dwSize = 0;
DWORD dwByteRead = 0;
DWORD dwByteWrite = 0;

char szBuffer1[MAX_PATH] = {NULL};
char szBuffer2[MAX_PATH] = {NULL};

__try
{
// Get the user define source folder
GetDlgItemText(hWnd, IDC_EDIT1, szBuffer1, sizeof(szBuffer1));
// Format the full file path
sprintf(szBuffer2,
"%s//%s",
szBuffer1, lpFindFileData->cFileName);

// STAGE #1
// Read the current binary file data
hFile = CreateFile(szBuffer2,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
// Check the return handle value
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
// Get the current file size
dwSize = (lpFindFileData->nFileSizeHigh*(MAXDWORD+1)) +
lpFindFileData->nFileSizeLow;
// Allocate local data buffer
lpData = (LPBYTE)LocalAlloc(LPTR, dwSize);
// Reset local data buffer
ZeroMemory(lpData, dwSize);

// Move the file pointer to the begining
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
// Read the binary data
ReadFile(hFile, lpData, dwSize, &dwByteRead, NULL);
}

// Close the open file handle
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;


// STAGE #2
// WRITE THE READ BINDARY DATA INTO THE TEMPORARY FILE
// Open the existing setup data file (szTmpBinFile2)
hFile = CreateFile(szTmpBinFile2,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);

// Check the return handle value
if (NULL == hFile || INVALID_HANDLE_VALUE == hFile)
{
// Open the existing setup.lst data file
hFile = CreateFile(szTmpBinFile2,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
NULL);
}

// Check the return handle value
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
// Move the file pointer
SetFilePointer(hFile, 0, 0, FILE_END);

// Write the data into setup list file
WriteFile(hFile,
lpData,
dwSize,
&dwByteWrite,
NULL);
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
// PUT YOUR ERROR HANDLING CODE HERE

}

// Release the allocated data buffer
if (NULL != lpData){LocalFree((LPBYTE)lpData);}
lpData = NULL;

// Close the open file handle
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;

}

Now, we have both the header and content data file, and it is time to merge these 2 files into a single file before proceeding to compress it using the Zlib* algorithm.

// Begin to merge the header and data file
if (TRUE == MergeSelfExtractData (hWnd))
{
// Compress the current data file to the user define location &
// Notify user about the process is completed
if (0 == Compress(szTmpBinFile1, szTmpBinFile3))
{
// Proceed to spawn the SetupEx.exe

// Proceed to inject the merge data
// file into the spawned SetupEx.exe
}
}
else
// Notify user about the error
MessageBox(hWnd,
"Fail to compress the self-extract file!",
APP_TITLE, MB_OK | MB_ICONSTOP);
BOOL MergeSelfExtractData (HWND hWnd)
{
UPDATEINFO ui = {NULL};

BOOL bResult = FALSE;

LPBYTE lpData = NULL;
HANDLE hFile = NULL;
DWORD dwSize = 0;
DWORD dwByteWrite = 0;
DWORD dwByteRead = 0;

__try
{
// STAGE #1
{
// Read the current binary file data
hFile = CreateFile(szTmpBinFile2,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
// Check the return handle value
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
// Get the current file size
dwSize = GetFileSize(hFile, 0);
// Allocate local data buffer
lpData = (LPBYTE)LocalAlloc(LPTR, dwSize);
// Reset local data buffer
ZeroMemory(lpData, dwSize);

// Move the file pointer to the begining
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
// Read the binary data
ReadFile(hFile, lpData, dwSize, &dwByteRead, NULL);
}

// Close the open file handle
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;
}


// STAGE #2
// Open the existing temporary data file
hFile = CreateFile(szTmpBinFile1,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);

// Check the return handle value
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
// Setup the UPDATEINFO structure
ui.dwFileCount = dwFileCount;

// Move the file pointer
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
// Write the total binary data file being included
WriteFile(hFile,
&ui,
sizeof(UPDATEINFO),
&dwByteWrite,
NULL);

// Move the file pointer
SetFilePointer(hFile, 0, 0, FILE_END);
// Append the actual binary data from the temp file
WriteFile(hFile,
lpData,
dwSize,
&dwByteWrite,
NULL);

// Set return value
bResult = TRUE;
}
else
// Set return value
bResult = FALSE;


}
__except (EXCEPTION_EXECUTE_HANDLER)
{
// PUT YOUR ERROR HANDLING CODE HERE

// Set default return value
bResult = FALSE;
}

// Release the allocated data buffer
if (NULL != lpData){LocalFree((LPBYTE)lpData);}
lpData = NULL;

// Close the open file handle
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;

// Return local result
return bResult;
}

Read data from custom resource table

Now, we have done the compressed merge data file. But before we can further make it become self-extract, we must require another module (SetupEx.exe) to perform a series of reverse processes. As listed at the beginning of this article, I'll not discuss more on this module. Because, what it does is basically the reverse process of what we have discussed above. If you know how to build the merge data file, sure it will not be a problem to understand the code (SetupEx.exe) that is used to restore the original file from a single compressed merge data file.

But before I move further, I have an important note for those wish to modify the SetupEx.exe. The following code snippet is the key code of the entire SetupEx.exe (restoring individual file from the merge data without distortion or loss of its original contents):

// Get the current data file size base on the information
// store in the lpefi:
// lpefi[dwIndex1].dwFileSizeHigh
// lpefi[dwIndex1].dwFileSizeLow
//
dwDataSize = (lpefi[dwIndex1].dwFileSizeHigh*(MAXDWORD+1)) +
lpefi[dwIndex1].dwFileSizeLow;

// Ensure the file size of not 0Byte
if (0 < dwDataSize)
{
// Move the file pointer to the begin of the file
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
// Write data into a temp file
WriteFile(hFile,
&lpBinData[dwDataPtr],
dwDataSize,
&dwByteWrite,
NULL);

// Ensure the byte write is equal to the calculated data size
if (dwByteWrite != dwDataSize)
{
// Notify user about the error
MessageBox(hWnd,
"Writing data error, updating process aborted!",
APP_TITLE, MB_OK | MB_ICONSTOP);
// Jump the the "RollBack" routine
goto RollBack;
}

// Update the filetime information
SetFileTime(hFile,
&lpefi[dwIndex1].CreateTime,
&lpefi[dwIndex1].LastAcessTime,
&lpefi[dwIndex1].LastWriteTime);

// Close the current open file handle
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;

// Increate the local data pointer (dwDataPtr)
dwDataPtr += dwDataSize;
}

The SetupEx.exe was embedded inside the SelfExtact.exe (the program that will create the self-extract executable file). So, we must first spawn the SetupEx.exe by reading its binary data from the custom resource table under the SelfExtract.exe (as shown in the figure below), then write it into a newly created file:

Embedded SetupEx.exe within the SelfExtract.exe.

The code snippet below shows how we can spawn the SetupEx.exe from the binary data (IDR_EXTRACTOR1) stored inside the custom resource table (EXTRACTOR).

// Get the total resource size
dwResSize = SizeofResource(hInst, hResource);
// Load the resource content
hResData = LoadResource(hInst, hResource);
// Checking
if (hResData != NULL && dwResSize != 0)
{
// Ensure the lpData is NULL
lpData = NULL;
// Obtain the string pointer from the loaded resource handle
lpData = LockResource(hResData);
// Save the current read data into a file
hFile2 = CreateFile(szTmpBinFile4,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);

// Check the return handle value
if (NULL != hFile2 && INVALID_HANDLE_VALUE != hFile2)
{
// Move the file pointer to the begin of the file
SetFilePointer(hFile2, 0, 0, FILE_BEGIN);
// Write the read data into a temp file
WriteFile(hFile2,
(LPBYTE)lpData,
dwResSize,
&dwByteWrite,
NULL);
// Close the current open data file
if (NULL != hFile2) {CloseHandle(hFile2);}
hFile2 = NULL;
}
else
{
// Notify user about the error
MessageBox(hWnd,
"Fail to spawning the self-extract kernel!",
APP_TITLE, MB_OK | MB_ICONSTOP);
// Jump the the "CleanExit" routine
goto CleanExit;
}
}
else
{
// Notify user about the error
MessageBox(hWnd,
"Fail to read the self-extract kernel binary data!",
APP_TITLE, MB_OK | MB_ICONSTOP);
// Jump the the "CleanExit" routine
goto CleanExit;
}

Inject data into spawned SetupEx.exe

Now, we have spawned the SetupEx.exe from the custom resource table and we should go further by injecting the compressed merge data file into the custom resource table (SETUP) of SetupEx.exe as IDC_SETUP1 (shown in figure below):

Embedded compressed merge data within the SetupEx.exe.

From this figure, you will see the initial compressed merge data content within the SetupEx.exe was just 1 byte. This is just a dummy entry mainly for debugging purpose during coding stage (it must be replaced with an actual compressed merge data file). Also, with this IDR_SETUP1 entry, I believe it will help everyone to have a better understanding on how the SetupEx.exe works. After so much talking here and there about the custom resource, here is the code snippet on injecting the compressed merge data into the custom resource table (SETUP) of the spawned SetupEx.exe.

// Open the file require to alter the resource table
hFile2 = BeginUpdateResource (szTmpBinFile4, FALSE);

// Check the return handle value
if (NULL != hFile2 && INVALID_HANDLE_VALUE != hFile2)
{
// Update the file resource table
if (FALSE == UpdateResource (hFile2,
"SETUP",
MAKEINTRESOURCE(IDR_SETUP1),
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
lpBinData,
dwFileSize))
{
// Notify user about the error
MessageBox(hWnd,
"Fail to update the resource table" +
" of the self-extract executable file!",
APP_TITLE,
MB_OK | MB_ICONSTOP);
// Reset the local variable
hFile2 = NULL;
// Jump the the "CleanExit" routine
goto CleanExit;
}

// Close the modify file
if (FALSE == EndUpdateResource (hFile2, FALSE))
{
// Notify user about the error
MessageBox(hWnd,
"Fail to modify the resource table" +
" of the self-extract executable file!",
APP_TITLE,
MB_OK | MB_ICONSTOP);
// Reset the local variable
hFile2 = NULL;
// Jump the the "CleanExit" routine
goto CleanExit;
}

// Reset the local variable
hFile2 = NULL;

// Release the allocated memory
if (NULL != lpBinData) {LocalFree((LPBYTE)lpBinData);}
lpBinData = NULL;
}
else
{
// Notify user about the error
MessageBox(hWnd,
"Fail to open the resource table " +
"of the self-extract executable file!",
APP_TITLE,
MB_OK | MB_ICONSTOP);
// Jump the the "CleanExit" routine
goto CleanExit;
}

After the injection, you should see the compressed merge data sit inside the SetupEx.exe by using the freeware tool (a tool you cannot miss!!!) Resource Hacker (freeware) Copyright © 1999-2002 Agnus Johnson.

The screen shot below shows the difference of the custom resource (IDR_SETUP1) as compared to the initial 1 byte data only.

Before inject compressed merge data within the SetupEx.exe.

After inject compressed merge data within the SetupEx.exe.

Note: The value of 2000 you saw in the above 2 screenshots was equivalent to IDR_SETUP1, as this was pre-defined under the resource.h of both the SetupEx.exe and SelfExtract.exe project files. So, the merge data file must be injected into this entry. Otherwise, the SetupEx.exe will not be able to extract the injected merge data. Because it will remain locating the initial IDR_SETUP1 from the newly spawned SetupEx.exe which only has 1 byte data inside.

#define IDR_SETUP1                      2000

Also, the value of 1033 you saw from the screenshot above is another key item, and you must carefully check and verify this value before you start modifying either SetupEx.exe or SelfExtract.exe. Because, this value means the current custom resource table language identifier. For instance, 1033 means primary language is ENGLISH (0x09), and sublanguage is ENGLISH US (0x01). Please refer to the MSDN Library (by searching for the MAKELANGID API) to get more information about the available language identifier.

Chances for injecting data into different resource tables

UpdateResource API was used in this project for injecting the compressed merge data file into the spawned SetupEx.exe in order to complete the self-extract executable file.

if (FALSE == UpdateResource (hFile2,
"SETUP",
MAKEINTRESOURCE(IDR_SETUP1),
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
lpBinData,
dwFileSize))

From this code snippet, the second, third, and forth arguments will be the key values which will cause merge data file being injected into a wrong resource table. Hence, I will explain each of these scenario one-by-one.

For the first scenario, if "SETUP" was changed to something like "ABC", the output will be like the 2 screenshots below. The differences can be clearly identified (there was an extra custom resource name "ABC"). As a result, the spawned self-extract executable will remain locating and loading the original dummy data stored inside "SETUP" (custom resource). Because, the compressed merge data was injected into "ABC" custom resource table instead of the pre-defined "SETUP".

Basically, "SETUP" represents the custom resource group name and it is very important when calling the UpdateResource API.

if (FALSE == UpdateResource (hFile2,
"ABC",
MAKEINTRESOURCE(IDR_SETUP1),
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
lpBinData,
dwFileSize))

Before inject compressed merge data within the SetupEx.exe.

After inject compressed merge data within the SetupEx.exe.

In the second scenario, if the MAKEINTRESOURCE(IDR_SETUP1) was changed to something like 2001 (MAKEINTRESOURCE(IDR_SETUP1) will return the value of 2000, as IDR_SETUP1 was defined as 2000 under the resource.h file), the output will be like the 2 screenshots below. The differences can be clearly identified (there was an extra custom resource identifier 2001). As a result, the spawned self-extract executable will remain locating and loading the original dummy data stored inside the 2000 custom resource. Because, the compressed merge data was injected into the 2001 custom resource instead of the pre-defined 2000.

The 2001 represents the custom resource identifier and it is also very important when calling the UpdateResource API. A wrong value will collapse the entire self-extract executable file.

if (FALSE == UpdateResource (hFile2,
"SETUP",
"2001",
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
lpBinData,
dwFileSize))

Before inject compressed merge data within the SetupEx.exe.

After inject compressed merge data within the SetupEx.exe.

For the last scenario, if the MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US) was changed to something like MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_UK), the output will be like the 2 screenshots below. The differences can be clearly identified (there was an extra custom resource identifier 2000 with the value of 2057 on the right hand side instead of 1033). As a result, the spawned self-extract executable will remain locating and loading the original dummy data stored inside the 2000 custom resource with language ID of 1033, and not the newly injected one with the same custom resource identifier but different language ID (2057).

The MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US) represents the language ID for the respective custom resource. So, it is another key factor when calling the UpdateResource API. A wrong value will collapse the entire self-extract executable file as well.

if (FALSE == UpdateResource (hFile2,
"SETUP",
MAKEINTRESOURCE(IDR_SETUP1),
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_UK),
lpBinData,
dwFileSize))

Before inject compressed merge data within the SetupEx.exe.

After inject compressed merge data within the SetupEx.exe.

Besides the actual language identifier, you also can use the language neutral identifier to inject the compressed merge data into the spawned SetupEx.exe. It still will work, although the compressed merge data was injected into the same group of custom resource table but under different language identifier 0 instead of 1033.

For this, I am still searching for the answer about why it will work even though the LANG_NEUTRAL and SUBLANG_NEUTRAL language identifiers were used during the injection with UpdateResource API, which does not match with the language identifier specified inside the SetupEx.exe.

if (FALSE == UpdateResource (hFile2,
"SETUP",
MAKEINTRESOURCE(IDR_SETUP1),
MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL),
lpBinData,
dwFileSize))

Before inject compressed merge data within the SetupEx.exe.

Before inject compressed merge data within the SetupEx.exe.

Please refer to the MAKELANGID API in MDSN library for the full list of Primary Language Identifier and Sub Language Identifier.

The language identifier

Since there is a dummy custom resource embedded inside the SetupEx.exe module, we can find out what language identifier is used by this custom resource, via the VC++ 6.0 IDE during design time, or the Resource Hacker (freeware) during runtime.

Before inject compressed merge data within the SetupEx.exe.

Before inject compressed merge data within the SetupEx.exe.

Verify the injected data

Everything we coded must go through a verification process and the tool that best fits to this project need is the Resource Hacker (freeware). The contribution of this tool in this project was shown on those screenshots in the previous 2 sections.

Good and bad things in this project

When it comes to real world, everything will have its own good and bad sides. As for this project, I foresee the following shortage and some enhancements which can be implemented in the next phase.

The bad thing is, the SelfExtract.exe will not be able run under Windows 95/98/ME platforms. Because the UpdateResource is not supported in these 3 platforms.

While the good things are, it leaves room for you to keep upgrading this project to support sub-folder scanning features. If you apply this project for application updating module like what I did, you are always given a chance to design your own update module GUI (modify the SetupEx.exe) or do extra processing like unloading the relevant application prior to updating the program file as well as updating the necessary registry which is related to your application.

The tool you must download

Last but not least, you must have the Resource Hacker (freeware) tool with you when you are reading, using or modifying this project's source code. Otherwise, you will be lost and will have no idea about what I have written in this article.

Revision history

Thanks for John's suggestion, because this update was based on his good suggestion.

Okay, I have implemented the "auto-exec" capability for the user defined file after extraction process was completed. Basically, I just replace the first 4 bytes (DWORD) value into a defined structure SETUPINFO which will be including both the original file count information as well as the "auto-exec" filename information (please refer to the new section Information stored in the setup info block for details). Subsequently the next change made was adding the ShellExecute API code right after successfully extracting all the files into the user defined destination folder.

Note, I only resubmit the source code (selfextract_src.zip), and those files inside the demo zip file will remain unchanged. Which means it does not have the "auto-exec" capability. So, you are required to build at least 1 sample self-extract EXE in order to see how this new implemented capability looks like.

P/s: John, your first suggestion will need more time to implement. Because, it involves the change of the GUI to support destination folder selection. So, will have another round of update for this.

History

  • Updated @ 18th May, 2004 09:09:45 AM UTC Time

    I have encountered some problem in uploading the modified source code, please be patient & will get it done ASAP.

  • Updated @ 18th May, 2004 10:04:58 AM UTC Time

    Finally, updated source code was posted.

  • Updated @ 19th May, 2004 01:18:45 AM UTC Time
<script defer="defer" language="javascript" type="text/javascript">Highlight(Array());</script>

About hackzai


克仔 was graduated from Double-E, and being certified as MCSD (VS6) at spring 2003.

Click here to view hackzai's online profile.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值