Skip to content

Instantly share code, notes, and snippets.

@Beej126
Last active February 18, 2023 07:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Beej126/f26e6649cfcc38accee3a0a8cc0a9d04 to your computer and use it in GitHub Desktop.
Save Beej126/f26e6649cfcc38accee3a0a8cc0a9d04 to your computer and use it in GitHub Desktop.
dnlib copy source dll methods to destination dll (patch tool)
dest.cs
dest/
save.ps1
source.cs
source.dll
# 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
:: 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
[InternetShortcut]
URL=https://adamtheautomator.com/ps1-to-exe/
This file has been truncated, but you can view the full file.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

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