InjectorTool 项目原理详解

InjectorTool(x86 x64 Ring3 Windows7 Windows10 MFC)

功能:

注入目标进程,加载DLL文件。一个包含常用注入方法的集合工具。

1)枚举进程,显示进程信息,枚举进程模块,显示模块信息

2)用户层的进程隐藏

3)常用注入方法:远线程注入、APC注入、全局钩子注入、导入表注入、反射式注入

 

实现

1.枚举进程信息:

利用ToolHelp API函数得到进程的PROCESSENTRY32结构体,获得进程名,进程ID,父进程ID,通过进程ID再获得进程的完整路径

获得进程的PROCESSENTRY32结构体:

1)调用CreateToolhelp32Snap为系统进程列表的进程创建一个快照

2)调用Process32First获得快照中第一个进程的信息

    通过进程ID再获得进程的完整路径:

    ①调用OpenProcess获得进程句柄

    ②调用微软未公开函数NtQueryObject查询句柄的对象类型是否是进程

    ③调用GetProcessImageFileName获得进程的DOS路径

    ③将DOS路径转为NT路径

      调用QueryDosDevice查询磁盘的DOS名,将DOS路径磁盘名后的\\Windows\\System32\\Taskmgr.exe与磁盘DOS名拼接,看是否是一致的,若一致将盘符与之拼接得到NT路径

3)调用Process32Next获得快照中下一个进程的信息

2.枚举进程模块信息:

x86_x86,X64_x64对x86,x64:

1)调用NtQueryInformationProcess获得目标进程的ProcessBasicInfo信息,从而得到PEB的地址

2)得到模块链表地址,遍历进程模块

3)调用ReadProcessMemory获得模块链表项信息

x64_x86对x86,x64:

1)调用NtWow64QueryInformationProcess64获得获得目标进程的ProcessBasicInfo信息,从而得到PEB的地址

2)得到模块链表地址,遍历进程模块

3)调用NtWow64ReadVirtualMemory64获得模块链表项信息

3.进程创建初期的注入:

  • 导入表注入

1)打开目标文件,获得文件句柄,解析PE结构,将PE文件加载到当前进程中(自己按内存粒度对齐)

2)改变目标进程的导入表,添加一个新的导入表描述符,以链接病毒DLL文件

   ①定位导入表的所在节,从而得到导入表所在节表头的地址

   ②计算填充新ThunkData需要的大小(需按ULONG对齐),如果原导入表写的下将新ThunkData放入原导入表位置,如果写不下,就将新ThunkData放在新导入表的后面

   ③计算某个节按文件粒度对齐后的空隙大小(ImageSectionHeader->SizeOfRawData - ImageSectionHeader->Misc.VirtualSize)

   ④判断节空隙能否放下新的导入表,如果能放下将新导入表放在节空隙里;如果放不下在所有节后面插入一个新节(在文件中扩充大小,在内存中在后面申请节的新内存)

   ⑤保存原导入表信息,将原导入表部分拷贝到新导入表,定位新添加的导入表描述符,得到OriginalFirstThunk和NewThunkData的地址

   ⑥填充NewThunkData信息,更新PE头中的几个值

     如果ThunkData在原IID处,设置节属性为可写的,如果没有用到新节,要把添加在缝隙的内容更新到节头的实际大小里ImageSectionHeader->Misc.VirtualSize += SizeNeed;

   ⑦清空绑定输入表,强迫加载器重新加载IAT,设置感染标记,保存内存中的修改内容到文件中

     先写PE头,再写新添加的导入表描述符的子结构(RVA->RAW),最后在末尾写入新导入表(RVA->RAW)

 

注意点:

1)为PE文件添加新节

    ①定位到最后一个节的末尾和最后一个节的节头

    ②计算新节的节区起始数据在内存中的偏移(根据最后一个节按内存粒度对齐的大小加起始位置)

    ③计算新节的节区起始数据在文件中的偏移(根据最后一个节按文件粒度对齐的大小加起始位置)

    ④计算新节的大小(按文件对齐粒度对齐),扩充文件大小(设置文件指针位置到末尾,截断文件)-->SetEndOfFile

    ⑤填充节头信息(Misc,VirtualAddress,PointerToRawData,SizeOfRawData,FileHeader->NumberOfSections,OptionalHeader->SizeOfImage

    ⑥设置文件指针指向文件开始处,将更新后的PE头写入文件

2)INT表和IAT表的8字节是因为这两张表至少包含一项内容,才会被系统加载,剩余的4字节为0标志着表的结束

2)新增节的属性(Characteristics)需要改为0xc0000040,否则会报0xc0000005错误

防御:

可使用校验的方式去检测有无被注入

 

4.进程创建后期的注入:

  • 远线程注入

1)提升当前进程令牌权限  SE_DEBUG_NAME:需要调试和调整另一个帐户拥有的进程的内存

2)利用OpenProcess获得目标进程句柄

3)利用VirtualAllocEx在注入进程中申请内存

4)利用WriteProcessMemory将DLL文件路径写入申请内存

5)利用GetProcAddress获得LoadLibraryA函数在目标进程的地址

6)利用CreateRemoteThread函数即可启动线程调用LoadLibraryA函数加载DLL模块

7)调用WaitForSingleObject等待线程结束

8)调用VirtualFreeEx释放内存

在此步骤后卸载模块:

1)用GetProcAddress函数获得FreeLibrary函数的起始地址。FreeLibrary函数位于Kernel32.dll中

2)用CreateRemoteThread函数让目标进程执行FreeLibrary来卸载被注入的dll。(其参数是第(6)步返回的模块句柄)

注意点:

1)被注入程序(notepad.exe)和注入模块(thief.dll)位数必须一致(32位或64位)。

2)注入程序拥有管理员权限(工程属性设置或动态提权)

3)注入模块和被注入程序如果不在同一个目录下,注入模块需要写全路径名,否则注入成功但找不到dll

4)由于SESSION 0隔离机制,对系统服务进程,创建的远程线程会被挂起,它不能显示UI,也不能通信

使用CreateRemoteThread执行远线程创建的时候,会调用底层函数ZwCreateThreadEx函数执行创建远程线程,在进行服务进程的注入的时候,ZwCreatedThreadEx的参数CreateSuspended(也就是CreatedThread标志位)一直为1,这也就直接导致注入的线程一直处于挂起状态,无法运行

解决方法:

使用ZwCreateThreadEx函数,将其第7个参数赋为0即可

防御:

1)枚举进程模块信息根据模块名和模块路径判断

2)调用GetThreadStartAddress获得线程入口点,判断线程的入口点是不是LoadLibraryA或者LoadLibraryW

 

  • APC注入

APC:异步过程调用,表示在指定现场上下文中异步调用一个函数,当一个APC插入到线程的调用队列中时,系统将会发出一个软件中断,之后每当线程从可警告状态恢复,就会去Apc队列中调用回调函数

1)通过OpenProcess函数打开目标进程,获取目标进程的句柄。

2)通过调用WIN32 API函数CreateToolhelp32Snapshot、Thread32First以及Thread32Next遍历线程快照,获取目标进程的所有线程ID。

3)调用VirtualAllocEx函数在目标进程中申请内存,并通过WriteProcessMemory函数向内存中写入DLL的注入路径。

4)遍历获取的线程ID,调用OpenThread函数以THREAD_ALL_ACCESS访问权限打开线程,获取线程句柄

5)调用QueueUserAPC函数向线程插入APC函数,设置APC函数的地址为LoadLibraryA函数的地址,并设置APC函数参数为上述DLL路径地址

注意点:

1)使用场景:多线程环境下,注入程序必须会进入可警告状态

2)可以注入系统服务进程

3)可警告状态:当进程调用SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, WaitForSingleObjectEx时才会进入的状态

防御:

可以通过查看进程加载模块的信息,来判断进程是否注入到了其他模块

 

  • 反射式注入

注射器实现

1)将待注入DLL读入自身内存(CreateFile,GetFileSize,ReadFile,HeapAlloc):此时按文件粒度对齐

2)获得DLL文件导出函数ReflectiveLoader的Raw(文件中的相对地址) 涉及遍历导出表,RVA->RAW

3)在目标进程中申请内存,获得目标进程中的ReflectiveLoader绝对地址(RAW+Base)

4)将DLL文件,将用户数据,ShellCode写入目标进程(ShellCode作用为调用ReflectiveLoader函数)

5)调用FlushInstructionCache将写入目标进程的数据刷新

 

 

6)根据位数调用不同的创建远程线程方法来执行ShellCode,启动位于目标进程中的ReflectiveLoader

ReflectiveLoader函数实现

1)获取几个关键函数(LoadLibraryA,GetProcAddress,VirtualAlloc,RtlExitUserThread,NtFlushInstructionCache)的地址

    PEB链表枚举模块,转换模块名的hash值,比对目标模块

    遍历模块导出表,HashValue比对查找目标函数

2)在目标进程分配一块内存,把DLL文件复制过去(分开头部和各个节表,需要将文件粒度转为内存粒度对齐)

    先拷贝头数据(头文件和内存粒度一样),再拷贝节数据(找到每个节的VA,将内容拷贝过去再定义下一个节的VA)

3)修复DLL文件的导入表,使导入函数能够正常运行

4)修复DLL文件的重定向表(程序装入内存时有预设基址IMAGE_OPTIONAL_HEADER.ImageBase,实际基址-预设基址得到差值,差值加上重定位块的预设值得到重定位块实际值)

5)通过OptionalHeader.AddressOfEntryPoint找到程序入口点,刷新缓存,将入口点换为DllMain函数指针,直接执行

6)获得DLL文件导出表函数地址,找到MyFunction函数,直接执行

7)完成工作,退出线程

检测点

1)强特征匹配_ReturnAddress()的函数。Reflectiveloader函数定位dos头的前置操作就是调用调用_ReturnAddress()函数获得当前dll的一个地址。

2)扫描定位pe开头位置的代码逻辑

3)扫描特定的hash函数和hash值。在dll注入过程中,需要许多dll句柄和函数地址,所以不得不使用hash对比dll名称和函数名称。我们可以匹配hash函数和这些特殊的hash值。

4)从整体上检测dll注入。在被注入进程存在两份dll文件,一份是解析前的原pe文件,一份是解析后的pe文件。可以检测这两份dll文件的关系来确定是反射式dll注入工具。

免杀

1)避免直接调用敏感api。例如不直接调用writeprocessmemory等函数,而是直接用syscall调用。这种免杀方式只能绕过用户态的hook。对于内核态hook可以解这个问题

2)dll在内存中的rwx权限进行去除,变成rx

3)擦除nt头和dos头。这种免杀方式会直接让检测点4)影响较大,不能简单的校验pe头了,需要加入更精确的确定两个dll的文件,比如说,首先通过读取未解析的dll的SizeOfImage的大小,然后去找此大小的内存块,然后对比代码段是否一致,去判断是否为同一pe文件。

4)抹除未解析pe文件的内存。这种免杀方式会导致检测点4)彻底失效

5)抹除reflectiveloader()函数的内存。这里关键是如何确定这块内存是pe结构,重建pe结构之后,可以通过导出表去看导出函数是否被抹除

 

5.利用系统机制注入:

  • 全局钩子注入

进程的地址空间是独立的,发生对应事件的进程不能调用其他进程地址空间的钩子函数。如果钩子函数的实现代码在DLL中,则在对应事件发生时,系统会把这个DLL加载到发生事件的进程地址空间中,使它能够调用钩子函数进行处理

在操作系统中安装全局钩子后,只要进程接收到可以发出钩子的消息,全局钩子的DLL文件就会由操作系统自动或强行地加载到该进程中,实现了DLL注入

1)设置WH_GETMESSAGE消息的全局钩子(将钩子与系统所有线程关联,拦截消息)

    因为WH_GETMESSAGE类型的钩子会监视消息队列,Windows系统是基于消息驱动的,所以所有进程都会有自己的一个消息队列,都会加载WH_GETMESSAGE类型的全局钩子DLL

2)成功设置全局钩子之后,只要进程有消息发送到消息队列中,系统才会自动将指定的DLL模块加载到进程中,实现DLL注入

3)当钩子不再使用时,卸载全局钩子,此时已经包含钩子回调函数的DLL模块的进程,将会释放DLL模块

注意点:

1)将钩子句柄传递给其他进程:在DLL中使用共享段

2)回调例程:简单的调用下一个钩子函数,在钩子函数间传递信息

防御:

1)user32.dll导出的gShareInfo全局变量可以枚举系统中所有全局钩子的信息,包括钩子的句柄、消息类型以及回调函数地址等。

2)PE结构的节属性Characteristics若包含IMAGE_SCN_MEM_SHARED 标志,则表示该节在内存中是共享的

3)HOOK自身进程的LoadLibraryExW这个函数,判断调用是否来自user32.dll,因为普通钩子注入时LoadLibraryExW的调用者为user32.dll

 

6.卸载DLL

1)根据进程ID创建该进程的模块快照

2)根据模块名或模块路径比对查找要卸载的dll模块

3)根据进程ID获取目标进程句柄

4)根据kernel32.dll获取FreeLibrary函数的地址,创建线程执行FreeLibrary卸载dll

 

7.进程隐藏

ZwQuerySystemInformation:

当SystemInformationClass指定为SystemProcessInformation(0x5),表示检索系统的进程信息。函数将会把得到的进程信息的内容保存到SYSTEM_PROCESS_INFORMATION结构数组,数组中的每一个元素都代表了一个进程信息。而数组的首地址将会保存到第二个参数SystemInformation中

NextEntryOffset:下一个SYSTEM_PROCESS_INFORMATION元素距离现在这个SYSTEM_PROCESS_INFORMATION元数的偏移

1)根据进程ID找到要隐藏进程

2)要隐藏进程的SYSTEM_PROCESS_INFORMATION的上一个元数的NextEntryOffset加上当前SYSTEM_PROCESS_INFORMATION的NextEntryOffset即可将它从结构体数组中断开

注意点:

1)不用考虑Hook重入问题(普通做法在fake函数里还需进行unhook,再执行原函数,最后再执行hook)

内核的进程检测和进程隐藏:(也可以Hook NtQuerySystemInformation函数,和用户层一样的操作)

1)遍历EPROCESS的活动进程链表即可检测出进程

    利用ntoskrnl.exe导出的PsInitialSystemProcess得到指向system进程的EPROCESS。这个结构成员EPROCESS.ActiveProcessLinks.Blink即指向PsActiveProcessHead,得到其ActiveProcessLinks地址,减去ActiveProcessLinks在EPROCESS中的偏移即可得到EPROCESS地址,可以由此得到进程名

2)对活动进程链表进行断链即可隐藏进程,摘除过后并不影响隐藏进程的线程调度,不能逃过PCHunter检测

 

 

8.注意点:

1.获得系统位数:

SYSTEM_INFO SystemInfo = { { 0 } };

GetNativeSystemInfo(&SystemInfo);

SystemInfo.wProcessorArchitecture为PROCESSOR_ARCHITECTURE_INTEL为32位,为PROCESSOR_ARCHITECTURE_AMD64为64位

2.GetProcessImageFileName内部实现

1)内部调用NtQueryInformationProcess函数

   Status = NtQueryInformationProcess(hProcess,

                                       ProcessImageFileName,

                                       ImageFileName,

                                       BufferSize,

                                       NULL);

   NtQueryInformationProcess函数内部调用

   ①调用ObReferenceObjectByHandle获得EPROCESS

     Status = ObReferenceObjectByHandle(ProcessHandle,

                                               PROCESS_QUERY_INFORMATION,

                                               PsProcessType,

                                               PreviousMode,

                                               (PVOID*)&Process,

                                               NULL);

   ②调用SeLocateProcessImageName获得进程全路径

     Status = SeLocateProcessImageName(Process, &ImageName);

     Process->SeAuditProcessCreationInfo.ImageFileName;

3.CreateRemoteThread的内部实现

 

 

 

4.PE注意点

1)PE文件加载到当前进程中(文件粒度转内存粒度、导入表与重定位表的修复)

   ①通过CreateFile函数得到文件句柄,通过GetFileSize函数获得文件大小

   ②通过ReadFile读取PE头,根据PE头判断是否是一个PE文件(查找DOS头的e_magic和NT头的Signature)

   ③解析原文件获得PE文件信息(exe模块基地址,DOS头,NT头,文件头,节数,可选头,重定向表,节表头,程序入口点OEP,程序的预设基址,导入表信息,导出表信息)

   ⑤申请动态内存(大小为PE文件在内存中的映射大小:SizeOfImage),在新申请的内存中写入PE头数据,将文件头部按内存粒度对齐,得到对齐大小,定位第一个节

   ⑥依次读取每个节在文件中的偏移地址(ImageSectionHeader->PointerToRawData),移动文件指针(SetFilePointerEx),从文件中读取各个节,将节按内存粒度对齐:

     读一个节数据,计算内存对齐后大小,再定位下一个节

   ⑦重新解析加载到当前进程中的PE文件信息(已按内存粒度对齐)

   ⑧修复重定位表

   ⑨修复导入表

2)处理重定位数据(按实际加载位置进行重定位)

   ①获得重定向表的地址:ImageNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]->VirtualAddress

   ②循环处理一个接一个的重定位块,最后一个重定位块以RAV=0结束

   ③在重定位块里循环处理重定位数据

     (需要重定位的个数,是本块的大小SizeOfBlock减去块头的大小,结果是以DWORD表示的大小,而重定位数据是16位的,得除以2)

 

 

     a.重定位表项的高4位是重定位类型,如果重定位类型为3,即双字32位都需要修复(才需要修复)

     b.计算需要重定位的数据RVA:重定位数据的低12位再加上本重定位块头的RVA即真正需要重定位的数据的RVA

     c.该数据RVA+新的基址即得到需要重定位数据的VA,对需要重定位的数据进行修正(修正方法:该数据减去IMAGE_OPTINAL_HEADER中的ImageBase,再加上新基址即可)

3)处理导入表数据

    ①循环遍历导入表描述符,获得每个模块名并在当前进程加载该模块(LoadLibraryA),得到模块在当前进程基地址,获得OriginalFirstThunk,FirstThunk的VA(RVA+模块在当前进程的基地址)

   ②修复每个模块的导入表函数地址

     根据名称导入还是索引导入(根据OriginalFirstThunk.u1判断)调用GetProcAddress函数获得导出表函数地址,将待修复函数地址(FirstThunk->u1.Function)改为真正函数地址

4)RVA->RAW计算

读取文件内容到动态内存中,此时文件内容按文件粒度对齐,但PE文件结构体中的地址偏移按内存粒度对齐,要获得导出表函数在动态内存中的偏移地址,就要将PE文件结构体中的地址偏移从内存粒度对齐转换为文件粒度对齐,然后计算相应的偏移地址

      

 

   ①如果查询RVA在PE文件头部,不用转换(因为RVA=RAW)

   ②如果在节里,判断Rva属于哪个节,根据公式计算,返回在文件中的偏移

   公式:0x5010(Rva)-0x5000(节的偏移地址)=Raw-0x1000(文件中节的偏移地址)(求???)

posted @ 2024-03-03 16:44  修竹Kirakira  阅读(36)  评论(0编辑  收藏  举报