Introduction

This article describes coding a C++ Win32 based utility application that allows the user to use point and click interface to add process source file names (the path and name of the executable) to the process exclusion list of Microsoft Security Essentials.

Please excuse the fact that the code is far from commercial quality. This was a two or three day hobby exercise for my personal amusement but is a fairly well tested effort so it should work correctly. Much of the code was copied from third parties (most if not all of the drag and drop source is based on the MSDN Magazine, October 2004 drag and drop code written by Paul DiLascia: Create Client Windows, Drag and Drop Between Listboxes) with the bulk of it based on examples from MSDN.

This program was written in C++ with Win32 libraries in Microsoft Visual Studio 8 and has been tested using Windows XP Professional and Windows 7 Ultimate.

image004.jpg

The complete source code is included. The download link is at the top of the article.

Background

Microsoft Security Essentials (MSE) is a free and remarkably good malware protection system. However, the user interface is a little primitive, particularly the Excluded Processes interface that makes the user search for each individual process executable file when attempting to make MSE more efficient by adding processes to ignore into the registry location:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft Antimalware\Exclusions 

The MSEPX (Microsoft Security Essentials Process Exclusion) utility which will be described here allows the user to select from a list of currently running process source file paths and add them to the MSE exclusion list using point and click and drag and drop techniques.

Enumerating processes and changing registry values is not complex, but security entries in the registry can have restricted permissions where the average user and even administrators can only read the values. Only the system user has complete access.

In order to become the system user, XP had security holes that could be exploited. The scheduled task trick: at 13:55 /interactive “cmd.exe” then killing and restarting explorer and the service control command launch method: sc create testsvc binpath= "cmd /K start" type= own type= interact could give you system access to do as you wished. These are clever but a little inelegant as a solution and Windows 7 eliminated these and other security loopholes so they are limited.

MSEPX began as an exercise in manipulating Windows security (using the fksec code as a base). The code would load the currently running processes and would change the registry location's permission settings allowing the user to alter the contents. This worked, but manipulating user permissions would be frowned upon in corporate or government environments.

To allow MSEPX to always be able to change the registry, a separate self loading Windows service was coded that MSEPX launched and communicated with using a named pipe inter-process communication. A Service, by being run by the local System account, can modify the registry, since the System account is allowed to do almost anything. Of course, when using Windows 7, the MSEPX program must be run as administrator in order to create a service.

The HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft Antimalware\Exclusions is usually editable by the administrator so creating and running a service is not absolutely required. However, the code is designed work even if the service is not available so interacting with the service can be very easily edited out of the source. Note that in Windows 7, MSEPX has to be run as administrator in order to install or restart the service. This is also true when running it in Visual Studio. Make sure that D:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe has the privilege level set to run as administrator.

image001.jpg

Using the Code

MSEPX is basically two list boxes and two buttons so it is not a challenging user interface exercise. The interface is straightforward. The only problem encountered was that the list boxes defaulted to only allowing a single selection and there was no multiple selection property to set to true. So, the MSEPX.rc file had to be edited directly and the LBS_MULTIPLESEL attribute added to both list boxes.

IDD_DLG_MAIN DIALOGEX 0, 0, 396, 239
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION
"MSEPX - Add or Remove Processes from Microsoft Essentials Process Exclusions"
MENU IDC_MSEPX
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
    LTEXT           "File paths of processes that are currently running",
    			IDC_STATIC,7,3,171,8
    LTEXT           "Programs to be excluded from Security Essentials",
    			IDC_STATIC,207,3,171,8
    LISTBOX         IDC_LIST_PROCESSES,7,16,190,191,LBS_SORT | 
    			LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | LBS_EXTENDEDSEL | 
    			WS_VSCROLL | WS_HSCROLL | WS_TABSTOP
    LISTBOX         IDC_LIST_EXCLUDED,205,15,184,202,LBS_SORT | 
    			LBS_MULTIPLESEL | LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | 
    			LBS_EXTENDEDSEL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP
    PUSHBUTTON      "Add Selected to MSE Exclusion List",
    			IDC_BUTTON_MSEX,7,222,138,14
    PUSHBUTTON      "Refresh",IDC_BUTTON_REFRESH,151,222,45,14
    PUSHBUTTON      "Remove Selected from MSE Exclusion List",
    			IDC_BUTTON_MSEX_RMV,205,222,184,14
    CONTROL         "Do not show processes that are in the exclusion list",
    			IDC_CHECK_DONOT_SHOW_EXCLUDED,
                    "Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,208,183,10
END

Excluded Processes

The MSEPX code starts first by enumerating the excluded processes in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft Antimalware\Exclusions and loading the excluded list box.

void SetExcludedList(HWND hListBox )
{
       int iExt = 0;  // for determining the horizontal needed for list box
       TCHAR    achClass[MAX_PATH] = TEXT("");  // buffer for class name
       DWORD    cchClassName = MAX_PATH;  	// size of class string
       DWORD    cSubKeys=0;               	// number of subkeys
       DWORD    cbMaxSubKey;              	// longest subkey size
       DWORD    cchMaxClass;              	// longest class string
       DWORD    cValues;              	// number of values for key
       DWORD    cchMaxValue;          	// longest value name
       DWORD    cbMaxValueData;       	// longest value data
       DWORD    cbSecurityDescriptor; 	// size of security descriptor
       _FILETIME ftLastWriteTime;      	// last write time
       DWORD i, retCode;
       TCHAR  achValue[MAX_VALUE_NAME];
       DWORD cchValue = MAX_VALUE_NAME;
       SendMessage(hListBox, LB_RESETCONTENT, 0, 0 );
       HKEY hKey;
       if( ERROR_SUCCESS == RegOpenKeyExW( HKEY_LOCAL_MACHINE,
                                           MS_ESS_REG_XP,0, KEY_READ, &hKey) )
       {
              // Get the class name and the value count.
              retCode = RegQueryInfoKey(
                     hKey,                    // key handle
                     achClass,                // buffer for class name
                     &cchClassName,           // size of class string
                     NULL,                    // reserved
                     &cSubKeys,               // number of subkeys
                     &cbMaxSubKey,            // longest subkey size
                     &cchMaxClass,            // longest class string
                     &cValues,                // number of values for this key
                     &cchMaxValue,            // longest value name
                     &cbMaxValueData,         // longest value data
                     &cbSecurityDescriptor,   // security descriptor
                     &ftLastWriteTime);       // last write time
              if (cValues)
              {
                     for(i=0, retCode = ERROR_SUCCESS ;
                         retCode == ERROR_SUCCESS && i < cValues; i++)
                     {
                           cchValue = MAX_VALUE_NAME;
                           achValue[0] = '\0';
                           retCode = RegEnumValue(hKey, i,
                                  achValue,
                                  &cchValue,
                                  NULL,
                                  NULL,
                                  NULL,
                                  NULL);
                           if (retCode == ERROR_SUCCESS )
                           {
                                  SendMessage(hListBox, LB_ADDSTRING, -1,
                                         (LPARAM)(LPCSTR)achValue);
                                  int ilen = GetTextLen(hMainDialog,achValue);
                                  if( iExt < ilen ) iExt = ilen;
                           }
                     }
              }
       }
       SendMessage(hListBox, LB_SETHORIZONTALEXTENT, iExt, 0);
}

There are no subkeys below processes to deal with, so the code is very simple.

image003.jpg - Click to enlarge image

Notice that there is a horizontal scroll bar for both list boxes. This is achieved with the last procedure call in the list populating code:

SendMessage(hListBox, LB_SETHORIZONTALEXTENT, iExt, 0); 

This sets the list box to display up to the longest item string listed.

The processes added to the list are placed in the registry immediately. They are stored as DWORD values of zero with the path and name of the process file used as the variable name.

Running Processes

Next, running processes are enumerated and the source executable file paths are loaded to the currently running processes list box.

void SetProcessList(HWND hListBox, HWND hListBoxExclusions, BOOL bFilter )
{
       HANDLE hSnapShot=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS |
                                                TH32CS_SNAPMODULE32,0);
       //HANDLE hSnapShot=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
       PROCESSENTRY32* processInfo=new PROCESSENTRY32;
       processInfo->dwSize=sizeof(PROCESSENTRY32);
 
       SendMessage( hListBox, LB_RESETCONTENT, 0, 0 ); //clear the list
 
       wchar_t filepath[MAX_PATH] = {0};
       int iExt = 0;  // horizontal extent required for the list box
       while(hSnapShot && Process32Next(hSnapShot,processInfo)!=FALSE)
       {
              DWORD RequiredAccess = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ;
 
              // Get a process handle
              HANDLE hProcess=OpenProcess
		( RequiredAccess, TRUE, processInfo->th32ProcessID);
              if( hProcess )
              {
                     GetModuleFileNameEx(hProcess, NULL, filepath, MAX_PATH );
                     if( *filepath )
                     {
                           if( !_tcsncmp(filepath, _T("\\??\\ "), 4 ) )
                           {
                                  wchar_t *s = filepath+4;
                                  wchar_t *t = filepath;
                                  while( *s ) *t++ = *s++;
                                  ++*t = _T('\0');
                           }
                           else if( !_tcsnicmp(filepath, _T("\\systemroot\\ "),12))
                           {
                                  wchar_t rootpath[MAX_PATH] = {0};
                                  char * root = new char[MAX_PATH];
                                  size_t sz = MAX_PATH;
                                  _dupenv_s(&root, &sz,"systemroot");
                                  if(*root)
                                  {
                                         MultiByteToWideChar(CP_ACP,MB_COMPOSITE,
                                                root, strlen(root),
                                                rootpath, sizeof(rootpath));
                                         wchar_t path[MAX_PATH] = {0};
                                         wcscpy_s(path,MAX_PATH,rootpath );
                                         wcscat_s(path,MAX_PATH,filepath+11 );
                                         wcscpy_s(filepath,MAX_PATH,path );
                                  }
                                  delete root;
                           }
                           BOOL bSkip = FALSE;
                           if( bFilter ) {
                                  bSkip = ( LB_ERR !=
                                         SendMessage(hListBoxExclusions,
                                                LB_FINDSTRINGEXACT, (WPARAM)-1,
                                                (LPARAM)(LPCSTR) filepath ) );
                           }
                           if( !bSkip && ( LB_ERR == SendMessage(hListBox,
                                  LB_FINDSTRINGEXACT, (WPARAM)-1, (LPARAM)(LPCSTR)
                                         filepath )) ) {
                                  SendMessageW(hListBox, LB_ADDSTRING, -1,
                                         (LPARAM)(LPCSTR)filepath);
                                  int ilen = GetTextLen( hMainDialog, filepath);
                                  if( iExt < ilen )
                                         iExt = ilen;
                           }
                     }
                     CloseHandle( hProcess );
              }
       }
       CloseHandle(hSnapShot);
       delete processInfo;
       SendMessage(hListBox, LB_SETHORIZONTALEXTENT, iExt, 0);
}

In order to get the file names, the PSAPI.LIB (Process Status API) library is required for calling the GetModuleFileNameEx function. If this program will be compiled to run on a 64 bit system, the flags in CreateToolhelp32Snapshot should be modified to TH32CS_SNAPPROCESS | TH32CS_SNAPMODULE32 to include both 64 and 32 bit processes.

Some of the file paths start with \??\ (like csrs.exe and winlogon) or with \SystemRoot\ (like smss.exe) code is added to trim or translate these values. Also, some processes are duplicated so care has to be taken to avoid duplicates like services hosts (svchost.exe).

Drag and Drop

Instead of using COM for drag and drop which is really more appropriate for general program interaction, the drag and drop (DD) code simply allows copying between two list boxes. Most of the drag and drop source is based on the MSDN Magazine, October 2004 drag and drop code written by Paul DiLascia: Create Client Windows, Drag and Drop Between Listboxes with some added fixes and additions.

The DD code is currently limited to only allowing a single selection at a time and only drags from the process box to the exclusion box.

The DD code uses mouse movements and events and custom windows messages:

#define WM_DD_DRAGENTER (WM_USER + 0x0001)
#define WM_DD_DRAGDROP (WM_USER + 0x0002)
#define WM_DD_DRAGABORT (WM_USER + 0x0003)
#define WM_DD_DRAGOVER (WM_USER + 0x0004)

When the mouse is over a DD source object and the left mouse button is pressed, the DD code sends a WM_DD_DRAGENTER message that the main dialog code intercepts and creates a new CDragDropText class containing the selected list box item text. An image of the text is displayed as the item is being dragged into the destination and when over the destination area, in this case the exclusion list box, and the mouse button is released, the text is added to the list.

Note that the CDragDropText::OnGetDragSize(HDC & dc) function not only is used to calculate the drag image size but sets the image content using a call to DrawText(dc, m_text, lstrlen(m_text)+1, &rc, DT_CALCRECT) function which provides both the image size and setting of the content.

Other Controls

Note that the user interface code also handles double clicks (selects or deselects all items) and short cut keys (accelerators):

IDC_MSEPX ACCELERATORS
BEGIN
    "D",            IDC_BUTTON_MSEX_RMV,    VIRTKEY, CONTROL, NOINVERT
    "R",            IDC_BUTTON_REFRESH,     VIRTKEY, CONTROL, NOINVERT
    "A",            IDC_BUTTON_MSEX,        VIRTKEY, CONTROL, NOINVERT
    "/",            IDM_ABOUT,              ASCII,  NOINVERT
    "?",            IDM_ABOUT,              ASCII,  NOINVERT
    VK_F1,          IDM_ABOUT,              VIRTKEY, NOINVERT
    "Q",            IDM_EXIT,               VIRTKEY, CONTROL, NOINVERT
    "X",            IDM_EXIT,               VIRTKEY, CONTROL, NOINVERT
    "F",            IDC_CHECK_DONOT_SHOW_EXCLUDED, VIRTKEY, CONTROL, NOINVERT
    VK_F5,          IDC_BUTTON_REFRESH,     VIRTKEY, NOINVERT
END 

There is also a refresh button that allows the user to launch new applications and then select refresh to include then in the process list.

MSEPXSVC Service

The MSEPXSVC program is coded as a self loading Windows service. Running as a service provides the code implicitly with System account privileges. MSEPXSVC uses a named pipe to communicate with the MSEPX client program and creates threads to process the commands that are sent through the pipe.

As mentioned, using a service in Windows 7 is not absolutely necessary to add to or delete from the MSE excluded process list. However, this did turn out to be a good example of coding a service, inter-process communications and using multi-threading to run tasks without delaying the user interface process.

An in depth introduction of services is available here: Introduction to Windows Service Applications.

Service Control

MSEPX first tries to start the service assuming the service was already installed. If not already an installed service, the restart fails and MSEPX launches the MSEPXSVC service code (included in the solution as a sub-project) and then waits for it to self install and again attempts to start the service. An argument (install) is sent to the MSEPXSVC process when calling the CreateProcess function. Without this argument, the service will not be installed. This is not really necessary since there are no other capabilities in the MSEPXSVC code. It was originally coded to allow debugging tests without having the service install and was left in case more debugging of the service was needed.

The MSEPXSVC code installs itself as the RevValSvc service. If looking at the running services with a task viewer like Sysinternals Process Explorer, it will be seen running as MSEPXSVC. In the Windows services.msc utility, it is seen as RegValSvc since this utility shows the services database rather than service processes.

In Windows 7, a service cannot be deleted and then recreated without restarting the machine. In Windows XP, this was possible to do. Because of this, MSEPX does not attempt to delete the service when it is closed. MSPEX only stops the MSEPXSVC RegValSvc service. However, if the users desire to completely remove the service, it can be done using the SC.EXE utility command:

SC delete RegValSvc 

Event Logging and Debugging a Service

Debugging a service is a bit painful but can be done with a minimum of pain. Debugging is done using three methods: pre-install, post-install, and event logging.

Pre-Install Service Debugging

Debugging involves two separate processes. First, the code sections concerned with loading the service is debugged and then the service itself. To do the first, the service code is run as a separate project and this allows the developer to trace through the code that installs the process. The service code itself does not run unless created to run automatically which is not the case here.

// Create the service
schService = CreateService(
              schSCManager,              // SCM database
              SVCNAME,                   // name of service
              SVCNAME,                   // service name to display
              SERVICE_ALL_ACCESS,        // desired access
              SERVICE_WIN32_OWN_PROCESS, // service type
              SERVICE_DEMAND_START,      // start type
              SERVICE_ERROR_NORMAL,      // error control type
              szPath,                    // path to service's binary
              NULL,                      // no load ordering group
              NULL,                      // no tag identifier
              NULL,                      // no dependencies
              NULL,                      // NULL means run as LocalSystem account
              NULL);                     // no password

The above code in the SvcInstall() function in svc.cpp uses the SERVICE_DEMAND_START flag making the service manually started and not the SERVICE_AUTO_START flag. However, newly installed services do not run until the system is restarted or are started by the user or a program no matter which flag is used.

Post-Install Service Debugging

Next the process itself is debugged. To do this, a break point is set in the MSEPX code just after the point the service is started. At that point, select the Debug menu and Attach to process. Make sure the Show process from all users and Show processes from all sessions options are selected because MSEPXSVC is being run by System not the developer and will not be seen in the list otherwise.

image005.jpg

Event Logging

Even with the debugging capabilities of Visual Studio, it’s nice to be able to keep an event log for a process. MSEPXSRC contains code that creates Windows application events in order to track the service execution. When an error is detected in a service, the code cannot stop and produce a nice little error message dialog for the user. A service can have no GUI based interaction with the user at all. Instead calls using the ReportEvent function are used to log information:

StringCchPrintf(mssg, MAX_SVC_MSSG_SZ,_T("Wrong arg count: %d. Service Name: %s"), 
                dwArgc, lpszArgv[0] ); 
SvcReportEvent(mssg);
 ... 

VOID SvcReportEvent(LPTSTR szFunction) {
       HANDLE hEventSource;
       LPCTSTR lpszStrings[2];
       WCHAR Buffer[80];
 
       hEventSource = RegisterEventSource(NULL, SVCNAME);
 
       if( NULL != hEventSource )
       {
              StringCchPrintf(Buffer,80, TEXT("%s failed with %d"),
				szFunction, GetLastError());
 
              lpszStrings[0] = SVCNAME;
              lpszStrings[1] = Buffer;
 
              ReportEvent(hEventSource,     	// event log handle
                     EVENTLOG_ERROR_TYPE, 	// event type
                     0,                   	// event category
                     SVC_ERROR,           	// event identifier
                     NULL,                	// no security identifier
                     2,                   	// size of lpszStrings array
                     0,                   	// no binary data
                     lpszStrings,         	// array of strings
                     NULL);               	// no binary data
 
              DeregisterEventSource(hEventSource);
       }
}

Events are then viewed using the Windows event viewer.

image006.jpg

Details on coding event logging are covered in the MSDN article: Improve Manageability through Event Logging.

Inter-process Communications: Named Pipe

To communicate between the MSEPX client and the MSEPXSVC server running as the RegValSvc Windows service, a named pipe is used. Unlike UNIX and Linux, in Windows, there is no command line interface. Also unlike in UNIX, a named pipe in Windows is allocated in the root directory of the named pipe file system and mounted under the special path \\.\pipe\ so a named pipe named XYZ has a path name of \\.\pipe\XYZ and cannot be created on any writable file system. Much of the thread and pipe code used came from the MSDN example: Multithreaded Pipe Server. The MSEPXSVC service creates the named pipe \\.\pipe\MSEPX and waits for the client to send a command through the pipe. When the command is received, the service processes it and sends back a message through the same pipe and then closes it.

Threading

Having the MSEPXSVC service create threads to process the registry edit commands from the MSEPX client has the advantage of being able to process commands from several different clients if necessary. The service has a main loop that sends status messages to the OS and listens for a service stop event. Once a stop event is encountered, the service stops until it is restarted by the MSEPX client. The service also creates a thread that handles processing the named pipe and that thread, when a command arrives through the pipe, then creates another thread that processes the command and adds to or deletes from the registry. The server is designed to take a serialized command from the pipe and construct a call to add or delete anything from the registry, not just Microsoft Security Essentials exclusions. When the pipe is read and the thread reading it creates the thread that will handle the command, the pipe handling thread closes the pipe and signals the service using an event that it has ended. The thread it created that handles the command is left to run and simply self destructs when finished.

Events

The MSEPXSVC service and the threads it creates both rely on another form of inter-process communication, the event object, to control how the code runs. The service code creates two event objects. The first is ghSvcStopEvent which is created in the service’s main entry function and is used to monitor if a stop service signal was received from the service manager in the registered service control function. The second event object is ThreadArgs.QuitEvent which is created in the main service loop and passed to the pipe handling thread when created so that the thread can signal it has ended and a new pipe handling thread has to be created. Using the following code:

// Look for a stop service
if( WAIT_OBJECT_0 == WaitForSingleObject(ghSvcStopEvent, 200 )) //INFINITE) )

a thread can wait or loop until the event is detected and act on the event being set. The second parameter is the wait time in milliseconds which can be set to INFINITE (xFFFFFFFF) to wait forever for the event to be set by the SetEvent(EventHandle) command.

Points of Interest

Messing around with services, pipes, and threads made me appreciate the capabilities of Sysinternals Suite, particularly the Process Explorer utility. It’s free and a must have for any Windows programmer.

History

This is version 1.0 of MSEPX created originally on November 12, 2010.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架