[译]NT远程和本地组以及用户帐户SID收集器工具
By robot-v1.0
本文链接 https://www.kyfws.com/applications/nt-remote-and-local-group-and-user-account-sid-col-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 14 分钟阅读 - 6855 个词 阅读量 0[译]NT远程和本地组以及用户帐户SID收集器工具
原文地址:https://www.codeproject.com/Articles/1653/NT-Remote-and-Local-Group-and-User-Account-SID-Col
原文作者:Lim Bio Liong
译文由本站 robot-v1.0 翻译
前言
Tool to collect SIDs of Group and User Accounts from a Local or Remote NT Machine and output in an INI file and an XML file.
从本地或远程NT机器收集组和用户帐户的SID并在INI文件和XML文件中输出的工具.
介绍(Introduction)
2000年初的某个时候,我的一个朋友找我写了一个工具,该工具能够收集NT机器(本地或远程)的所有组和用户帐户的所有SID.(Some time at the beginning of 2000, a friend of mine approached me to write a tool that is able to collect all the SIDs of all Group and User Accounts of an NT machine (local or remote).)
他的一名下级人员需要定期收集多台计算机的组和用户帐户SID.稍后,他将需要使用自己的报告生成器编程工具将所有这些信息整理为报告.我提议编写一个具有高级功能的DLL,该DLL封装了LAN Manager NET功能.据我的朋友说,那将是很棒的,但是如果我能简单地编写一个小型应用程序来收集必要的信息并将其存储到报表生成器程序以后可以读取和处理的文件中,那就更好了.当然,我回答.一个可以通过批处理文件启动的简单控制台程序就可以了.现在,您需要哪种格式的文件?他们无所适从.我出于以下原因建议使用Windows INI文件格式:(One of his junior staff had a need to periodically collect these Group and User Account SIDs of several machines. He would later need to collate all these information into a report, using his own report generator programming tool. I offered to write a DLL with high level functions that encapsulate the LAN Manager NET functions. That’ll be great, according to my friend but it’d be better if I could simply write a small application that collects the necessary information and store them into a file that the report generator program could later read and process. Sure, I replied. A simple console program that can be launched via a batch file will do. Now, what format do you need this file to be in? They were at a loss. I suggested the Windows INI file format for the following reasons:)
- 由于我们将收集的信息基本上是字符串(组名,用户帐户名,SID),因此可以毫无问题地将它们存储在INI文件中.(Since the information we will be collecting are basically strings (Group Names, User Account Names, SIDs), these can be stored in an INI file with no problems.)
- 相对简单且健壮的INI文件API的可用性(例如(Availability of relatively simple and robust INI file APIs (e.g.)
GetPrivateProfileString()
).().) 我们都同意,因此我编写了CollectSID控制台应用程序.它使用LAN Manager NET API来探查本地或远程NT计算机的组和用户帐户SID,并将它们全部输出到INI文件中(我将在后面的" INI文件格式"部分中介绍此INI文件的格式).本文).一年后,随着XML的广泛普及,我决定重新访问我编写的该工具,并将其扩展为包括XML文件输出.(We all agreed and so I wrote the CollectSID console application. It uses the LAN Manager NET APIs that probes local or remote NT machines for Group and User Account SIDs and outputs them all into an INI file (I’ll cover the format of this INI file in the section “INI File Format”, later in this article). One year on, and with the widespread popularity of XML, I decided to revisit this tool that I wrote and extend it to include XML file output.)
它是如何工作的 ?(How Does It Work ?)
CollectSID程序源由6个文件组成:(The CollectSID program source is composed of 6 files:)
- 主文件(main.h)-的头文件(- Header file for)main.cpp(main.cpp).(.)
- main.cpp(main.cpp)-主运行模块.(- Main running module.)
- CollectSID.h(CollectSID.h)-的头文件(- Header file for)CollectSID.cpp(CollectSID.cpp).(.)
- CollectSID.cpp(CollectSID.cpp)-包含用于处理本地或远程计算机的组和用户帐户的功能的模块.(- Module that contains functions to process the Group and User Accounts of a local or remote machine.)
- msxmlwrp.h(msxmlwrp.h)-的头文件(- Header file for)msxmlwrp.cpp(msxmlwrp.cpp).我们在那里声明了3个用于XML文件操作的简单类.(. We declare 3 simple classes there for XML file manipulation.)
- msxmlwrp.cpp(msxmlwrp.cpp)-包含封装XML文件创建的类的模块.我们还定义了将INI文件的内容转换为XML文件格式的函数.(- Module that contain classes that encapsulate XML file creation. We also define functions to transform the contents of an INI file into XML file format.)
main.cpp(main.cpp)
- 此处定义的唯一功能是(The only function defined here is)
main()
.(.)main()
将执行通常的命令行处理,然后在(will perform the usual command line processing and then calls on the)CollectGroups()
功能开始滚球.(function to start the ball rolling.) CollectGroups()
以后会打电话(will later call)CollectMembers()
和(and)LookupSID()
做其他必要的工作.(to do other necessary work.)- 如果(If)
CollectGroups()
成功完成,我们呼吁(completed successfully, we call on)ConvertINIToXML()
将输出的INI文件转换为XML格式的文件.(to convert the output INI file into an XML formatted file.)
CollectSID.cpp(CollectSID.cpp)
这里定义了三个功能:(Three functions are defined here:)
CollectGroups()
CollectMembers()
LookupSID()
让我们来看看(Let’s take a look at)CollectGroups()
更详细地.(in more detail.)
// The NetLocalGroupEnum() API retrieves
//information about each local group account
// on a target machine.
NetStatus = NetLocalGroupEnum
(
wszMachineName,
0,
&Data,
8192,
&Index,
&Total,
&ResumeHandle
);
if (NetStatus != NERR_Success || Data == NULL)
{
dwLastError = GetLastError();
bRet = FALSE;
goto CollectGroups_0;
}
// Write down in the output file's
// header how many groups there are.
sprintf (szOutString, "%d", Total);
WritePrivateProfileString ("HEADER",
"GROUP_COUNT", szOutString, lpszOutputFileName);
GroupInfo = (LOCALGROUP_INFO_0 *)Data;
CollectGroups()
用途(uses) NetLocalGroupEnum()
检索有关每个本地组帐户的信息.成功调用此函数后,有关目标计算机的组帐户的信息将存储在(to retrieve information about each local group account. After successfully calling this function, information on the Group Accounts of the target machine are stored in an array of) LOCALGROUP_INFO_0
struct
在中返回的s(s which is returned in the) Data
字节指针.返回的组帐户总数(byte pointer. The total number of Group Accounts is returned in) Total
.(.)
然后,我们将组帐户的总数存储在INI文件中的"(We then store the total number of Group Accounts in the INI file under the section) HEADER
并在键名下(and under the key name) GROUP_COUNT
.稍后将在" INI文件格式"部分中讨论有关INI文件格式的完整信息.(. Full information on the format of the INI file will be discussed in the section “INI File Format”, later.)
然后,我们遍历整个数组(We then iterate through the entire array of) LOCALGROUP_INFO_0
struct
并将每个组帐户的名称存储到目标INI文件中.(s and store the name of each Group Account into the target INI file.)
GroupInfo = (LOCALGROUP_INFO_0 *)Data;
for (i=0; i < Total; i++)
{
// Convert group name from UNICODE to ansi.
iRetOp = WideCharToMultiByte
(
(UINT)CP_ACP, // code page
(DWORD)0, // performance and mapping flags
(LPCWSTR)(GroupInfo->lgrpi0_name),
// address of wide-character string
(int)-1, // number of characters in string
(LPSTR)szAnsiName, // address of buffer for new string
(int)(sizeof(szAnsiName)), // size of buffer
(LPCSTR)NULL, // address of default for unmappable characters
(LPBOOL)NULL // address of flag set when default char used.
);
// Write down the name of each group in the
// HEADER section indexed by the key name GROUP_n
sprintf (szOutString, "GROUP_%d", i + 1);
WritePrivateProfileString ("HEADER", szOutString,
(LPCTSTR)szAnsiName, lpszOutputFileName);
// Find out the SID of the Group.
strcpy (szOutString, ""); // Initialise szOutString
LookupSID ((LPCTSTR)lpszMachineName,
(LPCTSTR)szAnsiName, (LPTSTR)szOutString);
// Write down details of each group in its own
// section named by the group name itself.
WritePrivateProfileString (szAnsiName,
"SID", szOutString, lpszOutputFileName);
// Now lookup all members of this group
// and record down their names and SIDs into
// the output file.
CollectMembers((LPCTSTR)lpszMachineName,
(LPCTSTR)szAnsiName, (LPCTSTR)lpszOutputFileName);
GroupInfo++;
}
使用组帐户的名称,我们进一步调用该功能(Using the name of the Group Account, we further call on the function) LookupSID()
获取此组帐户的SID.(to get the SID of this Group Account.)
现在,一旦我们获得了一个组帐户的SID,我们就立即在INI文件中为该组创建一个部分,并存储一个(Now, once we get the SID of a Group Account, we immediately CREATE a section in the INI file for that Group and also store a) SID
该部分中包含SID值的键.(key in that section containing the SID value.)
例如,一旦我们确定本地计算机具有8个组帐户,并且这些组之一的名称为" Administrators",则INI文件将包含以下信息:(For example, once we have determined that the local machine has got 8 Group Accounts, and that one of these groups has the name “Administrators”, the INI file will contain the following information:)
[HEADER]
GROUP_COUNT=8
GROUP_1=Administrators
...
...
...
[Administrators]
SID=S-1-5-32-544
...
...
...
请注意,(Notice that there will be an entry in the) HEADER
“管理员"和"管理员"部分将有其自己的部分,并带有(section for “Administrators” and “Administrators” will have its own section with a) SID
包含"管理员"的SID值的密钥.(key that contains the SID value for “Administrators”.)
让我们来看看(Let’s take a look at) CollectMembers()
更详细地.(in more detail.)
NetStatus = NetLocalGroupGetMembers
(
wszMachineName,
wszGroupName,
1,
&Data,
8192,
&Index,
&Total,
&ResumeHandle
);
if (NetStatus != NERR_Success || Data == NULL)
{
dwLastError = GetLastError();
bRet = FALSE;
goto CollectMembers_0;
}
// Write down in the output file's section
// for this group the totla nu,ber of
// members it has.
sprintf (szSID, "%d", Total);
WritePrivateProfileString (lpszGroupName,
"MEMBER_COUNT", szSID, lpszOutputFileName);
MemberInfo = (LOCALGROUP_MEMBERS_INFO_1 *)Data;
CollectMembers()
呼吁(calls on the) NetLocalGroupGetMembers()
检索特定组成员列表的API.该API的工作原理类似于(API to retrieve a list of the members of a particular Group. This API works similarly to) NetLocalGroupEnum()
通过将所有检索到的信息存储在数组中.这个数组是一个数组(by storing all retrieved information in an array. This array is an array of) LOCALGROUP_MEMBERS_INFO_1
struct
在中返回的s(s which is returned in the) LPBYTE
Data
.(.)
返回该组中的成员总数(The total number of members in this Group is returned in) Total
.此值存储在(. This value is stored in the) MEMBER_COUNT
组部分的键,该键已在(key of the Group’s section which has already been created in the) CollectGroups()(CollectGroups()) 功能.(function.)
然后,我们遍历此数组(We then iterate through this array of) LOCALGROUP_MEMBERS_INFO_1
struct
并将每个成员名称存储为"组"部分中的键值.密钥本身的形式(s and store each member name as a key value in the Group’s section. The key itself is of the form) MEMBER_n
哪里(where) n
是该组中的唯一编号.(is a unique number within that Group.)
然后我们打电话(We then call) LookupSID()
获取成员的SID字符串值并将此字符串值作为键值存储在"组"部分中.密钥本身就是成员名称.(to get the member’s SID string value and store this string value as a key value in the Group’s section. The key itself is the member name.)
例如,如果我们确定"管理员"组有4个成员,而"域管理员"是其中一个成员,则INI文件将包含以下信息:(For example, if we determined that the Group “Administrators” has got 4 members and that “Domain Admins” is one such member, then the INI file will contain the following information:)
[Administrators]
SID=<SID string for Administrators>
MEMBER_COUNT=4
...
...
...
MEMBER_2=Domain Admins
Domain Admins=<SID string for Domain Admnins>
...
...
...
让我们来看看(Let’s take a look at) LookupSID()
更详细地.(in more detail.)
关于NT安全标识符的主题的完整讨论超出了本文的范围.我将假定读者对SID的一般知识及其组成部分有所了解. Windows NT Security上的许多好书都会对此进行详细介绍,例如Keith Brown(Addison Wesley)撰写的” Programming Windows Security".在系统内部网站上,Mark Russinovich也有一篇很好的文章,标题为(A full discussion on the subject of NT Security Identifiers is beyond the scope of this article. I will assume that the reader has some knowledge on SIDs in general and its components. Many good books on Windows NT Security would cover this in detail, e.g. “Programming Windows Security” by Keith Brown (Addison Wesley). There is also a good article by Mark Russinovich at the Systems Internals Web Site, entitled) Windows NT安全性(Windows NT Security) ,其中包括SID.(, which covers SIDs.)
我将简要介绍NT SID. SID是长度可变的数字值,包含以下内容:(I’ll give a brief overview of an NT SID. A SID is a variable-length numeric value that consists of the following:)
- SID版本号.(SID revision number.)
- 48位标识符授权值.(48-bit identifier authority value.)
- 可变数量的32位子授权或相对标识符(RID)值.(A variable number of 32-bit subauthority or Relative Identifier (RID) values.)
Win32 API提供了SID(The Win32 API provides the SID)
struct
以及ASA(as well as a)PSID
指针,但不鼓励开发人员直接从此类结构中提取值.相反,提供了API以帮助我们从中检索单个值(pointer but does not encourage developers to extract values from such a structure directly. Instead, APIs are provided to help us retrieve individual values from this)struct
.唯一的字段值(. The only field value from this)struct
我直接提取的是(that I directly extract is the)REVISION
值.我没有找到任何可以帮助我从SID确定此值的API.(value. I have not found any API that can help me ascertain this value from a SID.)
标识符授权值标识发布SID的代理.该代理通常是NT本地系统或域.子权限值标识相对于颁发机构的受托人. RID为NT提供了一种从基本SID创建唯一SID的方法.(The identifier authority value identifies the agent that issued the SID. This agent is usually an NT local system or a domain. Subauthority values identify trustees relative to the issuing authority. RIDs provide a way for NT to create unique SIDs from a base SID.)
pSid = (PSID)bySidBuffer;
dwSidSize = sizeof(bySidBuffer);
dwDomainNameSize = sizeof(szDomainName);
bRetOp = LookupAccountName
(
(LPCTSTR)lpszMachineName, // address of string for system name
(LPCTSTR)lpszAccountName, // address of string for account name
(PSID)pSid, // address of security identifier
(LPDWORD)&dwSidSize, // address of size of security identifier
(LPTSTR)szDomainName, // address of string for referenced domain
(LPDWORD)&dwDomainNameSize, // address of size of domain string
(PSID_NAME_USE)&sidType // address of SID-type indicator
);
if (bRetOp == FALSE)
{
dwLastError = GetLastError();
lRet = -1; // Unable to obtain Account SID.
goto LookupSID_0;
}
bRetOp = IsValidSid((PSID)pSid);
if (bRetOp == FALSE)
{
dwLastError = GetLastError();
lRet = -2; // SID returned is invalid.
goto LookupSID_0;
}
LookupSID()
呼吁(calls on) LookupAccountName()
获取帐户的安全标识符(SID)以及在其上找到该帐户的域的名称.此API需要帐户名称和该帐户所在的计算机的名称.(to obtain the security identifier (SID) for an account and the name of the domain on which the account was found. This API requires the name of the account and the name of the machine on which the account exists.)
致电之前(Before calling on) LookupAccountName()
,我们首先声明一个指向SID的指针((, we first declare a pointer to an SID () pSid
(类型((type) PSID
))和一个()) and a) BYTE
缓冲 ((buffer () bySidBuffer
).这个(). This) BYTE
缓冲(buffer) bySidBuffer
之后将保存完整的SID数据(will hold full SID data after) LookupAccountName()
返回.(returns.)
尽管返回了域名和SID类型,但是我处理的项目没有使用这些值,因此我忽略了它们.如果读者修改该程序的源代码以输出这些值也将是很好的.成功致电后(Although the name of the domain and the SID type are returned, the project that I worked on did not have use for these values and so I ignored them. It will be good if the reader modifies the source of this program to output these values too. After successfully calling) LookupAccountName()
, 我们称之为(, we call) IsValidSid()
进一步验证返回的SID.(to further validate the returned SID.)
// We initialise our SID string with the current standard for SID strings.
// The "S" is the standard prefix.
// We extract the revision number
// directly from the SID struct itself.
// This revision number is the only field
// value from SID that we extract directly.
// The other field values must be enquired via APIs.
sprintf (szSID, "S-%d", (((SID*)pSid) -> Revision));
然后,我开始形成SID的字符串版本.字符串SID以标准前缀" S"开头,连字符将其各个组成部分分开.在" S"之后,存储修订号.我们直接从SID获取此修订号(I then begin forming a string version of the SID. A string SID starts with a standard prefix of “S” and hyphens separate its various components. After the “S”, the revision number is stored. We take this revision number directly from the SID) struct
还给我们.(returned to us.)
// Obtain via APIs the identifier authority value.
psid_identifier_authority = GetSidIdentifierAuthority ((PSID)pSid);
// Make a copy of it.
memcpy (&sid_identifier_authority,
psid_identifier_authority, sizeof(SID_IDENTIFIER_AUTHORITY));
// The value in IDENTIFIER AUTHORITY is an array of 6 bytes.
// However, we are only interested in the last byte of the array.
sprintf (szIdentAuthValue, "-%d", (sid_identifier_authority.Value)[5]);
strcat (szSID, szIdentAuthValue);
接下来将插入标识符授权值,我们使用(The identifier authority value is to be inserted next and we use the) GetSidIdentifierAuthority()
获得指向(to obtain a pointer to the) SID_IDENTIFIER_AUTHORITY
SID中包含的结构.该结构包含48位标识符授权值.此48位数字分为6个字节.当前,只有最后一个字节才有意义,因为前5个字节始终为0.(structure contained inside our SID. This structure contains the 48-bit identifier authority value. This 48-bit number is divided into 6 bytes. Currently, only the last byte is of any importance because the first 5 bytes are always 0.)
接下来,我们通过调用API获得与此SID相关联的子权限值的计数(We next obtain the count of subauthority values associated with this SID by calling on API) GetSidSubAuthorityCount()
.然后,我们执行一个循环,该循环通过SID从SID获取子权限值.(. We then perform a loop that obtains the subauthority values from the SID via the) GetSidSubAuthority()
API.每个检索到的子权限值都将附加到我们的SID字符串中.(API. Each retrieved subauthority value is appended into our SID string.)
// Determine how many sub-authority values there are in the current SID.
puchar_SubAuthCount = (PUCHAR)GetSidSubAuthorityCount((PSID)pSid);
// Assign it to a more convenient variable.
j = (unsigned char)(*puchar_SubAuthCount);
// Now obtain all the sub-authority values from the current SID.
for (i = 0; i < (int)j; i++)
{
DWORD dwSubAuth = 0;
PDWORD pdwSubAuth = NULL;
char szSubAuthValue[80];
// Obtain the current sub-authority
// DWORD (referenced by a pointer)
pdwSubAuth = (PDWORD)GetSidSubAuthority
(
(PSID)pSid, // address of security identifier to query
(DWORD)i // index of subauthority to retrieve
);
dwSubAuth = *pdwSubAuth;
sprintf (szSubAuthValue, "-%d", dwSubAuth);
strcat (szSID, szSubAuthValue);
}
msxmlwrp.cpp(msxmlwrp.cpp)
此源文件包含3个基本类的定义(This source file contains definitions for 3 basic classes) CDOM
,(,) CMSXML_DOMDocument
和(and) CMSXML_Element
.这些类封装了编写XML文件的基本功能.没有提供XML文件读取处理功能.我们只是想输出一个XML文件.(. These classes encapsulate basic functionalities to write an XML file. No XML file read processing functionalities are provided. We simply want to output an XML file.)
由于具有广泛的可用性,因此可以通过MSXML完成XML文件处理.为简单起见,我使用了(The XML file processing is done via MSXML due to its widespread availability. For simplicity, I have used the) IDispatch
接口(OLE自动化),用于与MSXML对话,并避免导入MSXML的类型库.(interface (OLE Automation) for talking to MSXML and avoided importing MSXML’s type library.)
感兴趣的主要功能(The main function that is of interest in)*msxmlwrp.cpp(msxmlwrp.cpp)*是(is) ConvertINIToXML()
它接受输出的INI文件,对其进行处理,并为其创建等效的XML文件.此功能还应作为有关如何处理INI文件的示例.(which takes the outputted INI file, processes it and creates an XML file equivalent for it. This function should also serve as an example on how to process the INI file.)
INI文件格式(INI File Format)
从该程序输出的INI文件将始终具有固定的(The INI file that is output from this program will always have a fixed) HEADER
部分.此部分将始终包含固定内容(section. This section will always contain a fixed) GROUP_COUNT
键:(key:)
[HEADER]
GROUP_COUNT=n
GROUP_1=...
GROUP_2=...
GROUP_3=...
GROUP_n=...
的关键值(The key value for) GROUP_COUNT
是一个整数,指示此INI文件中有多少个组.会有一个(is an integer that indicates how many groups there are in this INI file. There will be a) GROUP_n
每个定义的组的键名称,其中(key name for each group defined, where) n
是从1到1的数字(is a number from 1 through) GROUP_COUNT
.这些键的对应值将是每个组的名称.(. The corresponding values for these keys will be the name of each group.)
然后,INI文件将为每个组包含一个单独的部分.(The INI file will then contain an individual section for every group.)
[{GROUP NAME}]
SID=...
MEMBER_COUNT=n
MEMBER_1={Member 1 name}
{Member 1 name}=...
{Member 2 name}=...
...
...
...
{Member n name}=...
此部分将始终包含固定内容(This section will always contain a fixed) SID
密钥,其中将包含该组的SID值和一个固定值(key which will contain the SID value for the group and a fixed) MEMBER_COUNT
包含该组中成员总数的键.(key that will contain the total number of members contained in this group.)
会有一个(There will be a) MEMBER_n
该组中每个成员的密钥名称,其中(key name for each member in this group where) n
是从1到1的数字(is a number from 1 through) MEMBER_COUNT
.这些键的对应值将是成员的名称.(. The corresponding values for these keys will be the name of the member.)
每个成员在此部分中还将具有一个密钥(密钥名称本身就是成员名称),值是该成员的SID.(Each member will also have a key in this section (the key name is the member name itself) and the value is the SID of the member.)
例如,假设在本地计算机中定义了3个组.这些组是"管理员",“备份操作员"和"来宾”.以下是INI文件中的示例输出:(For example, let’s say there are 3 groups defined in a local machine. The groups are “Administrators”, “Backup Operators” and “Guests”. The following is a sample output in the INI file:)
[HEADER]
GROUP_COUNT=3
GROUP_1=Administrators
GROUP_2=Backup Operators
GROUP_3=Guests
[Administrators]
SID=...
MEMBER_COUNT=1
MEMBER_1=Administrator
Administrator=...
<Backup Operators>
SID=...
MEMBER_COUNT=0
[Guests]
SID=S-1-5-32-546
MEMBER_COUNT=1
MEMBER_1=Guest
Guest=...
局限性(Limitations)
- 只有Administrators或Account Operators本地组的成员才能成功使用此工具.这是因为LAN Manager NT API否则将不会成功.(Only members of the Administrators or Account Operators local group can successfully use this tool. This is because the LAN Manager NT APIs will not succeed otherwise.)
- 因为(Because)
CollectSID
使用标准的Windows API(uses the standard Windows API)WritePrivateProfielString()
,如果您未指定输出文件的完整路径,则生成的INI文件将被写入Windows目录.但是,生成的XML文件将输出到活动目录.当心这一事实,这可能会引起混乱.为了安全起见,请始终指定完整路径.(, if you do not specify a full path to your output file, the generated INI file will be written to the Windows directory. However, the generated XML file will be output to the active directory. Beware of this fact which may raise confusion. To be safe, always specify a full path.) - 也因为(Also because)
WritePrivateProfileString()
使用,如果已经存在与目标文件同名的现有INI文件,则不会首先删除该现有文件.其内容将被附加.请注意这一事实,由于INI文件中包含无关的信息,这也可能引起混乱.(is used, if an existing INI file with the same name as your target file already exists, this existing file will not first be deleted. Its contents will be appended. Be aware of this fact which may raise confusion as well due to irrelevant information being included in the INI file.)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C++ VC6 Windows Visual-Studio Dev 新闻 翻译