Krismile的文章

  • 磁盘盘符隐藏并访问隐藏磁盘的文件数据

    背景之前,帮一个小伙伴开发了一个程序,这个程序就是对磁盘盘符进行隐藏与显示。也就是说,我们打开资源管理器,在资源管理器中隐藏指定磁盘,不显示在界面上。而且,我们还可以使用程序对这个隐藏后的磁盘文件数据进行读写。
    这个程序的实现原理,主要是删除和创建卷加载点实现来实现的。其中,我们给出两种方法来创建隐藏磁盘,分别是使用WIN32 API 函数 DefineDosDeivce 以及 SetVolumeMountPoint 来实现。现在,我就把实现过程整理成文档,分享给大家。
    函数介绍QueryDosDevice 函数
    获取有关MS-DOS设备名称的信息。 该功能可以获得特定MS-DOS设备名称的当前映射。 该功能还可以获取所有现有MS-DOS设备名称的列表。
    函数声明
    DWORD WINAPI QueryDosDevice( _In_opt_ LPCTSTR lpDeviceName, _Out_ LPTSTR lpTargetPath, _In_ DWORD ucchMax);
    参数

    lpDeviceName [in,optional]指定查询目标的MS-DOS设备名称字符串。设备名称不能有尾随的反斜杠;例如,使用“C:”,而不是“C:\”。此参数可以为NULL。在这种情况下,QueryDosDevice功能将将所有现有的MS-DOS设备名称的列表存储到lpTargetPath指向的缓冲区中。lpTargetPath [out]指向将接收查询结果的缓冲区的指针。该函数用一个或多个以null结尾的字符串填充此缓冲区。最后以空值终止的字符串后跟一个额外的NULL。如果lpDeviceName不为NULL,则该函数将检索有关由lpDeviceName指定的特定MS-DOS设备的信息。存储在缓冲区中的第一个以null结尾的字符串是设备的当前映射。其他以null结尾的字符串表示设备的未删除的先前映射。如果lpDeviceName为NULL,则该函数将检索所有现有MS-DOS设备名称的列表。存储在缓冲区中的每个以null结尾的字符串都是现有MS-DOS设备的名称,例如\ Device \ HarddiskVolume1或\ Device \ Floppy0。ucchMax [in]lpTargetPath指向的缓冲区中可以存储的最大TCHAR数。
    返回值

    如果函数成功,则返回值是存储在lpTargetPath指向的缓冲区中的TCHAR数。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。如果缓冲区太小,则该函数失败,最后一个错误代码为ERROR_INSUFFICIENT_BUFFER。

    DefineDosDevice 函数
    定义,重新定义或删除MS-DOS设备名称。
    函数声明
    BOOL WINAPI DefineDosDevice( _In_ DWORD dwFlags, _In_ LPCTSTR lpDeviceName, _In_opt_ LPCTSTR lpTargetPath);
    参数

    dwFlags [in]DefineDosDevice功能的可控方面。 此参数可以是以下值中的一个或多个:



    VALUE
    MEANING




    DDD_EXACT_MATCH_ON_REMOVE
    如果此值与DDD_REMOVE_DEFINITION一起指定,则该函数将使用完全匹配来确定要删除的映射。 使用此值可确保不删除未定义的内容


    DDD_NO_BROADCAST_SYSTEM
    不要广播WM_SETTINGCHANGE消息。 默认情况下,该消息被广播以通知shell和应用程序的更改


    DDD_RAW_TARGET_PATH
    使用lpTargetPath字符串。 否则,它将从MS-DOS路径转换为路径


    DDD_REMOVE_DEFINITION
    删除指定设备的指定定义。 要确定要删除的定义,该函数将会遍历设备的映射列表,查找与此设备关联的每个映射的前缀的lpTargetPath的匹配。 匹配的第一个映射是删除的映射,然后该函数返回。如果lpTargetPath为NULL或指向NULL字符串的指针,则该函数将删除与设备关联的第一个映射,并弹出最近推送的映射。 如果没有什么可以弹出,设备名称将被删除。如果未指定此值,则由lpTargetPath参数指向的字符串将成为此设备的新映射。




    lpDeviceName [in]指向MS-DOS设备名称字符串的指针,指定功能正在定义,重新定义或删除的设备。 设备名称字符串不得有冒号作为最后一个字符,除非正在定义,重新定义或删除驱动器号。 例如,驱动器C将是字符串“C:”。 在任何情况下都不允许使用尾部反斜杠(“\”)。
    lpTargetPath [in]指向将实现此设备的路径字符串的指针。 字符串是一个MS-DOS路径字符串,除非指定了DDD_RAW_TARGET_PATH标志,在这种情况下,此字符串是一个路径字符串。

    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    DeleteVolumeMountPoint 函数
    删除驱动器号或安装的文件夹。
    函数声明
    BOOL WINAPI DeleteVolumeMountPoint( _In_ LPCTSTR lpszVolumeMountPoint);
    参数

    lpszVolumeMountPoint [in]要删除的驱动器号或安装的文件夹。 需要尾随的反斜杠,例如“X:\”或“Y:\ MountX \”。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    GetVolumeNameForVolumeMountPoint 函数
    检索与指定卷装入点(驱动器盘符,卷GUID路径或已装载文件夹)相关联的卷的卷GUID路径。
    函数声明
    BOOL WINAPI GetVolumeNameForVolumeMountPoint( _In_ LPCTSTR lpszVolumeMountPoint, _Out_ LPTSTR lpszVolumeName, _In_ DWORD cchBufferLength);
    参数

    lpszVolumeMountPoint [in]指向包含已安装文件夹路径(例如“Y:\ MountX \”)或驱动器盘符(例如“X:\”)的字符串的指针。 字符串必须以尾部反斜杠(’\’)结尾。lpszVolumeName [out]指向接收卷GUID路径的字符串的指针。 此路径的格式为“\?\ Volume {GUID} \”,其中GUID是用于标识卷的GUID。 如果该卷存在多个卷GUID路径,则仅返回安装管理器缓存中的第一个卷。cchBufferLength [in]输出缓冲区的长度,在TCHAR中。 缓冲区容纳最大容量GUID路径的合理大小为50个字符。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    SetVolumeMountPoint 函数
    将卷与驱动器号或另一卷上的目录相关联。
    函数声明
    BOOL WINAPI SetVolumeMountPoint( _In_ LPCTSTR lpszVolumeMountPoint, _In_ LPCTSTR lpszVolumeName);
    参数

    lpszVolumeMountPoint [in]与卷关联的用户模式路径。 这可能是驱动器号(例如“X:\”)或其他卷上的目录(例如“Y:\ MountX \”)。 字符串必须以尾部反斜杠(’\’)结尾。lpszVolumeName [in]卷的卷GUID路径。 此字符串的格式必须为“\?\ Volume {GUID} \”,其中GUID是用于标识卷的GUID。 “\?\”关闭路径解析,并作为路径的一部分被忽略,如命名卷所述。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。如果lpszVolumeMountPoint参数包含已安装文件夹的路径,即使目录为空,GetLastError返回ERROR_DIR_NOT_EMPTY。

    实现原理在资源管理器中隐藏磁盘显示的原理就是:把磁盘对应的卷加载点删除,这样,磁盘就没有相应的驱动器号了,就不会在资源管理器中显示。对于删除卷加载点,我们可以使用 DeleteVolumeMountPoint 函数实现。
    在资源管理器中还原回隐藏的磁盘,原理就是:重新创建磁盘的卷加载点,并未磁盘分配一个驱动器号。但是,在创建卷加载点之前,也就是在删除卷加载点之前,我们就要使用 GetVolumeNameForVolumeMountPoint 获取卷加载点对应的卷名。因为,当我们使用 SetVolumeMountPoint 函数的时候,需要用到卷名,为相应的卷名创建卷加载点,分配驱动器号。
    创建隐藏盘符,方便我们程序访问的原理是:我们为删除卷加载点的磁盘,分配一个非字母的的驱动器号,这样,磁盘在资源管理器中是不显示的。但是,我们的程序可以通过这个非字母的盘符,正常访问盘符里的文件数据,和正常的字母盘符一样访问。在此,创建一个非字母的磁盘设备,我们可以有两种实现方式,均可以达到上述所说的效果:

    使用 DefineDosDevice 函数来实现,在使用 DefineDosDevice 之前,就需要获取磁盘对应的 Dos 路径,也就是说, 在删除卷加载点之前,先调用 QueryDosDevice 函数获取磁盘对应的 Dos 路径。之后,再使用 DefineDosDevice 函数将 Dos 路径对应的磁盘创建一个非字母驱动器的路径。
    使用 SetVolumeMountPoint 函数来实现,在使用 SetVolumeMountPoint 之前,需要通过 GetVolumeNameForVolumeMountPoint 函数来获取磁盘对应的卷名。后来,我们通过 SetVolumeMountPoint 为卷名对应的磁盘分配一个非字母驱动器号的卷加载点。

    编码实现删除卷加载点,隐藏盘符// 隐藏磁卷加载点, 实现磁盘隐藏BOOL HideValume(char *pszDriver){ BOOL bRet = ::DeleteVolumeMountPoint(pszDriver); if (FALSE == bRet) { ShowError("DeleteVolumeMountPoint"); return FALSE; } return TRUE;}
    获取磁盘对应的卷名// 获取磁盘对应的卷名::GetVolumeNameForVolumeMountPoint("E:\\", szVolumeName, MAX_PATH);
    设置卷加载点,显示磁盘// 显示卷加载点, 恢复磁盘显示BOOL ShowValume(char *pszDriver, char *pszVolumeName){ /* 注意在使用SetVolumeMountPoint的时候,挂载点目录必须存在,而且必须为空目录,否则程序会运行失败 */ while (ERROR_DIR_NOT_EMPTY == ::SetVolumeMountPoint(pszDriver, pszVolumeName)) { // 更改加载盘符 pszDriver[0]++; } return TRUE;}
    使用 DefineDosDevice 创建隐藏磁盘// 创建隐藏盘符BOOL CreateHideVolume(char *lpszDosPath){ // 创建隐藏盘符,对于非字母盘符,在"我的电脑"里是不可见的,只有程序可以访问 if (::DefineDosDevice(DDD_RAW_TARGET_PATH, MY_HIDEN_DRIVER, lpszDosPath)) { return TRUE; } return FALSE;}
    删除隐藏磁盘路径// 删除 1:DeleteHideVolume(szDosPath);// 删除 2:HideValume("2:\\");
    程序测试我们在 main 函数中,调用上述封装好的函数进行测试。首先,我们先获取将要隐藏磁盘对应的卷名以及 Dos 路径。然后,我们开始删除卷加载点,实现磁盘的隐藏。接着,我们使用 DefineDosDeivce 的方法创建一个非字母驱动器路径 1:,并拷贝 520.exe 文件到非字符驱动器路径的根目录下 1:\ ,测试非字母路径能否正常访问。然后,我们使用 SetVolumeMountPoint 创建一个非字母的驱动器 2:\,并拷贝 520.exe 文件到非字符驱动器路径的根目录下 2:\ ,测试非字母路径能否正常访问。最后,我们便删除上述两种方法创建的非字母驱动器号路径,并恢复正确的磁盘路径,显示磁盘。
    int _tmain(int argc, _TCHAR* argv[]){ char szVolumeName[MAX_PATH] = { 0 }; char szDosPath[MAX_PATH] = { 0 }; // 获取磁盘对应的卷名 ::GetVolumeNameForVolumeMountPoint("E:\\", szVolumeName, MAX_PATH); // 获取磁盘路径对应的Dos路径 ::QueryDosDevice("E:", szDosPath, MAX_PATH); // 删除卷加载点来实现磁盘隐藏 HideValume("E:\\"); system("pause"); // 使用 DefineDosDevice 创建一个非字母驱动器号的磁盘路径 1: CreateHideVolume(szDosPath); system("pause"); // 复制文件到隐藏磁盘 if (FALSE == ::CopyFile("520.exe", "1:\\520__111111.exe", FALSE)) { printf("copy file error[%d].\n", ::GetLastError()); } printf("copy file ok.\n"); system("pause"); // 使用 SetVolumeMountPoint 创建一个非字母驱动器号的磁盘路径 2: ShowValume("2:\\", szVolumeName); system("pause"); // 复制文件到隐藏磁盘 if (FALSE == ::CopyFile("520.exe", "2:\\520_22222222.exe", FALSE)) { printf("copy file error[%d].\n", ::GetLastError()); } printf("copy file ok.\n"); system("pause"); // 删除 1: DeleteHideVolume(szDosPath); // 删除 2: HideValume("2:\\"); // 恢复正确磁盘路径 ShowValume("E:\\", szVolumeName); system("pause"); return 0;}
    我们以管理员权限运行程序,测试结果正确:


    总结要注意的是,程序是需要管理员或者管理员以上权限才可以正常执行。同时,也需要理解上述的两种方法实现的对隐藏磁盘数据文件的读写。理解 DefineDosDevice 和 SetVolumeMountPoint 函数的参数含义以及具体的使用方法。
    参考参考自《Windows黑客编程技术详解》一书
    2  留言 2018-12-19 21:17:03
  • 实现32位和64位系统的Inline Hook API

    背景API HOOK技术是一种用于改变API执行结果的技术,Microsoft 自身也在 Windows 操作系统里面使用了这个技术,如Windows兼容模式等。 API HOOK 技术并不是计算机病毒专有技术,但是计算机病毒经常使用这个技术来达到隐藏自己的目的。
    本文就是向大家讲解在 32 位系统和 64 位系统下的 Inline Hook Api 技术的具体实现,现在,我就把实现思路和原理整理成文档,分享给大家。
    实现原理我们程序的 Inline Hook API 实现原理不难理解,核心原理就是获取进程中,指定 API 函数的地址,然后修改该函数入口地址的前几个字节,修改为跳转到我们的新 API 函数,执行我们自己的操作。
    要注意区分 32 位系统和 64 位系统,因为 32 位和 64 位系统的指针长度是不同的,导致地址长度也不同。32 位下用 4 字节表示地址,而 64 位下使用 8 字节来表示地址。
    在 32 位下,汇编跳转语句为:
    jmp _dwNewAddress
    机器码为:
    e9 _dwOffset(跳转偏移)
    其中,要注意理解跳转偏移的计算方法:
    addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即 eip 的值addr2 --> 跳转地址的值,即 _dwNewAddress 的值跳转偏移 _dwOffset = addr2 - addr1
    在 64 位下,汇编跳转语句为:
    mov rax, _dwNewAddress(0x1122334455667788)jmp rax
    机器码为:
    48 b8 _dwNewAddress(0x1122334455667788)ff e0
    所以,32 位下要更改前 5 字节数据; 64 位下要更改前 12 字节数据。
    那么 HOOK 的流程为:

    首先,我们从进程中获取 HOOK API 对应的模块基址,这样,就可以使用 GetProcAddress 函数获取 API 函数的在进程中的地址。
    然后,我们根据 32 位和 64 位版本,计算 HOOK API 函数的前几字节数据。
    接着,修改 API 函数的前几字节数据的页面保护属性,更改为可读、可写、可执行,然后,我们便获取原来前几字节数据后,再写入新的跳转数据。
    最后,还原页面保护属性。

    UNHHOK 的流程基本上和 HOOK 的流程是一样的,只不过这次写入的数据是之前保存的前几字节数据。这样,API函数又恢复正常了。
    编码实现Hook API// Hook ApiBOOL HookApi_MessageBoxA(){ // 获取 user32.dll 模块加载基址 HMODULE hDll = ::GetModuleHandle("user32.dll"); if (NULL == hDll) { return FALSE; } // 获取 MessageBoxA 函数的导出地址 PVOID OldMessageBoxA = ::GetProcAddress(hDll, "MessageBoxA"); if (NULL == OldMessageBoxA) { return FALSE; } // 计算写入的前几字节数据, 32位下5字节, 64位下12字节#ifndef _WIN64 // 32位 // 汇编代码:jmp _dwNewAddress // 机器码位:e9 _dwOffset(跳转偏移) // addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即eip的值 // addr2 --> 跳转地址的值,即_dwNewAddress的值 // 跳转偏移 _dwOffset = addr2 - addr1 BYTE pNewData[5] = {0xe9, 0, 0, 0, 0}; DWORD dwNewDataSize = 5; DWORD dwOffset = 0; // 计算跳转偏移 dwOffset = (DWORD)NewMessageBoxA - ((DWORD)OldMessageBoxA + 5); ::RtlCopyMemory(&pNewData[1], &dwOffset, sizeof(dwOffset));#else // 64位 // 汇编代码:mov rax, _dwNewAddress(0x1122334455667788) // jmp rax // 机器码是: // 48 b8 _dwNewAddress(0x1122334455667788) // ff e0 BYTE pNewData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0}; DWORD dwNewDataSize = 12; ULONGLONG ullNewFuncAddr = (ULONGLONG)NewMessageBoxA; ::RtlCopyMemory(&pNewData[2], &ullNewFuncAddr, sizeof(ullNewFuncAddr));#endif // 设置页面的保护属性为 可读、可写、可执行 DWORD dwOldProtect = 0; ::VirtualProtect(OldMessageBoxA, dwNewDataSize, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 保存原始数据 ::RtlCopyMemory(g_pOldData, OldMessageBoxA, dwNewDataSize); // 开始修改 MessageBoxA 函数的前几字节数据, 实现 Inline Hook API ::RtlCopyMemory(OldMessageBoxA, pNewData, dwNewDataSize); // 还原页面保护属性 ::VirtualProtect(OldMessageBoxA, dwNewDataSize, dwOldProtect, &dwOldProtect); return TRUE;}
    Unhook Api// Unhook ApiBOOL UnhookApi_MessageBoxA(){ // 获取 user32.dll 模块加载基址 HMODULE hDll = ::GetModuleHandle("user32.dll"); if (NULL == hDll) { return FALSE; } // 获取 MessageBoxA 函数的导出地址 PVOID OldMessageBoxA = ::GetProcAddress(hDll, "MessageBoxA"); if (NULL == OldMessageBoxA) { return FALSE; } // 计算写入的前几字节数据, 32位下5字节, 64位下12字节#ifndef _WIN64 DWORD dwNewDataSize = 5;#else DWORD dwNewDataSize = 12;#endif // 设置页面的保护属性为 可读、可写、可执行 DWORD dwOldProtect = 0; ::VirtualProtect(OldMessageBoxA, dwNewDataSize, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 恢复数据 ::RtlCopyMemory(OldMessageBoxA, g_pOldData, dwNewDataSize); // 还原页面保护属性 ::VirtualProtect(OldMessageBoxA, dwNewDataSize, dwOldProtect, &dwOldProtect); return TRUE;}
    新 API 函数// 新的 APIint WINAPI NewMessageBoxA( HWND hWnd, // handle to owner window LPCTSTR lpText, // text in message box LPCTSTR lpCaption, // message box title UINT uType // message box style ){ // Unhook UnhookApi_MessageBoxA(); // 获取 user32.dll 模块加载基址 HMODULE hDll = ::GetModuleHandle("user32.dll"); if (NULL == hDll) { return FALSE; } // 获取 MessageBoxA 函数的导出地址 typedef_MessageBoxA OldMessageBoxA = (typedef_MessageBoxA)::GetProcAddress(hDll, "MessageBoxA"); if (NULL == OldMessageBoxA) { return FALSE; } int iRet = OldMessageBoxA(hWnd, "I am Nobody!!!", "Who Am I", MB_YESNO); // Hook HookApi_MessageBoxA(); return iRet;}
    程序测试我们直接运行一个加载程序,首先,先调用 MessageBoxA 函数,这是还没有执行 API HOOK,正常弹窗:

    然后,使程序加载 InlineHookApi_Test.dll ,这样,DLL 便会在入口点函数 DllMain 中就开始 Hook 进程中的 MessageBoxA 函数。在调用 MessageBoxA 函数进行测试,数据成功更改,HOOK API 成功:

    测试了 32 位版本系统成功,测试了 64 位版本系统也成功。
    总结要注意两个地方:

    一是在修改导出函数地址前几字节数据的时候,建议先对页面属性保护重新设置一下,可以调用 VirtualProtect 函数将页面属性保护设置成 可读、可写、可执行的属性 PAGE_EXECUTE_READWRITE。这样,我们在对这块内存操作的时候,就不会报错。
    二是我们在创建新的API来替代就的API函数的时候,新API函数声明一定要加上 WINAPI(__stdcall)的函数调用约定,否则新函数会默认使用 C 语言的调用约定。否则,会在函数返回,堆栈平衡操作的时候会报错的。

    参考参考自《Windows黑客编程技术详解》一书
    3  留言 2018-12-03 08:51:07
  • 创建系统服务实现开机自启动

    背景想必实现程序开机自启动,是很常见的功能了。无论是恶意程序,还是正常的应用软件,都会提供这个功能,方便用户的使用。程序开机自启动,顾名思义,就是计算机开机后,不用人为地去运行程序,程序就可以自己运行起来。对于这个功能的,一直都是杀软重点监测的地方。因为,对于病毒来说,重要的不是如何被破坏,而是如何启动。
    在过去写的大大小小的程序中,我也实现过程序自启动的功能。现在,我把这些自启动功能的实现方式进行下总结。常见的方式有:修改开机自启动注册表、开机自启动目录、创建开机自启计划任务、创建开机自启系统服务等方式。现在对这些技术一一进行分析,并形成文档分享给大家。本文介绍创建系统服务实现开机自启动的方式,其它的实现方式可以搜索我写的相关系列的文档。
    函数介绍OpenSCManager 函数
    建立了一个到服务控制管理器的连接,并打开指定的数据库。
    函数声明
    SC_HANDLE OpenSCManager( LPCTSTR lpMachineName, // computer name LPCTSTR lpDatabaseName, // SCM database name DWORD dwDesiredAccess // access type);
    参数

    lpMachineName指向零终止字符串,指定目标计算机的名称。如果该指针为NULL ,或者它指向一个空字符串,那么该函数连接到本地计算机上的服务控制管理器。
    lpDatabaseName指向零终止字符串,指定将要打开的服务控制管理数据库的名称。此字符串应被置为 SERVICES_ACTIVE_DATABASE。如果该指针为NULL ,则打开默认的 SERVICES_ACTIVE_DATABASE数据库。
    dwDesiredAccess指定服务访问控制管理器的权限。在授予要求的权限前,系统会检查调用进程的权限令牌,该令牌针对与服务控制管理器相关的安全描述符的权限控制列表。此外,该函数的调用将隐式地指定SC_MANAGER_CONNECT 的访问权限。此外,下列服务控制管理器对象的访问类型可以被指定:




    VALUE
    MEANING




    SC_MANAGER_ALL_ACCESS
    除了所有此表中列出的访问类型,还包括STANDARD_RIGHTS_REQUIRED


    SC_MANAGER_CONNECT
    可以连接到服务控制管理器


    SC_MANAGER_CREATE_SERVICE
    使要求的CreateService函数创建一个服务对象,并将其添加到数


    SC_MANAGER_ENUMERATE_SERVICE
    使要求的EnumServicesStatus功能清单的服务,这是在数据库


    SC_MANAGER_LOCK
    使要求的LockServiceDatabase功能获得锁定数据库


    SC_MANAGER_QUERY_LOCK_STATUS
    使要求的QueryServiceLockStatus检索功能锁定状态信息的数据



    返回值

    如果函数成功,返回值是一个指定的服务控制管理器数据库的句柄。如果函数失败,返回值为NULL 。要获得更详细的错误信息,可以使用GetLastError 获得错误代码。

    CreateService 函数
    创建一个服务对象,并将其添加到指定的服务控制管理器数据库的函数。
    函数声明
    SC_HANDLE CreateService( SC_HANDLE hSCManager, //服务控制管理程序的数据库句柄 LPCTSTR lpServiceName, //以NULL 结尾的服务名 LPCTSTR lpDisplayName, //以NULL 结尾的服务名 DWORD dwDesiredAccess, //指定服务返回类型 DWORD dwServiceType, //指定服务类型 DWORD dwStartType, //指定何时启动服务 DWORD dwErrorControl, //指定服务启动失败的严重程度 LPCTSTR lpBinaryPathName,//指定服务程序二进制文件的路径 LPCTSTR lpLoadOrderGroup,//指定顺序装入的服务组名 LPDWORD lpdwTagId, //忽略,NULL LPCTSTR lpDependencies, //指定启动该服务前必先启动的服务 LPCTSTR lpServiceStartName,//指定服务帐号 LPCTSTR lpPassword //指定对应的口令);
    参数

    hSCManager服务控制管理器数据库的句柄。 此句柄由OpenSCManager函数返回,并且必须 具有 SC_MANAGER_CREATE_SERVICE 的访问权限。 有关更多的信息请参阅Service安全和访问权限。
    lpServiceName要安装该服务的名称。 最大字符串长度为 256 个字符。 服务控制管理器数据库将保留在的字符的大小写,但是服务名称比较总是区分大小写。 正斜杠和一个反斜线不是有效的服务名称字符。
    lpDisplayName对被用户界面程序用来识别服务的显示名称。 此字符串具有最大长度为 256 个字符。 服务控制管理器中的情况下保留名称。 显示名称比较总是不区分大小写。
    dwDesiredAccess对服务的访问。 请求的访问之前,系统将检查调用进程的访问令牌。 一个值列表请参阅服务安全和访问权限。
    dwServiceType服务类型。 此参数可以是下列值之一:




    VALUE
    MEANING




    SERVICE_ADAPTER
    保留


    SERVICE_FILE_SYSTEM_DRIVER
    文件系统驱动服务程序


    SERVICE_KERNEL_DRIVER
    驱动服务程序


    SERVICE_RECOGNIZER_DRIVER
    保留


    SERVICE_WIN32_OWN_PROCESS
    运行于独立进程的服务程序


    SERVICE_WIN32_SHARE_PROCESS
    被多个进程共享的服务程序




    若使用了SERVICE_WIN32_OWN_PROCESS 或 SERVICE_WIN32_SHARE_PROCESS且使用LocalSystem帐号来运行该服务程序,则还可以附加使用下面的值:



    VALUE
    MEANING




    SERVICE_INTERACTIVE_PROCESS
    该服务可以与桌面程序进行交互操作




    dwStartType服务启动选项。此参数可以是下列值之一:



    VALUE
    MEANING




    SERVICE_AUTO_START
    系统启动时由服务控制管理器自动启动该服务程序


    SERVICE_BOOT_START
    用于由系统加载器创建的设备驱动程序, 只能用于驱动服务程序


    SERVICE_DEMAND_START
    由服务控制管理器(SCM)启动的服务


    SERVICE_DISABLED
    表示该服务不可启动


    SERVICE_SYSTEM_START
    用于由IoInitSystem函数创建的设备驱动程序




    dwErrorControl当该启动服务失败时产生错误的严重程度以及采取的保护措施。
    lpBinaryPathName服务程序二进制文件,完全限定路径。 如果路径中包含空格它必须被引用,以便它正确的解析。
    lpLoadOrderGroup在加载顺序此服务所属的组的名称。
    lpdwTagId指向接收 lpLoadOrderGroup 参数中指定的组中唯一的标记值的变量。
    lpDependencies空分隔名称的服务或加载顺序组系统必须在这个服务开始之前的双空终止数组的指针。 如果服务没有任何依赖关系,请指定为 NULL 或空字符串。
    lpServiceStartName该服务应在其下运行的帐户的名称。
    lpPassword由lpServiceStartName参数指定的帐户名的密码。

    返回值

    如果函数成功,返回值将是该服务的句柄。如果函数失败,则返回值为 NULL。 若要扩展的错误了解调用GetLastError。

    OpenService 函数
    打开一个已经存在的服务。
    函数声明
    SC_HANDLE WINAPI OpenService( _In_ SC_HANDLE hSCManager, _In_ LPCTSTR lpServiceName, _In_ DWORD dwDesiredAccess);
    参数

    hSCManager
    SCM数据库句柄。
    lpServiceName
    要打开服务的名字,这和CreateService形参lpServiceName一样,不是服务显示名称。
    dwDesiredAccess
    服务权限。

    返回值

    成功,返回服务句柄;失败返回NULL,可以通过GetLastError获取错误码。

    StartService 函数
    启动服务。
    函数声明
    BOOL WINAPI StartService( _In_ SC_HANDLE hService, _In_ DWORD dwNumServiceArgs, _In_opt_ LPCTSTR *lpServiceArgVectors);
    参数

    hService
    OpenService 或CreateService function返回的服务句柄,需要有SERVICE_START权限。
    dwNumServiceArgs
    下一个形参lpServiceArgVectors的字符串个数,如果lpServiceArgVectors为空,那么该参数设为0。
    lpServiceArgVectors
    传递给服务ServiceMain的参数,如果没有,可以设为NULL;否则,第一个形参lpServiceArgVectors[0]为服务的名字,其他则为需要传入的参数。

    注意

    驱动不接受这些参数,即lpServiceArgVectors为空,dwNumServiceArgs为0。
    返回值

    成功,返回非0;失败,返回0,错误码使用GetLastError获得。

    ControlService函数
    给指定的服务发送一个控制码。
    函数声明
    BOOL WINAPI ControlService( _In_ SC_HANDLE hService, _In_ DWORD dwControl, _Out_ LPSERVICE_STATUS lpServiceStatus);
    参数

    hService
    OpenService 或CreateService function返回的服务句柄;
    dwControl
    要发送的控制码,控制码可以是下列系统定义的控制码;也可以是用户自定义的控制码,范围是128-255,需要有SERVICE_USER_DEFINED_CONTROL权限。
    lpServiceStatus
    返回一个指向存储服务最新状态的结构体ServiceStatus,返回的服务信息来自SCM中最近的服务状态报告。

    返回值

    成功,返回非0;失败,返回0,错误码使用GetLastError获得。

    DeleteService 函数
    从服务控制管理器数据库中删除指定的服务。
    函数声明
    BOOL WINAPI DeleteService( _In_ SC_HANDLE hService);
    参数

    hService
    [输入]服务的句柄,可以由OpenService 或者 CreateService 函数返回,而且该服务必须要有删除权限。

    返回值

    成功,返回非0;失败,返回0,错误码使用GetLastError获得。

    StartServiceCtrlDispatcher 函数
    将服务进程的主线程连接到服务控制管理器,该线程将线程作为调用过程的服务控制分派器线程。
    函数声明
    BOOL WINAPI StartServiceCtrlDispatcher( _In_ const SERVICE_TABLE_ENTRY *lpServiceTable);
    参数

    lpServiceTable
    [输入]指向一系列SERVICE_TABLE_ENTRY结构的指针,其中包含可在调用进程中执行的每个服务的一个条目。 表中最后一个条目的成员必须具有NULL值来指定表的结尾。

    返回值

    成功,返回非0;失败,返回0,错误码使用GetLastError获得。

    RegisterServiceCtrlHandler 函数实现原理创建自启动服务的原理是:

    打开服务控制管理器数据库并获取数据库的句柄

    若要创建服务的操作,则开始创建服务,设置SERVICE_AUTO_START开机自启动;若是其他操作,则打开服务,获取服务句柄
    然后,根据服务句柄,若操作是启动,则使用StartService启动服务;若服务是停止,则使用ControlService停止服务;若操作是删除,则使用DeleteService删除服务
    关闭服务句柄和服务控制管理器数据库句柄

    其中,设置自启动服务启动的程序,并不是普通的程序,而是要求程序要创建了服务入口点函数,否则,不能创建系统服务。创建服务程序的原理是是:
    服务程序主函数(main)调用系统函数StartServiceCtrlDispatcher连接程序主线程到服务控制管理程序(其中定义了服务入口点函数是ServiceMain)。服务控制管理程序启动服务程序后,等待服务程序主函数调用StartServiceCtrlDispatcher。如果没有调用该函数,设置服务入口点,则会报错。
    执行服务初始化任务(同时执行多个服务的服务有多个入口点函数),首先调用RegisterServiceCtrlHandler定义控制处理程序函数(本例中是ServiceCtrlHandle),初始化后通过SetServiceStatus设定进行运行状态,然后运行服务代码。
    控制处理程序(Handler)在服务收到控制请求时由控制分发线程引用(最少要有停止服务的能力)。
    编码实现创建系统服务// 0 加载服务 1 启动服务 2 停止服务 3 删除服务BOOL SystemServiceOperate(char *lpszDriverPath, int iOperateType){ BOOL bRet = TRUE; char szName[MAX_PATH] = { 0 }; ::lstrcpy(szName, lpszDriverPath); // 过滤掉文件目录,获取文件名 ::PathStripPath(szName); SC_HANDLE shOSCM = NULL, shCS = NULL; SERVICE_STATUS ss; DWORD dwErrorCode = 0; BOOL bSuccess = FALSE; // 打开服务控制管理器数据库 shOSCM = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if (!shOSCM) { ShowError("OpenSCManager"); return FALSE; } if (0 != iOperateType) { // 打开一个已经存在的服务 shCS = OpenService(shOSCM, szName, SERVICE_ALL_ACCESS); if (!shCS) { ShowError("OpenService"); ::CloseServiceHandle(shOSCM); shOSCM = NULL; return FALSE; } } switch (iOperateType) { case 0: { // 创建服务 // SERVICE_AUTO_START 随系统自动启动 // SERVICE_DEMAND_START 手动启动 shCS = ::CreateService(shOSCM, szName, szName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, lpszDriverPath, NULL, NULL, NULL, NULL, NULL); if (!shCS) { ShowError("CreateService"); bRet = FALSE; } break; } case 1: { // 启动服务 if (!::StartService(shCS, 0, NULL)) { ShowError("StartService"); bRet = FALSE; } break; } case 2: { // 停止服务 if (!::ControlService(shCS, SERVICE_CONTROL_STOP, &ss)) { ShowError("ControlService"); bRet = FALSE; } break; } case 3: { // 删除服务 if (!::DeleteService(shCS)) { ShowError("DeleteService"); bRet = FALSE; } break; } default: break; } // 关闭句柄 if (shCS) { ::CloseServiceHandle(shCS); shCS = NULL; } if (shOSCM) { ::CloseServiceHandle(shOSCM); shOSCM = NULL; } return bRet;}
    创建服务程序int _tmain(int argc, _TCHAR* argv[]){ // 注册服务入口函数 SERVICE_TABLE_ENTRY stDispatchTable[] = { { g_szServiceName, (LPSERVICE_MAIN_FUNCTION)ServiceMain }, { NULL, NULL } }; ::StartServiceCtrlDispatcher(stDispatchTable); return 0;}
    void __stdcall ServiceMain(DWORD dwArgc, char *lpszArgv){ g_ServiceStatus.dwServiceType = SERVICE_WIN32; g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING; g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; g_ServiceStatus.dwWin32ExitCode = 0; g_ServiceStatus.dwServiceSpecificExitCode = 0; g_ServiceStatus.dwCheckPoint = 0; g_ServiceStatus.dwWaitHint = 0; g_ServiceStatusHandle = ::RegisterServiceCtrlHandler(g_szServiceName, ServiceCtrlHandle); g_ServiceStatus.dwCurrentState = SERVICE_RUNNING; g_ServiceStatus.dwCheckPoint = 0; g_ServiceStatus.dwWaitHint = 0; ::SetServiceStatus(g_ServiceStatusHandle, &g_ServiceStatus); // 自己程序实现部分代码放在这里 // !!注意!! 此处一定要为死循环, 否则在关机再开机的情况(不是点击重启), 不能创建用户进程 while (TRUE) { Sleep(5000); DoTask(); }}
    void __stdcall ServiceCtrlHandle(DWORD dwOperateCode){ switch (dwOperateCode) { case SERVICE_CONTROL_PAUSE: { // 暂停 g_ServiceStatus.dwCurrentState = SERVICE_PAUSED; break; } case SERVICE_CONTROL_CONTINUE: { // 继续 g_ServiceStatus.dwCurrentState = SERVICE_RUNNING; break; } case SERVICE_CONTROL_STOP: { // 停止 g_ServiceStatus.dwWin32ExitCode = 0; g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; g_ServiceStatus.dwCheckPoint = 0; g_ServiceStatus.dwWaitHint = 0; ::SetServiceStatus(g_ServiceStatusHandle, &g_ServiceStatus); break; } case SERVICE_CONTROL_INTERROGATE: { // 询问 break; } default: break; }}
    程序测试在 main 函数中调用上述封装好的函数接口,进行测试。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ BOOL bRet = FALSE; char szFileName[MAX_PATH] = "C:\\Users\\DemonGan\\Desktop\\AutoRun_Service_Test\\Debug\\ServiceTest.exe"; // 创建并启动服务 bRet = SystemServiceOperate(szFileName, 0); if (FALSE == bRet) { printf("Create Error!\n"); } bRet = SystemServiceOperate(szFileName, 1); if (FALSE == bRet) { printf("Start Error!\n"); } printf("Create and Start OK.\n"); system("pause"); // 停止并删除服务 bRet = SystemServiceOperate(szFileName, 2); if (FALSE == bRet) { printf("Stop Error!\n"); } bRet = SystemServiceOperate(szFileName, 3); if (FALSE == bRet) { printf("Delete Error!\n"); } printf("Stop and Delete OK.\n"); system("pause"); return 0;}
    测试结果:
    “以管理员身份运行程序”打开程序,提示系统服务创建并启动成功。

    查看任务管理器,发现“ServiceTest.exe”进程已经启动。

    接着,打开服务管理器,查看服务,“ServiceTest.exe”服务成功创建。

    查看服务属性,路径和启动类型正确。

    执行服务停止删除,程序提示执行成功。检查任务管理器和服务管理器,均为发现“ServiceTest.exe”服务,所以,删除成功。

    总结注意,创建系统服务,要求要有管理员权限。其中,创建系统服务启动的程序,它需要额外创建服务入口点函数ServiceMain,这样才能创建服务成功,否则会报错。
    系统服务是运行在Session 0,系统服务的Session 0隔离,阻断了系统服务和用户桌面进程之间进行交互和通信的桥梁。各个Session之间是相互独立的。在不同Session中运行的实体,相互之间不能发送Windows消息、共享UI元素或者是在没有指定他们有权限访问全局名字空间(并且提供正确的访问控制设置)的情况下,共享核心对象。这样也就是为什么,在系统服务不能显示程序界面,也不能用常规的方式创建有界面的进程。要想创建带界面的进程,要使用另外的方式,我已经写成相应的文档分享给大家了,大家可以搜索我写的相关文档来参考。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-11-24 17:08:53

发送私信

真正的强者不是要压倒一切,而是不被一切压倒

16
文章数
12
评论数
eject