Last active
February 18, 2023 07:53
-
-
Save Beej126/f26e6649cfcc38accee3a0a8cc0a9d04 to your computer and use it in GitHub Desktop.
dnlib copy source dll methods to destination dll (patch tool)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
dest.cs | |
dest/ | |
save.ps1 | |
source.cs | |
source.dll |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# tested under "pwsh" (known as "Powershell *CORE*", NOT "WINDOWS POWERSHELL") | |
# (currently @ version 7.2.1) | |
# pwsh official download: https://github.com/PowerShell/PowerShell/releases | |
# github repo: | |
param( | |
[string]$patchFilePath | |
) | |
$ErrorActionPreference = "Inquire" | |
Write-Output "" | |
try { | |
add-type -Path $PSScriptRoot\dnlib_v3.4.0.0.dll | |
} | |
catch { | |
if ($_.Exception.Message.Contains("not exist")) { | |
[Console]::Error.WriteLine(("you must download dnlib.lib and copy to this folder`r`n`r`n" + | |
"download from: https://www.nuget.org/packages/dnlib/`r`n" + | |
"open the nupkg file with 7-zip and copy the lib/netstandard2.0/dnlib.dll to this folder.`r`n" + | |
"more detailed instructions here: https://stackoverflow.com/questions/14894864/how-to-download-a-nuget-package-without-nuget-exe-or-visual-studio-extension/32681762#32681762`r`n")) | |
} | |
throw $_ | |
} | |
# ideally, we'd go directly from C# source to binary patching, but dnlib only works from a .net binary source... | |
# so we use powershell's super convenient add-type function to generate a .dll from our source .cs | |
# add-type also loads the .dll into memory which locks the dll as read-only until the end of the script... | |
# unfortunately there's no way to just compile without loading the dll | |
# forking this separate powershell session isolates loading the dll and immediately let's go of it at the close of the session... | |
# therefore freeing this momentary .dll "byproduct" to be cleaned up at the end | |
#scriptblock::create does the variable expansion: https://stackoverflow.com/questions/25551893/powershell-expand-variable-in-scriptblock/25552464#25552464 | |
$ps = [powershell]::Create() | |
$cmd = "Add-Type -TypeDefinition (gc -raw $patchFilePath) -OutputAssembly `"$($patchFilePath).dll`""; | |
$ps.AddScript([scriptblock]::Create($cmd) ) | out-null | |
$ps.Invoke() | |
# using get-content so we can drive script execution from unique file association ".nil" vs typical .cs required by add-type -Path arg | |
# -raw arg returns as one big string vs array of strings for each line | |
Add-Type -OutputAssembly "$($patchFilePath).dll" -TypeDefinition (gc -raw $patchFilePath) | |
$sourceModule = [dnlib.DotNet.ModuleDefMD]::Load("$($patchFilePath).dll") | |
# loop over all root types in the source.dll which represent each assembly to be patched | |
# circa 2023 Q1 latest pwsh or perhas .net core 7 stack added a few more embedded types to the top of the list so had to add HasNestedTypes check to correctly land on my custom class to drill into, hopefully this holds, it's not an air tight heuristic | |
$sourceModule.types | ? { $_.HasCustomAttributes -and $_.HasNestedTypes } | ForEach-Object { | |
$destDllPath = $_.customattributes.ConstructorArguments[0].Value | |
Write-Output "patching: $destDllPath" | |
$destModule = [dnlib.DotNet.ModuleDefMD]::Load($destDllPath) | |
# loop over all nested classes that have methods in the source ... | |
$_.NestedTypes | ? HasMethods | ForEach-Object { | |
$className = $_.Name | |
Write-Output " className: $className" | |
# loop over all the source type's methods... | |
$_.Methods | Where-Object { $_.Name -ne ".ctor" } | ForEach-Object { | |
$methodName = $_.Name | |
$sourceMethodBody = $_.Body | |
# get pointer to the corresponding class method in the destination | |
$destClass = $destModule.GetTypes() | Where-Object Name -eq $className | |
$destMethodBody = ($destClass.Methods | Where-Object Name -eq $methodName).Body | |
if (!$destMethodBody) { throw "method '$($className).$($_.method)' not found in destination" } | |
Write-Output " patching: $($methodName)" | |
# we're just doing a direct replace | |
$destMethodBody.Instructions.Clear() | |
$destMethodBody.ExceptionHandlers.Clear() | |
# this was really important to make the resulting method valid | |
# this shows that a valid method structure is not just comprised of the instructions but a few other properties as well... | |
# fortunately exceptionhandlers and variables were all that were necessary for the simple cases i tested so far | |
$destMethodBody.Variables.Clear() | |
$sourceMethodBody.Variables | ForEach-Object { | |
$destMethodBody.Variables.Add($_) | out-null | |
} | |
$sourceMethodBody.Instructions | ForEach-Object { | |
$destMethodBody.Instructions.Add($_) | |
} | |
} | |
Write-Output "" | |
} | |
try { | |
#can't write to the open file... the way dnlib has a lock on our dll we must write to new temp file, then close and rename | |
$destModule.Write("$($destDllPath)_temp") | |
} | |
catch { | |
if ($_.Exception.Message.Contains("is denied")) { | |
[Console]::Error.WriteLine(("`r`nit looks like you got an access denied error when trying to save the patched dll...`r`n" + | |
"there's basically two choices:`r`n" + | |
"A) run this script elevated or`r`n" + | |
"B) copy the file to a user accessible folder and change the patches.json path accordingly`r`n`r`n")) | |
} | |
throw $_ | |
} | |
$destModule.Dispose() | |
$destModule = $null | |
Write-Output "" | |
$fileVer = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($destDllPath).FileVersion ?? "orig" | |
$backupFilePath = $destDllPath.replace(".dll", ".$($fileVer).dll").replace(".exe", ".$($fileVer).exe") | |
if (![System.IO.File]::Exists($backupFilePath)) { | |
Write-Output "backing up existing version to: $backupFilePath" | |
Copy-Item $destDllPath $backupFilePath | |
} | |
else { | |
Write-Output "$backupFilePath already exists, leaving as-is." | |
} | |
# and finally replace it with the patched version | |
Move-Item -Force "$($destDllPath)_temp" $destDllPath | |
} | |
$sourceModule.Dispose(); | |
$sourceModule = $null | |
erase "$($patchFilePath).dll" -ErrorAction SilentlyContinue | out-null | |
[Console]::ForegroundColor = [ConsoleColor]::Yellow | |
Write-Output "`r`nDone. Successfully patched!`r`n" | |
[Console]::ResetColor() | |
pause |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:: elevation required so the script can modify files which are typically installed under c:\program files | |
:: elevator.exe repo here: https://github.com/Beej126/Elevator | |
:: "delims=" includes spaces in elevator path | |
@for /f "delims=" %%v in ('where elevator.exe') do set elevatorpath=%%~fv | |
@if "%elevatorpath%"=="" ( | |
echo elevator.exe not found. Must be in Windows global %%path%%. | |
echo Download here: https://github.com/Beej126/Elevator | |
echo Aborting. | |
echo, | |
pause | |
exit 1 /b | |
) | |
:: create new file type and associate it with new ProgId | |
reg add "HKEY_CURRENT_USER\Software\Classes\.nil" /f /ve /t REG_SZ /d DotNet_IL_Binary_Patcher | |
:: file type description (shows in explorer hover tooltip) | |
reg add "HKEY_CURRENT_USER\Software\Classes\DotNet_IL_Binary_Patcher" /f /ve /t REG_SZ /d ".Net DLL Patcher" | |
:: install icon for the .nil filetype | |
:: Patch icon created by Freepik - Flaticon - https://www.flaticon.com/free-icons/patch | |
reg add "HKEY_CURRENT_USER\Software\Classes\DotNet_IL_Binary_Patcher\DefaultIcon" /f /ve /t REG_SZ /d "%~dp0patch.ico,0" | |
reg add "HKEY_CURRENT_USER\Software\Classes\DotNet_IL_Binary_Patcher\shell\Run .Net Bin Patch\command" /f /ve /t REG_SZ /d "\"%elevatorpath%\" -elev high pwsh -File \\\"%~dp0beejNetILPatcher.ps1\\\" -patchFilePath \\\"%%L\\\"" | |
@echo off | |
echo, | |
echo as a convenience for editing .nil files with proper .cs syntax highlighting in vscode, | |
echo add this to your vscode settings.json: | |
echo, | |
echo "files.associations": { | |
echo "*.nil": "csharp" | |
echo } | |
echo, | |
pause |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[InternetShortcut] | |
URL=https://adamtheautomator.com/ps1-to-exe/ |
View raw
(Sorry about that, but we can’t show files that are this big right now.)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment