Search code examples
pythonc++winapiresourcespyinstaller

UpdateResource corrupts exe file


I have an EXE file generated from a Python script using PyInstaller, and I've added a text file as a resource to the EXE using Resource Hacker. I'm trying to modify the resource text file programmatically using the WinAPI UpdateResource function. The modification is successful, but it causes the EXE file to become corrupted - After the modification, the EXE's size is reduced from 75 MB to 271 KB, and I can no longer run it. When I attempt to run the corrupted EXE, I receive the error message "Cannot open PyInstaller from executable."

I've tried implementing the modification using both Python and C++, but neither seems to work. Here's the Python code I've used:

Python code:

exe_file = r"\path\to\exe_file.exe"
handle = win32api.BeginUpdateResource(exe_file, False)
resource_type = win32con.RT_RCDATA
new_data = "modified text".encode()
try:
    win32api.UpdateResource(handle, resource_type, 110, new_data)

    # Save the changes and close the file
    win32api.EndUpdateResource(handle, False)
except:
    win32api.EndUpdateResource(handle, True)

This is the C++ code (I handled in my actual code the cases when handles are invalid, but if I put it all here, the code gets much longer):

#include <windows.h>
#include <winbase.h>
#include <winuser.h>


int modify_resource(const LPCWSTR new_text, const LPCWSTR filename)
{
    const LPCWSTR res_type = MAKEINTRESOURCE(RT_RCDATA);
    const LPCWSTR res_name = MAKEINTRESOURCE(110); // resource ID

    HMODULE module = LoadLibraryEx(filename, NULL, LOAD_LIBRARY_AS_DATAFILE);
    HRSRC res_info = FindResource(module, res_name, res_type);

    DWORD res_size = SizeofResource(module, res_info);
    HGLOBAL res_data = LoadResource(module, res_info);

    LPVOID res_ptr = LockResource(res_data);
    FreeResource(res_data);
    HANDLE update_handle = BeginUpdateResource(filename, FALSE);

    BOOL result = UpdateResource(update_handle, res_type, res_name, MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), (LPVOID)new_text, (DWORD)(wcslen(new_text) + 1) * sizeof(wchar_t));
    if (!result)
    {
        EndUpdateResource(update_handle, TRUE);
        FreeLibrary(module);
        return 1;
    }

    result = EndUpdateResource(update_handle, FALSE);
    FreeLibrary(module);
    if (!result)
    {
        return 1;
    }
    return 0;
}

I am trying to find why this happens, or at least find another way to modify the resource text file. Thank you in advance.

EDIT: Here are minimal reproduction steps: convert the following python program to exe:

input("Press enter to exit")

With the pyinstaller command: pyinstaller --onefile --console <script_name.py>

and run the following c++ code:

# include <windows.h>
# define RES_ID 110

int main();
{
    const LPCWSTR res_type = MAKEINTRESOURCE(RT_RCDATA);
    const LPCWSTR res_name = MAKEINTRESOURCE(RES_ID);
    HMODULE module = LoadLibrary(L"exe\\to\\copy\\resource\\from.exe");
    HRSRC res_info = FindResource(module, res_name, res_type);
    HGLOBAL res_load = LoadResource(module, res_info);
    LPVOID res_ptr = LockResource(res_load);

    HANDLE update_handle = BeginUpdateResource(L"path\\to\\python\\exe.exe", FALSE);
    DWORD res_size = SizeofResource(module, res_info);

    BOOL result = UpdateResource(update_handle,
        res_type,
        res_name,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL),
        res_ptr,
        res_size);

    EndUpdateResource(update_handle, FALSE);
    FreeLibrary(module);
    return 0;
}


Solution

  • Re-reading your question I now get what you want to do: you already have the PyInstaller file (you are not generating it yourself), and you want to change it after the fact.

    This should be possible, by first separating the pure PE part of the executable from the rest (which is the archive that was appended to it), then modifying the pure PE part, and then re-appending the rest.

    I've written a small Python function that will do just that:

    import os, time
    import pefile
    import win32api, win32con
    from PyInstaller.utils.win32 import winutils
    
    def modify_pyinstaller(exe_file):
        modified_name = exe_file.removesuffix('.exe') + '_modified.exe'
    
        size = 0
        with pefile.PE(exe_file, fast_load=True) as pe:
            size = pe.OPTIONAL_HEADER.SizeOfHeaders
            for section in pe.sections:
                size += section.SizeOfRawData
    
        # Copy just the initial .exe part
        with open(exe_file, 'rb') as fin, open(modified_name, 'wb') as fout:
            fout.write(fin.read(size))
    
        # This part of the code is from what you wanted to do with
        # the executable...
        handle = win32api.BeginUpdateResource(modified_name, False)
        try:
            win32api.UpdateResource(handle, win32con.RT_RCDATA, 110, "modified text".encode())
            win32api.EndUpdateResource(handle, False)
        except:
            win32api.EndUpdateResource(handle, True)
            raise
        # End of the part that should be modified...
    
        # Now that we've modified the exe, copy the archive back (the rest of the original file)
        # (note the 'ab' mode to append data)
        with open(exe_file, 'rb') as fin, open(modified_name, 'ab') as fout:
            fin.seek(size, os.SEEK_SET)
            fout.write(fin.read())
    
        # Now that we're done also keep virus scanners happy and update
        # the timestamps and PE checksums. Re-use PyInstaller's
        # implementation, because they already figured this out.
        # (Note that since we're now using PyInstaller's functionality
        # here, this will technically make this code GPL-2+, because
        # that's the license of PyInstaller!)
        winutils.set_exe_build_timestamp(modified_name, int(time.time()))
        winutils.update_exe_pe_checksum(modified_name)
    

    The general logic is the following:

    • It uses the pefile package to extract the size of the executable part of the file, without the embedded archive (stores that in size)
    • It then creates a copy of the original file, but only the first size bytes (to only copy the executable)
    • It then performs the modifications that you want to do (please replace that part of the code)
    • It then appends the rest of the contents of the original file (starting at size bytes offset into the original file) onto the modified file
    • Because PyInstaller also does it, it updates the time stamp and the PE checksum of the newly generated file (to keep virus scanners happy) - I've imported PyInstaller for that and called their functionality here (otherwise you'd have to reimplement that) (you could technically skip this part if you just want a working .exe)
    • You don't have to understand what kind of format the archive is at all, you just have to copy the data

    At least with a trivial hello world Python script this appears to work for me, and it should work in the general case (with any self-extracting PE executable really, not just those generated by PyInstaller), but your mileage may vary.

    You can probably optimize this a bit more (I'm currently reading the entire data into memory before writing it out again), but the code should illustrate the principle.

    Disclaimer: please only do this if you can't recreate the PyInstaller file yourself. If you are creating the installer file anyway, the PyInstaller developers have already added the required functionality to add resources during the build process. (See my other answer to this question for details.)