整理一下 Windows 自启动项的存放位置
Windows 自启动项查询整理
目前有很多产品 都可以获取系统自启动项 如360安全卫士、autoRun、Dism++等等,在使用上各有不同,Windows的自启动项主要有以下几项:
- 自启动文件夹
- 注册表启动项
- 系统服务
- 系统驱动程序
- 任务计划
- 动态链接库
- 映像劫持
本文主要提供一下上述几项的信息和查询方式,另外需要补充的是,对于查询结果处理的准确性可结合多方面进行校验,这边罗列一下可以提供校验的工具:
- 【系统】。Windows提供了可以查看自启动软件(msconfig),服务(services.msc),定时任务的工具(管理工具)的工具,包括注册表也可以用来使用,以上这几个是系统提供的可供查询的路径。
- 【AutoRun】。这是一个软件,可以自行去找相关资源使用,这个软件很强大。可以参考这个去实现查询功能,里边很多设计的点都可以借鉴。
1. 自启动文件夹
自启动文件夹指的是Win系统上有专门的文件夹来存放自启动项,直接通过资源管理器查看即可:
1. %USERPROFILE%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
2. %ProgramData%\Microsoft\Windows\Start Menu\Programs\Startup
直接复制上述路径粘贴到资源管理器即可打开,如下所示两个路径下的应用程序也会是Win系统的启动项
如果是通过C++去查询,则需要使用函数去获取自启动项的绝对路径,使用 ExpandEnvironmentStrings可以获取环境变量所指向的绝对路径。
如果获取的是快捷方式,可以看看我的这篇文章:
2. 通过注册表启动项
注册表作为Windows一个核心,自然也少不了保存相关项来存储自启动程序,注册表下的自启动项路径主要有以下 8 个部分,其中可能有重复项,需要自己去过滤重复项:
1. HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
2. HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
3. HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
4. HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
5. HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
6. HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
7. HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnceEx
8. HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnceEx
此外,64位操作系统中,还存在一个重定向到32位的映射路径,如下:
9. HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run
注册表项相对较多,可以提供一个统一的接口,逐个调用获取每个注册表路径下的启动项就好。
接口的大致过程分为三个步骤:
RegOpenKeyEx()
进入目标路径- 使用
while循环
和RegEnumValue
遍历当前注册表路径- 使用
RegClose
关闭句柄
百度【枚举注册表子项 | RegEnumValue】能查到较多相关demo,既然都写了,那就贡献一个我自己写的接口:
其中获取文件属性的代码我注释掉了,如有需要,请到这里👇获取源码
【WIN】查询文件信息(公司、版本、版权、描述、厂商等)_欧恩意的博客-CSDN博客
typedef struct _AUTO_RUNS_ST
{
WCHAR szName[MAX_PATH]; //软件名称
WCHAR szPath[MAX_PATH]; //软件路径
WCHAR szVersion[MAX_PATH]; //软件版本
WCHAR szCompany[MAX_PATH]; //软件公司
WCHAR szDesp[MAX_PATH * 4]; //软件描述
WCHAR szCmdLine[MAX_PATH]; //启动命令行
}AUTO_RUNS_ST, *AUTO_RUNS_ST;
/*
* [in]Htype: 注册表句柄
* [in]lpSubKey: 子路径 如:“SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run”
* [in]bIs64KEY: 被查询机器是否是64bit
* [out]vecAutoItems: 查询结果的输出结构体
*/
bool EnumAutoRun( __in HKEY Htype,
__in LPCTSTR lpSubKey,
__in bool bIs64KEY,
__out std::vector<AUTO_RUNS_ST> &vecAutoItems )
{
bool bRet = FALSE;
DWORD dwSamDesire = 0;
// 1、自启动项查询
DWORD dwIndex = 0;
HKEY hKey = HKEY_LOCAL_MACHINE;
DWORD dwSizeValueName = MAX_PATH;
DWORD dwSizeofKeyName = MAX_PATH;
BYTE szValueName[MAX_PATH] = { 0 };
TCHAR szKeyName[MAX_PATH] = { 0 };
long nError = 0;
size_t nEnd;
if ( bIs64KEY )
{
dwSamDesire = bIs64KEY ? (KEY_READ | KEY_WOW64_64KEY) : (KEY_READ | KEY_WOW64_32KEY);
}
if (RegOpenKeyExW(Htype, lpSubKey, 0, dwSamDesire, &hKey))
{
printf(_T("EnumAutoRun RegOpenKeyExW Failed!Error code(%d)"), GetLastError());
goto _EXIT_;
}
// 2、遍历:根据得到的软件路径查询相关信息
do
{
dwSizeValueName = MAX_PATH;
dwSizeofKeyName = MAX_PATH;
AUTO_RUNS_ST stuAutoRun;
memset(stuAutoRun, 0, sizeof(stuAutoRun));
nError = RegEnumValueW(hKey, dwIndex++, szKeyName, &dwSizeofKeyName, NULL, &dwType, szValueName, &dwSizeValueName);
if (nError == ERROR_SUCCESS)
{
// 转string->截取字符串 -> 得到程序路径
std::wstring strFilePath = (wchar_t*)szValueName;
// 先处理带‘"’的字符串
int nReplace = strFilePath.find_first_of(L"\"");//here k3=1
while(nReplace != strFilePath.npos) //hint: here "string::npos"means find failed
{
strFilePath = strFilePath.erase(nReplace, 1);
nReplace = (int)strFilePath.find_first_of(L"\"", nReplace+1);
}
// 转小写
transform(strFilePath.begin(),strFilePath.end(),strFilePath.begin(), ::towlower);
nEnd = strFilePath.find(L"exe");
std::wstring wsNewFilePath;
// 2.2 保存 路径
if(nEnd != wstring::npos)
{
wsNewFilePath = strFilePath.substr(0, nEnd + 3);
}
_tcsncpy_s(stuAutoRun.szPath, ArraySize(stuAutoRun.szPath), wsNewFilePath.c_str(), ArraySize(stuAutoRun.szPath) - 1);
printf(_T("EnumAutoRun RegOpenKeyExW:%s->%s!"), szKeyName, wsNewFilePath.c_str());
// 2.3 保存 程序名和启动命令行
_tcsncpy_s(stuAutoRun.szName, ArraySize(stuAutoRun.szName), szKeyName, ArraySize(stuAutoRun.szVersion)-1);
_tcsncpy_s(stuAutoRun.szCmdLine, ArraySize(stuAutoRun.szCmdLine), (wchar_t*)szValueName, ArraySize(stuAutoRun.szVersion)-1);
// 查询对应软件信息,如有需要 以‘///’注释的行取消注释即可
/// std::wstring wsVersion = L" ";
/// std::wstring wsCompany = L" ";
/// std::wstring wsDesp = L" ";
/// FileInfoUtils::GetFileVersion(wsNewFilePath,wsVersion);
/// FileInfoUtils::GetCompanyName(wsNewFilePath,wsCompany);
/// FileInfoUtils::GetFileDescription(wsNewFilePath,wsDesp);
// 过滤Microsoft项,这里是过滤微软的部分启动项,根据公司和版权写的一个hideMicroSoftEntry,可自行实现
/*
if(hideMicroSoftEntry(wsCompany) && wsNewFilePath != L"cmd.exe") // 排除cmd.exe
{
bRet = TRUE;
continue;
}
*/
// 2.4 保存路径信息
/// _tcsncpy_s(stuAutoRun.szVersion, ArraySize(stuAutoRun.szVersion), wsVersion.c_str(), ArraySize(stuAutoRun.szVersion)-1);
/// _tcsncpy_s(stuAutoRun.szCompany, ArraySize(stuAutoRun.szCompany), wsCompany.c_str(), ArraySize(stuAutoRun.szVersion)-1);
/// _tcsncpy_s(stuAutoRun.szDesp, ArraySize(stuAutoRun.szDesp), wsDesp.c_str(), ArraySize(stuAutoRun.szVersion)-1);
wsNewFilePath.clear();
/// if( _tcslen(stuAutoRun.szName) != 0 || _tcslen(stuAutoRun.szCmdLine) != 0)
/// {
/// vecAutoItems.push_back(stuAutoRun);
/// }
}
} while (nError != ERROR_NO_MORE_ITEMS);
bRet = TRUE;
_EXIT_:
return bRet;
}
3. 通过系统服务(services)
Windows系统中,可以使用服务管理器(SCM,Service Control Manager,对应的进程为services.msc)查看、修改系统服务信息(Win+R输入services.msc
打开服务管理器)。如下图所示:
除此之外,在注册表项中也能查看到系统当前的服务有哪些。注册表路径为:HKLM\\System\\CurrentControlSet\\Services
,不但能查看到服务都有哪些,同时也能查看到服务所在路径,启动状态,类型等信息:
相对而言,注册表和SCM所展示的重点不同,注册表下的更详细,SCM更贴近用户使用。
对【自启动服务项】来说,查询方式有两种,一种是根据注册表使用前文所提到的注册表项的方式遍历HKLM\\System\\CurrentControlSet\\Services
注册表路径,另一种方式就是使用 Win API 提供的接口查询系统服务:
第一种方法前文已说过,读者适当修改即可,这里只罗列第二种方法的逻辑:
- 使用注册表
-
特别需要补充的是,在查询注册表项是,会查询到大量的
svchost
的服务项,这一原因主要是因为windows操作系统中,有一类特殊的服务。它们在运行时,任务管理器中不会显示它们的进程信息,真正的进程内容都被保存到了dll文件中,也就是保存到了一个进程的进程空间中去了,而这个dll则是通过svchost.exe
加载的。 -
另外一个需要注意的就是对查询到的键值项
ImagePath
需要进一步处理,因为他可能是文件路径、也可能是环境变量路径、也可能是命令行,我们要做的就是提取其中的路径进而得到争取的进程所在路径。 -
关于
svchost
的相关说明,可以看👇这篇文章。
void queryServices(){
HKEY hkeyOuter=nullptr;
const TCHAR * const rootMenu=_T("SYSTEM\\CurrentControlSet\\Services\\");
long openResult=::RegOpenKeyEx(
HKEY_LOCAL_MACHINE, //根键
rootMenu, // 子键路径
0,
&hkeyOuter
);
int i=0;
// 这里可直接使用前文的 EnumAutoRun
remnant = RegEnumKeyEx(hkeyOuter,i,innerFile,&cbMaxSubKey,NULL,NULL,NULL,NULL); //遍历目标文件夹下的每个子文件夹
while (remnant!=ERROR_NO_MORE_ITEMS) {
if(RegOpenKeyEx(HKEY_LOCAL_MACHINE,ultraPath,0,KEY_READ,&hkeyInner)==ERROR_SUCCESS){
//查询ImagePath
key_log=RegQueryValueEx(hkeyInner,_T("ImagePath"),NULL,&dwType,(BYTE*)&keyData,&dwSize);
//查询启动项优先级
long forStatus=RegQueryValueEx(hkeyInner,_T("Start"),NULL,&dwType2,(BYTE*)&statusData,&dwSize2);
if(key_log==ERROR_SUCCESS&&forStatus==ERROR_SUCCESS){
if(wcsstr(keyData, L".exe") != NULL&&statusData<=2){ //后缀为exe的服务文件,并且是自启动文件
//输出服务的启动优先级、image path等信息
}
}
}
i++;
remnant=RegEnumKeyEx(hkeyOuter,i,innerFile,&cbMaxSubKey,NULL,NULL,NULL,NULL);
}
//回收句柄
RegCloseKey(hkeyOuter);
}
- 使用系统接口
需要调用的 API 是 EnumServicesStatus
,通过dowhile
实现遍历操作,大致操作分为一下三个步骤:
- 使用
OpenSCManage
打开服务管理数据库 - 使用
EnumServicesStatus
遍历当前系统服务 - 使用
CloseServiceHandle
关闭服务管理数据库
相关实现还需读者自行查阅。
4. 通过系统驱动程序
实际上,系统驱动程序类似于WIndows服务程序,并且在注册表中,两者均位于 HKLM\System\CurrentControlSet\Services
注册表路径下,只不过,服务文件的后缀是exe
,而驱动程序的后缀是sys
。 可以看到,注册表中系统驱动的数目是明显多于服务项的。具体的驱动要根据每一个目录下imagepath
的保存项去判断。
其中每一个services
目录下的项目都有一个 start
键值项,该键值项
反应了驱动和服务项的启动级别:
Start数值 | 含义 |
---|---|
0 | 由核心装载器装载,默认随着开机BIOS启动而启动 |
1 | 由I/O子系统装载,跟随操作系统内核初始化而启动 |
2 | 自动启动,在“驱动服务”启动后自动加载 |
3 | 手工启动,在“驱动服务”点击后才加载 |
4 | 禁止启动,相当于禁用 |
由上表可知,数值越小,启动优先级越高;仅当数值不超过2时,才能被认定为“自启动项”,数值过大的系统驱动应当被过滤。
通过以上分析可得,系统服务(services)和系统驱动(drivers)的检测完全相同,只不过查看自启动系统驱动程序需要筛选出后缀为sys的注册条目,并且过滤Start数值大于2的非自启动驱动。除此以外,与“通过系统服务查找自启动项”并无二致。
综上所述,我将“通过系统驱动程序”查找自启动项’的函数命名为viaDrivers,运行逻辑与viaServices基本相同,不再赘述,下面直接给出伪代码:
void queryDrivers(){
HKEY hkeyOuter=nullptr;
const TCHAR * const rootMenu=_T("SYSTEM\\CurrentControlSet\\Services\\");
long openResult=::RegOpenKeyEx( //进入目标文件夹
HKEY_LOCAL_MACHINE, //根键
rootMenu, //子文件夹
0,
&hkeyOuter
);
int i=0;
remnant=RegEnumKeyEx(hkeyOuter,i,innerFile,&cbMaxSubKey,NULL,NULL,NULL,NULL); //遍历目标文件夹下的每个子文件夹
while (remnant!=ERROR_NO_MORE_ITEMS) {
if(RegOpenKeyEx(HKEY_LOCAL_MACHINE,ultraPath,0,KEY_READ,&hkeyInner)==ERROR_SUCCESS){
//查询ImagePath
key_log=RegQueryValueEx(hkeyInner,_T("ImagePath"),NULL,&dwType,(BYTE*)&keyData,&dwSize);
//查询启动项优先级
long forStatus=RegQueryValueEx(hkeyInner,_T("Start"),NULL,&dwType2,(BYTE*)&statusData,&dwSize2);
if(key_log==ERROR_SUCCESS&&forStatus==ERROR_SUCCESS){
if(wcsstr(keyData, L".sys") != NULL&&statusData<=2){ //筛选后缀为sys的系统驱动程序,并且是自启动文件
//输出驱动程序的启动优先级、image path等信息
}
}
}
i++;
remnant=RegEnumKeyEx(hkeyOuter,i,innerFile,&cbMaxSubKey,NULL,NULL,NULL,NULL);
}
//回收句柄
RegCloseKey(hkeyOuter);
}
对比 queryServices
与 queryDrivers
的伪代码,两者除了筛选的文件后缀不同,几乎一模一样,两者的相似性也可见一斑了。实际上,Windows服务与驱动程序的数目极多;如果不仔细寻找,很难发现一些异常的启动项。 当然,关闭的方式也很简单,只需要右键停止服务或驱动即可。但是停止这些服务或驱动可能会造成一些额外的问题(例如停止Apple Mobile Device Service将会导致iCloud应用程序停止工作),所以这一类自启动程序处理起来相对麻烦。
总的来说,相比于普通的Windows应用程序,Windows服务与驱动程序最重要的特征就是运行时间长,且不会显示任何用户UI界面。 一般服务或者驱动都是运行在后台的,并且启用的Windows服务都是从开机开始运行,直到用户关闭该服务或者是关机,才停止运行。因此,Windows服务和驱动是一种长期运行的后台程序,除了一些更新检查、统计数据等,都无需与用户进行交互。
Windows服务和驱动在启用后,将会在开机后被系统自动启动,可以达到自启动的效果。在隐蔽性上,由于都是在后台运行,并且无法弹出任何用户界面(如果尝试显示对话框,则有可能该服务会被终止运行),所以用户如果不查看服务的列表,将很难发现这个自启动程序的存在。
综上所述,通过Windows服务和驱动程序进行自启动,隐蔽性极高。
5. 通过任务计划
任务计划程序是指,操作系统上能够自动执行用户操作的例行任务。 可以通过Win + R 键入 taskschd.msc
可以打开任务计划程序。
要通过C++去查询计划任务,则需要使用COM组件,不懂的同学可以去翻阅《COM原理与应用》这本书。
COM的组件有应用计数的概念,使用步骤比较常规 :
- 调用
CoInitializeSecurity
初始化组件,返回一个句柄- 通过句柄申请COM对象(
IWbemLocator
、IWbemServices
、IWbemClassObject
、IEnumWbemClassObject
等COM对象)- 通过COM对象调用功能接口
- 释放COM接口
COM对象的Release方法
伪代码如下所示:
1. 初始化COM组件
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
2. 为COM组件申请安全令牌
hr = CoInitializeSecurity(
NULL,
-1,
NULL,
NULL,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IMPERSONATE,
NULL,
0,
NULL);
3. 创建COM实例,用于访问任务计划程序 task scheduler,程序如下:
ITaskService *pService = NULL;
hr = CoCreateInstance(CLSID_TaskScheduler,
NULL,
CLSCTX_INPROC_SERVER,
IID_ITaskService,
(void**)&pService);
4. 关联COM对象和任务计划服务
hr = pService->Connect(_variant_t(), _variant_t(),
_variant_t(), _variant_t());
5. 调用COM组件的方法访问注册的所有任务pTaskCollection
IRegisteredTaskCollection* pTaskCollection = NULL;
hr = pRootFolder->GetTasks(NULL, &pTaskCollection);
6. 遍历pTaskCollection中的每个计划任务,并输出其相关信息
for (LONG i = 0; i < numTasks; i++)
{
IRegisteredTask* pRegisteredTask = NULL;
pTaskCollection->get_Item(_variant_t(i + 1), &pRegisteredTask);
BSTR taskName = NULL; //任务名称
pRegisteredTask->get_Name(&taskName);
TASK_STATE taskState; //运行状态
pRegisteredTask->get_State(&taskState);
ITaskDefinition *itaskdef = NULL;
pRegisteredTask->get_Definition(&itaskdef);
IRegistrationInfo *regInfo = NULL;
itaskdef->get_RegistrationInfo(®Info);
BSTR taskInfo = NULL, author = NULL;
regInfo->get_Description(&taskInfo); //任务描述
regInfo->get_Author(&author); //任务发行者
}
以上就是自启动项计划任务项的相关说明和查询步骤。
6. 通过知名动态链接库
关于dll
的相关说明可以自行查阅,在这一部分,我们把dll
可以视为是exe的一个部分来处理。也可以查阅《Windows核心编程》一书中第四部分的内容
动态链接库的危险性在于:它使得进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个 DLL 文件中,该 DLL 包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。DLL 还有助于共享数据和资源。多个应用程序可同时访问内存中单个 DLL 副本的内容。
相应的,木马编写者可以将攻击函数存放在一个dll文件中,而木马的入口exe文件只是调用有威胁的dll文件,本身没有任何的攻击函数,那么木马的exe文件就可能绕过安全工具的检查。由上述分析易知,动态链接库存在巨大隐患,我们需要查看这些dll文件并分析其安全性。
在注册表中搜索关键字KnownDLL
可以找到系统中常用的动态链接库都有哪些,其注册表路径为:HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
在这里,我们同样可以使用前文的EnumAutoRun接口去查询该路径下的内容,稍作改动即可:
void queryKnownDlls(){
HKEY hkey=nullptr;
long openResult=RegOpenKeyExW( //进入注册表文件夹HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
HKEY_LOCAL_MACHINE,
L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs",
0,
KEY_QUERY_VALUE,
&hkey
);
//遍历所有项目该文件下所有注册表值(其实就是dll文件)
long traverseResult=RegEnumValue(
hkey,dwIndex,valueName,
&len,0,&keyType,
keyData,&dataLen
);
while (traverseResult==ERROR_SUCCESS) { //随着index不断增大,我们就能遍历每个项目
//value name 记录项目名称,keydata记录数据,keyType记录类型
dwIndex++;
traverseResult=RegEnumValue(
hkey,dwIndex,valueName,
&len,0,&keyType,
keyData,&dataLen
);
RegCloseKey(hkey);
}
7. 通过劫持劫持
映像劫持(Image Hijack,旧称Image File Execution Options,IFEO),是为一些在默认系统环境中运行时可能引发错误的程序执行体提供特殊的环境设定。由于这个项主要是用来调试程序用的,对一般用户意义不大。默认是只有管理员和local system有权读写修改。
映像劫持同样是存储在注册表中,注册表路径为:HKLM\SOFTWARE\Classes\htmlfile\shell\open\command
,依然是通过注册表查询的方式获取其信息。
void queryImageHijack(){
HKEY hkey=nullptr;
//进入注册表的目标文件夹HKLM\SOFTWARE\Classes\htmlfile\shell\open\command
long openResult=RegOpenKeyExW(
HKEY_LOCAL_MACHINE,
L"SOFTWARE\\Classes\\htmlfile\\shell\\open\\command",
0,
KEY_QUERY_VALUE,
&hkey
);
//遍历所有项目
long traverseResult=RegEnumValue(
hkey,dwIndex,valueName,
&len,0,&keyType,
keyData,&dataLen
);
int row=0;
while (traverseResult==ERROR_SUCCESS) {
//value name 记录项目名称,keydata记录数据,keyType记录类型
dwIndex++;
traverseResult=RegEnumValue( //index不断增加,就能遍历文件夹下的所有项目
hkey,dwIndex,valueName,
&len,0,&keyType,
keyData,&dataLen
);
}
RegCloseKey(hkey);
}