7C00.ME/houmu 2012-09-27

NAutorun研究报告

一、概述

NAutorun是查看Windows系统自启动项的一款系统工具,类似于Sysinternals推出的Autoruns工具。向Autoruns的开发者们致敬!

NAutorun 使用C# 4.0 开发,运行时依赖 .Net Framework (4.0)。部分操作,如读取某些注册表项和读取某些系统文件,需要管理员权限,因此,请以管理员身份运行。启动主程序(NAutorun.exe)即可运行,无需安装。

NAutorun 当前的版本提供对以下类别自启动项的查看功能,如下:

后续版本可能会提供对更多类别的自启动项的查看支持。

程序运行效果如下图:

NAutorun遵循Apache Licence 2.0,同时以源代码和二进制文件的方式发行,允许一定范围内的自由修改和传播。以上内容仅使用于NAutorun本身,不包括.Net Framework等其他非作者创作的内容。

作者保证,NAutorun开发没有使用除.Net Framework类库和Windows API之外的其他类库。对.Net Framework类库和Windows API的使用参考了一些其他作者的文章,但没有对代码段的直接使用或简单复制。请您尊重知识产权。

二、开发说明

1.NAutorun开发环境

操作系统:Windows 7 Ultimate 32bit, with Service Pack 1

开发工具:Microsoft Visual C# 2010 Express Version 10.0 SP1Rel + Microsoft .Net Framework Version 4.0 Rel

2.注意事项

对于源代码的项目文件(.sln或.vcsproj),需要用管理员权限启动 Visual C#(Visual Studio Professional 或更高版本亦可)打开,否则无法正常进行编译。

三、原理说明

1.程序设计结构

这里简单说明一下程序设计结构,便于查看阅读源代码,如下图所示。

MainWindow是NAutorun程序的主要窗口界面,使用WPF编写。界面上提供一个下拉列表控件(ComboBox)选择显示的启动项类别,实际是选择了一个IAutorunReader接口的实例,接口提供了更新 List<AutorunItem> 的方法,更新的 List<AutorunItem> 会更新到主界面上。AutorunItem是对启动项信息的抽象,是一个包含了启动项Entry、ImagePath、Publisher、Description等信息的类。IAutorunReader是读取启动项的接口,不同类别的启动项的读取方法不同,但是通过实现一致的接口,可以在主界面调用的时候屏蔽掉这些差异。这样的设计,也很方便进行程序的拓展。

上面的说明是为了便于查阅源代码,具体在实现会有细微的调整,请查阅源代码。

2.部分技术使用方法和代码片段

这里给出读取自启动项中涉及的部分技术使用方法和代码片段,下文中会用到这些内容,将不再重复。

注册表操作

在NAutorun中的注册表操作,主要都是”读”操作,使用的Microsoft.Win32名字空间下的Registry类,这个类实际是对Win32 API在.NET平台上的一套封装。具体涉及到的”读”操作如下表所示:

操作 相关的方法
打开注册表子键 RegistryKey OpenSubKey(string keyName)
读取所有子键的名字 string[] GetSubKeyNames()
读取所有值的名字 string[] GetValueNames()
读取值 object GetValue()

下面给出如果代码片段说明这些方法的使用(这些代码片段从上下文中摘录,不能独立运行,以示范上面部分方法的使用为主):

片段一,来自ServiceAutorunReader.cs

// ……
string servicesKeyEntry = @”System\CurrentControlSet\Services”;
RegistryKey servicesKey = Registry.LocalMachine.OpenSubKey(servicesKeyEntry);
string[] subKeyNames = servicesKey.GetSubKeyNames();
foreach (string keyName in subKeyNames)
{
	RegistryKey serviceKey = servicesKey.OpenSubKey(keyName);
	// 判断启动类型,3和4不是自启动类型
	object startObject = serviceKey.GetValue(“Start”);
	if (startObject == null) continue;
	string start = startObject.ToString();
	if (start.Equals(“3”) || start.Equals(“4”)) continue;
// ……

片段二,来自KnowDLLsAutorunReader.cs

// ……
string keyName = @”System\CurrentControlSet\Control\Session Manager\KnownDLLs”;
RegistryKey key = Registry.LocalMachine.OpenSubKey(keyName);
if (key == null) return;
string[] valueNames = key.GetValueNames();
string dllDirectory = Environment.GetEnvironmentVariable(“systemroot”) + @”\System32"; // 这里使用简化处理
foreach (string valueName in valueNames)
{
	object valueObject = key.GetValue(valueName);
	if (valueObject == null) continue;
// ……

获取程序文件的版本信息

这里指的是读取exe、dll、sys等类型文件的发布者和描述信息,使用的是System.Diagnostics名字空间下的FileVersionInfo这个类,具体使用可以参照MSDN上的相关文档,使用很简单。下面给出一段代码片段,来自DriversAutorunReader.cs(同样地代码片段的完全理解,需要对照上下文)。

string entry = subKeyName;
string description = ””;
string publisher = ””;
string imagePath = ””;
// 读取ImagePath,并做基本处理
object imagePathValue = serviceKey.GetValue(“ImagePath”);
if (imagePathValue == null) continue;
imagePath = ProcessImagePath(imagePathValue.ToString());
// 如果有注册表“Description”值,就用这个值,
// 需要的话,还要对这个值进行分析,如@%systemroot%\xxx.dll
object descriptionValue = serviceKey.GetValue(“Description”);
if (descriptionValue != null) description = descriptionValue.ToString();
if (System.IO.File.Exists(imagePath))
{
	FileVersionInfo info = FileVersionInfo.GetVersionInfo(imagePath);
	publisher = info.CompanyName;
	description = ProcessDescription(info.FileDescription);
}

读取字符串资源

在Drivers和Service的某些自启动项的Description中会出现”@%systemroot%/xxx.dll,-100”类似的值,这以为着需要从动态链接库中加载字符串资源。为了读取这了资源专门写了一个类,ResReader,并提供了一个静态方法GetString用于读取字符串资源。代码请参考ResReader.cs文件。

3.读自启动项

按照类别说明检查自启动项的方法和实现,分类的标准是参考Autoruns的分类。

Logon

Logon里面的启动项实现自启动的方式主要有两种,一种是通过Windows 启动目录自启动,另一种是基于注册表自启动。

通过Windows 启动目录自启动的位置在Windows 7上包括两个位置,一个位置是 %ALLUSERPROFILE%\microsoft\windows\start menu\programs\startup,另一个位置是 %USERPROFILE%\appdata\roaming\microsoft\windows\start menu\programs\startup

读取这个路径下的快捷方式,从中获取如ImagePath、Publisher、Description等信息,从而读取自启动项信息。由于我的系统中这两项为空白,而实际上使用这种方式实现自启动的程序(无论是恶意程序还是非恶意程序)已经非常少,所以这种方式的自启动项,我暂时略过。

基于注册表实现自启动的位置有25处,在我的系统中有5处,其他几处由于没有可参照的样本,所以暂时略过。对已实现读取的五处位置自启动项的读取,我在每个读取方法的前面都添加了详细的注释说明,如:

private void ReadAutorunItems_24()
{
/*
* location HKCU\Software\Microsoft\Windows\CurrentVersion\Run
*
* method:
*
* 1 打开Run键,名字是HKCU\Software\Microsoft\Windows\CurrentVersion\Run
* 2 读取Run键的所有值的名字,获得值名字字符串数组
* 3 遍历值名字数组(忽略默认值,即空字符串)
*   a 从值名字数组中选择一个值名字 name,name即为Entry
*   b 读取Run键的值,名字是name,值即为ImagePah(需要处理)
*   c 通过ImagePath,获得Description 和 Publisher
*   d 添加新的AutorunItem到List中
*
*/

其他位置的自启动项的发现也是完全基于注册表的,在实现上也仅仅是对注册的读操作以及文件信息查看(FileVersionInfo)、字符串处理等辅助性的操作,实现方法也都是大同小异,所以就不赘述了,可以查看源代码(包括注释)了解代码实现的详细信息。

该类运行效果如下图:

Internet Explorer

关于Internet Explorer的自启动技术,包括BHO、UrlSearchHook、ToolBar、Explorer Bar、Extension等,我实现了其中的4种。实质上他们也都是基于注册表的自启动技术,不同的是注册表的位置不同而已。对自动项的读取以BHO为例,说明如下:

/*
* 读取BHO过程
* 0 打开CLSID键,名字是 HKLM\Software\Classes\CLSID
* 1 打开BHO键,名字是 HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects
* 2 读取BHO键的全部子键名字,获得子键名字字符串数组
* 3 遍历BHO键的子键:
*   a 从子键名字字符串数组中选择一个字符串name
*   b 打开CLSID的子键,名字是name
*   c 读取上述子键的默认值,即为Entry
*   d 打开上述子键的子键,名字是InprocServer32
*   e 读取新打开子键的默认值,即为ImagePath
*   f 通过ImagePath,找到对应的exe或dll,即可获得Description和Publisher
*   g 新建一个AutorunItem的对象,添加到List中
*/

上面的文字是读取方法的注释说明,其他如UrlSearchHook等也可以从相应的方法中找到详细的注释说明,在此不再赘述。

该类运行效果如下图:

Service

Service类型的自启动项技术属于”系统服务自启动”,不过我们仍然是通过读注册表的方法来读取这些注册表项。实现的方法如下:

/*
* 读取Services 方法
*
* 1 打开Service键,名字 HKLM\System\CurrentControlSet\Services
* 2 读取Service键的全部子键名字,获得子键名字字符串数组
* 3 遍历子键名字数组
*   a 从数组中选择一个字符串keyName
*   b 打开Services键的子键,名字是keyName,即为Entry
*   c 读取上述子键的值,名字是Type,判断这个值存在且不为“1”和“2”,否则进入下一遍历项目
*   d 读取上述子键的值,名字是Start,判断这个值存在且不为“3”和“4”,否则进入下一遍历项目
*   e 读取上述子键的值,名字是ImagePath,Description,即为ImagePath和Description
*   f 读取子键的子键,名字是Parameters,如果存在,读取值,名字是ServiceDll,作为ImagePath
*   g 通过ImagePath,获得Publisher,或另一种Description
*   h 向List中添加新的AutorunItem
*/

从上面可以看出主要还是读注册表的操作,需要说明的是:

需要判断注册表子键的两个值一个名字是Type,另一个是Start,这两个值分别表明了服务类型和启动类型。通过查阅《Technical Reference to the Windows 2000 Registry》,可以得到下面两张表,如下。

第一张表,Start值和对应的含义。

Value Meaning
0 Boot: Loaded by kernel loader. Components of the driver stack for the boot (startup) volume must be loaded by the kernel loader.
1 System: Loaded by I/O subsystem. Specifies that the driver is loaded at kernel initialization.
2 Automatic: Loaded by Service Control Manager. Specifies that the service is loaded or started automatically.
3 Manual: Specifies that the service does not start until the user starts it manually, such as by using Device Manager.
4 Disabled: Specifies that the service should not be started.

第二张表,Type值和对应的含义。

Value MeaningH
1 A kernel-mode device driver.
2 A file system driver.
4 A set of arguments for an adapter.
8 A file system driver service, such as a file system recognizer.
16 A Win32 program that runs in a process by itself. This type of Win32 service can be started by the service controller.
32 A Win32 program that shares a process. This type of Win32 service can be started by the service controller.
272 A Win32 program that runs in a process by itself (like Type16) and can interact with users.
288 A Win32 program that shares a process and can interact with users.

从上面两张表可以看出,作为系统常规服务的Service的Type值一般是4或大于4的,否则则是系统驱动服务;Start值是1或2(0一般不存在),否则就不能自启动。

此外,读取Service的详细信息还需要读取Serivice自启动项子键的子键Parameters,从中获取准确的ImagePath信息。这个具有一定的隐蔽性。

该类运行效果如下图:

Drivers

和Services一样,Drivers类别的自启动技术也是”系统服务自启动”,我们读取的方法也是读注册表,甚至,Drivers所在的注册表位置和Service是一样的!应该说Drives,即系统驱动服务,实际也是一种服务,只不过比较特殊罢了。特殊的地方包括,”内核态”“.sys”等,这里不再展开。Drivers读取的方法如下:

/*
* 读取Drivers 方法
*
* 1 打开Service键,名字 HKLM\System\CurrentControlSet\Services
* 2 读取Service键的全部子键名字,获得子键名字字符串数组
* 3 遍历子键名字数组
*   a 从数组中选择一个字符串keyName
*   b 打开Services键的子键,名字是keyName,即为Entry
*   c 读取上述子键的值,名字是Type,判断这个值存在且为“1”,否则进入下一遍历项目
*   d 读取上述子键的值,名字是ImagePath,Description,即为ImagePath和Description
*   f 通过ImagePath,获得Publisher,或另一种Description
*   g 向List中添加新的AutorunItem
*/

这里和Service一样,对Type做了判断,但是没有对Start进行判断,因为,我发现Autoruns对于Start值大于2的也认为是自启动的,这其中或许有某些原因,暂时还不清楚。此外不用读”子键的子键”这种形式了,相比于Service要简单一些。

该类运行效果如下图:

Scheduled Task

Scheduled Task是(除了Logon中的Windows 启动目录自启动外)唯一非”基于注册表自启动”的自启动技术。当然,我把Service和Drivers也归入了基于注册表自启动的范畴,它们对系统配置的更改也是通过修改注册表的。Scheduled Task自启动也可以看作是Windows自动目录自启动技术的变种吧,这里的启动目录是%SystemRoot%\Tasks

和Logon中Startup自动不同,Scheduled Task自启动项必须通过”.job”格式的文件。job文件格式比较特殊,微软给出的官方参考文档是:http://msdn.microsoft.com/en-us/library/cc248285(v=prot.13).aspx

对于我们这次作业来说,了解这些应该就足够了:http://www.krnl.info/thread-3638-1-1.html

下面是我读取Scheduled Task的方法

/*
* 读取方法
*
* 1 读取%systemroot%\Tasks目录下的全部,.job文件
* 2 遍历每个job文件,
*   a 提取job文件名(不含.job),作为Entry
*   b 读取job文件 0x46位置的UInt16,作为长度
*   c 读取job文件 0x48位置的byte[], 按照上面的长度的两倍,转为string即为ImagePath
*   d 通过ImagePath获得Description和Publisher
*/

该类运行效果如下图:

BootExecute

这一类也是基于注册表的自启动,简单的读取注册表就行了,给出方法的描述,其他不详说了。

/*
* 读取Boot Execute自启动项的方法
*
* 1 读取注册表键,名字是 HKLM\System\CurrentControlSet\Control\Session Manager
* 2 读取该键的值,名字是 BootExecute,该值作为Entry
* 3 分析上述值,实际分析 %system%\autochk.exe程序
*
*/

该类运行效果如下图:

KnownDLLs

    和BootExecute一样,也是基于注册表的自启动技术,程序的实现,基本上也都是注册表的读取,我的方法注释讲的应该很详细了,如下:

/*
* 读取KnownDlls 自启动项的方法
*
* 1 打开KnownDLLs键,名字是名字是 HKLM\System\CurrentControlSet\Control\Session Manager\KnownDLLs
* 2 读取KnownDLLs键的值的全部名字,获得值名字数组
* 3 读取KnownDLLs键的值,名字是 DllDirectory,这一步用于确定DLL的查找路径,一般是%systemroot%\system32,
* 这里简化处理,不做这一步,使用常见值
* 4 遍历2中的值名字数组:
*   a 从数组中读取一个名字字符串,作为Entry的值
*   b 读取KnownDLLs键的值,名字是上述字符串,查找路径和值拼接构成 ImagePath
*   c 通过ImagePath查找Exe或DLL,获得 Publisher和Description
*/

该类运行效果如下图:

Winsock Provider

这个技术了解的不多,《Technical Reference to the Windows 2000 Registry》中在这里是没有文档的。只能根据Autoruns给出的信息,自己摸索了,而且Windows 7 和Windows XP可能还是不一样的,所以读取这部分内容可能会有一些不准确的地方。读取方法如下:

/*
* 读取WinSock Provider 自启动项的方法
*
* 1 打开Catalog_Entries键,名字是
* HKLM\System\CurrentControlSet\services\WinSock2\Parameters\Protocol_Catalog9\Catalog_Entries
* 2 读取Catalog_Entries全部子键的名字数组
* 3 遍历上述名字数组
*   a 从数组中选一个字符串,打开以其为名字的Catalog_Entries子键
*   b 读取上述子键的值,名字是ProtocolName,作为Entry
*   c 读取上述子键的值,名字是PackedCatalogItem,从中可以分析出ImagePath(需要进行二进制数据分析)
*   d 通过ImagePath获得Publisher和Description
*/

该类运行效果如下图:

4.以管理员权限运行

在Windows Vista开始引入了UAC机制,对应用程序访问系统资源进行了很多限制,部分系统调用或系统资源的访问需要管理员权限。对于我的这个程序,需要使用管理员权限的地方主要在:(1)读取Service子键的子键,即Parameters键,需要管理员权限;(2)读取部分Job文件时需要管理员权限;(3)分析某些程序的exe或dll时,需要管理员权限。因此为保证程序的正常运行,应该以管理员身份运行。

让程序以管理员身份运行,可以让用户右键选择”以管理员身份运行”,但是不保证用户一定会这么做。比较稳妥的办法是程序启动时就(可以说是带有强制性地)要求用户以管理员身份运行这个程序。具体做法是,

  1. 打开项目的Property,在Security里面去除对”Enable ClickOnce Security settings”的勾选,如下图:

  1. 打开Property文件夹下的app.manifest文档,这时一个XML文档,修改requestExecuteLevel的属性level为”requestAdministrator”,如下所示:

    <!– Run as Administrator old is <requestedExecutionLevel level=”asInvoker” uiAccess=”false” /> –> <requestedExecutionLevel level=“requireAdministrator“ uiAccess=“false“ />

  2. Save All,Rebuild Project即可。

【注意】上面的操作是在Visual C# 2010 Express中进行的,Visual Studio 的其他版本可能有差异,但大同小异。第(3)步必须是用管理员身份运行的Visual Studio(标题栏会出现”Administrator”或”管理员”的字样),否则会出现错误。

四、总结

做完这个作业的一个感受就是Windows自启动管理非常混乱,散布在注册表或系统目录的各个位置,即使有Autoruns这样的工具,也仍然感觉管理不方便。此外,即使是基于注册表的自启动,注册的形式也各不相同,给人以凌乱的感觉。曾经听说Windows 8要取消注册表机制,后来又为了应用程序的兼容性,保留了注册表。在我看来,注册表机制发展到现在确实需要升级一下了。但从自启动这一个方面就可以看出很多。