|
using System; |
|
using System.Diagnostics; |
|
using System.IO; |
|
using System.Runtime.InteropServices; |
|
using System.Text; |
|
|
|
class ClaudePatcher |
|
{ |
|
// Win32 API declarations |
|
[DllImport("kernel32.dll", SetLastError = true)] |
|
static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, |
|
[Out] byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesRead); |
|
|
|
[DllImport("kernel32.dll", SetLastError = true)] |
|
static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, |
|
byte[] lpBuffer, int nSize, out IntPtr lpNumberOfBytesWritten); |
|
|
|
[DllImport("kernel32.dll")] |
|
static extern bool VirtualQueryEx(IntPtr hProcess, IntPtr lpAddress, |
|
out MEMORY_BASIC_INFORMATION lpBuffer, uint dwLength); |
|
|
|
[DllImport("kernel32.dll", SetLastError = true)] |
|
static extern bool VirtualProtectEx(IntPtr hProcess, IntPtr lpAddress, |
|
UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); |
|
|
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] |
|
static extern bool CreateProcess( |
|
string? lpApplicationName, |
|
string lpCommandLine, |
|
IntPtr lpProcessAttributes, |
|
IntPtr lpThreadAttributes, |
|
bool bInheritHandles, |
|
uint dwCreationFlags, |
|
IntPtr lpEnvironment, |
|
string? lpCurrentDirectory, |
|
ref STARTUPINFO lpStartupInfo, |
|
out PROCESS_INFORMATION lpProcessInformation); |
|
|
|
[DllImport("kernel32.dll", SetLastError = true)] |
|
static extern uint ResumeThread(IntPtr hThread); |
|
|
|
[DllImport("kernel32.dll", SetLastError = true)] |
|
static extern bool CloseHandle(IntPtr hObject); |
|
|
|
[DllImport("kernel32.dll", SetLastError = true)] |
|
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); |
|
|
|
[DllImport("kernel32.dll", SetLastError = true)] |
|
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode); |
|
|
|
private const uint CREATE_SUSPENDED = 0x00000004; |
|
private const uint INFINITE = 0xFFFFFFFF; |
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] |
|
struct STARTUPINFO |
|
{ |
|
public int cb; |
|
public string lpReserved; |
|
public string lpDesktop; |
|
public string lpTitle; |
|
public int dwX; |
|
public int dwY; |
|
public int dwXSize; |
|
public int dwYSize; |
|
public int dwXCountChars; |
|
public int dwYCountChars; |
|
public int dwFillAttribute; |
|
public int dwFlags; |
|
public short wShowWindow; |
|
public short cbReserved2; |
|
public IntPtr lpReserved2; |
|
public IntPtr hStdInput; |
|
public IntPtr hStdOutput; |
|
public IntPtr hStdError; |
|
} |
|
|
|
[StructLayout(LayoutKind.Sequential)] |
|
struct PROCESS_INFORMATION |
|
{ |
|
public IntPtr hProcess; |
|
public IntPtr hThread; |
|
public int dwProcessId; |
|
public int dwThreadId; |
|
} |
|
|
|
[StructLayout(LayoutKind.Sequential)] |
|
struct MEMORY_BASIC_INFORMATION |
|
{ |
|
public IntPtr BaseAddress; |
|
public IntPtr AllocationBase; |
|
public uint AllocationProtect; |
|
public IntPtr RegionSize; |
|
public uint State; |
|
public uint Protect; |
|
public uint Type; |
|
} |
|
|
|
private const uint MEM_COMMIT = 0x1000; |
|
private const uint PAGE_READONLY = 0x02; |
|
private const uint PAGE_READWRITE = 0x04; |
|
private const uint PAGE_EXECUTE_READ = 0x20; |
|
private const uint PAGE_EXECUTE_READWRITE = 0x40; |
|
|
|
static string? FindClaudeExe() |
|
{ |
|
// Check same directory as patcher |
|
string appDir = Path.GetDirectoryName(Environment.ProcessPath) ?? ""; |
|
string localPath = Path.Combine(appDir, "claude.exe"); |
|
if (File.Exists(localPath)) |
|
return localPath; |
|
|
|
// Check user's .local/bin directory |
|
string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); |
|
string userLocalPath = Path.Combine(userProfile, ".local", "bin", "claude.exe"); |
|
if (File.Exists(userLocalPath)) |
|
return userLocalPath; |
|
|
|
return null; |
|
} |
|
|
|
static void Main(string[] args) |
|
{ |
|
string? claudeExe = FindClaudeExe(); |
|
if (claudeExe == null) |
|
{ |
|
Console.WriteLine("Error: claude.exe not found"); |
|
Console.WriteLine("Searched locations:"); |
|
Console.WriteLine($" - {Path.GetDirectoryName(Environment.ProcessPath)}\\claude.exe"); |
|
string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); |
|
Console.WriteLine($" - {Path.Combine(userProfile, ".local", "bin", "claude.exe")}"); |
|
Environment.Exit(1); |
|
} |
|
|
|
try |
|
{ |
|
STARTUPINFO si = new STARTUPINFO(); |
|
si.cb = Marshal.SizeOf(si); |
|
|
|
PROCESS_INFORMATION pi; |
|
string cmdLine = claudeExe + (args.Length > 0 ? " " + string.Join(" ", args) : ""); |
|
|
|
if (!CreateProcess(null, cmdLine, IntPtr.Zero, IntPtr.Zero, false, |
|
CREATE_SUSPENDED, IntPtr.Zero, null, ref si, out pi)) |
|
{ |
|
Console.WriteLine($"Failed to create process. Error: {Marshal.GetLastWin32Error()}"); |
|
Environment.Exit(1); |
|
} |
|
|
|
StringBuilder diagnostics = new StringBuilder(); |
|
int patchCount = SearchAndPatch(pi.hProcess, diagnostics); |
|
|
|
ResumeThread(pi.hThread); |
|
WaitForSingleObject(pi.hProcess, INFINITE); |
|
|
|
uint exitCode; |
|
GetExitCodeProcess(pi.hProcess, out exitCode); |
|
|
|
CloseHandle(pi.hThread); |
|
CloseHandle(pi.hProcess); |
|
|
|
// Only print diagnostics if patching failed |
|
if (patchCount < 1) |
|
{ |
|
Console.WriteLine($"Warning: No patches applied"); |
|
Console.WriteLine(diagnostics.ToString()); |
|
} |
|
|
|
Environment.Exit((int)exitCode); |
|
} |
|
catch (Exception ex) |
|
{ |
|
Console.WriteLine($"Error: {ex.Message}"); |
|
Environment.Exit(1); |
|
} |
|
} |
|
|
|
static int SearchAndPatch(IntPtr hProcess, StringBuilder diagnostics) |
|
{ |
|
// Full patterns to search and replace |
|
byte[] oldBytes = Encoding.ASCII.GetBytes(".normalize().replaceAll(`\\r\n`,`\n`)"); |
|
byte[] newBytes = Encoding.ASCII.GetBytes(".normalize().replaceAll(`\\v\n`,`\n`)"); |
|
|
|
if (oldBytes.Length != newBytes.Length) |
|
{ |
|
diagnostics.AppendLine("Error: Search and replace patterns must be same length"); |
|
return 0; |
|
} |
|
|
|
int patchCount = 0; |
|
IntPtr address = IntPtr.Zero; |
|
MEMORY_BASIC_INFORMATION mbi = new MEMORY_BASIC_INFORMATION(); |
|
uint mbiSize = (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION)); |
|
|
|
while (VirtualQueryEx(hProcess, address, out mbi, mbiSize)) |
|
{ |
|
// Advance to next region |
|
long nextAddress = mbi.BaseAddress.ToInt64() + mbi.RegionSize.ToInt64(); |
|
if (nextAddress <= 0 || nextAddress > 0x7FFFFFFFFFFF) |
|
break; |
|
address = new IntPtr(nextAddress); |
|
|
|
// Skip non-committed memory |
|
if (mbi.State != MEM_COMMIT) |
|
continue; |
|
|
|
// Skip non-readable memory |
|
if (mbi.Protect != PAGE_READONLY && |
|
mbi.Protect != PAGE_READWRITE && |
|
mbi.Protect != PAGE_EXECUTE_READ && |
|
mbi.Protect != PAGE_EXECUTE_READWRITE) |
|
continue; |
|
|
|
long regionSize = mbi.RegionSize.ToInt64(); |
|
|
|
// Skip invalid or excessively large regions (> 500MB) |
|
if (regionSize <= 0 || regionSize >= 500 * 1024 * 1024) |
|
continue; |
|
|
|
byte[] buffer = new byte[regionSize]; |
|
|
|
if (!ReadProcessMemory(hProcess, mbi.BaseAddress, buffer, |
|
(int)regionSize, out IntPtr bytesRead)) |
|
continue; |
|
|
|
// Search for pattern in this region |
|
for (long i = 0; i <= bytesRead.ToInt64() - oldBytes.Length; i++) |
|
{ |
|
bool match = true; |
|
for (int j = 0; j < oldBytes.Length; j++) |
|
{ |
|
if (buffer[i + j] != oldBytes[j]) |
|
{ |
|
match = false; |
|
break; |
|
} |
|
} |
|
|
|
if (!match) |
|
continue; |
|
|
|
IntPtr patchAddress = IntPtr.Add(mbi.BaseAddress, (int)i); |
|
diagnostics.AppendLine($"Found pattern at 0x{patchAddress.ToString("X")}"); |
|
|
|
uint oldProtect; |
|
if (!VirtualProtectEx(hProcess, patchAddress, (UIntPtr)newBytes.Length, |
|
PAGE_EXECUTE_READWRITE, out oldProtect)) |
|
{ |
|
diagnostics.AppendLine($" Failed to change protection (Error: {Marshal.GetLastWin32Error()})"); |
|
continue; |
|
} |
|
|
|
if (WriteProcessMemory(hProcess, patchAddress, |
|
newBytes, newBytes.Length, out IntPtr written)) |
|
{ |
|
diagnostics.AppendLine($" Patched successfully"); |
|
patchCount++; |
|
} |
|
else |
|
{ |
|
diagnostics.AppendLine($" Failed to write (Error: {Marshal.GetLastWin32Error()})"); |
|
} |
|
|
|
VirtualProtectEx(hProcess, patchAddress, (UIntPtr)newBytes.Length, |
|
oldProtect, out oldProtect); |
|
} |
|
} |
|
|
|
return patchCount; |
|
} |
|
} |