又push了一阵参数,call了7C95646B,之后就ZwClose关闭句柄,并跳回LdrQueryImageFileExecutionOptions中下一行代码了。
7C95646B,终于“偷梁换柱”:

call ZwQueryValueKey时的堆栈:

0012E5E4开始写入内容,0012E5F0开始保存UNICODE字符串C:\WINDOWS\system32\cmd.exe,此地址前面四字节则是字符串长度。
之后申请内存、memmove的就从略了,最后ZwClose后回到LdrQueryImageFileExecutionOptions,然后就回到kernel32.dll的领空了,又跟了一段之后:

“偷梁换柱”在这里初步完成。下面就是按新的路径完成剩下的工作了。
从以上内容的不厌其烦的分析中,我们可以找到一些“关键点”:
1. CreateProcessInternalW里的检测dwCreationFlags(看第一个图):
7C8190C0 F645 20 03 test byte ptr [ebp+20], 3 ; dwCreationFlags
7C8190C4 0F85 059A0200 jnz 7C842ACF
在第二行代码前下断,断下来后,在OD的寄存器窗口,将ZF寄存器的值置0,则此时的“跳转未实现”的箭头变红了,跳转可实现,F9,SREng的窗口出来了,IFEO检测没有进行。
2. LdrQueryImageFileExecutionOptions中call完了ZwOpenKey后回来的最后:
7C93D364 E8 25FEFFFF call 7C93D18E ; 是否存在相应IFEO项的Debugger键
7C93D369 8BF0 mov esi, eax
7C93D36B 85F6 test esi, esi
7C93D36D 0F8D CD900100 jge 7C956440 ; 存在则读其键值
7C93D373 8BC6 mov eax, esi
7C93D375 5E pop esi
7C93D376 5D pop ebp
7C93D377 C2 1800 retn 18
这里
jge 7C956440
是一个跳去调用ZwQueryKey去查找键值的地方,如果我们让它不跳,则在此下断,jge满足的条件是(SF xor OF)==0,现在把寄存器窗口中的SF或OF改一下,跳转的箭头又变成了未实现的灰色。这时把这个断点先禁用,F9,呵呵,SREng的窗口又出来了。
结论:
1. MSDN并没有骗人,CreateProcess的确是在其用户态部分,准确地说是在CreateProcessInternalW中检测dwCreationFlag,当dwCreationFlag中包括DEBUG_PROCESS 或DEBUG_ONLY_THIS_PROCESS时,系统将直接跳过对Image File Execution Options的检测。
2. CreateProcessInternalW调用了ntdll.dll导出的LdrQueryImageFileExecutionOptions函数来检测Image File Execution Options项目,而LdrQueryImageFileExecutionOptions调用了ZwOpenKey和(如果ZwOpenKey证实项目存在)ZwQueryKey去检测并获得(如果存在)Debugger键值,之后回到kernel32.dll,把Debugger键值和原程序路径Append,并在堆栈中替换掉原程序路径的地址。
3. 以上过程是循环的,即如果此时Debugger键值中的程序又被IFEO了,将再来……直到不再有。但是如果造成了死循环,最后命令行长度越来越长,似乎要等于长度超过系统的限制,才跳出循环并提示“找不到文件”(当然这个提示是Windows在自欺欺人)。
4. 根据1及2,我们在程序中要创建进程,如果想不受IFEO的限制,除了干脆加上DEBUG_PROCESS 或DEBUG_ONLY_THIS_PROCESS参数,把自己作为新进程的调试器外,还可以弄些较“猥琐”的方法,比如从前面提到的几个关键跳转处作文章,或者或干脆SSDT HOOK等方法搞掉自身进程空间中的或整个系统级上的ZwOpenKey等(HOOK这些的话,由于一般情况下应用程序CreateProcess都要IFEO检测,HOOK了这个还可以反过来“虚拟”一个Debugger键出来,作为隐蔽的启动自身组件的方法)。
5. IFEO的Debugger键结果,并不只是启动另一个程序,而且还把原程序的地址当成参数传递了,这一点是我们这些菜鸟提到这一键的作用时经常忽略的。
所以,如果一个病毒利用IFEO劫持的目的,不包括把安全软件禁用掉,而只是为了在原程序被触发时,作为启动病毒自身的方式,那么,就很容易利用这一点,只需要病毒程序自身启动后,GetCommandLine,得到原先要启动的程序地址,再利用上面说的方法,启动原先的正常程序即可。这样做可以让多个程序的IFEO劫持Debugger键指向同一个病毒程序,而每次却都能够正常启动原程序。这样一般用户在触发这一项时,根本不会有感觉,毕竟如果不是用Process Explorer等工具,而是光看任务管理器的话,用户将难以看出自己运行的正常程序,却是建立在一个可疑的有危险的病毒父进程之下的。
==================================
后记:在调试完之后,写这篇文章的过程中,搜了网上关于Image File Execution Options的文章,发现一篇比较不错的“详解WINDOWS映像劫持技术”,显然作者也是“同道中人”,而且文笔不错。其中提到:
这个招数被广大使用“映像劫持”技术的恶意软件所青睐,随着OSO这款超级U盘病毒与AV终结者(随机数病毒、8位字母病毒)这两个灭杀了大部分流行安全工具和杀毒软件的恶意程序肆虐网络以后,一时之间全国上下人心惶惶……
呵呵,还有人记得OSO.exe啊,那的确是我印象中最早大规模使用IFEO劫持的U盘病毒,当时我抢先分析了一下,虽然分析得很菜,被MJ狂贬,但是却印象颇深。当时用OD,只会看字符串,跟现在这篇文章的调试比起来,真是差太远了。
对于IFEO的检测,该文提到:
一个程序启动时是否会调用到IFEO规则取决于它是否“从命令行调用”的……为了与用户操作区分开来,系统自身加载的程序、调试器里启动的程序,它们就不属于“从命令行调用”的范围,从而绕开了IFEO,避免了这个加载过程无休止的循环下去。
从编程角度来说明“命令行调用”,那就是取决于启动程序时CreateProcess是使用lpCommandLine(命令行)还是 lpApplicationName(程序文件名)来执行,默认情况下大部分程序员编写的调用习惯是lpCommandLine——命令行调用
经过这次调试,我个人觉得这种说法不是很妥当。调试器启动程序仍然是用lpCommandLine启动,区别只不过是dwCreationFlags的值的设置。而就算用lpApplicationName,到了CreateProcessInternalW里面那个判断,以及接下来的步骤,都是一样的,应该是在此之前就已经把整个命令行连起来处理了。所以结果是,仍然受到IFEO限制,没什么不同。上面这几句话,容易让人以为调试器调试程序不是用用lpCommandLine启动的,或是用lpApplicationName就不受IFEO限制,这是误解。