Drunkmars's Blog

自启动技术详解

字数统计: 2.8k阅读时长: 12 min
2021/10/27

本文将对自启动技术进行分析总结。

注册表

在我们的计算机里面,有一些程序是可以设置成开机自启的,这种程序一般都是采用往注册表里面添加键值指向自己的程序路径来实现开机自启

在windows里面开机自启的注册表路径如下

1
2
3
4
//用户级

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce

image-20211027110759165

1
2
3
4
5
//管理员权限

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run

image-20211027110819564

那么我们知道了计算机系统启动程序时会自动加载注册表里面的路径,那么我们实现的思路就是打开Run这个目录的注册表,然后修改注册表数据这个项目指向的路径即可

User

首先使用RegOpenKeyEx打开Software\\Microsoft\\Windows\\CurrentVersion\\Run这个注册表

1
::RegOpenKeyEx(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hKey)

这里作一个判断,如果不存在这个路径则用RegCreateKeyW这个api创建注册表路径

1
::RegCreateKeyW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", &hKey)

然后使用RegSetValueEx这个api来修改注册表值,完成后关闭句柄即可

1
2
3
::RegSetValueEx(hKey, lpszValueName, 0, REG_SZ, (BYTE*)lpszFileName, (::lstrlenW(lpszFileName) + 16))

::RegCloseKey(hKey)

完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
DWORD REUserRegedit(LPWSTR lpszValueName, LPWSTR lpszFileName)
{
HKEY hKey;

if (ERROR_SUCCESS != ::RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hKey))
{
printf("[!] RegOpenKeyEx not found, try create keyvalue");

if (ERROR_SUCCESS != ::RegCreateKeyW(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", &hKey))
{
printf("[!] RegCreateKeyW failed, error is : %d\n\n", GetLastError());
return FALSE;
}

else
{
printf("[*] RegCreateKeyW successfully!\n\n");
}
}
else
{
printf("[*] RegOpenKeyEx successfully!\n\n");
}

if (ERROR_SUCCESS != ::RegSetValueEx
(hKey, lpszValueName, 0, REG_SZ, (BYTE*)lpszFileName, (::lstrlenW(lpszFileName) + 16)))
{
::RegCloseKey(hKey);

printf("[!] RegSetValueEx failed, error is : %d\n\n", GetLastError());
return FALSE;
}
else
{
printf("[*] RegSetValueEx successfully!\n\n");
}

::RegCloseKey(hKey);
return TRUE;

}

这里我带参数进去调试一下

image-20211027145040752

可以看到注册表已经添加成功,重启之后就会自动去运行这个路径的exe

image-20211027145144308

Administrator

这里我本来以为把RegOpenKeyEx改成HKEY_LOCAL_MACHINE就可以了

image-20211027145542659

但是报错如下,说不存在这个路径,但是我去注册表里面看却是存在这个路径的

image-20211027145703956

image-20211027145806880

这里百度过后发现在64位系统里面关键的注册表被重定位了,修改后的路径为

1
HKEY_LOCAL_MACHINE\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run

修改一下即可,这列重启后计算机就会自动去启动

image-20211027150332742

之前我们提到有一个HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce这个路径,也是能够添加指向程序的路径来实现自启动的,这里跟之前有一点不同的就是这个路径下的key在重启后执行一次就会自动删除,而Run目录下的注册表如果不去管他他会一直存在

快速启动目录

快速启动目录是一种不用修改任何系统数据,并且实现起来最为简单的开机自启动方法。只要把程序放入快速启动文件夹中,系统在启动时就会自动加载并运行相应的程序,实现开机自启动功能。

不过对于不同的计算机启动目录都是不一样的,原因是因为进入Users这个目录之后,账户的名称都不相同,默认情况下快速启动的路径在C:\Users\用户名\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup,如图所示,我们将自己的exe放到这个目录下即可

image-20211027172619144

因为这里每台计算机的目录都不同,所以需要用到api来获取路径,使用到的是SHGetSpecialFolderPath

1
2
3
4
5
BOOL SHGetSpecialFolderPath(
HWND hwndOwner,
LPTSTR lpszPath,
int nFolder,
BOOL fCreate)

首先获取当前计算机的快速启动目录

1
::SHGetSpecialFolderPath(NULL, szStartupPath, CSIDL_STARTUP, TRUE)

使用wsprintf放入缓冲区

1
::wsprintf(szDestFilePath, (LPCWSTR)"%s\\%s", szStartupPath, lpszDestFileName);

然后使用CopyFile拷贝到快速启动路径

1
::CopyFile(lpszSrcFilePath, szDestFilePath, FALSE)::CopyFile(lpszSrcFilePath, szDestFilePath, FALSE)

完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
DWORD AutoSetup(char* lpszSrcFilePath, char* lpszDestFileName)
{
CHAR szStartupPath[MAX_PATH] = { 0 };
CHAR szDestFilePath[MAX_PATH] = { 0 };

if (FALSE == ::SHGetSpecialFolderPathA(NULL, szStartupPath, CSIDL_STARTUP, TRUE))
{
printf("[!] Get szStartupPath failed, error is : %d\n\n", GetLastError());
return FALSE;
}
else
{
printf("[*] The szStartupPath is : %s\n\n", szStartupPath);
}

::wsprintfA(szDestFilePath, "%s\\%s", szStartupPath, lpszDestFileName);

if (FALSE == ::CopyFileA(lpszSrcFilePath, szDestFilePath, FALSE))
{
printf("[!] CopyFile failed, error is : %d\n\n", GetLastError());
return FALSE;
}
else
{
printf("[*] CopyFile successfully!\n\n");
}

return TRUE;
}

实现效果如下,可以看到已经被复制到了启动项,重启后即会自动启动

image-20211027174910826

计划任务

这部分在进行代码实现的过程中会用到COM组件的相关知识,因为知识储备的原因,这里我就不展开细说这部分的具体操作,只能大概说一下实现的流程

使用COM组件之前需要调用CoInitialize来初始化COM接口环境,再使用CoCreateInstance创建服务对象ITaskService并连接到任务服务上,再从ITaskService对象中获取根任务Root Task Folder的指针对象ITaskFolder,指向的是新注册的任务,到这里初始化操作就已经完成,代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
CMyTaskSchedule::CMyTaskSchedule(void)
{
m_lpITS = NULL;
m_lpRootFolder = NULL;
// 初始化COM
HRESULT hr = ::CoInitialize(NULL);
if (FAILED(hr))
{
printf("[!] CoInitialize failed\n\n", hr);
return FALSE;
}
// 创建一个任务服务(Task Service)实例
hr = ::CoCreateInstance(CLSID_TaskScheduler,
NULL,
CLSCTX_INPROC_SERVER,
IID_ITaskService,
(LPVOID*)(&m_lpITS));
if (FAILED(hr))
{
printf("[!] CoCreateInstance failed\n\n", hr);
return FALSE;
}
// 连接到任务服务(Task Service)
hr = m_lpITS->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t());
if (FAILED(hr))
{
printf("[!] ITaskService Connect failed\n\n", hr);
return FALSE;
}
// 获取Root Task Folder的指针,这个指针指向的是新注册的任务
hr = m_lpITS->GetFolder(_bstr_t("\\"), &m_lpRootFolder);
if (FAILED(hr))
{
printf("[!] ITaskService GetFolder failed\n\n", hr);
return FALSE;
}
}

然后是创建计划任务的操作,首先从ITaskService创建一个任务定义对象ITaskDefinition

1
2
ITaskDefinition *pTaskDefinition = NULL;
HRESULT hr = m_lpITS->NewTask(0, &pTaskDefinition);

设置计划任务的注册信息&作者信息

1
2
3
4
5
6
7
8
9
10
11
12
13
IRegistrationInfo *pRegInfo = NULL;
CComVariant variantAuthor(NULL);
variantAuthor = lpszAuthor;
hr = pTaskDefinition->get_RegistrationInfo(&pRegInfo);

if(FAILED(hr))
{
printf("pTaskDefinition::get_RegistrationInfo failed\n\n", hr);
return FALSE;
}

hr = pRegInfo->put_Author(variantAuthor.bstrVal);
pRegInfo->Release();

再设置主题信息,包括登陆类型与运行权限

1
2
3
4
5
6
7
8
9
10
11
12
IPrincipal *pPrincipal = NULL;
hr = pTaskDefinition->get_Principal(&pPrincipal);
if(FAILED(hr))
{
printf("pTaskDefinition->get_Principal failed\n\n", hr);
return FALSE;
}

hr = pPrincipal->put_LogonType(TASK_LOGON_INTERACTIVE_TOKEN);

hr = pPrincipal->put_RunLevel(TASK_RUNLEVEL_HIGHEST);
pPrincipal->Release();

设置其他信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ITaskSettings *pSettting = NULL;
hr = pTaskDefinition->get_Settings(&pSettting);
if(FAILED(hr))
{
printf("pTaskDefinition->get_Settings failed\n\n", hr);
return FALSE;
}

hr = pSettting->put_StopIfGoingOnBatteries(VARIANT_FALSE);
hr = pSettting->put_DisallowStartIfOnBatteries(VARIANT_FALSE);
hr = pSettting->put_AllowDemandStart(VARIANT_TRUE);
hr = pSettting->put_StartWhenAvailable(VARIANT_FALSE);
hr = pSettting->put_MultipleInstances(TASK_INSTANCES_PARALLEL);
pSettting->Release();

创建执行动作,包括设置执行程序的路径和参数

1
2
3
4
5
6
7
8
9
10
11
IActionCollection *pActionCollect = NULL;
hr = pTaskDefinition->get_Actions(&pActionCollect);
if(FAILED(hr))
{
ShowError("pTaskDefinition::get_Actions", hr);
return FALSE;
}
IAction *pAction = NULL;

hr = pActionCollect->Create(TASK_ACTION_EXEC, &pAction);
pActionCollect->Release();

再设置执行程序路径和参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CComVariant variantProgramPath(NULL);
CComVariant variantParameters(NULL);
IExecAction *pExecAction = NULL;
hr = pAction->QueryInterface(IID_IExecAction, (PVOID *)(&pExecAction));
if(FAILED(hr))
{
pAction->Release();
ShowError("IAction::QueryInterface", hr);
return FALSE;
}
pAction->Release();

variantProgramPath = lpszProgramPath;
variantParameters = lpszParameters;
pExecAction->put_Path(variantProgramPath.bstrVal);
pExecAction->put_Arguments(variantParameters.bstrVal);
pExecAction->Release();

创建触发器,实现用户登录的自启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ITriggerCollection *pTriggers = NULL;
hr = pTaskDefinition->get_Triggers(&pTriggers);
if (FAILED(hr))
{
ShowError("pTaskDefinition::get_Triggers", hr);
return FALSE;
}

ITrigger *pTrigger = NULL;
hr = pTriggers->Create(TASK_TRIGGER_LOGON, &pTrigger);
if (FAILED(hr))
{
ShowError("ITriggerCollection::Create", hr);
return FALSE;
}

注册计划任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
IRegisteredTask *pRegisteredTask = NULL;
CComVariant variantTaskName(NULL);
variantTaskName = lpszTaskName;
hr = m_lpRootFolder->RegisterTaskDefinition(variantTaskName.bstrVal,
pTaskDefinition,
TASK_CREATE_OR_UPDATE,
_variant_t(),
_variant_t(),
TASK_LOGON_INTERACTIVE_TOKEN,
_variant_t(""),
&pRegisteredTask);
if(FAILED(hr))
{
pTaskDefinition->Release();
ShowError("ITaskFolder::RegisterTaskDefinition", hr);
return FALSE;
}
pTaskDefinition->Release();
pRegisteredTask->Release();

return TRUE;

这里总代码有点长,就不贴了,然后就是计划任务的删除就简单许多,只需要调用ITaskFolderDeleteTask接口函数即可,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOL CMyTaskSchedule::Delete(char *lpszTaskName)
{
if(NULL == m_lpRootFolder)
{
return FALSE;
}
CComVariant variantTaskName(NULL);
variantTaskName = lpszTaskName;
HRESULT hr = m_lpRootFolder->DeleteTask(variantTaskName.bstrVal, 0);
if(FAILED(hr))
{
return FALSE;
}

return TRUE;
}

这里尝试一下创建名为cs的计划任务成功,如下所示

image-20211027204308855

系统服务

在任务管理器里面可以发现有许多系统服务进程在后台运行,而且有很多应用事随着系统的启动而启动的,如下图所示

image-20211027205446158

系统服务运行在session0,有点基础的同学都有一个0环、3环的概念,在0环里面各个对话是相互独立的,在不同的会话中相互之间是不能够进行通信的,这也是为什么在系统服务中不能显示程序界面的原因

这里使用系统服务创建自启动服务的思路应该是打开句柄判断操作,若为StartService则启动服务,若为ControlService则停止服务,若为DeleteService则删除服务。

服务程序主函数(main)

调用系统函数StartServiceCtrlDispatcher连接程序主线程到服务控制管理程序(其中定义了服务入口点函数是ServiceMain)。服务控制管理程序启动服务程序后,等待服务程序主函数调用StartServiceCtrlDispatcher。如果没有调用该函数,设置服务入口点,则会报错。

执行服务初始化任务(同时执行多个服务的服务有多个入口点函数),首先调用RegisterServiceCtrlHandler定义控制处理程序函数(本例中是ServiceCtrlHandle),初始化后通过SetServiceStatus设定进行运行状态,然后运行服务代码。

控制处理程序(Handler)

在服务收到控制请求时由控制分发线程引用(最少要有停止服务的能力)。

我们首先创建启动系统服务,首先是获取文件名的操作

1
2
::lstrcpyA(szName, lpszDriverPath);
::PathStripPathA(szName);

然后使用OpenSCManager打开服务控制管理器数据库

1
OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

打开数据库后我们打开一个已经存在的服务

1
OpenServiceA(shOSCM, szName, SERVICE_ALL_ACCESS);

这里写一个switch…case…的分支,0为加载服务,1为启动服务,2为停止服务,3为删除服务,这里CreateServiceA的第四个参数有两种方式,一种是SERVICE_AUTO_START开机自启,另外一种是SERVICE_DEMAND_START手动启动

1
2
3
4
5
6
7
8
9
10
11
12
// 创建服务
shCS = ::CreateServiceA(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);

// 启动服务
::StartService(shCS, 0, NULL);

// 停止服务
::ControlService(shCS, SERVICE_CONTROL_STOP, &ss);

// 删除服务
::DeleteService(shCS);

最后关闭句柄

1
2
::CloseServiceHandle(shCS);
::CloseServiceHandle(shOSCM);

我们再进行系统服务程序的编写,自启动服务程序要求程序创建服务入口点函数,否则就不能创建系统服务。

首先我们注册服务入口函数

1
2
SERVICE_TABLE_ENTRY stDispatchTable[] = { { g_szServiceName, (LPSERVICE_MAIN_FUNCTION)ServiceMain }, { NULL, NULL } };
::StartServiceCtrlDispatcher(stDispatchTable);

然后就是ServiceMain入口函数如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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;
}
}

首先编译生成ServiceTest.exe

image-20211027215300006

这里运行主程序之后发现ServiceTest.exe已经启动

image-20211027222051065

打开服务管理,发现ServiceTest.exe已经正常运行

image-20211027222315465

CATALOG
  1. 1. 注册表
  2. 2. 快速启动目录
  3. 3. 计划任务
  4. 4. 系统服务