::  ::  ::  ::  :: 管理

Image File Execution Options,说熟悉又陌生,一年来大家谈起的映像劫持,都是它的Debugger键值的问题。实现IFEO劫持,只是在注册表写一个键而已,轻松到没 技术含量。然而,系统到底是怎么识别的呢,或者说,系统的这个功能,是怎么使它自己走入圈套的呢?

早前给电脑报写了一篇简单谈IFEO的文章,当时托大家找了些资料,TK翻到MSDN里的相关描述,明确指出“当父进程不是作用子进程的调试器 时,CreateProcess在其用户态部分检测Image File Execution Options项”。当时就想在文章最后说一下这点,以及为什么那些系统关键进程是不会被IFEO影响的。但是一来自己当时太菜,根本不懂得去看 CreateProcess的代码来调试,所以也自己确认不了。同时那篇文章也是给一般用户看的,无谓说些让他们坠入迷雾的东西,所以就作罢了。

而最近这两三天里,由于前一篇blog里提到的看shellcode的xor加密代码的激励,突然对汇编逆向调试感兴趣了,于是整天盯着OD看反汇 编代码。今天又遇到一个求助者,他不慎将D盘在资源管理器下隐藏了,于是教他从组策略里改回来。最后随口说了一句“系统应该是在 FindFirstFile和FindNextFile的用户态部分检查这个键值,以隐藏驱动器吧,不至于到调用NATIVE API甚至进ring0时才检测,就像CreateProcess在用户态检测IFEO一样”。

于是突然有想法,何不确认一下?

自己写个小程序,button的onclick里就简单地CreateProcess。
打开注册表编辑器,将SREngPS.exe给IFEO劫持到cmd.exe。

OD载入运行,忽略所有异常,开工。
先看了CreateProcessA、WinExec、ShellExecuteA这几个函数,果然全都是内部调用了 CreateProcessInternalA来完成,而后者又调用了其UNICODE版本,即CreateProcessInternalW,来完成实 际的工作。
在CreateProcessInternalW出口处下断,断到了,一路跟下去。这次的重点在于找出“系统判断父进程是否为子进程的调试器,是则检测IFEO项”的相应代码。
也就是说,要找到系统对dwCreationFlags进行检测并决定是否进入IFEO检测的代码。

父进程成为子进程的调试器的dwCreationFlags参数有两个:
DEBUG_PROCESS = 1;
DEBUG_ONLY_THIS_PROCESS = 2;

找了半天,这函数要做的步骤还真不是一般的多,调用NATIVE API创建进程对象的顺序就不仔细看了,留待以后吧,现在重点不是这些。最后终于柳暗花明:

用断点的红色标示的这两句,其中byte ptr [ebp+20]正是传入的dwCreationFlags,与3进行test,如dwCreationFlags中包含1或2,则结果非零(ZF=0),这时就跳走,绕过下面的检测。
这里我们用了0参数,所以当F8到jnz的时候,会发现提示窗口写着“跳转未实现”。
而下面这个检测,就是跳到ntdll.dll导出的LdrQueryImageFileExecutionOptions函数中了,看看它是怎么检测的:

进入那个call中,又是冗长的一大堆,看得头痛,拣其中一些:

IFEO项的地址,IFEO的路径所为字符串常量保存在ntdll.dll中。


进行完这两个mov,esi中是指向SREngPS.exe的全路径的地址,注意这时已经是UNC标准的UNICODE(前面有\??\)


循环,eax初始值指向全路径地址末尾,每次向前推进,直到找到"\",再跳出循环到最后这一行。
跳出循环到最后这一行时,eax保存的便是指向相对路径("SREngPS.exe“)开头的地址,而[ebp-2d4]里的则是前面的文件夹路径的长度。
。。。。


调用ZwOpenKey去访问键值了。
回到LdrQueryImageFileExecutionOptions中test得到的eax值(已传到esi),然后就
7C93D36D   /0F8D CD900100   jge     7C956440                   ; 存在则读其键值

看下这个7C956440:
7C956440    FF75 1C         push    dword ptr [ebp+1C]
7C956443    FF75 18         push    dword ptr [ebp+18]
7C956446    FF75 14         push    dword ptr [ebp+14]
7C956449    FF75 10         push    dword ptr [ebp+10]
7C95644C    FF75 0C         push    dword ptr [ebp+C]
7C95644F    FF75 08         push    dword ptr [ebp+8]
7C956452    E8 14000000     call    7C95646B
7C956457    FF75 08         push    dword ptr [ebp+8]
7C95645A    8BF0            mov     esi, eax
7C95645C    E8 2571FDFF     call    ZwClose
7C956461 ^ E9 0D6FFEFF     jmp     7C93D373

又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限制,这是误解。