【滴水基础】4.Win32Api调用(下2)
第三十:模块隐藏
---使用API,遍历进程有那些模块
#模块隐藏之断链
#线程环境块:TEB(Thread Environment Block)
---用户层的结构体,它记录了相关线程的信息。每一个线程都有自己的TEB
---FS:[0]就是当前线程的TEB
#进程环境块:PEB(Process Environment Block)
---用户层的结构体,它记录了相关进程的信息。每一个线程都有自己的PEB
---哪个线程在执行的时候,fs就存储那个线程的TEB
---TEB偏移0x30即FS:[0x30],就是当前线程的TEB
---使用Windbg(我的Win11直接在搜索框里面搜索)

---随便打开一个exe模块,使用"dt _TEB"命令来查看TEB结构体信息

---可以发现TEB信息如下,这里存在三个主要的指针,指向三个重要的结构体
---这里打开的是X64的WinDbg
---查看_PEB结构体(可以看出,这里x64位系统PEB和TEB的偏移因该是0x60)
#x86和x64的区别(32位系统也称x86,64位系统也称x64)
----1、内存寻址能力区别:32位系统寻址能力是4G容量,而64位系统可以支持128GB大内存,甚至更大。
---2、运算速度区别:x64的CPU数据宽度为64位,64位指令集可以运行64位数据指令,
---处理器一次可提取64位数据(只要两个指令,一次提取8个字节的数据)
----比32位(四个指令,一次提取4个字节的数据),性能会相应提升一倍。
---3.通常情况下,x86(32位)的软件可在64位和32位上的系统运行,但x64(64位)的软件在32位系统上有可能出现不兼容的情形。
#查看_PEB_LDR_DATA结构体信息
---理解其三个成员的顺序,其指向_LDR_DATA_TABLE_ENTRY元素中开始的三个成员
--- _LDR_DATA_TABLE_ENTRY 中存储着就是关于有关模块信息的元素(比如模块名等)
---什么是双向链表

---这里我搞错了一点,X86和X64不仅仅是操作系统,还有exe文件
---比如我用VC6编译的Test.exe文件就是X86系统的(可以在资源管理器查看)
---Test.exe就是打印0-9
---还有就是我系统自带的WinDbg(X64)

---可以通过命令设置WinDbg转为x86模式,需要切换到该 32 位进程转储的 32 位视图

---x64切换x86模式代码如下(emmm找了好久这个代码)
---再来查看_TEB,发现_PEB和TEB的偏移是0x30
---查看_NT_TIB
---查看_CLIENT_ID
---查看_PEB_LDR_DATA
---其中_LIST_ENTRY存储的是指向_LDR_DATA_TABLE_ENTRY的双向链表指针
---32位的结构体存储信息

---用DTDebug打开之前的Test.exe(这个没有dll,打开就是main函数的领空)

---查看FS的值:2C8000(注意:每次打开的都不一样)

---查看对应内存的值:dd 2C8000

---查看_CLIENT_ID结构体信息(偏移0x20)

---则获取到了:进程ID、线程ID

---找到TEB的偏移0x30,右键:follow in Dump进入指针指向的地址
---因为TEB的偏移0x30的内存里面,存储者指向PEB的指针

---follow in Dump之后,发现来到了PEB的内存地址

---查看PEB中LDR的偏移

---找到0xC的偏移

---再次follow in Dump到_PEB_LDR_DATA的内存地址
---然后根据0xC的偏移,来到InLoadOrderModuleList的位置

---里面存储的指针也就是_LDR_DATA_TABLE_ENTRY的起始位置
---注意:前面加_的应该是存储的结构体指针

---可以发现InLoadOrderModuleList的单位 _LIST_ENTRY里面
---起始就是双向链表的两个指针,可见_PEB_LDR_DATA这也是存在的3个 _LIST_ENTRY
--- _LIST_ENTRY这个双链表指向进程装载的模块,结构中的每个指针,指向了一个LDR_DATA_TABLE_ENTRY 的结构:

---查看_LDR_DATA_TABLE_ENTRY结构体的成员
---选择第一个follow in dump,进入_LDR_DATA_TABLE_ENTRY的起始位置
---PEB_LDR_DATA 中的三个LIST_ENTRY全部是双向链表结构
---它的两个成员Flink,Blink都指向LDR_DATA_TABLE_ENTRY
---首先看Test.exe加载了几个模块

---这里也是_LIST_ENTRY的Flink

---BaseDllName的偏移是0x024

---发现_LDR_DATA_TABLE_ENTRY的InLoadOrderModuleList的Blink正好是_LDR_DATA_DATA的InLoadOrderModuleList的Flink

---查看 BaseDllName 的单位_UNICODE_STRING(重要)

---偏移4个单位才是模块名称,也是对的上的

#总结
---_TEB偏移0x30,里面存储着指向_PEB的指针
---_PEB偏移0xC,里面存储着指向_PEB_LDR_DATA结构体的指针
---_PEB_LDR_DATA偏移0xC,里面存储着_LIST_ENTRY结构体的2个指针
---通过_LIST_ENTRY的Flink成员获取_LDR_DATA_TABLE_ENTRY结构体地址
---(注意:这里的Flink指向的是_LDR_DATA_TABLE_ENTRY结构中的InLoadOrderLinks成员)
---通过_LDR_DATA_TABLE_ENTRY的偏移获取BaseDllName或FullDllName成员信
---这里感觉很难,可以再梳理一下

---TEB > PEB > _PEB_LDR_DATA > _LDR_DATA_TABLE_ENTRY 的关系如下

---Ldr中有的三个List,根据MSDN规定LIST_ENTRY的结构,其中有两个成员:Flink和Blink,他们分别指向着一个LDR_DATA_TABLE_ENTRY
---不同模块之间,通过Flink和Blink连接

------而且不管怎样,第一个加载的一定是Test.exe模块(参考进程创建的流程)

---不难发现,PEB_LDR_DATA给出的是三个List(InLoadOrderModuleList,InMemoryOrderModuleLis以及InInitializationOrderModuleList)
---模块加载首个基址,也可以看成是整个List双向链表的表头,然后通过这个双向循环链表的不断的遍历,来依此获取不同List加载的顺序。
---同时系统为每一个DLL维护的一个LDR_DATA_TABLE_ENTRY,
---该结构中,每一个DLL在不同List中,不但包含着着前继加载模块或者后继加载模块,还有着非常详细的各个加载模块的信息,包括加载基址和DLL名称等等。
---这样,我们根据PEB_LDR_DATA 后找到LDR_DATA_TABLE_ENTRY
---通过不断地遍历,读取其中的各项结构,至此,我们可以得出每一个List的在测试系统下,模块的加载的依此顺序:

----总结:关系如下

#模块隐藏的思路:
---对于指向Test.exe的双链表进行断链操作,跳过第一个Test.exe模块(让PEB_LDR_DATA的Flink指向模块A,让模块A的Blink指向PEB_LDR_DATA)
---由于存在3个List,所以上面的操作要进行三次
---这里的断链思想是这样的:通过改变双向链表的指针
---进而将代表Kernel32.dll的_LDR_DATA_TABLE_ENTRY结构体进行跳过

---运行该exe,用DTDebug的attach打开,点E查看模块加载

---按回车尝试隐藏模块

---发现模块已经隐藏

#但是通过WinDbg查看内核的情况还是可以看到该模块
---通过查看内存情况(我的Windows11无法打开kernel debug,因该是不支持本地内核调试)
---发现kernel32.dll没有被隐藏
---而且这里不能断链,如果断链,内存就卸载了该模块

#在PE结构中查看模块在内存中存储的规律(PE指纹)
---查看Kernel32.dll的基址0x75860000

---db 0x75860000,开头5A 4D对应的MZ
---然后往后找64个Byte,发现:00 00 00 E8
---然后将0x75860000偏移E8,对应的PE
---这里的MZ、PE,是任何模块都存在的指纹
---因此:最好的隐藏:无模块注入,也就是代码注入

第三十一:代码注入
#回顾远程线程
---原来的远程线程是执行目标进程的已有函数
---代码注入:1.将自定义函数复制到目标进程 2.指定远程线程执行自定义函数

#代码注入的问题:
---1.复制的函数本质是什么?
---2.复制的函数可以正常执行的前提条件是什么?
#函数的本质
---函数在本质就是机器码,而VC6将机器码翻译成汇编代码来帮助人们查看
#不能使用全局变量进行拷贝
---g=1:对应的汇编:
---00427e34是全局变量的地址,将1存储到这个地址中
---机器码:05 34 7E 42 00代表全局变量的地址:00427e3205
---01是存储的值,C7代表的是mov
---但是如果直接复制机器码会失败,因为另外一个进程没有00427e3205的全局变量的地址
#不能使用常量字符串
---查看汇编和机器码
---字符串的的本质:在堆中申请内存,将字符串以数组的形式存储到堆内存,然后将堆内存数组的首地址放到缓冲区中[ebp-4]
---机器码(6C 2F 42 00)代表堆内存地址,但是另一个进程不存在该堆内存的地址
#不能使用系统调用
---对应的汇编和机器码
---程序将MessageBox的函数(根据exe的导入表)地址放入堆内存,
---MessageBox()的调用流程

#不能嵌套调用其它函数
---查看汇编:直接call的是Test的函数地址
---但是在另外一个进程中,这个函数地址是不一致的
#总结
---发现限制太多,基本做不了什么事情

#代码注入思路
---在同一个OS中,系统函数的地址都是一样的,如LoadLibrary()
---在当前进程,创建相关系统函数,然后将系统函数的地址,传到目标进程中

---对之前的Test.exe进行代码注入,任务管理器查看Pid:2060
---注意:我的VC是32位,所以只能注入32位的exe
---注意:一般的程序顶不住,我最后写了一个死循环才注入成功
----注入代码:在D盘创建一个Test.txt文件
---之前试了很多次,都没有成功,但是我的调试语句却输出创建文件成功(还以为是我的代码问题,copy了网上成功的代码还是不行)
---猜测是被注入的程序在被注入的时候,直接崩了,所以没有顶到文件创建成功,因此写了一个死循环,发现文件创建成功
---发现文件被创建

#代码注入的总结

#为什么要进行修正函数的
---对于调用函数来说,会call一个函数的地址:00401005
---但是从00401005跟进去发现,并不是Fun函数的真正地址
---而是JMP到真正的Fun函数的地址
---E9代表的是JMP命令,16代表的:当前的跳转地址的下一行地址,和Fun()地址相差16
---当前跳转地址下一行=00401005+5个机器码=0040100A
---Fun()地址:0x0040100A+0x16=401020
#总结
---真正的地址 = 汇编Call的地址 + 5 + JMP下一行到真正函数地址的偏移
#再来看代码里面的修正函数起始地址的代码
---原来的地址装在DWORD里面,是小端存储,所以 *(00401005)=E9 16 00 00 00
---但是如果将DWORD类型的dwFunAddr转换为BYTE,那么*(BYTE*)(00401005)=E9(因为我们只需要读取1Byte,所以转为Byte类型)
---获取JMP下一行到真正函数地址的偏移:读取dwFunAddr的后面4个Byte,所以需要转化为DWORD类型
---dwFunAddr+1则是跳过E9进行读取
#总结代码注入
---优点:难以检测
---缺点:繁琐复杂