Last active
March 23, 2025 04:09
-
-
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
# github repo for updates: | |
# tested under pwsh.exe and traditional powershell.exe | |
param( | |
[string]$patchFilePath | |
) | |
$ErrorActionPreference = "Stop" #if your powershell window is closing before seeing an error, temporarily change this to "Inquire" | |
Write-Output "" | |
try { | |
add-type -Path $PSScriptRoot\dnlib.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")) | |
} | |
pause | |
throw $_ | |
} | |
# first delete any previous .dll that may be hanging around | |
erase "$($patchFilePath).dll" -ErrorAction SilentlyContinue | out-null | |
# ideally, we'd go directly from C# source to binary patching, but dnlib only works from a .net binary source (i.e. exe or dll) | |
# so, we use powershell's super convenient add-type function to generate a .dll from our source .cs | |
# yet add-type also unavoidably loads the .dll into memory which locks the dll as read-only until the end of the script... | |
# unfortunately there's no mechanism to just compile without loading the dll | |
# so, forking a separate session via [powershell]::Create() isolates loading the dll and allows it to be unloaded when that session closes and releases | |
# therefore freeing this momentary .dll "byproduct" to be cleaned up by the end of the script | |
$ps = [powershell]::Create() | |
# 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 | |
$cmd = "Add-Type -TypeDefinition (Get-Content -raw `"$patchFilePath`") -OutputAssembly `"$($patchFilePath).dll`"" | |
# echo $cmd | |
# scriptblock::create does the variable expansion: https://stackoverflow.com/questions/25551893/powershell-expand-variable-in-scriptblock/25552464#25552464 | |
$ps.AddScript( [scriptblock]::Create($cmd) ) | out-null | |
$ps.Invoke() | |
# now we load the mini-dll that was just compiled from our patched source code into dnlib to help us iterate over the compiled methods | |
$sourceModule = [dnlib.DotNet.ModuleDefMD]::Load("$($patchFilePath).dll") | |
######################################################### | |
# the basic approach is copying the method body instructions for our replacements into the turbotax dll we want to patch... | |
######################################################### | |
# loop over all root types in the source.dll which represent each assembly to be patched | |
# circa 2023 Q1 latest pwsh (or perhaps .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 | Where-Object { $_.HasCustomAttributes -and $_.HasNestedTypes } | ForEach-Object { | |
$destDllPath = $_.CustomAttributes[0].ConstructorArguments[0].Value | |
if (!(test-path $destDllPath)) { | |
$sourceModule.Dispose(); | |
erase "$($patchFilePath).dll" -ErrorAction SilentlyContinue | out-null | |
Write-Output "`r`nthe dll to be patched does not exist: $destDllPath`r`n" | |
Write-Output "please double check the path in your patch file`r`n" | |
pause | |
exit | |
} | |
Write-Output "patching: $destDllPath ...`r`n" | |
$destModule = [dnlib.DotNet.ModuleDefMD]::Load($destDllPath) | |
# loop over all nested classes that have methods in the source ... | |
$_.NestedTypes | Where-Object 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 | |
$sourceMethod = $_ | |
# get pointer to the corresponding class method in the destination | |
$destClass = $destModule.GetTypes() | Where-Object Name -eq $className | |
$destMethod = $destClass.Methods | Where-Object Name -eq $methodName | |
if (!$destMethod.Body) { throw "method '$($className).$($_.method)' not found in destination" } | |
Write-Output " patching: $($methodName)" | |
# so far we're just doing a direct replace on the method body instructions, variables and attributes | |
# getting things to work i trial and error discovered that variables and exceptionhandlers are 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 variables and exceptionhandlers all that were necessary for the simple cases i tested so far | |
$destMethod.Body.Variables.Clear() | |
$sourceMethod.Body.Variables | ForEach-Object { | |
$destMethod.Body.Variables.Add($_) | out-null | |
} | |
$destMethod.Body.ExceptionHandlers.Clear() | |
$destMethod.Body.Instructions.Clear() | |
$sourceMethod.Body.Instructions | ForEach-Object { | |
$destMethod.Body.Instructions.Add($_) | |
} | |
$destMethod.CustomAttributes.Clear() | |
$sourceMethod.CustomAttributes | ForEach-Object { | |
$destMethod.CustomAttributes.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) re-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") | |
} | |
pause | |
throw $_ | |
} | |
$destModule.Dispose() | |
$destModule = $null | |
Write-Output "" | |
$fileVer = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($destDllPath).FileVersion | |
if (-not $fileVer) { $fileVer = "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
@echo off | |
:: need to be elevated so the script can modify files which are typically installed under c:\program files | |
:: so cool sudo can be enabled on windows 11 nowadays!! | |
where sudo.exe || (echo "sudo.exe not found... on modern Windows 11 you can now enable this under Developer Settings" && start ms-settings:developers && timeout /t 5 && exit /b 1) | |
:: 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 "sudo pwsh -File \"%~dp0beejNetILPatcher.ps1\" -patchFilePath \"%%L\"" | |
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://www.nuget.org/packages/dnlib/ |
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