谈一个Kernel32当中的ANSI到Unicode转换的问题

众所周知,Windows的几乎所有带有字符串参数的API都是有W和A两个版本,分别对应于Unicode和ANSI版本。同时,Windows内部是使用Unicode的,因此所有的A版本的API函数都是实际上调用了一次ANSI到Unicode的字符集转换之后,再调用Unicode版本的函数。

你真的清楚这中间的这一步转换吗?普通的工程师确实不需要了解太深刻,但是我们是做安全软件的,安全软件的一个基本实现机制就是Hook,就是在原本的API调用路径上面拐一个弯,改变原本的API执行流程,以便在其中插入安全检查等逻辑。

这当中,如果没有对于所要Hook的函数的深刻理解,也许会有很多看似灵异的事件出现。在我们的代码当中,我们就使用了Hook的机制,然后很幸运的(很不幸?),遇到了这样一个灵异问题:

在测试当中,发现某个软件一旦被我们Hook,就总是崩溃。必现的崩溃总是很容易解决的定位的,很快,我们发现崩溃来源于下面一个调用序列:

HMODULE hMod = LoadLibraryA("Kernel32");
PROC pfxIsWow64Process = GetProcAddress(hMod, "IsWow64Process");

对LoadLibrary的调用返回没有检查返回值,进而继续调用GetProcAddress。那么,当hMod = NULL的时候,GetProcAddress并不检查hMod是否是0,而直接试图去寻找其中的导出表,于是导致了非法的内存访问。

但是,这里的代码当中,调用的是LoadLibraryA(“kernel32”),kernel32.dll作为系统的核心链接库,通常情况下几乎必然是已经加载到了进程空间当中,即使没有,加载此DLL也不应该失败。

但是调试的结果显示,这个返回值的确是NULL,看起来相当灵异。但是我们相信,计算机没有灵异事件。为了排错,我们祭出WinDbg和IDAPro,从汇编层面一步步跟踪LoadLibraryA函数,看看中间究竟发生了什么。经过漫长的错误定位工作,在此略过不表(关于整个排查过程可以写一篇专门的文章),我们终于发现了问题的所在。

没错,加载了Hook就会造成崩溃,因此崩溃的原因在于Hook。

我们使用Inline Hook技术修改了LoadLibrary函数的执行流程,使得在进行真正的DLL加载之前进行某些校验和检查的工作。LoadLibrary函数其实是一个函数族,包括LoadLibraryA,LoadLibraryW,LoadLibraryExA,和LoadLibraryExW。它们之间的调用关系如下图:

我们选择了在整个调用依赖树的最底端的LoadLibraryExW的入口点作为Hook点,当一个函数调用LoadLibraryExW的时候,其实是调用到我们的Hook函数,Hook函数再调用真正的LoadLibraryExW,经过Hook,一个典型的LoadLibraryA的调用流程如下图:

问题就出在了这个CheckLibrary函数里面。在这个函数里面,我们调用了一个公共模块当中的日志函数,这个日志函数的逻辑是,以追加模式打开一个日志文件,写入然后关闭。且不论这个日志函数这样做的合理性,先来看看这段代码:

void Log(const char* msg) {
    FILE* pf = fopen(_g_log_file, "a");
    fprintf(pf, "%sn", msg);
    fclose(pf);
}

非常直观的程序,怎么会带来问题呢?我们来继续分析fopen函数。fopen函数是c库提供的文件IO函数,最终调用到Windows API CreateFileA,然后CreateFileA会调用到CreateFileW。把这个调用关系加上,调用序列如下图所示:

在这个函数调用序列当中,存在两次ANSI 到 Unicode 的转换,即图中标记有五角星的两个地方。那么我们来看看两个转换的代码是什么样子的:

LoadLibraryExA:

CreateFileA

注意到其中高亮的部分了么?两个函数在进入之后,都调用了一个公共的函数 _Base8bitStringToStaticUnicodeString,从名字就可以看出来,这段代码负责将输入的ANSI字符串转换为Unicode。那么这个函数做了什么呢?跟进去看看(这个使用了Hex-Rays的反编译功能,看得更清楚一点):

再仔细看给RtlAntiStringToUnicodeString这个函数传入的参数地址pUnicodeString,来自*MK_FP(__FS__, 24) + 3064,这是Hex-Rays的表示法,其实对应下面一串汇编序列(由于编译器的优化导致的乱序,重点看红线标出来的指令):

large fs:18h 这句魔法一样的指令,其实是 TEB 的地址。这个地址对于同一个线程来说是一样的,因此,如果在一个线程当中调用这个函数,总是会引用到同一个地址,这个地址就是当前线程的TEB当中的一个静态缓冲区(StaticUnicodeString & StaticUnicodeBuffer)。

很明显,这个静态缓冲区的作用是性能优化,对于ANSI到Unicode的转换,复用一个thread-specific的缓冲区来接受目标Unicode字符串。对于不太长的函数参数字符串,这个优化可以避免一次堆分配的过程,对性能的提升是很明显的。正常情况下这个优化是合理而且有效的。但是在存在Hook的情况下,事情就不是这么简单了。考虑一下上面那张图当中的LoadLibrary的调用序列:

解释一下:

  1. 在第1次ANSI到Unicode转换的时候, LoadLibraryA将目标Unicode字符串 L”kernel32″ 存入 StaticUnicodeBuffer,然后将这个字符串的首地址传递给LoadLibraryW
  2. LoadLibraryW在调用到LoadLibraryExW之前,先调用到了Hook函数
  3. Hook函数调用到了CreateFileA,传入日志文件名(ANSI)作为函数参数
  4. CreateFileA进行第二次ANSI到Unicode转换,将目标Unicode字符串(日志文件名)存入了StaticUnicodeBuffer,然后将这个字符串首地址传给CreateFileW
  5. CreateFileW返回,
  6. CreateFileA返回,
  7. Hook函数返回,
  8. 调用到实际的LoadLibraryExW。此时LoadLibraryExW从参数指针当中取出字符串试图进行加载动态库的操作,但是,它取出的是被在第4步当中覆盖掉的字符串,也就是Unicode版本的日志文件名!试图加载日志文件,自然会收到LoadLibrary失败的返回。‘
  9. LoadLibraryExW返回NULL,LoadLibraryW返回NULL,LoadLibraryA返回NULL。

然后,没有检查LoadLibraryA返回值的程序,在调用GetProcAddress的时候,biu~ 掉了。

好了,知道了为什么,怎么解决也就很容易了,在CheckLibrary当中,先备份传入的参数的内容,然后继续走流程,等到需要调用原本函数的时候,将备份的参数传给原本函数。

这就是Hook爱好者们的义务:当你试图改变某个执行流程的时候,请务必清楚地了解关于这个流程的一切。

关于 “谈一个Kernel32当中的ANSI到Unicode转换的问题” 的 22 个意见

  1. 您好,360终于开了博客了,支持 多多学习 请教一下,您们这个wordpress站点是怎么把后台登陆改了的,怎么修改后台登陆地址,wp-admin修改了,怎么样修改,谢谢了

  2. 转自 360安全卫士技术博客 众所周知,Windows的几乎所有带有字符串参数的API都是有W和A两个版本,分别对应于Unicode和ANSI版本。同时,Windows内部是使用Unicode的,因此所有的A版本的API函数都是实际上调用了一次ANSI到Unicode的字符集转换之后,再调用Unicode版本的函数。 你真的清楚这中间的这一步转换吗?普通的工程师确实不需要了解太深刻,但是我们是做安全软件的,安全软件的一个基本实现机制就是Hook,就是在原本的API调用路径上面拐一个弯,改变原本的API执行流程,以便在其中插入安全检查等逻辑。

  3. 您好,360终于开了博客了,支持 多多学习 请教一下,您们这个wordpress站点是怎么把后台登陆改了的,怎么修改后台登陆地址,wp-admin修改了,怎么样修改,谢谢了

  4. 您好,360终于开了博客了,支持 多多学习 请教一下,您们这个wordpress站点是怎么把后台登陆改了的,怎么修改后台登陆地址,wp-admin修改了,怎么样修改,谢谢了

评论关闭。