一、内核对象简介
1.1 内核对象有哪些
令牌对象 token、事件对象 Event、文件对象 File、文件映射对象 Mapping_File、线程对象 Thread、时钟对象 Timer、线程池对象 ThreadPool、I/O完成端口对象 Completion port、工作对象 job、邮槽对象 mailslot、互斥对象 Mutex、管道对象 pipe、进程对象 process、信号灯对象 semaphore
1.2 内核对象简介
每个进程创建的内核对象是一个索引,通过这个索引会在每个进程的内核对象表中找到这个内核对象的内存块,同一个内核对象在不同进程中的句柄是不一样的
内核对象用一个句柄来标识。
内核对象的内存块位于操作系统的内核空间,应用程序不能直接操作内核对象,需要系统给定的函数来操作
内核对象的结构:公用部分(安全描述符、计数)和个性部分
1.3 内核对象计数的作用
如果一个内核对象能被多个进程共用。每当一个进程使用这个内核对象的时候,内核对象计数值就加1;每当有一个进程不使用这个内核对象的时候,内核对象计数值就减1。当计数值为0的时候,系统会释放这个内核对象资源
1.4 区分内核对象和用户对象
区分应用层对象和内核对象是看创建对象的函数,如果创建对象的函数有安全描述符,那么这个函数创建的对象就是内核对象
内核对象:
HANDLE CreateFileMapping(
HANDLE hFile, //物理文件句柄
LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
DWORD flProtect, //保护设置
DWORD dwMaximumSizeHigh, //高位文件大小
DWORD dwMaximumSizeLow, //低位文件大小
LPCTSTR lpName //共享内存名称
);
应用层对象:
BOOL CreateBitmap(
int nWidth, // 指定位图的宽度(以像素为单位)
int nHeight, // 指定位图的高度(以像素为单位)
UINT nPlanes, // 指定位图中的颜色平面的数量
UINT nBitcount, // 指定每个显示像素的颜色位的数量
const void* lpBits // 指向包含的出师未土值的字节的数组。如果为NULL,则不初始化新图
);
1.5 继承选项的含义
创建进程内核
BOOL CreateProcess(
LPCTSTR lpApplicationName, // 应用程序名称
LPTSTR lpCommandLine, // 命令行字符串
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程的安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程的安全属性
BOOL bInheritHandles, // 是否继承父进程的属性
DWORD dwCreationFlags, // 创建标志
LPVOID lpEnvironment, // 指向新的环境块的指针
LPCTSTR lpCurrentDirectory, // 指向当前目录名的指针
LPSTARTUPINFO lpStartupInfo, // 传递给新进程的信息
LPPROCESS_INFORMATION lpProcessInformation // 新进程返回的信息
);
如果上面创建进程内核函数中的继承选项传入了TRUE,父进程在创建子进程的时候会把内核对象继承给他。也就是子进程也有父进程的内核对象
1.6 关闭内核对象
CloseHandle(HANDLE);
1.7 内核对象信息获取与修改
1、修改内核对象信息
BOOL SetHandleInformation(
[in] HANDLE hObject,
[in] DWORD dwMask,
[in] DWORD dwFlags
);
[in] dwMask
一个掩码,指定要更改的位标志。 使用 dwFlags 说明中显示的相同常量。
[in] dwFlags
指定对象句柄的属性的位标志集。 此参数可以是 0 或以下一个或多个值。
值 | 含义 |
---|---|
HANDLE_FLAG_INHERIT 0x00000001 | 如果设置了此标志,则使用 CreateProcess 设置为 TRUE 的 bInheritHandles 参数创建的子进程将继承对象句柄。 |
HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002 | 如果设置了此标志,则调用 CloseHandle 函数不会关闭对象句柄。 |
2、获取内核信息
BOOL GetHandleInformation(
[in] HANDLE hObject,
[out] LPDWORD lpdwFlags
);
函数的使用
GetHandleInformation(handle, &dw);
if (dw & HANDLE_FLAG_INHERIT)
{
该对象可以被继承
}
二、防止多开的程序
2.1 命名内核对象
命名内核对象主要是为了进程间共享内核对象。创建内核对象的函数中,如果有pszName参数,说明这个内核对象可以被命名,也就是可以创建命名内核对象。命名内核对象可以在不同进程中使用
2.2 用互斥的内核对象实现防止多开程序
#include <Windows.h>
#include <stdio.h>
#include <fcntl.h>
#include <Windows.h>
#include <tchar.h>
int _tmain()
{
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("huan"));
// 创建了一个名字叫huan的互斥内核对象
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
_tprintf(TEXT("Can't open this program.\n"));
_gettchar();
return 0;
}
DWORD sessionid = 0;
ProcessIdToSessionId(GetProcessId(NULL), &sessionid);
_tprintf(TEXT("%s,\n"), TEXT("this is the first instance! and session id is"));
_gettchar();
return 0;
}
2.3 破解防止软件多开
方法1:反汇编,修改判定 if (GetLastError() == ERROR_ALREADY_EXISTS) 这个判定条件,让这个条件不成立
方法2:使用Process explorer(进程资源管理器)删除内核
官方下载链接:进程资源管理器 - Sysinternals | Microsoft Learn
1、以管理员权限打开软件
2、显示进程内核对象
3、关闭内核对象句柄
注意:我们编写的软件可以轻松列举其他进程用到的所有内核对象,但是想要实现杀死内核对象很困难,首先要注入到所有用到内核对象的程序,然后关闭内核对象,最终杀死内核对象。而微软内部员工有源码,所以编写上面的程序相比比较轻松
三、命名空间
3.1 session会话
windows还没人登录的时候就会创建一个session0会话,windows服务程序在session0中。每当有一个用户登录就会创建一个session
1、查看当前程序运行在哪个session中
获取当前进程句柄,根据句柄获取当前进程id,根据id获取程序运行在哪个会话中,获取成功返回非0
#include <Windows.h>
#include <stdio.h>
#include <fcntl.h>
#include <Windows.h>
#include <tchar.h>
int _tmain()
{
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("huan")); // 创建了一个名字叫huan的互斥内核对象
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
_tprintf(TEXT("Can't open this program.\n"));
_gettchar();
return 0;
}
DWORD sessionid = 0;
DWORD processid = 0;
processid = GetProcessId(GetCurrentProcess());
ProcessIdToSessionId(processid, &sessionid);
if (ProcessIdToSessionId(processid, &sessionid)) // 获取当前程序运行在哪个session会话中
_tprintf(TEXT("%s, %d\n"), TEXT("this is the first instance! and session id is"), sessionid);
_gettchar();
return 0;
}
3.2 会话和命名空间
第1部分 全局命名空间
一个远程桌面的服务可以为自己的命名内核对象设置多个命名空间,这些内核对象包括:event、sernaphores、waitable timers、file-mapping、job object。在client/server类型的应用程序中,服务都有一个全局命名空间global namespace。除此之外,每个客户会话都可以有自己单独的命名空间,来防止自己独有的内核对象。
独立的客户会话命名空间能够让多个客户运行同一个应用程序而不互相干扰。在一个话中使用使用会话名称作为内核对象的缺省命名空间, 另外,进程还可以使用全局命名空间,使用的方法是在内核对象的名字前,加上"Global\" 前缀
通俗说就是程序命名前面不加global前缀,其他的会话就访问不到你会话中的内核对象,全局命名空间内核对象创建方法如下:
CreateEvent(NULL, FALSE, FALSE, "Global\\CSAPP");
第2部分 本地命名空间
如果要在本会话下创建一个命名对象,可以内核对象前加上“Local\”前缀,注意关键字是大小写敏感的。"Session\"前缀被系统保留,不要使用这个前缀来创建命名内核对象
第3部分 私有命名空间
不想让别人访问这个空间
创建步骤
1、创建一个边界标识符BoundaryDescriptor
CreateBoundaryDescriptor
2、获取至少一个SID
CreateWellKnownSid
3、将SID放到边界标识符中,只有具有这个SID的程序,才能打开这个边界标识符;
AddSidToBoundaryDescriptor
// 你可以将这个函数理解为,为边界描述符创建边界。有一个程序想要使用边界描述符,它要打开这个边界描述符的时候,操作系统要检查这个程序的SID(每个程序至少有一个SID,我们不给SID,系统也会给SID。在同一个会话中,系统会给一个固定的SID)。如果程序SID在这个边界描述符里面,程序就能打开这个边界描述符
4、创建一个安全强述符 (允许访问安全隔述符):
ConvertStringSeourityDescriptorToSecurityDescriptor
将一个字符串安全描述符转换成真正的安全描述符,字符串安全描述符是用字符串安全描述符定义语言写出来的。因为私有命名空间是内核对象,创建内核对象的时候都要有一个安全描述符
5、用这个边界相述符和安全描述符,来创建一个私有命名空间。同时给这个私有命名空间一个别名,例如:"ydm"
CreatePrivatecNamespace(Boundary_Handle, &Name_Handle, L"ydm");
OpenPrivateNamespace;{ }
同一个私有命名空间可以有不同的别名,关键要边界描述符句柄(Boundary_Handle)相同,后面都是同一个命名空间
6、要在这个私有命名空间中。创建个内核对象, 例如一个Mutex,那么如下就可以
CreatetMutex(NULL, FALSE, L"ydm\\object.name");
3.3 私有命名空间程序
#include <Windows.h>
#include <tchar.h>
#include <Sddl.h>
#include <strsafe.h>
#include "resource.h"
HANDLE h_Bundary; // 边界描述符句柄
HANDLE h_Namespace; // 私有命名空间句柄
HANDLE hMutex; // 内核对象句柄
BOOL opened = FALSE; // 使用ClosePrivateNamespace函数销毁私有命名空间的时候,需要知道私有命名空间是否是本程序创建的,
// 如果是,那么这个ClosePrivateNamespace函数的第二个参数为1,否则为0
INT_PTR CALLBACK Dlg_pro( // 消息处理函数
__in HWND hwndDlg,
__in UINT uMsg,
__in WPARAM wParam,
__in LPARAM lParam
);
BOOL CheckInstance(HWND); // 检查是不是有实例在运行
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR lpCmdLine, int nCmdShow)
{
DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, Dlg_pro);
if (hMutex)
CloseHandle(hMutex); // 杀死内核对象
if (h_Namespace) // 销毁私有命名空间
{
if(opened)
ClosePrivateNamespace(h_Namespace, 0); // 如果是打开的私有命名空间,ClosePrivateNamespace的第二个参数是0
else
ClosePrivateNamespace(h_Namespace, 1); // 如果是创建的私有命名空间,ClosePrivateNamespace的第二个参数是1
}
if(h_Bundary)
DeleteBoundaryDescriptor(h_Bundary); // 销毁边界描述符
return 0;
}
// 消息处理函数
INT_PTR CALLBACK Dlg_pro(__in HWND hwndDlg,
__in UINT uMsg,
__in WPARAM wParam,
__in LPARAM lParam
)
{
switch (uMsg)
{
case WM_INITDIALOG: // 初始化对话框消息
{
SetDlgItemText(hwndDlg, IDC_STATIC1, L""); // 将文本输入框中的内容情况
CheckInstance(hwndDlg);
}
break;
case WM_COMMAND: // 退出消息
{
if(HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDCANCEL)
EndDialog(hwndDlg, 0);
}
}
return 0;
}
BOOL CheckInstance(HWND hwnd)
{
// 1.创建一个边界描述符
h_Bundary = CreateBoundaryDescriptor(L"ydm", 0);
BYTE LocalAdmiSID[SECURITY_MAX_SID_SIZE] ;
DWORD cbSID = sizeof(LocalAdmiSID);
// 2.创建一个管理员sid
CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, &LocalAdmiSID, &cbSID);
// 3.将管理员SID加入到边界描述符中。此后,只有管理员才能够打开这个边界描述符对于的私有命名空间
AddSIDToBoundaryDescriptor(&h_Bundary, &LocalAdmiSID);
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES) ;
sa.bInheritHandle = FALSE;
// 4.创建了一个安全描述符。
// D:这个代表这个安全描述符中有DACL(访问控制列表)
// A:表示这个安全描述符是一个允许安全描述符,如果是D,那么表示拒绝安全描述符
// GA:表示访问的权限是所有权限
// BA:代表了管理员SID
ConvertStringSecurityDescriptorToSecurityDescriptor(TEXT("D:(A;;GA;;;BA)"), SDDL_REVISION_1, &sa.lpSecurityDescriptor, NULL);
// 5.创建私有命名空间,注意最后一个参数只是这个命名空间的别名
h_Namespace = CreatePrivateNamespace(&sa, h_Bundary, TEXT("ygg")); // 如果这个命名空间以及创建,那么我们打开这个私有命名空间
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
h_Namespace = OpenPrivateNamespace(h_Bundary, TEXT("ygg")); // 打开私有命名空间
opened = TRUE; // 设置打开标志,用于ClosePrivateNamespace函数最后一个参数的设置
}
// ConvertstringsecurityDescriptorTosecurityDescriptor函数。会自动给被创建的安全描述符分配内存
// 当这个安全描述符不再使用时。使用LocalFree函数释放他。
LocalFree(sa.lpSecurityDescriptor);
TCHAR szMutex[64];
// 形成一个L"ygg\\mutex"字符串
StringCchPrintf(szMutex, _countof(szMutex), L"%s\\%s", L"ygg", L"mutex");
// 6.在ygg命名空间中创建一个互斥内核对象
hMutex = CreateMutex(NULL, FALSE, szMutex);
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
SetDlgItemText(hwnd, IDC_STATIC1, L"已经存在一个程序");
}
else
{
SetDlgItemText(hwnd, IDC_STATIC1, L"这是第一个程序实例");
}
return TRUE;
}
四、内核对象的复制
4.1 GetCurrentProcess() 获取到的是伪句柄
GetCurrentProcess() 能获取当前进程id,当前进程句柄永远返回-1,但是我们用句柄的时候不能写(HANDLE)-1,防止微软后面修改了GetCurrentProcess() 的返回值
GetCurrentProcess() 获取伪句柄-1,如何怎么获取真正的句柄呢?
4.2 获取真实的进程句柄
方法1 OpenProcess函数
HANDLE WINAPI OpenProcess(
__in DWORD dwDesiredAccess, // 希望获得的进程内核对象的句柄具有什么样的权限
__in BOOL bInheritHandle, // 这个句柄是否可以被继承
__in DWORD dwProcessId // 要获得进程内核对象句柄的进程ID
);
// 如果成功,返回句柄,否则返回NULL
方法2 DuplicateHandle函数
BOOL WINAPI DuplicateHandle(
__in HANDLE hScourceProcessHandle, // 源进程的进程句柄
__in HANDLE hScourceHandle, // 源内核对象句柄
__in HANDLE hTargetProcessHandle, // 目标进程的进程句柄
__out LPHANDLE lpTargetHandle, // 指向接收目标内核对象句柄内核的指针
__in DWORD dwDesiredAccess, // 访问权限
__in BOOL bInheritHandle, // 是否可以被继承
__in DWORD dwOptions
);
指向同一个内核对象的两个句柄不一样