双机调试
当需要分析一个程序时,这个程序一定是可以调试的,操作系统也不例外。在调试过程中下断点是很重要的
当我们对一个应用程序下断点时,应用程序是挂起的。但当我们对操作系统的内核程序下断点时,被挂起的不是内核程序而是整个操作系统。当操作系统被挂起时,网卡显卡等功能无法使用,进而无法在挂起的操作系统的机器上进行任何操作,也就无法调试内核程序了。
为了能够调试内核程序,Windows提供了一个调试子系统,它是一个独立于操作系统的的子系统,负责与调试器进行数据的交互进而实现调试内核程序。当内核程序下断点时,整个操作系统都被挂起,而这个调试子系统却不会被挂起。因此我们可以通过调试子系统去调试内核程序。
双机调试:由于挂起的操作系统我们无法进行任何操作,因此当调试内核程序时,我们需要在另一个机器上进行调试,这就是双机调试的由来。但我们使用两台物理机器来调试内核程序,这显然不现实,因此我们通常在本机上再运行一个虚拟机从而达到双机的效果。虚拟机上运行我们要调试的内核程序的机器,而我们在本机上进行调试内核程序。
调试过程:本机中的windbg首先连接到被调试的内核程序的机器的调试子系统,然后虚拟机通过串口把被调试的内核程序的数据发送给本机的windbg中从而进行信息的交互,进而实现调试。而我们在windbg发送的指令实际上是给调试子系统发送指令
注意:我们需要在操作系统刚开始运行时被迅速打开windbg,否则可能出现无法连接调试子系统的情况
此处具体双击调试环境配置步骤就不多说了,诸位自行百度
驱动开发
大多的驱动程序的启动是基于服务加载,服务基于注册表。驱动程序不像我们写的应用程序,关闭窗口就关闭程序,它必须有一个卸载,才可以关闭程序
一般的驱动程序都是用C写到,因为足够底层。但也可以用C++写,不过不建议
接下来我们在本机上写一个简单的驱动程序:
C驱动程序
#include <ntifs.h>
//SYS 驱动程序后缀:.SYS
VOID DriverUnload(PDRIVER_OBJECT pDriverObject)//卸载函数,名字自定义
{
DbgPrint("Hello DriverUnload\r\n");//打印一句话
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath)//参数:驱动对象 注册表路径
{
pDriverObject->DriverUnload = DriverUnload;//指向卸载函数
DbgPrint("Hello DriverEntry\r\n");//一定要加上\r\n,否则可能打印失败
return STATUS_SUCCESS;//必须返回成功
}
C++驱动程序
#include <ntifs.h>
EXTERN_C VOID DriverUnload(PDRIVER_OBJECT pDriverObject)//由于C++具有名称粉碎机制,因此以C的形式导出,保留名称
{
DbgPrint("Hello C++ DriverUnload\r\n");
}
EXTERN_C NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,PUNICODE_STRING pRegPath)
{
pDriverObject->DriverUnload = DriverUnload;
DbgPrint("Hello C++ DriverEntry\r\n");
return STATUS_SUCCESS;
}
系统调用
大多数的应用程序的函数调用无疑是调用了系统调用(内核函数),而使用系统调用则需要进入内核,进入内核有如下几种方式:
1.int 2E(比较早期)
2.sysenter(x86)
3.syscall(x64)
注意:在进入内核前,通常需要携带参数:系统服务号,有时候也需要携带其他的参数,如进程ID等等。系统服务号用于在内核的SSDT表(系统描述符表)中进行对照寻找系统调用,其充当了索引的作用
在我们日常使用的应用程序,分为x86应用程序和x64应用程序,不同的应用程序有不同的运行环境:x86的应用程序可以在x86操作系统上运行,也在x64操作系统上运行。而x64的应用程序只能在x64的操作系统上运行。
在不同的操作系统上,应用程序的函数调用有不同的情况,接下来我们观察这两种不同情况的应用程序的函数调用流程,也就是观察如何调用系统调用。
现我们写如下程序并生成x86和x64版本的exe
#include<iostream>
#include<Windows.h>
int main()
{
OpenProcess(PROCESS_ALL_ACCESS, FALSE, 1234)
system("pause");
return 0;
}
x86应用程序
我们将上文的exe文件拖入x32dbg中进行观察,接下来我们将通过一步步的步入观察该函数由表入里的调用过程:从函数到系统调用
x86OS
1.我们搜索所有用户模块的跨模块调用找到OpenProcess的调用处。
2.跟进该函数,我们发现程序来到Kernel32.dll模块进行一个跳转
3.程序来到kernelbase.dll模块,调用ZwOpenProcess(同NTOpenProcess)
4.程序来到ntdll.dll模块,调用KiFastSystemCall
5.程序在ntdll.dll模块,调用sysenter进入内核
x64OS
1. 我们搜索所有用户模块的跨模块调用找到OpenProcess的调用处。
2.步入该函数,我们发现程序来到Kernel32.dll模块进行一个跳转
3.程序来到kernelbase.dll模块,调用NTOpenProcess
4.程序来到ntdll.dll模块,调用Wow64Transition
Wow64Transition用于处理64位操作系统下32位程序,实现模拟的32位环境并进行转发,然后调用64位函数进入内核
x64应用程序
1. 我们搜索所有用户模块的跨模块调用找到OpenProcess的调用处。
2.步入该函数,我们发现程序来到Kernel32.dll模块进行一个跳转
3.程序来到kernelbase.dll模块,调用NTOpenProcess
4.程序在ntdll.dll模块,调用syscall或int 2E进入内核
函数地址动态寻找
所谓的壳不过是在目标程序中加一个区段使得程序可以执行我们自己的代码,从而达到加密的效果。但在这个实现的过程中,存在一个问题:我们无法知晓目标程序中是否包含我们自己的代码中所使用的一些函数的相关库。如果没有相关库的话,也就无法在我们的代码中使用相关函数。为保险起见,我们需要自己加载相关的动态链接库。但由于我们并不清楚目标程序中包含了哪些头文件,因此也无法使用LoadLibrary加载动态链接库。因此我们便需要动态寻找函数地址,从而实现加载动态链接库,进而实现我们自己的代码书写
为实现以上操作,有以下两个结构很重要:
1.TEB(线程环境块):描述线程的关键成员
2.PEB(进程环境块):描述进程的关键成员
x86应用程序
x86OS
接下来我们将针对x86程序在x86环境下,通过这两个结构,完成函数地址动态寻找
一.我们通过双机调试寻找Kernel32的地址
1.Windbg中输入命令!Process 0 0:显示简略的系统进程相关信息
此处我们找到了目标程序INSTDRV.EXE
2.输入命令.process /i 切换到目标程序环境下
注意:结束以后需要F5重新加载一下上下文
3.输入命令! process,查看目标程序信息
4.输入命令dt _TEB 0x7ffde00,获取TEB结构,从而获取PEB结构
5.查看PEB结构:获取_PEB_LDR_DATA结构
6.查看_PEB_LDR_DATA结构,其中存在三个重要的双向链表
7._LIST_ENTRY结构记录了双向链表的信息。
如InLoadOrderModuleList的_LIST_ENTRY记录了加载模块列表的相关信息
8.查看InLoadOrderModuleList的第一个_LDR_DATA_TABLE_ENTRY结构,观察第一个进程加载的模块信息
9.查看InLoadOrderModuleList的第二个_LDR_DATA_TABLE_ENTRY结构
10.查看InLoadOrderModuleList的第三个_LDR_DATA_TABLE_ENTRY结构,此处我们找到了Kernel32模块的相关信息
二.在Kernel32的导出表中寻找到GetProcAddress,该函数用于获取动态链接库(DLL)中指定导出函数的地址
三.找到LoadLibrary地址,该函数用于加载动态链接库
四.加载动态链接库
代码实现
#include <iostream>
#include <Windows.h>
DWORD GetKernel32Address() {
DWORD dwKernel32 = 0;
//获取当前线程的TEB指针
_TEB *pTeb = NtCurrentTeb();
//在TEB中找到PEB,于TEB+0x30位置
PDWORD pPeb = (PDWORD)*(PDWORD)((DWORD)pTeb + 0x30);
//PEB中找到PEB_LDR_DATA,于PEB+0xC位置
PDWORD pLdr = (PDWORD)*(PDWORD)((DWORD)pPeb + 0xC);
//PEB_LDR_DATA中找到InLoadOrderModuleList,于PEB_LDR_DATA+0xC的位置
PDWORD InLoadOrderModuleList = (PDWORD)((DWORD)pLdr + 0xC);
//找到exe模块
PDWORD pModuleExe = (PDWORD)*InLoadOrderModuleList;
//找到Ntdll模块
PDWORD pModuleNtdll = (PDWORD)*pModuleExe;
//找到Kernel32模块
PDWORD pModuleKernel32 = (PDWORD)*pModuleNtdll;
//保存Kernel32模块基址
dwKernel32 = pModuleKernel32[4];
//返回Kernel32模块基址
/模块基址也就是模块句柄
return dwKernel32;
}
//获取GetProcAddress函数地址
DWORD GrkGetProcessAddress() {
//获取Kernel32基址
DWORD dwBase = GetKernel32Address();
//获取DOS头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)dwBase;
//获取NT头
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
//获取PE可选头
PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeaders->OptionalHeader;
//获取Kernel32的导出表
PIMAGE_DATA_DIRECTORY pExportDir = &(pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(dwBase + pExportDir->VirtualAddress);
DWORD dwFuncCount = pExport->NumberOfFunctions;
DWORD dwFuncNameCount = pExport->NumberOfNames;
//导出地址表
PDWORD pEAT = (PDWORD)(dwBase + pExport->AddressOfFunctions);
//导出名称表
PDWORD pENT = (PDWORD)(dwBase + pExport->AddressOfNames);
//导出序号表
PWORD pEIT = (PWORD)(dwBase + pExport->AddressOfNameOrdinals);
//遍历导出表,获取我们需要的函数地址
for (size_t i = 0; i < dwFuncCount; i++)
{
if (!pEAT[i])
{
continue;
}
DWORD dwFunAddrOffset = pEAT[i];
for (size_t index = 0; index < dwFuncNameCount; index++)
{
if (pEIT[index] == i)
{
DWORD dwNameOffset = pENT[index];
char * szFuncName = (char *)(((DWORD)dwBase) + dwNameOffset);
if (strcmp(szFuncName,"GetProcAddress") == 0)
{
return dwBase + dwFunAddrOffset;
}
}
}
}
}
EXTERN_C typedef HMODULE
(WINAPI *
fnLoadLibraryA)(
_In_ LPCSTR lpLibFileName
);
EXTERN_C typedef FARPROC
(WINAPI *
fnGetProcAddress)(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
EXTERN_C typedef int
(WINAPI *
fnMessageBoxA)(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType);
EXTERN_C typedef VOID
(WINAPI *
fnExitProcess)(
_In_ UINT uExitCode
);
int main()
{
fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)GrkGetProcessAddress();
//获取Kernel32.dll基址
HMODULE hKernel32 = (HMODULE)GetKernel32Address();
//获取LoadLibraryA地址
fnLoadLibraryA pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress(hKernel32, "LoadLibraryA");
//加载user32.dll
HMODULE hUesr32 = pfnLoadLibraryA("user32.dll");
//获取MessageBoxA地址
fnMessageBoxA pfnMessageBoxA = (fnMessageBoxA)pfnGetProcAddress(hUesr32, "MessageBoxA");
//获取ExitProcess地址,用于退出进程
fnExitProcess pfnExitProcess = (fnExitProcess)pfnGetProcAddress(hKernel32, "ExitProcess");
//调用MessageBoxA
pfnMessageBoxA(NULL, "rkvir", "Msg", MB_OK);
//调用ExitProcess,退出进程
pfnExitProcess(0);
system("pause");
return 0;
}
x64OS
双机调试流程同x86大致一样,只是地址由32位变为64位,以及系统进入内核方式由Kernel32所在库变为wow64库。具体流程此处不再演示。
代码实现同上
x64应用程序
代码实现
此处代码实现与x86的代码实现一致,只需把DWORD改为ULONGLONG即可
#include <iostream>
#include <Windows.h>
ULONGLONG GetKernel32Address()
{
ULONGLONG dwKernel32 = 0;
_TEB *pTeb = NtCurrentTeb();
PULONGLONG pPeb = (PULONGLONG)*(PULONGLONG)((ULONGLONG)pTeb + 0x60);
PULONGLONG pLdr = (PULONGLONG)*(PULONGLONG)((ULONGLONG)pPeb + 0x18);
PULONGLONG InLoadOrderModuleList = (PULONGLONG)((ULONGLONG)pLdr + 0x10);
PULONGLONG pModuleExe = (PULONGLONG)*InLoadOrderModuleList;
PULONGLONG pModuleNtdll = (PULONGLONG)*pModuleExe;
PULONGLONG pModuleKernel32 = (PULONGLONG)*pModuleNtdll;
dwKernel32 = pModuleKernel32[6];
return dwKernel32;
}
ULONGLONG GrkGetProcessAddress()
{
ULONGLONG dwBase = GetKernel32Address();
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)dwBase;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + dwBase);
PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory;
pExportDir = &(pExportDir[IMAGE_DIRECTORY_ENTRY_EXPORT]);
DWORD dwOffset = pExportDir->VirtualAddress;
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(dwBase + dwOffset);
DWORD dwFuncCount = pExport->NumberOfFunctions;
DWORD dwFuncNameCount = pExport->NumberOfNames;
DWORD dwModOffset = pExport->Name;
PDWORD pEAT = (PDWORD)(dwBase + pExport->AddressOfFunctions);
PDWORD pENT = (PDWORD)(dwBase + pExport->AddressOfNames);
PWORD pEIT = (PWORD)(dwBase + pExport->AddressOfNameOrdinals);
for (size_t i = 0; i < dwFuncCount; i++)
{
if (!pEAT[i])
{
continue;
}
DWORD dwOrdinal = pExport->Base + i;
ULONGLONG dwFunAddrOffset = pEAT[i];
for (size_t index = 0; index < dwFuncNameCount; index++)
{
if (pEIT[index] == i)
{
ULONGLONG dwNameOffset = pENT[index];
char * szFuncName = (char *)(((ULONGLONG)dwBase) + dwNameOffset);
if (strcmp(szFuncName,"GetProcAddress") == 0)
{
return dwBase + dwFunAddrOffset;
}
}
}
}
}
EXTERN_C typedef HMODULE
(WINAPI *
fnLoadLibraryA)(
_In_ LPCSTR lpLibFileName
);
EXTERN_C typedef FARPROC
(WINAPI *
fnGetProcAddress)(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
EXTERN_C typedef int
(WINAPI *
fnMessageBoxA)(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType);
EXTERN_C typedef VOID
(WINAPI *
fnExitProcess)(
_In_ UINT uExitCode
);
int main()
{
fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)GrkGetProcessAddress();
HMODULE hKernel32 = (HMODULE)GetKernel32Address();
fnLoadLibraryA pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress(hKernel32, "LoadLibraryA");
HMODULE hUesr32 = pfnLoadLibraryA("user32.dll");
fnMessageBoxA pfnMessageBoxA = (fnMessageBoxA)pfnGetProcAddress(hUesr32, "MessageBoxA");
fnExitProcess pfnExitProcess = (fnExitProcess)pfnGetProcAddress(hKernel32, "ExitProcess");
pfnMessageBoxA(NULL, "rkvir", "Msg", MB_OK);
pfnExitProcess(0);
system("pause");
return 0;
}