diff --git a/.github/workflows/msbuild.yml b/.github/workflows/msbuild.yml index 43fff7c..6f79b5a 100644 --- a/.github/workflows/msbuild.yml +++ b/.github/workflows/msbuild.yml @@ -43,6 +43,10 @@ jobs: # See https://docs.microsoft.com/visualstudio/msbuild/msbuild-command-line-reference run: msbuild /m /p:Configuration=${{env.BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}} + - name: Build PATCHED sections from ASM files + shell: cmd + run: build-patch.bat -All + - name: Copy Files Legacy shell: cmd run: | diff --git a/README_BUILD_PATCH.md b/README_BUILD_PATCH.md new file mode 100644 index 0000000..e1b85c2 --- /dev/null +++ b/README_BUILD_PATCH.md @@ -0,0 +1,183 @@ +# Build Patch Script + +This document explains how to use the `build-patch.ps1` script to automatically generate the PATCHED section of `.binpatch.txt` files from `.asm` (assembly) files. + +## Overview + +The MCPatcher project uses binary patching to modify game executables. Each patch consists of: +- An `.asm` file containing the x64 assembly code for the patch +- A `.binpatch.txt` file containing: + - `ORIGINAL` section: The original machine code to be replaced + - `PATCHED` section: The new machine code (generated from the .asm file) + +The `build-patch.ps1` script automates the generation of the PATCHED section by: +1. Assembling the `.asm` file using Microsoft MASM (ml64.exe) +2. Extracting the machine code from the resulting object file +3. Formatting it to match the binpatch.txt format +4. Updating the PATCHED section in the corresponding `.binpatch.txt` file + +## Requirements + +- Windows operating system +- Microsoft Visual Studio with C++ build tools (provides ml64.exe) + - Visual Studio 2022 (any edition) with "Desktop development with C++" workload + - Or Visual Studio Build Tools 2022 with C++ tools + +## Usage + +### Command Line + +#### Build all patches +```batch +build-patch.bat -All +``` +or +```powershell +.\build-patch.ps1 -All +``` + +#### Build a specific patch +```batch +build-patch.bat -AsmFile XStoreQueryGameLicenseAsync.asm +``` +or +```powershell +.\build-patch.ps1 -AsmFile XStoreQueryGameLicenseAsync.asm +``` + +#### Verify existing patches (without rebuilding) +```batch +build-patch.bat -Verify -All +``` +or +```powershell +.\build-patch.ps1 -Verify -All +``` + +### Parameters + +- `-AsmFile `: Specifies a single .asm file to process +- `-All`: Process all .asm files in the current directory +- `-Verify`: Display existing PATCHED sections without rebuilding + +If no parameters are specified, `-All` is used by default. + +## How It Works + +### Assembly File Format + +Assembly files should be in MASM x64 format with the following structure: + +```asm +option casemap:none +.code + +XStoreQueryGameLicenseAsync proc + push rdi ; 57 + sub rsp, 20h ; 48 83 EC 20 + ; ... more instructions + ret ; C3 +XStoreQueryGameLicenseAsync endp +end +``` + +Comments showing the expected machine code are optional but helpful for verification. + +### Binpatch File Format + +The `.binpatch.txt` files have the following format: + +``` +ORIGINAL +48 8B C4 57 48 83 EC 30 +48 C7 40 E8 FE FF FF FF +... +PATCHED +57 48 83 EC 20 48 89 D7 +48 31 C0 48 C7 47 18 00 +... +``` + +- Each line contains up to 8 bytes in hexadecimal format (uppercase) +- Bytes are separated by spaces +- The last line may contain fewer than 8 bytes + +### Script Process + +1. **Locate ml64.exe**: The script searches for ml64.exe in: + - System PATH + - Visual Studio 2022 installations (all editions) + - Common installation directories + +2. **Assemble**: Uses ml64.exe with `/c` flag to assemble the .asm file to .obj + +3. **Extract**: Reads the COFF object file and extracts the `.text` section containing machine code + +4. **Format**: Converts the binary machine code to hexadecimal string format (8 bytes per line) + +5. **Update**: Replaces the PATCHED section in the corresponding .binpatch.txt file + +## CI/CD Integration + +The script is integrated into the GitHub Actions workflow (`.github/workflows/msbuild.yml`): + +```yaml +- name: Build PATCHED sections from ASM files + shell: cmd + run: build-patch.bat -All +``` + +This step runs after the main MSBuild step and before copying files to artifacts, ensuring that all binpatch.txt files are up-to-date with the latest assembly code. + +## Troubleshooting + +### "ml64.exe not found" + +**Solution**: Install Visual Studio 2022 with the "Desktop development with C++" workload, or install the Visual Studio Build Tools 2022. + +### "Assembly failed" + +**Causes**: +- Syntax errors in the .asm file +- Incorrect MASM directives +- Missing section definitions + +**Solution**: Review the error message from ml64.exe and fix the assembly syntax. + +### "Failed to extract machine code" + +**Causes**: +- The .obj file doesn't contain a .text section +- The object file is corrupted + +**Solution**: Verify that the .asm file creates executable code (not just data). + +## File Locations + +- `build-patch.ps1` - Main PowerShell script +- `build-patch.bat` - Batch file wrapper for easier execution +- `*.asm` - Assembly source files +- `*.binpatch.txt` - Binary patch definition files + +## Example + +Given these files: +- `XStoreQueryGameLicenseAsync.asm` - Assembly source +- `XStoreQueryGameLicenseAsync.binpatch.txt` - Patch definition (with ORIGINAL section) + +Running: +```batch +build-patch.bat -AsmFile XStoreQueryGameLicenseAsync.asm +``` + +Will: +1. Assemble `XStoreQueryGameLicenseAsync.asm` to create `XStoreQueryGameLicenseAsync.obj` +2. Extract the machine code from the object file +3. Update the PATCHED section in `XStoreQueryGameLicenseAsync.binpatch.txt` + +## Notes + +- The script creates temporary files in `%TEMP%` during processing +- Temporary files are automatically cleaned up after processing +- The ORIGINAL section in .binpatch.txt files is never modified +- Backup your .binpatch.txt files before first use if needed diff --git a/build-patch.bat b/build-patch.bat new file mode 100644 index 0000000..29a79f4 --- /dev/null +++ b/build-patch.bat @@ -0,0 +1,19 @@ +@echo off +REM Batch wrapper for build-patch.ps1 +REM This script builds PATCHED sections from .asm files using MASM + +setlocal + +set "SCRIPT_DIR=%~dp0" +set "PS_SCRIPT=%SCRIPT_DIR%build-patch.ps1" + +REM Check if PowerShell script exists +if not exist "%PS_SCRIPT%" ( + echo ERROR: build-patch.ps1 not found! + exit /b 1 +) + +REM Run PowerShell script with all parameters +powershell.exe -ExecutionPolicy Bypass -File "%PS_SCRIPT%" %* + +exit /b %ERRORLEVEL% diff --git a/build-patch.ps1 b/build-patch.ps1 new file mode 100644 index 0000000..c42336c --- /dev/null +++ b/build-patch.ps1 @@ -0,0 +1,323 @@ +# PowerShell script to build PATCHED section of binpatch.txt from .asm files +# This script uses Microsoft MASM (ml64.exe) to assemble x64 assembly files +# and extracts the machine code to update the PATCHED section in binpatch.txt files + +param( + [Parameter(Mandatory=$false)] + [string]$AsmFile = "", + [Parameter(Mandatory=$false)] + [switch]$All = $false, + [Parameter(Mandatory=$false)] + [switch]$Verify = $false +) + +# Function to find ml64.exe +function Find-ML64 { + # Try to find ml64.exe in PATH first + $ml64 = Get-Command ml64.exe -ErrorAction SilentlyContinue + if ($ml64) { + return $ml64.Source + } + + # Try to find it in Visual Studio installations + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (Test-Path $vswhere) { + $vsPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if ($vsPath) { + $ml64Path = Get-ChildItem -Path "$vsPath" -Filter ml64.exe -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($ml64Path) { + return $ml64Path.FullName + } + } + } + + # Try common installation paths + $commonPaths = @( + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\*\bin\Hostx64\x64\ml64.exe", + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\*\bin\Hostx64\x64\ml64.exe", + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\*\bin\Hostx64\x64\ml64.exe", + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\*\bin\Hostx64\x64\ml64.exe", + "${env:ProgramFiles}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\*\bin\Hostx64\x64\ml64.exe", + "${env:ProgramFiles}\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\*\bin\Hostx64\x64\ml64.exe", + "${env:ProgramFiles}\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\*\bin\Hostx64\x64\ml64.exe", + "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\*\bin\Hostx64\x64\ml64.exe" + ) + + foreach ($path in $commonPaths) { + $found = Get-Item $path -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + return $found.FullName + } + } + + throw "ml64.exe not found. Please ensure Visual Studio with C++ tools is installed." +} + +# Function to assemble ASM file and extract machine code +function Build-PatchFromAsm { + param( + [string]$AsmFilePath, + [string]$ML64Path + ) + + $asmFile = Get-Item $AsmFilePath + $baseName = $asmFile.BaseName + $binpatchFile = Join-Path $asmFile.DirectoryName "$baseName.binpatch.txt" + + Write-Host "Processing: $($asmFile.Name)" -ForegroundColor Cyan + + # Create temporary directory for build artifacts + $tempDir = Join-Path $env:TEMP "mcpatcher_build_$(Get-Random)" + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + + try { + # Copy ASM file to temp directory + $tempAsmFile = Join-Path $tempDir "$baseName.asm" + Copy-Item $asmFile.FullName $tempAsmFile + + # Assemble the file + $objFile = Join-Path $tempDir "$baseName.obj" + Write-Host " Assembling with ml64.exe..." -ForegroundColor Gray + + $assembleArgs = @("/c", "/Fo$objFile", $tempAsmFile) + $process = Start-Process -FilePath $ML64Path -ArgumentList $assembleArgs -NoNewWindow -Wait -PassThru -RedirectStandardError (Join-Path $tempDir "error.txt") + + if ($process.ExitCode -ne 0) { + $errorContent = Get-Content (Join-Path $tempDir "error.txt") -Raw + Write-Host " ERROR: Assembly failed!" -ForegroundColor Red + Write-Host $errorContent -ForegroundColor Red + return $false + } + + if (-not (Test-Path $objFile)) { + Write-Host " ERROR: Object file not created!" -ForegroundColor Red + return $false + } + + # Extract machine code from .obj file + Write-Host " Extracting machine code..." -ForegroundColor Gray + $machineCode = Extract-MachineCode -ObjFilePath $objFile + + if (-not $machineCode) { + Write-Host " ERROR: Failed to extract machine code!" -ForegroundColor Red + return $false + } + + # Format machine code for binpatch.txt + $formattedCode = Format-MachineCode -MachineCode $machineCode + + # Update or create binpatch.txt file + if (Test-Path $binpatchFile) { + Update-BinpatchFile -BinpatchFilePath $binpatchFile -PatchedSection $formattedCode + Write-Host " Updated: $baseName.binpatch.txt" -ForegroundColor Green + } else { + Write-Host " WARNING: $baseName.binpatch.txt not found, skipping update" -ForegroundColor Yellow + return $false + } + + return $true + } + finally { + # Clean up temp directory + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + } +} + +# Function to extract machine code from OBJ file +function Extract-MachineCode { + param([string]$ObjFilePath) + + # Read the OBJ file as bytes + $objBytes = [System.IO.File]::ReadAllBytes($ObjFilePath) + + # COFF format: Find the .text section + # COFF header is at offset 0 + # Section headers start after COFF header and optional header + + # Read COFF header + $machine = [BitConverter]::ToUInt16($objBytes, 0) + $numberOfSections = [BitConverter]::ToUInt16($objBytes, 2) + $sizeOfOptionalHeader = [BitConverter]::ToUInt16($objBytes, 16) + + # Section headers start after COFF file header (20 bytes) + optional header + $sectionHeaderOffset = 20 + $sizeOfOptionalHeader + + # Each section header is 40 bytes + for ($i = 0; $i -lt $numberOfSections; $i++) { + $offset = $sectionHeaderOffset + ($i * 40) + + # Read section name (first 8 bytes) + $nameBytes = $objBytes[$offset..($offset + 7)] + $sectionName = [System.Text.Encoding]::ASCII.GetString($nameBytes).TrimEnd([char]0) + + if ($sectionName -eq ".text" -or $sectionName.StartsWith(".text")) { + # Found .text section + $sizeOfRawData = [BitConverter]::ToUInt32($objBytes, $offset + 16) + $pointerToRawData = [BitConverter]::ToUInt32($objBytes, $offset + 20) + + if ($sizeOfRawData -gt 0 -and $pointerToRawData -gt 0) { + # Extract the machine code + $code = $objBytes[$pointerToRawData..($pointerToRawData + $sizeOfRawData - 1)] + return $code + } + } + } + + return $null +} + +# Function to format machine code for binpatch.txt +function Format-MachineCode { + param([byte[]]$MachineCode) + + $lines = @() + $bytesPerLine = 8 + + for ($i = 0; $i -lt $MachineCode.Length; $i += $bytesPerLine) { + $lineBytes = $MachineCode[$i..[Math]::Min($i + $bytesPerLine - 1, $MachineCode.Length - 1)] + $hexBytes = $lineBytes | ForEach-Object { $_.ToString("X2") } + $lines += ($hexBytes -join " ") + } + + return $lines -join "`n" +} + +# Function to update the PATCHED section in binpatch.txt +function Update-BinpatchFile { + param( + [string]$BinpatchFilePath, + [string]$PatchedSection + ) + + # Read the existing file + $content = Get-Content $BinpatchFilePath -Raw + + # Check if there's already a PATCHED section + if ($content -match "(?s)(.*ORIGINAL.*?)(?:PATCHED.*)?$") { + # Replace or add PATCHED section + if ($content -match "PATCHED") { + # Replace existing PATCHED section + $content = $content -replace "(?s)(PATCHED).*$", "PATCHED`n$PatchedSection" + } else { + # Add PATCHED section + $content = $content.TrimEnd() + "`nPATCHED`n$PatchedSection" + } + } else { + Write-Host " WARNING: ORIGINAL section not found in $BinpatchFilePath" -ForegroundColor Yellow + return + } + + # Ensure the file ends with a newline + if (-not $content.EndsWith("`n")) { + $content += "`n" + } + + # Write back to file + [System.IO.File]::WriteAllText($BinpatchFilePath, $content) +} + +# Function to verify the PATCHED section matches the ASM file +function Verify-Patch { + param([string]$AsmFilePath) + + $asmFile = Get-Item $AsmFilePath + $baseName = $asmFile.BaseName + $binpatchFile = Join-Path $asmFile.DirectoryName "$baseName.binpatch.txt" + + if (-not (Test-Path $binpatchFile)) { + Write-Host " WARNING: $baseName.binpatch.txt not found" -ForegroundColor Yellow + return $false + } + + # Read existing PATCHED section + $content = Get-Content $binpatchFile -Raw + if ($content -match "(?s)PATCHED\s+([\s\S]+)$") { + $existingPatched = $matches[1].Trim() + Write-Host " Existing PATCHED section:" -ForegroundColor Gray + Write-Host " $($existingPatched -replace "`n", "`n ")" -ForegroundColor Gray + return $true + } else { + Write-Host " WARNING: No PATCHED section found" -ForegroundColor Yellow + return $false + } +} + +# Main script logic +try { + Write-Host "`n=== MCPatcher - Build PATCHED Section from ASM ===" -ForegroundColor Cyan + Write-Host "" + + # Find ml64.exe + Write-Host "Locating ml64.exe..." -ForegroundColor Cyan + $ml64Path = Find-ML64 + Write-Host "Found: $ml64Path" -ForegroundColor Green + Write-Host "" + + # Get list of ASM files to process + $asmFiles = @() + + if ($All) { + # Process all .asm files in the current directory + $asmFiles = Get-ChildItem -Path . -Filter "*.asm" | Where-Object { -not $_.Name.StartsWith("_") } + } elseif ($AsmFile) { + # Process specified file + if (Test-Path $AsmFile) { + $asmFiles = @(Get-Item $AsmFile) + } else { + Write-Host "ERROR: File not found: $AsmFile" -ForegroundColor Red + exit 1 + } + } else { + # Default: process all .asm files in current directory + $asmFiles = Get-ChildItem -Path . -Filter "*.asm" | Where-Object { -not $_.Name.StartsWith("_") } + } + + if ($asmFiles.Count -eq 0) { + Write-Host "No ASM files found to process." -ForegroundColor Yellow + exit 0 + } + + Write-Host "Found $($asmFiles.Count) ASM file(s) to process:" -ForegroundColor Cyan + foreach ($file in $asmFiles) { + Write-Host " - $($file.Name)" -ForegroundColor Gray + } + Write-Host "" + + # Process each file + $successCount = 0 + $failCount = 0 + + foreach ($asmFile in $asmFiles) { + if ($Verify) { + Verify-Patch -AsmFilePath $asmFile.FullName + } else { + if (Build-PatchFromAsm -AsmFilePath $asmFile.FullName -ML64Path $ml64Path) { + $successCount++ + } else { + $failCount++ + } + } + Write-Host "" + } + + # Summary + if (-not $Verify) { + Write-Host "=== Summary ===" -ForegroundColor Cyan + Write-Host "Successfully processed: $successCount" -ForegroundColor Green + if ($failCount -gt 0) { + Write-Host "Failed: $failCount" -ForegroundColor Red + exit 1 + } + } + + Write-Host "" + Write-Host "Done!" -ForegroundColor Green + +} catch { + Write-Host "" + Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red + Write-Host $_.ScriptStackTrace -ForegroundColor Red + exit 1 +}