你是否遇到过这些场景?
MemoryStream
和 Socket
都不够“暴力”?Exception
类永远“捕不到真相”?今天,我们就用 C# + Windows API 的组合拳,教你如何:
核心武器: OpenProcess
+ ReadProcessMemory
+ WriteProcessMemory
// 示例1:获取目标进程的句柄
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(
uint processAccess, // 权限标志(VM_READ/VM_WRITE)
bool bInheritHandle, // 是否继承句柄
int processId // 进程ID
);
// 权限常量(按位或组合)
private const uint PROCESS_VM_READ = 0x0010;
private const uint PROCESS_VM_WRITE = 0x0020;
private const uint PROCESS_QUERY_INFORMATION = 0x0400;
// 使用示例
Process targetProcess = Process.GetProcessesByName("notepad")[0];
IntPtr hProcess = OpenProcess(
PROCESS_VM_READ | PROCESS_VM_WRITE,
false,
targetProcess.Id
);
// 如果返回值为IntPtr.Zero,说明失败了!
if (hProcess == IntPtr.Zero) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}
注释详解:
OpenProcess
是“权限令牌”的起点,必须指定正确的权限(如 VM_READ
和 VM_WRITE
)。System
或其他高权限进程,普通用户程序会收到“拒绝访问”的回应(别慌,这属于正常现象)。// 示例2:读取目标进程的内存数据
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadProcessMemory(
IntPtr hProcess, // 进程句柄
IntPtr lpBaseAddress, // 要读取的地址
byte[] lpBuffer, // 存储结果的缓冲区
uint dwSize, // 读取字节数
out uint lpNumberOfBytesRead // 实际读取的字节数
);
// 读取示例:假设我们知道目标地址是 0x12345678
IntPtr targetAddress = new IntPtr(0x12345678);
byte[] buffer = new byte[4]; // 读取4字节
uint bytesRead;
bool success = ReadProcessMemory(
hProcess,
targetAddress,
buffer,
(uint)buffer.Length,
out bytesRead
);
if (!success || bytesRead != 4) {
throw new Exception("读取内存失败或数据不完整!");
}
// 将字节数组转换为整数(小端序)
int value = BitConverter.ToInt32(buffer, 0);
Console.WriteLine($"读取到的值:{value}");
注释详解:
BitConverter.ToInt32
默认使用小端序(与大多数 x86/x64 架构一致)。Encoding.UTF8.GetString(buffer)
解码。// 示例3:向目标进程写入数据
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool WriteProcessMemory(
IntPtr hProcess, // 进程句柄
IntPtr lpBaseAddress, // 目标地址
byte[] lpBuffer, // 要写入的数据
uint dwSize, // 数据大小
out uint lpNumberOfBytesWritten // 实际写入字节数
);
// 写入示例:将目标地址的值改为 9999
int newValue = 9999;
byte[] newBuffer = BitConverter.GetBytes(newValue); // 小端序
uint bytesWritten;
bool success = WriteProcessMemory(
hProcess,
targetAddress,
newBuffer,
(uint)newBuffer.Length,
out bytesWritten
);
if (!success || bytesWritten != newBuffer.Length) {
throw new Exception("写入内存失败!");
}
注释详解:
PAGE_READWRITE
权限(否则会触发访问冲突)。.rdata
段),需要先调用 VirtualProtectEx
修改保护属性。核心武器: CreateFileMapping
+ MapViewOfFile
// 示例4:创建命名文件映射(共享内存)
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateFileMapping(
IntPtr hFile, // 句柄(-1 表示使用系统页面文件)
IntPtr lpFileMappingAttributes, // 安全属性(null 表示默认)
uint flProtect, // 保护标志(PAGE_READWRITE)
uint dwMaximumSizeHigh, // 最大尺寸(高位)
uint dwMaximumSizeLow, // 最大尺寸(低位)
string lpName // 映射名称(用于跨进程识别)
);
// 创建示例:共享内存大小为 1024 字节
IntPtr hMapFile = CreateFileMapping(
new IntPtr(-1), // 使用系统页面文件
IntPtr.Zero, // 默认安全属性
0x04, // PAGE_READWRITE
0, 0, // 总大小(4294967295 * 0 + 1024 = 1024 字节)
"MySharedMemory" // 映射名称
);
if (hMapFile == IntPtr.Zero) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}
注释详解:
hFile
为 new IntPtr(-1)
表示使用系统页面文件(不绑定磁盘文件)。lpName
是共享内存的“身份证号”,其他进程通过这个名字找到它。// 示例5:映射共享内存到当前进程
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr MapViewOfFile(
IntPtr hFileMappingObject, // 文件映射句柄
uint dwDesiredAccess, // 访问权限(FILE_MAP_ALL_ACCESS)
uint dwFileOffsetHigh, // 偏移量(高位)
uint dwFileOffsetLow, // 偏移量(低位)
IntPtr dwNumberOfBytesToMap // 映射大小(0 表示全部)
);
// 映射示例:读写共享内存
IntPtr pMapView = MapViewOfFile(
hMapFile,
0xF001F, // FILE_MAP_ALL_ACCESS
0, 0, // 偏移量为 0
IntPtr.Zero // 映射整个区域
);
if (pMapView == IntPtr.Zero) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}
// 写入数据到共享内存
string message = "Hello from Process A!";
byte[] data = Encoding.UTF8.GetBytes(message);
Buffer.MemoryCopy(data, 0, pMapView, data.Length);
注释详解:
MapViewOfFile
将共享内存映射到当前进程的地址空间,相当于“打开共享文件”。Buffer.MemoryCopy
是 .NET Core 中的高效内存复制方法(兼容性请自行验证)。核心原则: CloseHandle
+ UnmapViewOfFile
+ 异常处理
// 示例6:释放共享内存资源
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool UnmapViewOfFile(IntPtr lpBaseAddress);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
// 释放共享内存视图
bool unmapSuccess = UnmapViewOfFile(pMapView);
if (!unmapSuccess) {
Console.WriteLine("警告:共享内存视图未正确释放!");
}
// 关闭文件映射句柄
bool closeSuccess = CloseHandle(hMapFile);
if (!closeSuccess) {
Console.WriteLine("警告:文件映射句柄未正确关闭!");
}
注释详解:
UnmapViewOfFile
会解除内存映射,否则会导致内存泄漏(尤其是长时间运行的服务程序)。CloseHandle
是通用的句柄关闭方法(适用于文件、进程、线程等)。// 示例7:封装内存操作到 try-catch-finally
try {
// 执行内存读写或共享内存操作
} catch (Win32Exception ex) {
Console.WriteLine($"Windows API 错误:{ex.Message}");
} catch (Exception ex) {
Console.WriteLine($"通用异常:{ex.Message}");
} finally {
// 确保资源释放
if (pMapView != IntPtr.Zero) UnmapViewOfFile(pMapView);
if (hMapFile != IntPtr.Zero) CloseHandle(hMapFile);
}
注释详解:
Win32Exception
是 Windows API 错误的标准异常类型。finally
块能保证即使抛出异常,资源也能被释放。场景 | 推荐方案 | 注意事项 |
---|---|---|
游戏金币修改 | ReadProcessMemory + WriteProcessMemory |
需提前用 CE 分析内存地址 |
跨进程通信 | CreateFileMapping + MapViewOfFile |
名称需全局唯一,避免冲突 |
调试器开发 | OpenProcess + VirtualProtectEx |
需处理权限和异常 |
终极建议:
ReadProcessMemory
和 WriteProcessMemory
。CreateFileMapping
和 MapViewOfFile
。