【C语言】初识SSDT
本帖子主要是作为小弟的学习笔记。昨天学习到了(准确说是前天,因为现在已经过0点了)TA大佬教程的关于SSDT讲解的章节。为了尽可能多的了解SSDT我还是仔细的上网上查了查并且教程上的步骤我也都反复试验过,为了对这个玩意儿更加熟悉。
小弟简单介绍一下SSDT:
SSDT全称为System Services Descriptor Table即为系统服务描述符表
其作用是将ring3的win32 api和ring0的内核api联系起来。
平时在写应用层程序时候用的win32 api都要经过SSDT或者SSSDT(Shadow SSDT)来转化为相应的内核api。
SSDT主要处理Kernel32.dll中的系统调用,比如OpenProcess,ReadFile等函数
ShadowSSDT主要处理user32.dll,GDI32.dll中调用的函数。
SSDT不仅仅是一个地址索引表,其还包括地址索引的基地址,服务函数个数等。
结构体:
typedef struct _SYSTEM_SERVICE_TABLE{
PVOID ServiceTableBase; //指向系统服务函数的地址表
PVOID ServiceCounterTableBase;
ULONGLONG NumberOfServices; //服务函数的个数
PVOID ParamTableBase;
}SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;
小弟再聊一下对文章中的程序的想法,还是学到了很多新知识的。
1,首先是特征码,第一次听到这个词没记错是在一篇报道江民杀毒软件的文章中,说早期的杀毒软件查杀病毒用的是查找特征码来判断这个(软件)是不是一个病毒或者这个软件是不是一个恶意程序。
这次我TA大佬在文章中写的根据内存来查找特征码为的就是查找相应的汇编指令(因为汇编指令是会转化为机器码的,而机器码在内存中,我们可以在遍历内存,找到符合要求的机器码,找到了这个机器码也就找到了相应的汇编指令),感觉很新奇,哈哈。
2,在计算SSDT地址的时候,搜索到特征码后,我对那个计算过程当时萌比了,后来自己猜了猜又实验了几次弄明白了,后来我翻了翻书(汇编语言第二版王爽著,16位汇编教程 但是原理一样的),发现我原先学习汇编语言的时候总是关注指令和地址了,当时对计算偏移量很有动力(尤其是jmp等指令那里)而忽略了对偏移量和机器码之间的对应关系,实际上书上当时是有讲到的,只不过我没伤心。
3,最近一段时间的学习发现对于shellcode的知识多了起来,然而我还不懂,但是TA大佬教程里的汇编多少可以看明白一些。
4,关于取SSDT中函数的索引号。
课后作业留的是打印出来SSDT所有函数地址,自从之前我从教程上学到用for来枚举进程,用for来搜索特征码,那我先通过SYSTEM_SERVICE_TABLE结构体中的第三个成员:ULONGLONG NumberOfServcies得到SSDT中函数的个数,然后用for循环从0到那个个数进行枚举他们每个的地址,就是不知道想的对不对。
5,关于取SSDT中函数的索引号。
TA大佬教程中index是使用的windbg来查看的,他的例程中实验了NtOpenProcess和NtTernimateProcess,他们的索引号分别是0x23与0x29。而我自己通过windbg来进行实验的时候发现在找index的过程中,他们的关键汇编指令,比如对找NtOpenProcess的index来说:汇编指令为:mov eax,23h 对应的机器码为b823000000,在找NtTernimateProcess的index时候,汇编指令为:mov eax,29h 对应的机器码为b829000000。我发现b8是它们共同的(其他的mov指令为别的),而b8后面的一个字节为他们的index,所以我用windbg来查找一下ntdll的基址:u ntdll在我的win7 x64虚拟机上是00000000`77680000,我试着搜索一下特征码b8,发现效果不是很好,有一些重复的,并且有一些index超出了191(SSDT函数的个数),但是令人欣慰的是那里面大多数都还是符合要求的,并且出现了0x29(NtTernimateProcess的index)。
以下为小弟在TA大佬教程的例程基础上完成的作业:
VS2013 + wdk8.1win7 x64
//TA大佬教程课后练习orz
#include <ntddk.h>
#include <windef.h>
#include <wdm.h>
#pragma intrinsic(__readmsr)
typedef UINT64(__fastcall *SCFN)(UINT64, UINT64);
SCFN scfn;
//获取SSDT的地址
ULONGLONG MyGetKeServiceDescriptorTable64()
{
PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082); //读取C0000082寄存器的值,里面存的是KiSystemCall64的地址。
PUCHAR EndSearchAddress = StartSearchAddress + 0x500;
PUCHAR i = NULL;
UCHAR b1 = 0;
UCHAR b2 = 0;
UCHAR b3 = 0;
ULONGLONG templong = 0;
ULONGLONG addr = 0;
for (i = StartSearchAddress; i < EndSearchAddress; i++)
{
if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2))
{
b1 = *i;
b2 = *(i + 1);
b3 = *(i + 2);
/*
搜索特征码
在我的电脑上
fffff800`03ee1772 4c8d15c7202300lea r10,
4c8d15为lea的字节码,则猜测后面四个字节c7202300为偏移地址,由于小端存储,所以偏移量为002320c7
测试程序程序如下:
memcpy(&templong,i+3,4);
DbgPrint("%llx\n", templong); 输出:2320c7
DbgPrint("0x%X,0x%X,0x%X,0x%X\n", *(i + 3), *(i + 4), *(i + 5), *(i + 6));输出:0xC7,0x20,0x23,0x0
证明猜的是对的
*/
if (b1 == 0x4C && b2 == 0x8D && b3 == 0x15)
{
memcpy(&templong, i + 3, 4); //获取地址i+3后面的四个字节的内容,为002320c7,即为偏移地址。
//计算KeServiceDescriptorTable的地址
//基址i + 偏移量templong + lea r10,指令长度(7)
addr = (ULONGLONG)templong + (ULONGLONG)i + 7;
//addr的结果和KeServiceDescriptorTable的地址fffff800`04113840是一致的,进一步证明002320c7这段字节码为lea后的偏移量
return addr;
}
}
}
//如果return 0就等着蓝屏吧。
return 0;
}
//照葫芦画瓢获取ShadowSSDT的地址
ULONGLONG GetKeServiceDescriptorTableShadow64()
{
PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082);
PUCHAR EndSearchAddress = StartSearchAddress + 0x500;
PUCHAR i = NULL;
UCHAR b1 = 0, b2 = 0, b3 = 0;
ULONG templong = 0;
ULONGLONG addr = 0;
for (i = StartSearchAddress; i < EndSearchAddress; i++)
{
if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && (i + 2))
{
b1 = *i;
b2 = *(i + 1);
b3 = *(i + 2);
//就把特征码修改一下。
if (b1 == 0x4C && b2 == 0x8D && b3 == 0x1D)
{
memcpy(&templong, i + 3, 4);
addr = (ULONGLONG)templong + (ULONGLONG)i + 7;
return addr;
}
}
}
//到了这步就等着蓝屏吧。
return 0;
}
//小弟用windbg上看到ntdll的基址为00000000`77680000,试着用特征码搜索一下index
INT GetSSDTFunctionIndex64_WM(INT *index)
{
PUCHAR StartSearchAddress = 0x77680000;
PUCHAR EndSearchAddress = StartSearchAddress + 0x77680000;
DbgPrint("EndSearchAddress = 0x%X\n", EndSearchAddress);
PUCHAR i = NULL;
UCHAR b1 = 0;
INT k = 0;
for (i = StartSearchAddress; i < EndSearchAddress; i++)
{
if (MmIsAddressValid(i))
{
b1 = *i;
if (b1 == 0xB8)
{
DbgPrint("0x%X\n", *(i + 1)); //打印index
index = *(i + 1);
}
}
}
DbgPrint("index 寻找完毕\n");
return k;
}
//shellcode
VOID Initxxxx()
{
UCHAR strShellCode = "\x48\x8B\xC1\x4C\x8D\x12\x8B\xF8\xC1\xEF\x07\x83\xE7\x20\x4E\x8B\x14\x17\x4D\x63\x1C\x82\x49\x8B\xC3\x49\xC1\xFB\x04\x4D\x03\xD3\x49\x8B\xC2\xC3";
/*
mov rax, rcx ;rcx=index
lea r10, ;rdx=ssdt
mov edi,eax
shr edi,7
and edi,20h
mov r10, qword ptr
movsxd r11,dword ptr
mov rax,r11
sar r11,4
add r10,r11
mov rax,r10
ret
*/
scfn = ExAllocatePool(NonPagedPool, 36);
memcpy(scfn, strShellCode, 36);
}
//获取SSDT函数地址
ULONGLONG GetSSDTFunctionAddress64(ULONGLONG NtApiIndex)
{
ULONGLONG ret = 0;
ULONGLONG ssdt = MyGetKeServiceDescriptorTable64();
if(scfn==NULL)
{
Initxxxx();
}
ret = scfn(NtApiIndex,ssdt);
return ret;
}
//结构体声明
typedef struct _SYSTEM_SERVICE_TABLE{
PVOID ServiceTableBase;
PVOID ServiceCounterTableBase;
ULONGLONG NumberOfServices;
PVOID ParamTableBase;
}SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;
//另一种获取SSDT函数地址的方法,为上面shellcode对应汇编指令的C语言版本。
ULONGLONG GetSSDTFunctionAddress64_2(ULONGLONG Index)
{
LONG dwTemp = 0;
ULONGLONG qwTemp = 0, stb = 0, ret = 0;
PSYSTEM_SERVICE_TABLE ssdt = (PSYSTEM_SERVICE_TABLE)MyGetKeServiceDescriptorTable64(); // 获取ssdt地址
//DbgPrint(" ssdt中函数个数:%llx\n", ssdt->NumberOfServices); 191个
stb = (ULONGLONG)(ssdt->ServiceTableBase);
qwTemp = stb + 4 * Index;
dwTemp = *(PLONG)qwTemp;
dwTemp = dwTemp >> 4;
ret = stb + (LONG64)dwTemp;
return ret;
}
VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
DbgPrint("DriverUnload...\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryString)
{
pDriverObject->DriverUnload = DriverUnload;
DbgPrint("SSDT: %llx\n", MyGetKeServiceDescriptorTable64());
DbgPrint("ShadowSSDT: %llx\n", GetKeServiceDescriptorTableShadow64());
//win7x64硬编码,要有ssdt函数的index
//测试
DbgPrint("NtOpenProcess: %llx", GetSSDTFunctionAddress64(0x23));
DbgPrint("NtTerminateProcess:%llx", GetSSDTFunctionAddress64(0x29));
DbgPrint("NtOpenProcess: %llx", GetSSDTFunctionAddress64_2(0x23)); //WIN7X64 HARDCODE
DbgPrint("NtTerminateProcess: %llx", GetSSDTFunctionAddress64_2(0x29));//WIN7X64 HARDCODE
//将ssdt中函数的地址打印出来。
DbgPrint("***************************************************\n");
//用i来替代index进行遍历打印。
for (int i = 0; i < 0xBF; i++) //191个
{
DbgPrint("%llx\n",GetSSDTFunctionAddress64_2(i));
}
//搜索特征码得来的index
DbgPrint("以下为自己用特征码搜索来的index,结果有点不靠谱...\n");
int index = { 0 }; //用来存放SSDT函数index的数组。
int k = GetSSDTFunctionIndex64_WM(index);
//打印结果
for (int i = 0; i < k; i++)
{
DbgPrint("%llx\n", GetSSDTFunctionAddress64_2(index));
}
return STATUS_SUCCESS;
}
由于这篇帖子是小弟刚开始认识SSDT并且有很多自己的猜想,所以肯定会有很多漏洞和片面的观点希望各位大佬多多指正,本贴也是作为学习笔记作为交流:D
最后附上站长写的关于TA大佬驱动知识学习的帖子,希望更多伙伴对它感些兴趣:https://www.0xaa55.com/forum.php?mod=viewthread&tid=1108&highlight=%E9%A9%B1%E5%8A%A8
简单提醒几点:
没必要硬编码ntdll.dll的基地址。你可以自己解析ntdll.dll这个文件。不需要纠结系统盘符的问题,直接用\SystemRoot\System32\ntdll.dll这个路径即可。
如果不想解析文件,那就走EPROCESS->Peb->Ldr来找ntdll.dll,注意走Ldr需要解析双向链表。(用PsGetProcessPeb拿到PEB地址,然后自己抄个PEB的结构即可,PEB结构是不会根据系统版本变的)
区别是:读文件可以拿到更真实的数据,读内存可以最快拿到数据。钩子搜索原理就是比对文件和内存。
暴力搜索B8是有问题的算法,最好的做法是解析PE导出表,使用二分法搜索ZwXXX名字的函数(因为导出表是排过序的,搜索的时间复杂度可以由线性复杂度优化为对数复杂度),再判断首字节是否为B8。
就算是暴力搜索,i++也是不好的,应该是i+=16,MSVC编译的函数是按照16字节边界对齐的。
我找KeServiceDescriptorTable地址是通过反汇编引擎来搜索lea r10指令的。
值得注意的是,有些SSDT上函数的索引可以直接读函数对应的ZwXXX函数,以NtTerminateProcess函数为例:
nt!ZwTerminateProcess:
fffff800`02a8d6e0 488bc4 mov rax,rsp
fffff800`02a8d6e3 fa cli
fffff800`02a8d6e4 4883ec10 sub rsp,10h
fffff800`02a8d6e8 50 push rax
fffff800`02a8d6e9 9c pushfq
fffff800`02a8d6ea 6a10 push 10h
fffff800`02a8d6ec 488d051d2d0000lea rax,
fffff800`02a8d6f3 50 push rax
fffff800`02a8d6f4 b829000000 mov eax,29h
fffff800`02a8d6f9 e942640000 jmp nt!KiServiceInternal (fffff800`02a93b40)
在+0x14位置上是一个mov eax指令,其源操作数就是NtTerminateProcess在SSDT上的索引。这个方法不通用,因为不是所有SSDT上的函数对应的ZwXXX函数都被导出。 tangptr@126.com 发表于 2019-2-11 03:29
简单提醒几点:
没必要硬编码ntdll.dll的基地址。你可以自己解析ntdll.dll这个文件。不需要纠结系统盘符的 ...
好的好的,小弟我再仔细看看,非常感谢tangptr大佬指导!
页:
[1]