Repository: pyinstaller/pyinstaller

Issue:
Set correct PE checksum for .exe files (helps with virus false-positives)
**Is your feature request related to a problem? Please describe.**
As you've probably all seen before, virus scanner false-positives are a recurring problem for PyInstaller-generated executables, as well as PyInstaller's bootloader (see e.g. #5490, #5474, #5479, ...).

One piece of information virus scanners seem to use is the [PE checksum](https://practicalsecurityanalytics.com/pe-checksum/). Quoting from that article:

> - 83% of malware had invalid checksums
> - 90% of legitimate files had valid checksums.

It looks like anything generated from PyInstaller (as well as the Windows bootloader in the release?) seems to have a checksum of `00 00 00 00`, which is thus invalid.

**Describe the solution you'd like**
PyInstaller should set a correct PE checksum. This is possible using `pefile`, which it already seems to depend on:

```python
import pefile
pe = pefile.PE('filename.exe')
pe.OPTIONAL_HEADER.CheckSum = pe.generate_checksum()
pe.close()
pe.write('filename.exe')
```

(You can use `pe.verify_checksum()` to verify that it is correct)

After doing this, e.g. PyInstaller's `Windows-64bit/runw.exe` (in the 4.2 release) goes down from [12 false-positives](https://www.virustotal.com/gui/file/749666ba5af516692ff3716000b992cbcf2857e825c9c1d94fac7ac8ba11759e/detection) to [only 5](https://www.virustotal.com/gui/file/f4d86605cf8d8070c9bc4b0e6ad6c6170a6cdec9d44c8fba20f614b7005c2129/detection) - most notably, McAfee and Avast (which are the two affected engines I've actually heard of before...) now are happy with the file.

Doing this with my own project (which is built with PyInstaller), @bitraid found that:

> the number of false positives for v2.0.2 goes down from [31](https://www.virustotal.com/gui/file/cb416f27e5dead44499a7e4d7e35c49697bfea262b28308a7a4f5eb7579670ef/detection) to [7](https://www.virustotal.com/gui/file/6ff31f225fc3a7386aea976ca27c457eb35b9788d7fb0737abec3abda7cbe353/detection). Similarly v2.0.0 goes down to [6](https://www.virustotal.com/gui/file/484840b3821f039bc32e773fc8da2506c75d6476066cbfe72b109fbcdd611b58/detection) from [29](https://www.virustotal.com/gui/file/e9da9077e793cde92bd6bc101659fec8238a74149b14c06dc4b1810141f8cd50/detection) and v2.0.1 goes down to [8](https://www.virustotal.com/gui/file/bb2aab3cf8a401d619a0d47177fd6e394dadc3089bf26b4cd85c846504ddd46d/detection) from [15](https://www.virustotal.com/gui/file/1f1be4d41a03d059dd87583cabcd8d93b2631c117bff3dcb988f91f268c8787d/detection).

**Describe alternatives you've considered**
Submitting false-positive reports to antivirus vendors. A pain.

Also, doing this after PyInstaller has generated the exe - that works, but it seems to me like this should be done in PyInstaller itself.

**Additional context**
See https://github.com/qutebrowser/qutebrowser/issues/6194


Discussion:
Ah yes! Nice one. How that makes an executable any more trustworthy seeing as anyone could do it is beyond me but if it shuts up the bogus AV software then great.
@The-Compiler Fancy making the changes or shall I?
Feel free! ~~I don't think I've contributed before~~ ([looks like I did :laughing:](https://github.com/pyinstaller/pyinstaller/pulls?q=is%3Apr+author%3Athe-compiler+is%3Aclosed))

I'm not really sure what the best place for such a change would be - but I also try to avoid working on Windows stuff if I can, so if you don't mind, go ahead. :+1: 
Alright then. I'll blow the cobwebs off my own Windows partition and do it...
I've been following the virus false-positive stuff for a while and struggling with it myself. Here are the before VirusTotal Windows bootloader analysis so we can compare after. The bigger one I have issues with (that I am mot sure the source for) is Windows Defender. Will be interesting to see if this helps that case.

Windows 32bit:
* `run_d.exe` 1/70: https://www.virustotal.com/gui/file/5c8ddb668b8fa768d2037879a22214db104e48cf26136cc311e159f007e6436e/detection
* `run.exe` 11/70: https://www.virustotal.com/gui/file/2e4dc3fd4cb960cdc0dd0c8abc176fe80209dbf353c789e79dd0f271ad02a785/detection
* `runw_d.exe` 2/70: https://www.virustotal.com/gui/file/e70c7cfb63a9ca41c55cbca60ff7acd521b67606ab33ccf2661400e876268c7a/detection
* `runw.exe` 14/70: https://www.virustotal.com/gui/file/ddab59c787d9673a8098a1338098effeedca938dc338e96c5fd4654ed0b76dfa/detection

Windows 64bit:
* `run_d.exe` 2/71: https://www.virustotal.com/gui/file/8ef748a59cd81d99b0ee3f5008c2275b5630d5bf8e1aecc2ed7b8a288bea21d7/detection
* `run.exe` 4/70: https://www.virustotal.com/gui/file/0367cd6d43be86912de0f30dd118087ac9e79a9b5f7440e496703b1de4c44fd7/detection
* `runw_d.exe` 5/70: https://www.virustotal.com/gui/file/c27d3749d3614e1b3022c84ff240fbcb3605ba6b72fcc1cf9729088fc4345ccc/detection
* `runw.exe` 12/71: https://www.virustotal.com/gui/file/749666ba5af516692ff3716000b992cbcf2857e825c9c1d94fac7ac8ba11759e/detection

Resolving patch:
diff --git a/PyInstaller/building/api.py b/PyInstaller/building/api.py
--- a/PyInstaller/building/api.py
+++ b/PyInstaller/building/api.py
@@ -640,6 +640,10 @@ def assemble(self):
             logger.info("Fixing EXE for code signing %s", self.name)
             import PyInstaller.utils.osx as osxutils
             osxutils.fix_exe_for_code_signing(self.name)
+        if is_win:
+            # Set checksum to appease antiviral software.
+            from PyInstaller.utils.win32.winutils import set_exe_checksum
+            set_exe_checksum(self.name)
 
         os.chmod(self.name, 0o755)
         # get mtime for storing into the guts
diff --git a/PyInstaller/utils/win32/winutils.py b/PyInstaller/utils/win32/winutils.py
--- a/PyInstaller/utils/win32/winutils.py
+++ b/PyInstaller/utils/win32/winutils.py
@@ -155,3 +155,17 @@ def convert_dll_name_to_str(dll_name):
         return str(dll_name, encoding='UTF-8')
     else:
         return dll_name
+
+
+def set_exe_checksum(exe_path):
+    """Set executable's checksum in its metadata.
+
+    This optional checksum is supposed to protect the executable against
+    corruption but some anti-viral software have taken to flagging anything
+    without it set correctly as malware. See issue #5579.
+    """
+    import pefile
+    pe = pefile.PE(exe_path)
+    pe.OPTIONAL_HEADER.CheckSum = pe.generate_checksum()
+    pe.close()
+    pe.write(exe_path)


Test patch:
diff --git a/tests/functional/test_basic.py b/tests/functional/test_basic.py
--- a/tests/functional/test_basic.py
+++ b/tests/functional/test_basic.py
@@ -585,3 +585,24 @@ def test_several_scripts2(pyi_builder_spec):
     Verify each script has it's own global vars (basic test).
     """
     pyi_builder_spec.test_spec('several-scripts2.spec')
+
+
+@pytest.mark.win32
+def test_pe_checksum(pyi_builder):
+    import ctypes
+    from ctypes import wintypes
+
+    pyi_builder.test_source("print('hello')")
+    exes = pyi_builder._find_executables('test_source')
+    assert exes
+    for exe in exes:
+        # Validate the PE checksum using the official Windows API for doing so.
+        # https://docs.microsoft.com/en-us/windows/win32/api/imagehlp/nf-imagehlp-mapfileandchecksumw
+        header_sum = wintypes.DWORD()
+        checksum = wintypes.DWORD()
+        assert ctypes.windll.imagehlp.MapFileAndCheckSumW(
+            ctypes.c_wchar_p(exe),
+            ctypes.byref(header_sum),
+            ctypes.byref(checksum)) == 0
+
+        assert header_sum.value == checksum.value