zoukankan      html  css  js  c++  java
  • [阅读笔记]恶意样本分析手册

    1 基础理论

    汇编

    If语句
    if语句的条件跳转指令与if语句的判断结果是相反的

    if语句的一般流程如下:
    //先执行各类影响标志位的指令
    //其后是各种条件跳转指令
    jxx    xxxx
    

    if…else…语句
    if…else…的大致流程如下:
    先执行影响标志位的相关指令

    jxx else_begin    //该地址为else语句块的首地址
    if_begin
    ……               //if语句块内的执行代码
    if_end
    jmp else_end  //跳转到else语句块的结束地址
    else_begin
    ……               //else语句块内的执行代码
    else_end
    

    如果遇到以上指令序列,先考察其中的两个跳转指令,当第一个条件跳转指令跳转到地址else_begin处之前有个jmp指令,则可将其视为由if…else…组合而成的双分支结构。根据这两个跳转指令可以得到if和else语句块的代码边界。通过cmp和jxx可还原出if的比较信息,jmp指令之后即为else块的开始。

    if构成的多分支结构:
    多分支结构类似于if…else…的组合方式,在if…else…的else之后再添加一个else if进行二次比较,这样就可以进行多次比较,再次选择程序流程,形成多分支流程。它的c++语法格式为:if…else if…else if…,可重复后缀为else if。当最后为else时,便到了多分支结构的末尾处。
    一般流程如下:

    jxx指出了下一个else if的起始点,而jmp指出了整个多分支结构的末尾地址以及当前if或者else if语句块的末尾。最后的else块的边界也很容易识别,如果发现多分支块内的某一段代码在执行前没有判定,即可定义为else块。
    //会影响标志位的指令
    jxx else_if_begin //跳到下一条else if语句块的首地址
    if_begin
    ……                        //if语句块内的执行代码
    if_end
    jmp end                //跳转到多分枝结构的结尾地址
    else_if_begin        //else if语句块的起始地址
    //可影响标志位的指令
    jxx else_begin
    ……
    else_if_end:
    jmp end
    else_begin:
    ……
    end
    ……
    

    当每个条件跳转指令的跳转地址之前都紧跟jmp指令,并且他们的跳转地址值都一样时,可视为一个多分支结构。

    Switch
    switch是比较常用的多分支结构,使用起来也非常方便,并且效率也高于if…else if多分枝结构。switch语句将所有的条件跳转都放置在了一起,并没有发现case语句块的踪影,通过条件跳转指令,跳转到相应的case语句块中。因此每个case的执行是由switch比较结果引导跳过来的。
    一般流程如下:

    mov reg,mem //取出switch中考察的变量
    //影响标志位的指令
    jxx  xxxx
    //影响标志位的指令
    jxx  xxxx
    //影响标志位的指令
    jxx  xxxx
    jmp end  //跳到switch语句块的结尾地址出
    ……  //case语句块首地址
    jmp end  //跳到switch语句块的结尾地址出
    ……  //case语句块首地址
    jmp end  //跳到switch语句块的结尾地址出
    ……  //case语句块首地址
    jmp end  //跳到switch语句块的结尾地址出
    end:
    ……
    

    当分支数小于4的情况下,VC 6.0会采取模拟if else的方法
    当分支数大于3,并且case的判定值存在明显线性关系组合时,它会制作一份case地址数组(case地址表),这个数组保存了每个case语句块的首地址,并且数组下标以0起始。如果每两个case值之间的差值小于等于6,并且case语句数大于等于4,编译器就会形成这种线性结构。
    对于非线性的switch结构,会进行索引表优化,需要两张表:一张为case语句块地址表,另一张为case语句块索引表
    地址表中的每一项保存了一个case语句块的首地址,有几个case语句就有几项。此情况适用于差值小于等于255的情况,大于255的话可以通过树方式优化。

    do循环:
    do循环的工作流程清晰,识别起来也相对简单。根据其特性,先执行语句块,再进行比较判断,当条件成立时,会继续执行语句块。
    if语句的比较是相反的,并且跳转地址大于当前代码的地址,是一个向下跳转的过程;而do中的跳转地址小于当前代码的地址,是一个向上跳转的过程,所以条件跳转的逻辑与源码中的逻辑相同
    do循环的一般流程:

    do_begin
    ……. //循环语句块
    ;影响标记位的指令
    jxx do_begin
    

    while循环:
    while循环和do循环正好相反,在执行循环语句块之前,必须要进行条件判断,根据比较结果再选择是否执行循环语句块。识别while循环,查看条件跳转地址,如果这个地址上面有一个jmp指令,并且此指令跳转到的地址小于当前代码地址,那么明显是一个向上跳转的地址。要完成语句循环,就需要修改程序流程,回到循环语句处,因此向上跳转就成了循环语句的明显特征。在条件跳转的地址附近会有jmp指令修改程序流程。
    while循环用了两次跳转,因此比do循环效率低一些。

    while循环的一般流程:
    while_begin
    ;影响标记位的指令
    jxx while_end
    ……
    jmp while_begin
    while_end:
    

    for循环:
    for循环是三种循环结构中最复杂的一种。for循环由赋初值,设置循环条件,设置循环步长这三条语句组成。由于for循环更符合人类的思维方式,在循环结构中被使用的频率也很高。

    for循环的一般流程:
    mov mem/reg ,xxx    //赋初值
    jmp for_cmp              //跳到循环条件判定部分
    for_step:
    //修改循环变量step
    mov reg,step
    add reg,xxxx        //修改循环变量的计算过程,在实际分析中,视算法不同而不同
    mov step,eax
    for_cmp:              //循环条件判定部分
    mov ecx,dword ptr step
    //判定循环变量和循环终止条件stepend的关系,满足条件则退出for循环
    cmp ecx,stepend
    jxx for_end             //条件成立则结束循环
    ……
    jmp for_step           //向上跳转,修改流程回到步长计算部分
    for_end:
    

    在计数器变量被赋初值后,利用jmp跳过第一次步长计算,然后,可以通过三个跳转指令还原for循环的各个组成部分:第一个jmp指令之前的代码为初始化部分;从第一个jmp指令到循环条件比较处(也就是上面代码中for_cmp标号的位置)之间的代码为步长计算部分;在条件跳转指令jxx之后寻找一个jmp指令,这jmp指令必须是向上跳转的,且其目标是到步长计算的位置,在jxx和这个jmp(也就是上面代码的省略号所在的位置)之间的代码是循环语句块。

    异常

    异常通常是CPU在执行指令是因为检测到预先定义的某个(或多个)条件而产生的同步事件,异常的来源有3种,第一种是程序错误,即当CPU在执行程序指令时遇到操作数有错误(执行除法指令时遇到除数是0)或检测到指令规范中定义的非法情况(用户模式下执行特权指令等)。第二种来源是某些特殊指令,这些指令的预期行为就是产生相应的异常,比如INT3指令,该指令的目的就是产生一个断点异常,让CPU中断进调试器。第三种来源是奔腾CPU引入的机器检查异常,即当CPU执行指令期间检测到CPU内部或外部的硬件错误。

    异常分为3类,错误,陷阱和中止
    错误类异常
    导致错误类异常的情况通常可以被纠正,而且一旦纠正后,程序可以无损失的恢复执行。此类异常的一个最常见的例子就是内存页错误。页错误异常的发生是因为它是虚拟内存的基础。因为物理内存的空间有限,所以操作系统会把某些在那时不用的内存以页为单位交换到外部存储器上。当有程序访问到这些不在物理内存种的页所对应的内存地址时,CPU便会产生一个页错误异常(缺页错误、缺页异常),并转去执行该异常的处理程序,后者会调用内存管理器的函数把对应的内存页交换回物理内存,然后再让CPU返回到导致该异常的那条指令处恢复执行。当第二次执行刚才导致异常的指令时,对应的内存页已经在物理内存中(错误情况被纠正),因此就不会再产生页错误异常了。
    当CPU报告错误类异常时,CPU将其状态恢复成导致该异常的指令被执行之前的状态。而且在CPU转去执行异常处理程序前,在栈中保存的CS和EIP指针是指向导致异常的这条指令的(而不是下一条指令)。因此,当异常处理程序返回继续执行时,CPU接下来执行的第一条指令仍然是刚才导致异常的那条指令。所以,如果导致异常的情况还没有被消除,那么CPU会再次产生异常。
    陷阱类异常:
    当CPU报告陷阱类异常时,导致该异常的指令已经执行完毕,压入栈的CS和EIP值是导致该异常的指令执行后紧接着要执行的下一条指令。值得说明的是,下一条指令并不一定是与导致异常的指令相邻的下一条。如果导致异常的指令是跳转指令或函数调用指令,那么下一条指令可能是内存地址不相邻的另一条指令。
    导致陷阱类异常的情况通常也是可以无损失的恢复执行的。比如INT 3指令导致的断点异常就属于陷阱类异常,该异常会使CPU中断到调试器,从调试器返回后,被调试程序可以继续执行。
    中止类异常:
    中止类异常主要用来报告严重的错误,比如硬件错误和系统表中包含非法值或不一致的状态等。这类异常不允许恢复继续执行。首先,当这类异常发生时,CPU并不总能保证报告的异常的指令地址是精确地。另外,出于安全性的考虑,这类异常可能是由于导致该异常的程序执行非法操作导致的,因此就应该强迫其中止退出。
    导致错误类异常的情况通常可以被纠正,而且一旦纠正后,程序可以无损失的恢复执行。此类异常的一个最常见的例子就是内存页错误。页错误异常的发生是因为它是虚拟内存的基础。因为物理内存的空间有限,所以操作系统会把某些在那时不用的内存以页为单位交换到外部存储器上。当有程序访问到这些不在物理内存种的页所对应的内存地址时,CPU便会产生一个页错误异常(缺页错误、缺页异常),并转去执行该异常的处理程序,后者会调用内存管理器的函数把对应的内存页交换回物理内存,然后再让CPU返回到导致该异常的那条指令处恢复执行。当第二次执行刚才导致异常的指令时,对应的内存页已经在物理内存中(错误情况被纠正),因此就不会再产生页错误异常了。

    当CPU报告错误类异常时,CPU将其状态恢复成导致该异常的指令被执行之前的状态。而且在CPU转去执行异常处理程序前,在栈中保存的CS和EIP指针是指向导致异常的这条指令的(而不是下一条指令)。因此,当异常处理程序返回继续执行时,CPU接下来执行的第一条指令仍然是刚才导致异常的那条指令。所以,如果导致异常的情况还没有被消除,那么CPU会再次产生异常。
    陷阱类异常:

    当CPU报告陷阱类异常时,导致该异常的指令已经执行完毕,压入栈的CS和EIP值是导致该异常的指令执行后紧接着要执行的下一条指令。值得说明的是,下一条指令并不一定是与导致异常的指令相邻的下一条。如果导致异常的指令是跳转指令或函数调用指令,那么下一条指令可能是内存地址不相邻的另一条指令。

    导致陷阱类异常的情况通常也是可以无损失的恢复执行的。比如INT 3指令导致的断点异常就属于陷阱类异常,该异常会使CPU中断到调试器,从调试器返回后,被调试程序可以继续执行。
    中止类异常:

    中止类异常主要用来报告严重的错误,比如硬件错误和系统表中包含非法值或不一致的状态等。这类异常不允许恢复继续执行。首先,当这类异常发生时,CPU并不总能保证报告的异常的指令地址是精确地。另外,出于安全性的考虑,这类异常可能是由于导致该异常的程序执行非法操作导致的,因此就应该强迫其中止退出。

    函数调用约定

    cdecl

    cdecl调用约定又称为C调用约定,是c/c++语言缺省的调用约定。参数按照从右至左的方式入栈,函数本身不清理栈,此工作有调用者负责,返回值在eax中。由于由调用者清理栈,所以允许可变参数函数存在。
    stdcall

    stdcall很多时候被称为pascal调用约定。pascal语言是早期很常见的一种教学用计算机程序设计语言,其语法严谨,参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在eax中。
    fastcall

    fastcall的调用方式运行相对快,因为它通过寄存器来传递参数。它使用ecx和edx传送两个双字或更小的参数,剩下的参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在eax中。
    naked

    naked是一个很少见的调用约定,一般不建议使用。编译器不会给这种函数增加初始化的清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果,此调用约定必须跟declspec同时使用,例如声明一个函数,如_declspec(naked) int add(int a,int b);
    pascal

    这是pascal语言的调用约定,跟stdcall一样,参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在 eax中,vc已经废弃了这种调用方式,因此在写vc程序时,建议使用stdcall。
    thiscall

    这是c++语言特有的一种调用方式,用于类成员函数的调用约定。如果参数确定,this指针存放于ecx寄存器,函数自身清理堆栈;如果参数不确定,this指针在所有参数入栈后再入栈,调用者清理栈。Thiscall不是关键字,程序员不能使用。参数按照从右至左的方式入栈。

    PE文件格式/ELF文件格式

    Windows内核加载

    在Windows NT/XP/2003系统中,Windows内核加载器指的是NTLDR文件,而在Vista/Windows 7中,指的是bootmgr文件。这里主要说ntldr文件。它位于系统分区的根目录下,如C:/ntldr,是一个隐藏的,只读的文件,去除“隐藏受保护的操作系统文件”就可以看见。Ntldr的主要作用是引导和加载操作系统。
    Ntldr是可执行的16进制文件,有两部分组成:前半部分是startup.com,称为su模块(一部分是16位程序,在实模式下运行,另一部分是32位程序,在保护模式下运行)。后半部分是osloader.exe,称为loader模块(32位程序,主要在保护模式下运行)。
    引导驱动器读取第一个扇区到0x7c00后,控制权交给MBR,MBR代码再搜索系统活动分区表,加载活动分区第一扇区到特定的内存地址(如物理内存地址0xd000).这个扇区称为操作系统分区引导记录(Partition Boot Record ,PBR)。MBR接着将控制权交给PBR。PBR代码解析FAT和NTFS格式找到引导内核的文件NTLDR,并将NTLDR文件加载到指定物理内存地址(0x20000),最后将控制权转移交给NTLDR。NTLDR的SU模块首先获得控制权,前半部分主要是在实模式下工作,检测物理内存,开始A20地址线,重定位GDT和IDT。开启保护模式后,SU解析osloader.exe文件,将其加载到物理地址0x00400000,最后控制权交给Loader。

    主引导记录MBR讲解

    Hook RootKit

    第六章:Hook、RootKit
    6.1 使用注册表来注入DLL

    在注册表路径HKEY_LOCAL_MACHINESoftwareMicrosoftWindows NTCurrentVersioinWindows下,AppInit_Dlls键的值可能会包含一个DLL的文件名活一组DLL的文件名(通过空格或逗号分隔)。将自己写的DLL文件的路径值写入AppInit_Dlls中,再创建一个名为LoadAppInit_Dlls,类型为DWORD的注册表项,并将其值设为1.当User32.dll被映射到一个新的进程时,会受到DLL_PROCESS_ATTACH通知。当User32.dll对它进行处理的时候,会获取上述注册表键的值,并调用LoadLibrary来载入这个字符串中指定的每个DLL。
    6.2 使用Widows挂钩来注入DLL

    调用函数SetWindowsHookEx来安装钩子,此函数的声明如下:

    HHOOK WINAPI SetWindowsHookEx(

    In int idHook,

    In HOOKPROC lpfn,

    In HINSTANCE hMod,

    In DWORD dwThreadId

    );

    idHook表示要安装的挂钩的类型,lpfn是一个函数的地址,在窗口即将处理一条消息的时候,系统应该调用这个函数,hMod标识一个DLL,这个DLL包含了lpfn函数,dwThreadId表示要给哪个线程安装挂钩。如果这个参数传0,表示要给系统中所有GUI线程安装挂钩。

    例如进程A使用SetWindowsHookEx(WH_GETMESSAGE,GetMsgProc,hInstDll,0)函数安装挂钩后:

    进程B中的一个线程准备向一个窗口派送一条消息
    系统检查该线程是否安装了WH_GETMESSAGE挂钩
    系统检查GetMsgProc所在的DLL是否已经被映射到进程B的地址空间中,如果DLL尚未被映射,那么系统会强制将该DLL映射到进程B的地址空间中,并将进程B中该DLL的锁计数器递增
    由于DLL的hInstDll是在进程B中映射的,因此系统会对他进行检查,看他在进程A中的位置是否相同,如果相同,那么在两个进程空间中,GetMsgProc函数位于相同的位置,系统就可以直接在进程A的地址空间中调用GetMsgProc。如果不相同,那么系统必须确定GetMsgProc函数在进程B的地址空间中的虚拟内存地址。使用公式GetMsgProc B=hInstDll B+(GetMsgProc A-hInstDll A)获得
    系统在进程B中递增该DLL的锁计数器
    系统在进程B的地址空间中调用GetMsgProc函数
    当GetMsgProc返回的时候,系统递减该DLL在进程B中的锁计数器
    

    6.3 使用远程线程来注入DLL

    使用函数VirtualAllocEx在远程进程的地址空间中分配一块内存
    使用函数WriteProcessMemory函数把DLL的路径名复制到第一步分配的内存中
    使用函数GetProcAddress得到LoadLibrary函数的实际地址
    使用函数CreateRemoteThread函数在远程进程中创建一个线程,让新线程调用正确的LoadLibrary函数并在参数中传入第一步分配的内存地址。现在远程进程中有一块内存,它是在第一步分配的,DLL也还在远程进程的地址空间中。为了对它进行清理,需要在远程线程退出之后执行后续步骤
    使用函数VirtualFreeEx释放第一步分配的内存
    使用函数GetProcAddress来得到FreeLibrary函数的实际地址
    使用函数CreateRemoteThread在远程进程中创建一个线程,让该线程调用FreeLibrary函数并在参数中传入远程DLL的
    

    6.4 动态库劫持

    简单来说就是DLL文件替换。通俗说法如下:

    A.exe想要调用B.dll,并且使用里面的FunC函数,这样的话我们把B.Dll改名BB.Dll(有的不用,直接根据路径劫持),然后我们自己写一个B.Dll(假的)里面有一个FunC这个函数,然后我们在这个函数里加载BB.Dll(原B.Dll),并且调用里面的FunC函数,之后我们在干一些自己的事,对于A.exe来说通常没什么异常感觉,这样我们的目的就达到了,记住此时的你,也就是B.dll(假的)的权限和内存归属都是A的,也即是你和A是一家的了,类似于代码注入之后直接修改内存一样。

    WIndows上的Dll加载有一个默认的规则,就是先在主程序目录下查找B.dll,如果没有就在系统路径下找,如果还没有,就去环境变量路径里找,就因为这个我们可以轻松的在相应的位置给做劫持,然后问题就是如果实现劫持,就要知道B.Dll里面的所有函数名字以及函数参数,这个地方比较不好搞,此地不考虑。
    6.5 APC注入

    APC注入的原理是利用当线程被唤醒时APC中的注册函数会被执行的机制,并以此去执行我们的DLL加载代码,进而完成DLL注入的目的,其具体流程如下:

    1)当EXE里某个线程执行到SleepEx()或者WaitForSingleObjectEx()时,系统就会产生一个软中断。

    2)当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数。

    3)利用QueueUserAPC()这个API可以在软中断时向线程的APC队列插入一个函数指针,如果我们插入的是Loadlibrary()执行函数的话,就能达到注入DLL的目的。
    6.6 使用CreateProcess注入代码

    用CreateProcess以CREATE_SUSPENDED的方式启动目标进程
    找到目标进程的入口
    将目标进程入口的代码保存起来
    在目标进程的入口写LoadLibrary(MyDll)实现Dll注入
    用ResumeThread运行目标进程
    目标进程就运行了LoadLibrary(MyDll),实现DLL的注入
    目标进程运行完LoadLibrary(MyDll)后,将原来的代码写回目标进程的入口
    目标进程jmp到原来的入口,继续运行程序
    

    InlineHook

    InlineHook的工作方式如下所示:

    在内存中对要拦截的函数进行定位,从而得到它的内存地址
    把这个函数起始的几个字节保存到我们自己的内存中
    使用jmp指令来覆盖这个函数起始的几个字节,这条jmp指令用来跳转到我们的替代函数的内存地址。当然,我们的替代函数的函数签名必须与要拦截的函数的函数签名完全相同:所有的参数必须相同,返回值必须相同,调用约定也必须相同
    现在,当线程调用被拦截函数的时候,跳转指令实际上会跳转到我们的替代函数。这时,我们就可以执行自己想要执行的任何代码
    为了撤销对函数的拦截,需要把第二步保存下来的字节放回被拦截函数起始的几个字节中
    我们调用被拦截函数(现在已经不再对它进行拦截了),让函数执行它正常处理
    当原来的函数返回时,我们再次执行第二步和第三步,这样替代函数将来还会被调用到。
    

    6.7 IDT Hook

    IDT=Interrupt Descriptor Table中断描述表。IDT是一个有256个入口的线形表,每个IDT的入口是8字节的描述符,所以整个IDT表的大小为256*8=2048 bytes,每个中断向量关联了一个中断处理过程。所谓的中断向量就是把每个中断或者异常用一个0-255的数字识别。Intel称这个数字为向量(vector)。

    对于中断描述表,操作系统使用IDTR寄存器来记录idt位置和大小。IDTR寄存器是48位寄存器,用于保存idt信息。其中低16位代表IDT的大小,大小为7FFH,高32位代表IDT的基地址。我们可以利用指令sidt读出IDTR寄存器中的信息,从而找到IDT在内存中的位置。

    IDT有三种不同的描述符或者说是入口,分别是:

    1。任务门描述符

    2。中断门描述符

    3。陷阱门描述符

    也就是说,在保护模式下,80386只有通过中断门、陷阱门或任务门才能转移到对应的中断或异常处理程序。

    中断分为两种类型:可屏蔽中断–它在短时间片段里可被忽略;不可屏蔽中断–它必须被立即处理。例如:硬件失败为不可屏蔽中断,IRQS(中断请求)失败为可屏蔽中断。

    异常被分为不同的两类:处理器产生的异常(Faults, Traps, Aborts)和编程安排的异常(用汇编指令int or int3 触发)。后一种就是我们经常说到的软中断。

    下图是三种描述符的图示:

    其中:后两种描述符,非常的相似,只有1个bit位的差别。在处理上,采用相同的处理方式。如图所示,在这后两类的描述符里面记录了一个中断服务程序(ISR )的地址offset. 在IDT的256个向量中,除3个任务门入口外,其他都是这两种门的入口。并且所有的trap/interrupt gate的入口,他们的segment selector都是一样的,即:08h. 我们察看GDT中Selector = 8的描述符,描述的是00000000h ~ 0ffffffffh的4G地址空间。 因此,在描述符中的中断服务程序(ISR )的地址offset就代表了函数的入口地址。windows在处理的时候,按照下图方式,来处理这两类的描述符入口。即:根据segment selector在GDT中找出段基地址等信息,然后跟描述符中的中断服务程序(ISR )的地址offset相加得到代码段中的函数入口地址。然后调用该函数。

    这个过程,我写得比较直接,在操作系统执行这过程时,还有很多的出错判断和异常保护,这里我们略过。

    下图是任务门描述符的情况

    首先,根据IDT中任务门描述符的TSS Segment Selector ,我们在GDT中找出这个选择子。在这个选择子中,对应一个tss描述符,即:任务状态段描述符。这个描述符大小为068h, 即104字节。
    下面是这个任务状态段描述符的格式。

    在这个描述符中记录了任务状态段的位置和大小。

    根据任务状态段描述符中的base Address, 找到TSS的内存位置。然后就可以进行任务切换。所谓任务切换是指,挂起当前正在执行的任务,恢复或启动另一任务的执行。在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到TR所指定的TSS中;然后,下一任务的TSS的选择子被装入TR;最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中。由此可见,通过在TSS中保存任务现场各寄存器状态的完整映象,实现任务的切换。 TR寄存器可见部分保存了tss selector, 不可见部分,保存了任务状态段的位置和大小.

    任务状态段TSS的基本格式如下图所示。

    从图中可见,TSS的基本格式由104字节组成。这104字节的基本格式是不可改变的,但在此之外系统软件还可定义若干附加信息。

    知道了IDT的基本知识后,再来理解IDT Hook的原理就比较简单了。就是将将系统原来的中断处理函数地址替换为我们自己的函数的地址。这样系统在处理相应的中断时,就会调用我们的处理函数。

    比如:出现页错误,调用IDT中的0x0E。或用户进程请求系统服务(SSDT)时,调用IDT中的0x2E。而系统服务的调用是经常的,这个中断就能触发。所以方法就是先在系统中找到IDT,然后确定0x2E在IDT中的地址,最后用我们的函数地址去取代它,这样以来,用户的进程(可以特定设置)调用系统服务,我们的hook函数即被激发。

    使用sidt指令可以在内存中找到IDT的地址,返回一个IDTINFO结构的地址。这个结构中国含有IDT的高半地址和低半地址。IDT有最多256个入口。将IDT看作 是一排有256间房组成的线性结构,那么只要知道了整个入口结构,就相当于知道了每间房的长度,先获取所有的入口idt_entrys,那么第0x2E个房间的地址就可以确定了。即idt_entrys[0x2E]。找到目标入口后,将我们的函数与其原来的函数进行替换即可。
    6.8 SSDT Hook、SSSDT Hook

    SSDT 既 System Service Dispath Table。在Windows NT 下, NT 的 executive( NTOSKRNL.EXE 的一部分)提供了核心系统服务。由于子系统不同, API 函数的函数名也不同。 例如,要用Win32API 打开一个文件,应用程序会调用 CreateFile(),而要用 POSIXAPI,则应用程序调用 open() 函数。这两种应用程序最终都会调用 NT executive 中的NtCreateFile() 系统服务。

    用户模式( User mode)的所有调用,如 Kernel32,User32.dll,Advapi32.dll等提供的API, 最终都封装在Ntdll.dll中,然后通过Int 2E或SYSENTER进入到内核模式, 通过服务ID,在System Service DispatcherTable中分派系统函数。例如下图:

    SSDT就是一个表,这个表中有内核调用的函数地址。从上图可见,当用户层调用FindNextFile函数时, 最终会调用内核层的 NtQueryDirectoryFile函数, 而这个函数的地址就在SSDT表中, 如果我们事先把这个地址改成我们特定函数的地址,那么就实现了SSDT Hook。

    下面来介绍以下SSDT的结构:

    KeServiceDescriptorTable是由内核(ntoskrnl.exe)导出的一个表,这个表是访问SSDT的关键,结构形式如下:

    typedef struct ServiceDescriptorTable {

    PVOID ServiceTableBase;

    PVOID ServiceCounterTable(0);

    unsigned int NumberOfServices;

    PVOID ParamTableBase;

    ServiceTableBase: System Service Dispatch Table 的基地址。

    NumberOfServices :由 ServiceTableBase 描述的服务的数目。

    ServiceCounterTable: 此域用于操作系统的 checked builds,包含着 SSDT 中每个服务被调用次数的计数器。这个计数器由 INT 2Eh 处理程序 (KiSystemService)更新。

    ParamTableBase: 包含每个系统服务参数字节数表的基地址。

    System Service Dispath Table( SSDT):系统服务分发表,给出了服务函数的地址,每个地址4子节长。

    System Service Parameter Table(SSPT):系统服务参数表,定义了对应函数的参数字节,每个函数对应一个字节。如在0x804AB3BF处的函数需0x18字节的参数。

    要对SSDT进行Hook,首先需要改变SSDT的内存保护,因为系统对SSDT都是只读的,不能写。如果视图去写,就会造成蓝屏。一般可以修改内存的方法有通过cr0寄存器和Memory Descriptor List(MDL)。

    通过cr0寄存器:

    Windows对内存的分配,是采用的分页管理,其中有个cr0寄存器,其中第一位叫做保护属性位,控制着页的读或写属性。如果为1,则可以读/写执行;如果为0,则只可以读执行。所以我们要将这一位设为1.

    通过MDL

    将原来的SSDT的区域映射到我们自己的MDL区域中,并把这个区域设置成可写就行了。

    接下来获得SSDT中函数的地址。使用四个有用的宏。

    SYSTEMSERVICE macro:可以获得由ntoskrnl.exe导出函数,以Zw开头函数的地址, 这个函数的返回值就是Nt函数, Nt*函数的地址就在SSDT中。

    SYSCALL_INDEXmacro: 获得Zw函数的地址并返回与之通信的函数在SSDT中的索引。这两个宏之所以能工作,是因为所有的 Zw函数都开始于opcode: MOV eax, ULONG, 这

    里的ULONG就是系统调用函数在SSDT中的索引。

    HOOK_SYSCALL和UNHOOK_SYSCALLmacros: 获得Zw*函数的地址, 取得他的索引,

    自动的交换SSDT中索引所对应的函数地址和我们hook函数的地址。

    还有一个这样的表,叫做KeServiceDescriptorTableShadow,它主要包含GDI服务,也就是我们常用的窗口,桌面相关,具体存在于Win32k.sys。如下图:

    右侧的服务分布就通过KeServiceDescriptorTableShadow。

    SSSDT Hook和SSDT Hook的方式差不多,在此不再进行介绍。
    6.9 IAT Hook

    IAT即Import Address Table 是PE(可以理解为EXE)的输入地址表,我们知道一个程序运行时可以要调用多个模块,或者说要调用许多API函数,但这些函数不一定都在EXE本身中,例如你调用Messagebox来显示一个对话框时,你只需要调用它,你并没有编写Messagebox的函数的实现过程,Messagebox的函数的实现过程实际上是在user32.dll这个库文件中,当这个程序运行时会在user32.dll中找到Messagebox并调用它。

    下图是导入表中的部分结构图:

    IMAGE_THUNK_DATA指向 IMAGE_IMPORT_BY_NAME 结构的RVA,OriginalFirstThunk 和 FirstThunk 所指向的这两个数组大小取决于PE文件从DLL中引入函数的数目。当PE文件被装载到内存时,PE装载器将查找IMAGE_THUNK_DATA 和 IMAGE_IMPORT_BY_NAME 这些结构数组,以此决定引入函数的地址。然后用引入函数真实地址来替代由FirstThunk指向的 IMAGE_THUNK_DATA 数组里的元素值。因此当PE文件准备执行时,上图已转换下图所示:

    所以IAT Hook的原理就是把后面的目标函数的地址改成我们自己写的函数的地址。这样,当在此调用目标函数的时候,就会调用我们的函数的地址。
    6.10 EAT Hook

    函数导入的函数的地址是再运行时候才确定的,比如我们的一个驱动程序导入了PsGetCurrentProcessId这个ntkrnlpa.exe导出的函数,那在我们驱动程序加载运行的时候,装载程序会确定ntkrnlpa.exe在内存的基地址,接着遍历它的导出表,在AddressOfNames指向的”函数名字表”中找到PsGetCurrentProcessId的位置,也就是如果在AddressOfNames[i]中找到PsGetCurrentProcessId,那就用i在AddressOfNameOrdinals中索引,假使得到是X,那么AddressOfFunctions[index]的值就是PsGetCurrentProcessId的RVA了,最后就可以知道PsGetCurrentProcessId在内存的值是MM=ntkrnlpa.exe在内存的基地址+PsGetCurrentProcessId的RVA,然后转载程序就把这个值写到我们驱动程序的IAT中,好了知道这些后,EAT HOOK就是修改PsGetCurrentProcessId的RVA,使得PsGetCurrentProcessId的RVA(修改后的)+ntkrnlpa.exe在内存的基地址=我们自己函数的值,这样装载程序会把我们的函数的地址写入那些调用PsGetCurrentProcessId的驱动程序的IAT,那么当那些驱动程序调用PsGetCurrentProcessId时,实际上是执行了我们自己的函数

    调试器原理

    调试程序的第一步就是使用OllyDbg来加载程序,加载的过程是通过创建新进程来完成的。OllyDbg通过CreateProcess以调试的方式开启新进程。在创建调成程序前,OllyDbg需要进行一些必要的检查工作。
    首先是针对快捷方式的检查。OllyDbg根据可执行程序的后缀名来判断分析程序是否为一个快捷方式,如果是快捷方式,则会找到这个快捷方式所对应的可执行程序的全路径。通过检查DOS头与NT头来判定分析文件是否为合法的PE文件。当调试文件为DLL动态库时,Olly/Dbg会使用自带的LoadDll.exe将Dll文件进行加载。当调试文件为exe可执行程序时,会跳过Dll文件的处理部分,直接获取相关的配置文件信息并进行加载和调试。

    工具

    分析

    -file、PEID、Detect It Easy、
    -010Editor binwalk

    行为监控

    process monitor
    Process Monitor 是一款非常著名的系统进程监视软件,用户可以利用 Process Monitor 对系统中的文件系统,网络行为,进程线程行为以及注册表操作等方面同时进行监视和记录。 通过监控系统中相应位置的变化, 对于帮助诊断系统故障或是发现恶意软件、病毒或木马来说,非常有用。
    wireshark

    调试

    OD
    F2:设置断点
    F8:单步步过,不进入子函数
    F7:单步步入进入子函数
    F9:运行
    F4:运行到光标所在处
    CTR+F9:执行到返回,此命令在执行到一个ret指令时暂停,如果进入到一个函数中,代码量比较大,并且没有什么意义,可以使用此命令,直接运行到当前函数的结尾处。
    ALT+F9:执行到用户代码。可用于从系统领空快速返回到我们调试的程序领空。
    F12:暂停程序执行
    IDA 创建结构体

  • 相关阅读:
    公式编辑器mathtype中一些符号显示方框的解决方法
    I got my first job
    我的第二个面试通知
    清空visual studio2010的查找历史
    King Back
    IIS中“使用 XSL 样式表无法查看 XML 输入”问题的解决
    JDBC 各种连接方式[转载]
    力扣每日刷题(1)
    力扣每天刷题(3)
    力扣每天刷题(2)
  • 原文地址:https://www.cnblogs.com/rookieDanny/p/13372924.html
Copyright © 2011-2022 走看看