Hook与RootKit(第6课)

1、使用注册表来注入DLL

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

 

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中的锁计数器

 

3、使用远程线程来注入DLL

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

 

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里面的所有函数名字以及函数参数,这个地方比较不好搞,此地不考虑。

 

5、APC注入

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

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

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

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

 

6、使用CreateProcess注入代码

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

 

InlineHook

InlineHook的工作方式如下所示:

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

 

7、IDT Hook

我们首先来了解下什么是IDT?

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

 

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

1。任务门描述符

2。中断门描述符

3。陷阱门描述符

 

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

 

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

 

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

 

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

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

 

其中:后两种描述符,非常的相似,只有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相加得到代码段中的函数入口地址。然后调用该函数。

 

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

 

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

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

 

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

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

 

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

任务状态段TSS的基本格式

 

从图中可见,TSS的基本格式由104字节组成。这104字节的基本格式是不可改变的,但在此之外系统软件还可定义若干附加信息。TSS的基本格式由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]。找到目标入口后,将我们的函数与其原来的函数进行替换即可。

 

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的方式差不多,在此不再进行介绍。

 

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的原理就是把后面的目标函数的地址改成我们自己写的函数的地址。这样,当在此调用目标函数的时候,就会调用我们的函数的地址。

 

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时,实际上是执行了我们自己的函数。

头像
  • ¥ 388.0元
  • 市场价:388.0元
  • ¥ 59.0元
  • 市场价:99.0元
  • ¥ 29.0元
  • 市场价:99.0元
  • ¥ 198.0元
  • 市场价:398.0元

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: