使用 .NET 钩子替换默认的 Windows 日历
寻找另一种解决方案来更改或修改原生的 Windows API 方法调用,以我们自己的实现替换系统的默认输出。
引言
现在已是 2013 年(年底),Windows 机器上仍然没有真正原生的波斯日历。1382/7/15 是一个波斯日期,我敢肯定,对你们中的许多人来说毫无意义。我们(伊朗人)对 2013/10/8 也有同样的感受。对我们来说毫无意义,因为公历日期的不同部分与波斯日期不匹配。我们不知道这里的“10”是指我们的第十个月还是其他意思。但 Linux 机器根本没有这个问题。它是开源的,易于更改,并且已经支持真正的波斯日历。Windows 呢?不行,我们无法更改其默认日历,我们需要寻找另一种解决方案来更改或修改原生的 Windows API 方法调用,以我们自己的实现替换系统的默认输出。
引入 EasyHook
可以通过一种称为“钩子”或“Windows Hooks”的机制来拦截 Windows API 方法调用。这不是一个新概念,有很多辅助库可以简化这个操作。微软甚至有一个用于此类操作的辅助库,称为 Detours。它需要使用 C 或类似的本地语言编写 Windows 钩子。在 .NET 世界中,有一个非常有用的钩子库,它允许我们使用 .NET 语言编写 .NET 钩子。它名为 EasyHook,您可以在此处下载:https://easyhook.codeplex.com/。
要编写 Windows 钩子,我们需要找出是哪个方法发出了某个操作。幸运的是,有很多 Windows API“监视器”工具可以完成这项任务。我在这里使用的是“API Monitor”程序:http://www.rohitab.com/apimonitor。
如果要监视 32 位程序,请运行 apimonitor-x86.exe,如果要跟踪 64 位程序,请运行 apimonitor-x64.exe 文件。例如,如果您在 API Monitor 的进程列表中找不到 firefox.exe,则表示您没有使用它的 x86 版本。因为通常提供的 Firefox 版本仍然是 32 位应用程序。“API Monitor”带有数千个预定义的 Windows API 定义。我们不知道 Windows Explorer 使用哪个(或哪些)方法来显示日期和时间。因此,我们开始在“API Monitor”的“API Filter”部分选择所有包含“date”或“time”的方法名称。
然后,在“API Monitor”的进程列表部分,右键单击 explorer.exe 并选择“Start monitoring”。
现在打开一个新的 Explorer 窗口,查看跟踪的 API 方法调用。
正如您所见,Windows Explorer 使用 GetDateFormatW
和 GetTimeFormatW
方法来显示日期和时间信息。它首先将 lpDate
或 lpTime
参数发送到一个提到的方法,然后从 lpDateStr
或 lpTimeStr
参数中获取返回值。现在我们的任务是拦截这些方法调用,并用我们自己的实现替换 lpDateStr
或 lpTimeStr
值!
为 GetDateFormatW 和 GetTimeFormatW 方法编写 .NET 钩子
假设您已经从 https://easyhook.codeplex.com/ 下载了最新版本的 EasyHook。现在创建一个包含一个控制台应用程序和一个新的类库项目的新解决方案。控制台应用程序将安装钩子,类库项目将包含钩子。因此,这两个项目都应该引用 EasyHook.dll 库。
钩子类的签名应类似于以下的 GetDateTimeFormatInjection
类。
namespace ExplorerPCal.Hooks
{
public class GetDateTimeFormatInjection : IEntryPoint
{
public GetDateTimeFormatInjection(RemoteHooking.IContext context, string channelName)
{
// connect to host...
_interface = RemoteHooking.IpcConnectClient<MessagesReceiverInterface>(channelName);
_interface.Ping();
}
public void Run(RemoteHooking.IContext context, string channelName)
{
}
}
}
它应该实现 IEntryPoint
接口。这是一个空接口,仅用于将此类标记为钩子类。此外,钩子类应该包含一个构造函数和一个具有上述签名的 Run
方法。因为您可以在此处指定多于两个参数,所以这些方法不包含在 IEntryPoint
接口中。
我们将使用 Run
方法来编写我们自己的钩子,以拦截 Windows API 方法调用。
public void Run(RemoteHooking.IContext context, string channelName)
{
GetDateFormatHook = LocalHook.Create(
InTargetProc: LocalHook.GetProcAddress("kernel32.dll", "GetDateFormatW"),
InNewProc: new GetDateFormatDelegate(getDateFormatInterceptor),
InCallback: this);
}
例如,这里我们使用 LocalHook.Create
方法为拦截 kernel32.dll 的 GetDateFormatW
方法调用创建一个回调委托。
此委托的签名很重要。如果 kernel32.dll 的 GetDateFormatW
的签名是
[DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetDateFormatW(
uint locale,
uint dwFlags, // NLS_DATE_FLAGS
SystemTime lpDate,
[MarshalAs(UnmanagedType.LPWStr)] string lpFormat,
StringBuilder lpDateStr,
int sbSize);
此委托应具有相同的参数,并且还应使用 UnmanagedFunctionPointer
属性进行修饰。
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Auto, SetLastError = true)]
private delegate int GetDateFormatDelegate(
uint locale,
uint dwFlags,
SystemTime lpDate,
[MarshalAs(UnmanagedType.LPWStr)] string lpFormat,
StringBuilder lpDateStr,
int sbSize);
现在,我们的回调将首先接收真正的 GetDateFormatW
调用,然后再由 Explorer 处理,它也应该具有相同的参数。
private int getDateFormatInterceptor(
uint locale,
uint dwFlags,
SystemTime lpDate,
string lpFormat,
StringBuilder lpDateStr,
int sbSize)
{
}
就是这样!现在我们有时间处理接收到的 lpDate
参数并设置我们自己的 lpDateStr
值。Windows 将稍后使用该值来显示日期信息。
安装 EasyHook
在安装我们的钩子之前,我们应该通过 Visual Studio 中每个项目的“Signing”选项卡为控制台应用程序和类库项目添加数字签名(一个 snk 文件)。然后,向安装程序项目(控制台应用程序)添加一个新类,其签名如下
public class MessagesReceiverInterface : MarshalByRefObject
{
public void Ping()
{
}
}
EasyHook 将使用此类通过 .NET Remoting 功能接收来自已安装钩子的消息。
var channel = RemoteHooking.IpcCreateServer<MessagesReceiverInterface>(ref _channelName, WellKnownObjectMode.SingleCall);
此通道将通过使用 RemoteHooking.IpcCreateServer
调用创建,该调用接受提到的 MarshalByRefObject
类作为其泛型参数。然后,通过调用 RemoteHooking.Inject
方法,它将我们的钩子(此处为 ExplorerPCal.Hooks.dll)注入到目标进程。
RemoteHooking.Inject(
explorer.Id,
InjectionOptions.Default | InjectionOptions.DoNotRequireStrongName,
"ExplorerPCal.Hooks.dll", // 32-bit version (the same, because of using AnyCPU)
"ExplorerPCal.Hooks.dll", // 64-bit version (the same, because of using AnyCPU)
_channelName
);
它的第一个参数是目标进程的 PID,可以通过 Process.GetProcesses()
方法找到。因为我们的钩子类库在其属性中使用“Any CPU”设置,所以它可以用于 32 位和 64 位版本的 Windows。最后一个参数可以是一个参数列表,这就是为什么 GetDateTimeFormatInjection
的构造函数和 Run
方法可以接受多个参数的原因。
.ctor(IContext, %ArgumentList%)
void Run(IContext, %ArgumentList%)
它有效吗?!
是的!您可以在此处看到这些钩子对 Windows 日历和日期/时间格式的影响
您可以从当前文章的开头下载其完整的源代码。
重要提示
- 目前 EasyHook 与 Windows 8 不兼容(在 Windows XP 和 7 上运行良好)。
- 您应该部署 EasyHook 的所有相关 .dll 和 .exe 文件,仅 EasyHook.dll 文件是不够的。
- 如果您的新钩子立即导致目标进程崩溃,则表示其 Win32 API 签名是错误的。例如,许多网站上
GetDateFormatW
的签名使用 C# 结构来定义。但有时 Windows 会发送一个空值来接收当前时间,而 C# 结构是值类型,不接受空值。 - 当您安装 EasyHook 时,它无法被卸载,目标进程应被终止。这在开发过程中会很麻烦。
- 您需要以这种方式将 EasyHook 库保留在内存中
try
{
while (true)
{
Thread.Sleep(500);
_interface.Ping();
}
}
catch
{
_interface = null;
// .NET Remoting will raise an exception if host is unreachable
}
否则它会被 CLR 立即卸载。此方法会 ping MessagesReceiverInterface
类,只要它可用,它就会继续工作。