谈一个Kernel32当ä¸çš„ANSI到Unicode转æ¢çš„问题
ã€è¿™ç¯‡æ–‡ç« 就是我在twitterä¸Šè¯´è¿‡çš„é‚£ç¯‡æ–‡ç« ï¼Œæ—©å·²å†™å¥½ï¼Œä½†ç”±äºŽä¸€äº›åŽŸå› åŽ‹åˆ°çŽ°åœ¨æ‰å‘表,深表æ‰æ„】
众所周知,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, "%s\n", 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次ANSI到Unicode转æ¢çš„时候, LoadLibraryAå°†ç›®æ ‡Unicodeå—符串 L”kernel32″ å˜å…¥ StaticUnicodeBuffer,然åŽå°†è¿™ä¸ªå—符串的首地å€ä¼ 递给LoadLibraryW
- LoadLibraryW在调用到LoadLibraryExW之å‰ï¼Œå…ˆè°ƒç”¨åˆ°äº†Hook函数
- Hook函数调用到了CreateFileAï¼Œä¼ å…¥æ—¥å¿—æ–‡ä»¶å(ANSI)作为函数å‚æ•°
- CreateFileA进行第二次ANSI到Unicode转æ¢ï¼Œå°†ç›®æ ‡Unicodeå—符串(日志文件å)å˜å…¥äº†StaticUnicodeBuffer,然åŽå°†è¿™ä¸ªå—符串首地å€ä¼ ç»™CreateFileW
- CreateFileW返回,
- CreateFileA返回,
- Hook函数返回,
- 调用到实际的LoadLibraryExW。æ¤æ—¶LoadLibraryExW从å‚数指针当ä¸å–出å—ç¬¦ä¸²è¯•å›¾è¿›è¡ŒåŠ è½½åŠ¨æ€åº“çš„æ“作,但是,它å–出的是被在第4æ¥å½“ä¸è¦†ç›–掉的å—符串,也就是Unicode版本的日志文件åï¼è¯•å›¾åŠ 载日志文件,自然会收到LoadLibrary失败的返回。‘
- LoadLibraryExW返回NULL,LoadLibraryW返回NULL,LoadLibraryA返回NULL。
然åŽï¼Œæ²¡æœ‰æ£€æŸ¥LoadLibraryA返回值的程åºï¼Œåœ¨è°ƒç”¨GetProcAddress的时候,biu~ 掉了。
好了,知é“了为什么,怎么解决也就很容易了,在CheckLibrary当ä¸ï¼Œå…ˆå¤‡ä»½ä¼ 入的å‚数的内容,然åŽç»§ç»èµ°æµç¨‹ï¼Œç‰åˆ°éœ€è¦è°ƒç”¨åŽŸæœ¬å‡½æ•°çš„时候,将备份的å‚æ•°ä¼ ç»™åŽŸæœ¬å‡½æ•°ã€‚
这就是Hookçˆ±å¥½è€…ä»¬çš„ä¹‰åŠ¡ï¼šå½“ä½ è¯•å›¾æ”¹å˜æŸä¸ªæ‰§è¡Œæµç¨‹çš„时候,请务必清楚地了解关于这个æµç¨‹çš„一切。