欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

逆向面试常见问题(陆续更新ing)

程序员文章站 2024-01-11 18:54:58
...

PE

怎么判断PE是DLL,还是EXE

文件值属性

文件头里面有一个文件值属性IMAGE_FILE_HEADER.Characteristics
EXE:010F(H)
DLL:210(H)

加载基址

exe和dll的加载基址不一样,NT头偏移0x34位置的4字节数据,记录着程序的默认加载基址
exe的默认加载基址:0x00400000
DLL的默认加载基址:0x10000000

怎么判断PE文件是32位还是64位

NT头的扩展头(IMAGE_OPTIONAL_HEADER )的magic:WORD Magic;
32位是0x010B,64位是0x020B,0x0170代表ROM镜像。

PE加载过程

调用CreateProcess启动,生产内核对象,分配4GB进程空间,系统加载器加载dll文件以及完成重定位,IAT,接着创建进程的主线程,
之后主线程为每个DLL调用_DLLMainCRTStartup函数,然后主线程根据EXE是GUI(图形化)程序还是CUI(控制台)程序执行MainCRTStartup
最后一步执行WinMain(或者main函数)
ReadFile读取文件头信息,判断是否是PE文件
获取映像总大小,申请可读可写可执行的内存全部填为0
获取区段信息,循环读取每一个区段内容,将ReadFile读取的数据映射到内存中
修复重定位
修复IAT
创建线程,线程回调指向OEP

基础

Windows下的调用约定

  • cdecl:是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。注意:对于可变参数的成员函数,始终使用 cdecl的转换方式。C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。平衡堆栈是从由调用者平衡堆栈(函数外部平衡堆栈),例如 printf 函数
  • stdcall:函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈,通常用于Win32 API中。平衡堆栈是在函数内部平衡堆栈,例如:windows的函messagebox
  • fastcall:它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回 前清理传送参数的内存栈)。
  • thiscall:仅仅应用于"C++"成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。只不过前两个参数右寄存器ecx和edx传参(就是rcx,rdx,r8,r9传参)

通用寄存器有哪些?

16位cpu通用寄存器共有 8 个:AX,BX,CX,DX,BP,SP,SI,DI.
32位cpu通用寄存器共有 8 个:EAX,EBX,ECX,EDX,EBP,ESP,ESI,EDI功能和上面差不多

  • AX:累加器(Accumulator),使用频度最高
  • BX:基址寄存器(Base Register),常存放存储器地址
  • CX:计数器(Count Register),常作为计数器
  • DX:数据寄存器(Data Register),存放数据
  • SI:源变址寄存器(Source Index),常保存存储单元地址
  • DI:目的变址寄存器(Destination Index),常保存存储单元地址
  • BP:基址指针寄存器(Base Pointer),表示堆栈区域中的基地址
  • SP:堆栈指针寄存器(Stack Pointer),指示堆栈区域的栈顶地址

段存器

  • 代码段寄存器CS(Code Segment)
    存放当前正在运行的程序代码所在段的段基址,表示当前使用的指令代码可以从该段寄存器指定的存储器段中取得,相应的偏移量则由IP提供。
  • 数据段寄存器DS(Data Segment)
    指出当前程序使用的数据所存放段的最低地址,即存放数据段的段基址。
  • 堆栈段寄存器SS(Stack Segment)
    指出当前堆栈的底部地址,即存放堆栈段的段基址。
  • 附加段寄存器ES(Extra Segment)
    指出当前程序使用附加数据段的段基址,该段是串操作指令中目的串所在的段。

怎么找到函数地址

通过API:GetProcAddress,根据函数名获取函数地址。
通过名称找地址 : 遍历名称表,得到序号表中保存的地址表下标,然后从地址表中索引出地址

调试与反调试

INT3

INT 3 指令

  1. INT3 指令
    机器码是:(0xcc) ,一个字节
  2. 软件中断指令INT 3
    3.机器码是 (0xcd03),两个字节
Intel的手册
INT n/INTO/INT 3—Call to Interrupt Procedure
Opcode Instruction Description
CC INT 3 A Valid Valid Interrupt 3 — trap to debugger.
CD ib INT imm8 Interrupt vector number specified by immediate byte.

【CC】
The INT 3 instruction generates a special one byte opcode (CC) that is intended for
calling the debug exception handler. (This one byte form is valuable because it can be
used to replace the first byte of any instruction with a breakpoint, including other one
byte instructions, without over-writing other code).

【CD 03】
Note that the “normal” 2-byte opcode for INT 3 (CD03) does not have these special
features. Intel and Microsoft assemblers will not generate the CD03 opcode from any
mnemonic, but this opcode can be created by direct numeric code definition or by
self-modifying code.

INT 3 中断调试处理流程

  1. 调试器如何下INT 3 断点
    1.1 调试器使用ReadProcessMemory,读取断点内存地址的字节。
    1.2 调试器再使用WriteProcessMemory,将指定的内存地址指令写为0xCC。
    1.3 当程序执行到这里,遇到CC指令,会从IDT表中查03号段描述符,其会定向到Trap03函数。
    1.4 经过一系列操作(下面会介绍),此时调试器就会接收到INT 3中断,程序暂停,用户可以用来进行一些处理。
    1.5 恢复时,将1.1中读取的字节再重新写入到CC处,这样就可以保证程序的正常执行。
  2. INT 3 异常指令如何派发到调试器
    在1.3中,其会经过 Trap03,因此INT3异常信息肯定是在这里发送的。
    由于异常等派发,其检测到存在三环调试器时会发送到三环调试器,因此其派发也就是在这时进行的。
     派发流程大体如下:逆向面试常见问题(陆续更新ing)

Windows反调试有哪些?

加壳
花指令
时间检查
API:IsDebuggerPresent和CheckRemoteDebuggerPresent
PEB的调试标记位:BeingDebugged //一个char类型.为1表示调试状态.为0表示没有调试.可以用于反调试. API也是从这里获取的标志
TLS Callback,利用TLS回调函数:在TLS回调函数中加入反调试代码的方法来进行的反调试手段
检测是否有OD,x64dbg,windbg等调试器进程

反调试技术总结

一、探测Windows调试器

恶意代码会使用多种技术探测调试器调试它的痕迹,其中包括使用Windows API、手动检测调试器人工痕迹的内存结构和查询调试器遗留在系统中的痕迹等。调试器探测是恶意代码最常用的反调试技术。

1.使用Windows API

1.1 IsDebuggerPresent
IsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。
1.2 CheckRemoteDebuggerPresent
CheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不仅可以探测系统其他进程是否被调试,通过传递自身进程句柄还可以探测自身是否被调试。
1.3 NtQueryInformationProcess
这个函数是Ntdll.dll中一个API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。
1.4 GetLastError
编写应用程序时,经常需要涉及到错误处理问题。许多函数调用只用TRUE和FALSE来表明函数的运行结果。一旦出现错误,MSDN中往往会指出请用GetLastError()函数来获得错误原因。恶意代码可以使用异常来破坏或者探测调试器。调试器捕获异常后,并不会立即将处理权返回被调试进程处理,大多数利用异常的反调试技术往往据此来检测调试器。多数调试器默认的设置是捕获异常后不将异常传递给应用程序。如果调试器不能将异常结果正确返回到被调试进程,那么这种异常失效可以被进程内部的异常处理机制探测。
对于OutputDebugString函数,它的作用是在调试器中显示一个字符串,同时它也可以用来探测调试器的存在。使用SetLastError函数,将当前的错误码设置为一个任意值。如果进程没有被调试器附加,调用OutputDebugString函数会失败,错误码会重新设置,因此GetLastError获取的错误码应该不是我们设置的任意值。但如果进程被调试器附加,调用OutputDebugString函数会成功,这时GetLastError获取的错误码应该没改变。
1.5 ZwSetInformationThread
ZwSetInformationThread拥有两个参数,第一个参数用来接收当前线程的句柄,第二个参数表示线程信息类型,若其值设置为ThreadHideFromDebugger(0x11),使用语句ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);调用该函数后,调试进程就会被分离出来。该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程序,因为该函数隐藏了当前线程,调试器无法再收到该线程的调试事件,最终停止调试。还有一个函数DebugActiveProcessStop用来分离调试器和被调试进程,从而停止调试。两个API容易混淆,需要牢记它们的区别。

2.手动检测数据结构

2.1 检测BeingDebugged属性
Windows操作系统维护着每个正在运行的进程的PEB结构,它包含与这个进程相关的所有用户态参数。这些参数包括进程环境数据,环境数据包括环境变量、加载的模块列表、内存地址,以及调试器状态。
进程运行时,位置fs:[30h]指向PEB的基地址。为了实现反调试技术,恶意代码通过这个位置检查BeingDebugged标志,这个标志标识进程是否正在被调试。
2.2 检测ProcessHeap属性
Reserved数组中一个未公开的位置叫作ProcessHeap,它被设置为加载器为进程分配的第一个堆的位置。ProcessHeap位于PEB结构的0x18处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建。这些属性叫作ForceFlags和Flags。在Windows XP系统中,ForceFlags属性位于堆头部偏移量0x10处;在Windows 7系统中,对于32位的应用程序来说ForceFlags属性位于堆头部偏移量0x44处。
2.3 检测NTGlobalFlag
由于调试器中启动进程与正常模式下启动进程有些不同,所以它们创建内存堆的方式也不同。系统使用PEB结构偏移量0x68处的一个未公开位置,来决定如何创建堆结构。如果这个位置的值为0x70,我们就知道进程正运行在调试器中。

3.系统痕迹检测

通常,我们使用调试工具来分析恶意代码,但这些工具会在系统中驻留一些痕迹。恶意代码通过搜索这种系统痕迹,来确定你是否试图分析它。
3.1 查找调试器引用的注册表项
下面是调试器在注册表中的一个常用位置。
SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系统)
SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系统)
该注册表项指定当应用程序发生错误时,触发哪一个调试器。默认情况下,它被设置为Dr.Watson。如果该这册表的键值被修改为OllyDbg,则恶意代码就可能确定它正在被调试。
3.2 查找窗体信息
FindWindow函数检索处理*窗口的类名和窗口名称匹配指定的字符串。
3.3 查找进程信息
如:OllyDBG.EXE,OllyICE.exe,x64_dbg.exe,windbg.exe,ImmunityDebugger.exe

二、识别调试器行为

在****中,为了帮助恶意代码分析人员进行分析,可以使用调试器设置一个断点,或是单步执行一个进程。然而,在调试器中执行这些操作时,它们会修改进程中的代码。因此,恶意代码常使用几种反调试技术探测软件/硬件断点、完整性校验、时钟检测等几种类型的调试器行为。直接运行恶意代码与在调试器中运行恶意代码也会在一些细节上不同,如父进程信息、STARTUPINFO信息、SeDebugPrivilege权限等。

1.软件断点检查

调试器设置断点的基本机制是用软件中断指令INT 3临时替换运行程序中的一条指令,然后当程序运行到这条指令时,调用调试异常处理例程。INT 3指令的机器码是0xCC,因此无论何时,使用调试器设置一个断点,它都会插入一个0xCC来修改代码。恶意代码常用的一种反调试技术是在它的代码中查找机器码0xCC,来扫描调试器对它代码的INT 3修改。repne scasb指令用于在一段数据缓冲区中搜索一个字节。EDI需指向缓冲区地址,AL则包含要找的字节,ECX设为缓冲区的长度。当ECX=0或找到该字节时,比较停止。

2.硬件断点检查

在OllyDbg的寄存器窗口按下右键,点击View debug registers可以看到DR0、DR1、DR2、DR3、DR6和DR7这几个寄存器。DR0、Dr1、Dr2、Dr3用于设置硬件断点,由于只有4个硬件断点寄存器,所以同时最多只能设置4个硬件断点。DR4、DR5由系统保留。 DR6、DR7用于记录Dr0-Dr3中断点的相关属性。如果没有硬件断点,那么DR0、DR1、DR2、DR3这4个寄存器的值都为0。

3.执行代码校验和检查

恶意代码可以计算代码段的校验并实现与扫描中断相同的目的。与扫描0xCC不同,这种检查仅执行恶意代码中机器码CRC或者MD5校验和检查。

4.时钟检测

被调试时,进程的运行速度大大降低,例如,单步调试大幅降低恶意代码的运行速度,所以时钟检测是恶意代码探测调试器存在的最常用方式之一。有如下两种用时钟检测来探测调试器存在的方法。
记录一段操作前后的时间戳,然后比较这两个时间戳,如果存在滞后,则可以认为存在调试器。
记录触发一个异常前后的时间戳。如果不调试进程,可以很快处理完异常,因为调试器处理异常的速度非常慢。默认情况下,调试器处理异常时需要人为干预,这导致大量延迟。虽然很多调试器允许我们忽略异常,将异常直接返回程序,但这样操作仍然存在不小的延迟。
4.1 使用rdtsc指令
较常用的时钟检测方法是利用rdtsc指令(操作码0x0F31),它返回至系统重新启动以来的时钟数,并且将其作为一个64位的值存入EDX:EAX中。恶意代码运行两次rdtsc指令,然后比较两次读取之间的差值。
4.2 使用QueryPerformanceCounter和GetTickCount
同rdtsc指令一样,这两个Windows API函数也被用来执行一个反调试的时钟检测。使用这种方法的前提是处理器有高分辨率能力的计数器-寄存器,它能存储处理器活跃的时钟数。为了获取比较的时间差,调用两次QueryPerformanceCounter函数查询这个计数器。若两次调用之间花费的时间过于长,则可以认为正在使用调试器。GetTickCount函数返回最近系统重启时间与当前时间的相差毫秒数(由于时钟计数器的大小原因,计数器每49.7天就被重置一次)。

5.判断父进程是否是explorer.exe

一般双击运行的进程的父进程都是explorer.exe,但是如果进程被调试父进程则是调试器进程。也就是说如果父进程不是explorer.exe则可以认为程序正在被调试。

6.判断STARTUPINFO信息

explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0。所以可以利用STARTUPINFO来判断程序是否在被调试。

7.判断是否具有SeDebugPrivilege权限

默认情况下进程是没有SeDebugPrivilege权限的,但是当进程通过调试器启动时,由于调试器本身启动了SeDebugPrivilege权限,当调试进程被加载时SeDebugPrivilege也就被继承了。所以我们可以检测进程的SeDebugPrivilege权限来间接判断是否存在调试器,而对SeDebugPrivilege权限的判断可以用能否打开csrss.exe进程来判断。

三、干扰调试器的功能

恶意代码可以用一些技术来干扰调试器的正常运行。例如线程本地存储(TLS)回调、插入中断、异常等。这些技术当且仅当程序处于调试器控制之下时才试图扰乱程序的运行。

1.使用TLS回调

Thread Local Storage(TLS),即线程本地存储,是Windows为解决一个进程中多个线程同时访问全局变量而提供的机制。TLS可以简单地由操作系统代为完成整个互斥过程,也可以由用户自己编写控制信号量的函数。当进程中的线程访问预先制定的内存空间时,操作系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性。下面是一个简单的TLS回调的例子,TLS_CALLBACK1函数在main函数执行前调用IsDebuggerPresent函数检查它是否正在被调试。

2.利用中断

因为调试器使用INT 3来设置软件断点,所以一种反调试技术就是在合法代码段中插入0xCC(INT 3)欺骗调试器,使其认为这些0xCC机器码是自己设置的断点。
除了使用_try和_except以外还可以直接使用汇编代码安装SEH。在下面的代码中如果进程没有处于调试中,则正常终止;如果进程处于调试中,则跳转到非法地址0xFFFFFFFF处,无法继续调试。
双字节操作码0xCD03也可以产生INT 3中断,这是恶意代码干扰WinDbg调试器的有效方法。在调试器外,0xCD03指令产生一个STATUS_BREAKPOINT异常。然而在WinDbg调试器内,由于断点通常是单字节机器码0xCC,因此WinDbg会捕获这个断点然后将EIP加1字节。这可能导致程序在被正常运行的WinDbg调试时,执行不同的指令集(OllyDbg可以避免双字节INT 3的攻击)。
INT 2D原为内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。但程序调试运行时不会触发异常,只是忽略。INT 2D指令在ollydbg中有两个有趣的特性。在调试模式中执行INT 2D指令,下一条指令的第一个字节将被忽略。使用StepInto(F7)或者StepOver(F8)命令跟踪INT 2D指令,程序不会停在下一条指令开始的地方,而是一直运行,就像RUN(F9)一样。在下面的代码中,程序调试运行时,执行INT 2D之后不会运行SEH,而是跳过NOP,把bDebugging标志设置为1,跳转到normal_code;程序正常运行时,执行INT 2D之后触发SEH,在异常处理器中设置EIP并把bDebugging标志设置为0。
片内仿真器(ICE)断点指令ICEBP(操作码0xF1)是Intel未公开的指令之一。由于使用ICE难以在任意位置设置断点,因此ICEBP指令被设计用来降低使用ICE设置断点的难度。运行ICEBP指令将会产生一个单步异常,如果通过单步调试跟踪程序,调试器会认为这是单步调试产生的异常,从而不执行先前设置的异常处理例程。利用这一点,恶意代码使用异常处理例程作为它的正常执行流程。为了防止这种反调试技术,执行ICEBP指令时不要使用单步。

3.设置陷阱标志位

EFLAGS寄存器的第八个比特位是陷阱标志位。如果设置了,就会产生一个单步异常。

4.使用异常

前面已经讨论了各种使用异常机制的反调试手段。
4.1 RaiseException
RaiseException函数产生的若干不同类型的异常可以被调试器捕获。
4.2 SetUnhandledExceptionFilter
进程中发生异常时若SEH未处理或注册的SEH不存在,会调用UnhandledExceptionFilter,它会运行系统最后的异常处理器。UnhandledExceptionFilter内部调用了前面提到过的NtQueryInformationProcess以判断是否正在调试进程。若进程正常运行,则运行最后的异常处理器;若进程处于调试,则将异常派送给调试器。SetUnhandledExceptionFilter函数可以修改系统最后的异常处理器。下面的代码先触发异常,然后在新注册的最后的异常处理器内部判断进程正常运行还是调试运行。进程正常运行时pExcept->ContextRecord->Eip+=4;将发生异常的代码地址加4使得其能够继续运行;进程调试运行时产生无效的内存访问异常,从而无法继续调试。
在OllyDbg中,选择Options->Debugging Options->Exceptions来设置把异常传递给应用程序。

四、调试器漏洞

与所有软件一样,调试器也存在漏洞,有时恶意代码编写者为了防止被调试,会攻击这些漏洞。这里我们展示几种OllyDbg调试器处理PE格式文件时的常见漏洞。

1.PE头漏洞

OllyDbg非常严格地遵循了微软对PE文件头部的规定。在PE文件的头部,通常存在一个叫作IMAGE_OPTIONAL_HEADER的结构。
需要特别注意这个结构中的最后几个元素。NumberOfRvaAndSizes属性标识后面DataDirectory数组中的元素个数。DataDirectory数组表示在这个可执行文件中的什么地方可找到其他导入可执行模块的位置,它位于可选头部结构的末尾,是一个比IMAGE_DATA_DIRECTORY略大一些的数组。数组中每个结构目录都指明了目录的相对虚拟地址和大小。DataDirectory数组的大小被设置为IMAGE_NUMBEROF_DIRECTORY_ENTRIES,它等于0x10。因为DataDirectory数组不足以容纳超过0x10个目录项,所以当NumberOfRvaAndSizes大于0x10时,Windows加载器将会忽略NumberOfRvaAndSizes。OllyDbg遵循了这个标准,并且无论NumberOfRvaAndSizes是什么值,OllyDbg都使用它。因此,设置NumberOfRvaAndSizes为一个超过0x10的值,会导致在程序退出前,OllyDbg对用户弹出一个窗口。如图所示,使用LordPE打开可执行文件,修改RVA数及大小并保存,再用OllyDbg打开,会提示错误Bad or unknown format of 32-bit executable file。
另一种PE头的欺骗与节头部有关。文件内容中包含的节包括代码节、数据节、资源节,以及一些其他信息节。每个节都拥有一个IMAGE_SECTION_HEADER结构的头部。
VirtualSize和SizeOfRawData是其中两个比较重要的属性。根据微软对PE的规定,VirtualSize应该包含载入到内存的节大小,SizeOfRawData应该包含节在硬盘中的大小。Windows加载器使用VirtualSize和SizeOfRawData中的最小值将节数据映射到内存。如果SizeOfRawData大于VirtualSize,则仅将VirtualSize大小的数据复制入内存,忽略其余数据。因为OllyDbg仅使用SizeOfRawData,所以设置SizeOfRawData为一个类似0x77777777的大数值时,会导致OllyDbg崩溃。如图所示,使用LordPE打开可执行文件,点击区段,在区段表上右击,点击编辑区段,修改物理大小并保存,再用OllyDbg打开,会提示同样的错误。
对抗这种反调试技术的最简单方法是用类似的编辑器手动修改PE头部。OllyDbg2.0和WinDbg不存在这种漏洞。

2.OutputDebugString漏洞

恶意代码常尝试利用OllyDbg1.1的格式化字符串漏洞,为OutputDebugString函数提供一个%s字符串的参数,让OllyDbg崩溃。因此,需要注意程序中可疑的OutputDebugString调用,例如OutputDebugString("%s%s%s%s%s%s%s%s%s")。如果执行了这个调用,OllyDbg将会崩溃。

HOOK

钩子(Hook),是Windows消息处理机制的一个平台,应用程序可以在上面设置子程以监视指定窗口的某种消息,而且所监视的窗口可以是其他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。钩子机制允许应用程序截获处理window消息或特定事件。
钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。

Hook原理

每一个Hook都有一个与之相关联的指针列表,称之为钩子链表,由系统来维护。这个列表的指针指向指定的,应用程序定义的,被Hook子程调用的回调函数,也就是该钩子的各个处理子程序。当与指定的Hook类型关联的消息发生时,系统就把这个消息传递到Hook子程。一些Hook子程可以只监视消息,或者修改消息,或者停止消息的前进,避免这些消息传递到下一个Hook子程或者目的窗口。最近安装的钩子放在链的开始,而最早安装的钩子放在最后,也就是后加入的先获得控制权。
逆向面试常见问题(陆续更新ing)
微软官方提供的方法Ring3
SetWindowsHookEx
UnhookWindowsHookEx
CallNextHookEx

非官方方法 Ring0/Ring3
使用汇编代码编替换函数开始的汇编代码,使其跳转到我们的Hook函数中,非官方叫法:Inline hook
使用Hook函数地址替换调用函数地址(IAT hook/SSDT hook……)
API Hook - Inline Hook、SEH Hook、IAT Hook、SSDT Hook (Ring0)

应用层
消息Hook:SetWindowsHookEx
注入Hook:Inline HOOK、IAT HOOK、HotFix Hook
调试Hook:DebugActiveProcess

驱动层
SSDT HOOK、IDT HOOK、object HOOK、sysenter HOOK

HOOK技术主要分为两大类,一是内核层HOOK,一是用户层HOOK。
用户层HOOK也就是在ring3环境下hook kenerl32.dll、
User3.dll、Gui32.dll、Advapi.dll等导出的函数。
内核层HOOK就是HOOK只有ring0级别环境下才能操作写入改变的内核
对象,例如SSDT系统服务描述符表等。综合而言,主要有以下9种HOOK技术。

有哪些HOOK

  1. 消息钩子
    消息钩子是最常见的钩子之一,例如常见的键盘鼠标钩子,很多木马就是通过消息钩子获取密码的。消息钩子是由Win32子
    系统提供,用户通过它注册全局钩子。当系统获取某些事件,比如用户按键,键盘driver将扫描码等传入win32k的KeyEvent
    处理函数,处理函数判断有无相应hook,有则通过回调函数处理。此时,系统取得Hook对象信息,若目标进程没有装载对应
    的Dll,则装载之。
  2. IAT HOOK
    IAT HOOK[4]是最常见和普遍的HOOK之一。IAT表示导入地址表(Import Address Table),导入函数就是被程序调用但其执行
    代码又不在程序中的函数,当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数
    实际所处的地址联系起来(动态连接),这种操作就需要导入表完成。其中导入地址表就指示函数实际地址。程序每个调用的
    API 函数地址都保存在 IAT 表中,而每个调用 API 函数的 CALL 指令所使用的地址都是相应函数登记在 IAT 表的地址。
    IATHOOK原理是在将 IAT 表中的地址换成用户自己的 函数地址,这样每个 API 调用都是先调用用户自己的 函数。在这个
    函数中我们可以完成函数名称的记录、参数的记录、调用原来的过程,并在返回时记录结果。
  3. EAT HOOK
    EAT HOOK的原理是根据替换 PE 格式导出表中的相应函数来实现的。EAT表示导出地址表(Export Address Table),EAT存在
    于PE文件中的edata节,保存了可执行文件(如DLL 文件)的导出的可供其他模块来调用的函数和公共变量,包括函数名称
    和地址等。通过替换Windows 系统某些重要DLL中的输出函数地址,即可实现目标函数的挂接。EAT记录DLL中可供其他程序
    使用的函数,可执行文件装载时会使用相应DLL的EAT表来初始化IAT表。
    EAT HOOK原理是通过替换EAT表中的函数地址,使依赖于本DLL的程序得到一个假的地址。
  4. SSDT HOOK
    SSDT是最常见的内核层 HOOK。SSDT的全称是系统服务描述符表(System Services Descriptor Table)。SSDT是关联ring3的
    Win32 API和ring0的内核API的重要数据结构,它存储着Windows把需要调用的所有内核API地址。
    SSDT HOOK原理是将内核层 API地址修改为指向其位于Ring0层的驱动入口,这样每次系统执行到这个函数时,都会通过SSDT表将原始调用引向修改后的模块中。
  5. IDT HOOK
    IDT HOOK是Win2000操作系统上常用的一种HOOK。IDT是中断描述表,可以替换其中的中断处理程序。这种方法对于跟踪、分析系统调用来说用的比较多。
    IDT HOOK的原理是通过替换 IDT表中的 INT 2E 中断,使之指向新的中断服务处理例程来实现的。它首先保存出特定的中断
    向量中断服务程序(ISR), 然后直接修改该中断向量的ISR为自定义的函数,每当这个中断向量对应的中断产生时,就会调用
    自定义的函数。由于我们自定义的函数里面执行完我们的功能后再跳转到原ISR处执行。
  6. SYSENTRY HOOK
    SYSENTRY是Windows XP之后的操作系统进入ring0的函数。Win2000中通过 int2e系统调用机制,涉及到的
    Interrupt/Exception Handler的调用都是通过 call/trap/task这一类的gate来实现的,这种方式会进行栈切换,并且系统
    栈的地址等信息由TSS提供,可能会引起多次内存访问 (来获取这些切换信息),系统开销较大。SYSENTER通过汇编指令实现快速系统调用机制。
    SYSENTER HOOK的原理是首先Ntdll 加载相应的请求服务号到EAX 寄存器中,同时EDX 寄存器存贮当前的栈指针ESP,然后
    Ntdll发出SYSENTER 指令,该指令转移控制权到寄存器IA32_SYSENTER_EIP 存贮的地址中[21],通过修改这个地址,可实现
    相应的挂接。
  7. Inline HOOK
    inline hook是直接在以前的函数体内修改指令,用一个跳转或者其他指令来实现挂钩的目的。
    而普通的hook只是修改函数的调用地址,而不是在原来的函数体里面做修改。
    Inline hook原理是解析函数开头的几条指令,把他们Copy到数组保存起来,然后用一个调用我们的函数的几条指令来替换
    ,如果要执行原函数,则在我们函数处理完毕,再执行我们保存起来的开头几条指令,然后调回我们取指令之后的地址执行
    。它需要在程序中嵌入汇编代码(Inline Assembly)以操作堆栈和执行内核API对应的部分汇编指令。
  8. OBJECT HOOK
    OBJECT HOOK是相对于IAT HOOK之类的 API HOOK而言,API HOOK是挂钩应用层函数,而OBJECT HOOK是挂钩内核层函数。其原理与API HOOK类似。
  9. IRP HOOK
    IRP是 I/O request packets,在Windows中几乎所有的I/O都是通过包(packet)驱动的,每个单独的I/O由一个工作命令描
    述,此命令将会告诉驱动程序需要一些什么操作,并通过I/O子系统跟踪处理过程。这些工作命令就表现为一个个被称为IRP
    的数据结构。
    IRP HOOK原理是拦截管理器发向文件或网络系统等驱动程序的IRP。一般通过创建一个上层过滤器设备对象并将之加入系统
    设备所在的设备堆栈中。也有部分IRP HOOK通过拦截传递IRP请求包的函数IofCallDriver或MajorFunction函数表来实现的。

Windows注入方式有哪些

一.DLL注入

在注入技术中,DLL注入是最常见最有效的技术。作者通常把代码编写成一个动态链接库,然后把这个动态链接库加载到正常进程中去,因为任何一个进程在加载动态链接库时都要执行DLL文件里入口函数DllMain中的代码,这样代码才得以执行。

1.修改注册表

修改 HKEY_LOCAL_MACHINE/Software/Microsoft/WindowsNT/CurrentVersion/Windows/AppInit_DLLs 的键值,改为待注入DLL的路径,这样一来,程序运行后只要加载了user32.dll,都会加载我们写入的DLL。

2.全局消息钩子注入

Windows应用程序是基于消息驱动的,任何线程只要注册窗口类都会有一个消息队列用于接收用户输入的消息和系统消息。为了拦截消息,Windows提出了钩子的概念。钩子(Hook)是Windows消息处理机制中的一个监视点,钩子提供一个回调函数。当在某个程序中安装钩子后,它将监视该程序的消息,在指定消息还没到达窗口之前钩子程序先捕获这个消息。这样就有机会对此消息进行过滤,或者对Windows消息实现监控。 消息钩子分为局部钩子和全局钩子。局部钩子是指仅拦截某一个进程的消息,全局钩子将拦截系统中所有进程的消息。
  该技术需要我们写一个简单的加载该DLL的程序,使其调用DLL的钩子函数,该函数会下一个全局消息钩子,强迫所有发生该消息的进程加载该DLL,这样就实现了DLL注入。

3.手工修改导入表

该方法需要我们熟悉PE结构,找到导入表位置在其后面添加一个新节,写入待注入的DLL,并将该DLL的导出函数的一些信息写到正确的位置,若导出函数过多将花费很多时间在手工修改上,并且可能出现空间不足写入的情况。

4.远程线程注入DLL(常问)

通过利用2000/XP等系统所有进程加载Kernel32.dll的模块基址一致的特点,先使用OpenProcess函数打开远程进程的句柄,再用VirtualAllocExWriteProcessMemory将待注入DLL路径写入目标进程,找到LoadLibraryW的地址,最后用CreateRemoteThread使目标进程调用LoadLibraryW加载DLL,参数为写入的DLL路径。

1. 获取指定进程句柄
OpenProcess
 2. 在进程中申请一段内存
VirtualAllocEx
 3. 把dll路径写入到进程内存中
WriteProcessMemory
 4. 创建远程线程
CreateRemoteThread
 5. 等待线程,释放空间
WaitForSingleObject

5.APC注入

APC注入的原理是利用当线程被唤醒时APC中的注册函数会被执行的机制,并以此去执行我们的DLL加载代码,进而完成DLL注入的目的。利用QueueUserAPC()可以向APC队列投入Loadlibrary函数指针完成注入,其实这种方法配合CreateProcess使用注入最为简单,先挂起打开线程,再QueueUserAPC(),再恢复线程,完成注入。

6.DLL劫持

自己实现应用程序的某个DLL,完成其导出函数,并将其放于应用程序目录下,当程序打开并加载DLL时会优先加载该目录下的DLL,原来的DLL若在该目录下降其移走即可。

7.输入法注入

利用输入法在工作时需要向进程中加载Ime文件(其实就是个Dll),我们构造自己的Ime文件,在Ime文件注入对方进程的时候加载我们自己的DLL完成注入

二.代码注入

对于病毒来讲,虽然远程注入DLL相对隐蔽,但是因为在被注入进程中多了个DLL模块,所以也没有做到完全隐蔽,而且也很容易将病毒清除。所以对于那些更高级的病毒则使用了更加隐蔽,更加难以清除的方法:远程代码注入。这是指,在远程进程中开辟足够大的内存空间,然后将要执行的代码直接写过去,最后使用远程线程的方法运行注入代码。这样做可以使其他进程执行病毒代码,而又不会在对方进程中产生新的模块,从而使病毒能够更隐蔽地运行。

1.手工修改执行起始地址

使用二进制编辑工具打开目标程序,找到合适的地方写入二进制代码,将程序执行起始地址(AddressOfEntryPoint)改为注入代码处,在执行完后跳到源地址。

2.挂起线程注入

OpenThread–>SuspendThread–>申请内存–>写入代码–>GetThreadContext–>获取EIP–>修改EIP–>SetThreadContext–>ResumeThread。

3.挂起进程注入

CreateProcess注入方法之一,CREATE_SUSPENDED以挂起的方式打开进程,后面方法与挂起线程注入相似。

4.调试器注入

CreateProcess注入方法之二,DEBUG_ONLY_THIS_PROCESS以调试的方法打开进程,利用CREATE_PROCESS_DEBUG_EVENT,向目标程序中写入我们的ShellCode完成相应功能,并且我们的ShellCode中写入以CC断点,代码执行指令时触发EXCEPTION_DEBUG_EVENT事件,在EXCEPTION_DEBUG_EVENT的处理函数中回到原来的执行流程。
参考书籍:《计算机病毒揭秘与对抗》
参考网址:https://bbs.pediy.com/thread-217722.htm