05. Dec. 2023: Fixed bugs regarding the UpdateResource_ Windows API call. Added automatic trimming of imported manifest files (remove any leading or trailing CR, LF, TAB or space). Add compiler warning if not compiled in console mode.
Please use the updated code from this post.
________________________________________________________________________________________________
Hi,
due to recent discussions, I wrote a commandline tool which is able to replace the manifest of a given executable file (.exe). It is also able to show the current manifest. The source code is below.
Thanks to ChrisR for providing a very good starting point to me.
Usage:
Code: Select all
manifest_replace.exe - replace manifest in windows executables
Usage:
manifest_replace.exe filename.exe [newManifest.xml] [newExeFilename.exe]
- If only filename.exe is given, it only outputs existing contained manifest.
- If newManifest.xml is given without newExeFilename.exe, it replaces the manifest in given exe.
- If all three parameters are given, it creates a copy of exe with replaced manifest.
The tool retrieves the following attributes from <assemblyIdentity /> out of the existing manifest of the input executable and offers them as placeholder for the new manifest template:
version → %version%
name → %name%
type → %type%
processorArchitecture → %processorArchitecture%
language → %language%
publicKeyToken → %publicKeyToken%
Also, it determines the architecture from the given executable and offers %arch% as placeholder.
This is a possible manifest template to be used as new/replaced manifest (for a non GUI tool):
Code: Select all
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<assemblyIdentity
version="%version%"
processorArchitecture="%arch%"
name="%name%"
type="%type%"
language="%language%" />
<description></description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="%type%"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="%arch%"
publicKeyToken="6595b64144ccf1df"
language="*" />
</dependentAssembly>
</dependency>
</assembly>
Source:
Code: Select all
; manifest replacement tool
; @license: Public Domain
;
; Tested on PB 6.03 LTS (x86 & x64)
EnableExplicit
CompilerIf #PB_Compiler_ExecutableFormat <> #PB_Compiler_Console
CompilerWarning "You should compile with option 'Executable format: Console'!"
CompilerEndIf
; GetBinaryType_ constants
#SCS_32BIT_BINARY = 0
#SCS_64BIT_BINARY = 6
#SCS_DOS_BINARY = 1
#SCS_OS216_BINARY = 5
#SCS_PIF_BINARY = 3
#SCS_POSIX_BINARY = 4
#SCS_WOW_BINARY = 2
; helper TRIM function to trim all replaceChar.s characters from in.s
; works from both right and left!
; Example: Debug betterTrim(#CRLF$+" "+#TAB$+"1234567890987654321 "+#CRLF$, " 12"+#CRLF$+#TAB$)
Procedure.s betterTrim(in.s, replaceChars.s)
Protected l.i = Len(in.s)
Protected x.i = 0
; from left
For x.i = 1 To l.i
Protected c.s = Mid(in.s, x.i, 1)
If FindString(replaceChars.s, c.s) = 0: Break: EndIf
Next
in.s = Mid(in.s, x.i)
; from right
Repeat
c.s = Right(in.s, 1)
If FindString(replaceChars.s, c.s) = 0: Break: EndIf
in.s = Left(in.s, Len(in.s) - 1)
ForEver
ProcedureReturn in.s
EndProcedure
; Examines given xml and returns attribute
Procedure.s getManifestVal(manifest.s, path.s, attribute.s, defaultValue.s = "")
Protected xml.i = ParseXML(#PB_Any, manifest.s)
If XMLStatus(xml.i) <> #PB_XML_Success
Print("Failed to parse exe manifest xml! Replacement values will be default!" + #CRLF$)
Print("XML-Error: " + XMLError(xml.i) + " in line " + XMLErrorLine(xml.i) + #CRLF$)
ProcedureReturn defaultValue.s
EndIf
Protected *main = MainXMLNode(xml.i)
Protected *myNode = XMLNodeFromPath(*main, path.s)
If *myNode = 0
Print("Failed to parse exe manifest node "+path.s+"! Replacement value will be default!" + #CRLF$)
FreeXML(xml.i)
ProcedureReturn defaultValue.s
EndIf
Protected result.s = GetXMLAttribute(*myNode, attribute.s)
FreeXML(xml.i)
If result.s = "": ProcedureReturn defaultValue.s: EndIf
ProcedureReturn result.s
EndProcedure
; returns manifest of given executable
Procedure.s loadManifest(fileName.s)
; load to memory
Protected handle.i = LoadLibraryEx_(@FileName, 0, #LOAD_LIBRARY_AS_DATAFILE)
If handle.i = 0
Print("Error reading manifest from existing exe. Is there no manifest yet?" + #CRLF$)
ProcedureReturn ""
EndIf
; extract manifest
Protected ressource.i = FindResource_(handle.i, 1, #RT_MANIFEST)
If ressource.i = 0
Print("Error reading manifest from existing exe. Is there no manifest yet?" + #CRLF$)
ProcedureReturn ""
EndIf
Protected load.i = LoadResource_(handle.i, ressource.i)
Protected size.i = SizeofResource_(handle.i, ressource.i)
Protected manifest.s = PeekS(load.i, size.i, #PB_UTF8)
FreeLibrary_(handle.i)
ProcedureReturn manifest.s
EndProcedure
Procedure.b updateManifest(fileName.s, newManifestFile.s, newExeFile.s)
Protected lpBinaryType.i
; get original manifest from exe file
Protected originalManifest.s = loadManifest(fileName.s)
If originalManifest.s = ""
ProcedureReturn #False
EndIf
; output manifest if no input manifest was given
If newManifestFile.s = ""
Print("Manifest of " + fileName.s + ":" + #CRLF$ + #CRLF$)
Print(originalManifest.s)
Print(#CRLF$ + #CRLF$)
ProcedureReturn #True
EndIf
; retrieve replacement values
; - mandatory values
Protected version.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "version", "1.0.0.0")
Protected name.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "name")
Protected type.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "type")
; - optional values
Protected language.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "language", "*")
Protected processorArchitecture.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "processorArchitecture")
Protected publicKeyToken.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "publicKeyToken")
; load new manifest
Protected file.i = ReadFile(#PB_Any, newManifestFile.s)
If file.i = 0
Print("Error opening new manifest file" + #CRLF$)
ProcedureReturn #False
EndIf
Protected manifest.s = ReadString(file.i, #PB_UTF8 | #PB_File_IgnoreEOL)
CloseFile(file.i)
; determine binary type from exe
Protected res.i = GetBinaryType_(@fileName, @lpBinaryType)
If res.i = 0
Print("Error determining executable type. Is it really a exe file?" + #CRLF$)
ProcedureReturn #False
EndIf
Protected arch.s = ""
Select lpBinaryType
Case #SCS_64BIT_BINARY : arch.s = "amd64"
Case #SCS_32BIT_BINARY : arch.s = "X86"
EndSelect
; make a copy and replace there if second exe was given
If newExeFile.s <> ""
; use new exe filename
If FileSize(newExeFile.s) >= 0: DeleteFile(newExeFile.s): EndIf ; cleanup
CopyFile(fileName.s, newExeFile.s)
fileName.s = newExeFile.s
EndIf
; open exe file
Protected handle.i = BeginUpdateResource_(@fileName, #False)
If handle.i = 0
Print("Error opening executable file" + #CRLF$)
ProcedureReturn #False
EndIf
; update manifest replacement placeholders with retrieved values
manifest.s = ReplaceString(manifest.s, "%arch%", arch.s)
manifest.s = ReplaceString(manifest.s, "%version%", version.s)
manifest.s = ReplaceString(manifest.s, "%language%", language.s)
manifest.s = ReplaceString(manifest.s, "%name%", name.s)
manifest.s = ReplaceString(manifest.s, "%type%", type.s)
manifest.s = ReplaceString(manifest.s, "%processorArchitecture%", processorArchitecture.s)
manifest.s = ReplaceString(manifest.s, "%publicKeyToken%", publicKeyToken.s)
; clean start and ending of manifest (no linebreaks, no spaces, no tabs allowed at the ends)
manifest.s = betterTrim(manifest.s, #CRLF$+#TAB$+" ")
; prepare utf8 memory buffer
Protected *buffer = AllocateMemory(StringByteLength(manifest.s, #PB_UTF8))
Protected lenNewManifest.i = PokeS(*buffer, manifest.s, -1, #PB_UTF8)
; update ressource
UpdateResource_(handle.i, #RT_MANIFEST, 1, 1033, *buffer, lenNewManifest.i)
EndUpdateResource_(handle.i, #False)
FreeMemory(*buffer); cleanup
Print("Manifest successful replaced." + #CRLF$)
ProcedureReturn #True
EndProcedure
Procedure main()
Protected filename.s = ProgramParameter(0)
Protected manifest.s = ProgramParameter(1)
Protected newFilename.s = ProgramParameter(2)
If filename.s = "" Or filename.s = "-h" Or filename.s = "--help" Or filename.s = "/?"
Print("manifest_replace.exe - replace manifest in windows executables" + #CRLF$)
Print("Usage:" + #CRLF$)
Print(" manifest_replace.exe filename.exe [newManifest.xml] [newExeFilename.exe]" + #CRLF$ + #CRLF$)
Print(" - If only filename.exe is given, it only outputs existing contained manifest." + #CRLF$)
Print(" - If newManifest.xml is given without newExeFilename.exe, it replaces the manifest in given exe." + #CRLF$)
Print(" - If all three parameters are given, it creates a copy of exe with replaced manifest." + #CRLF$ + #CRLF$)
ProcedureReturn
EndIf
updateManifest(filename.s, manifest.s, newFilename.s)
EndProcedure
OpenConsole()
main()
CloseConsole()