I recently got a new Windows 2008 server online and like always you can see in the Windows
EventLog how the Remote Desktop Protocol (RDP) is being brute-forced. Also it's just a small machine thus the endless authorization attempts take quite a big part of the server's processor power. So I started manually blocking the IPs extracted from the
EventLog entries, but of course it didn't really help for long. So I decided to create a Windows service to do the task.
Shown above is an example for the Windows
EventLog Explorer showing Audit Failure entries - the ones indicating a brute-force attack.
How the Service Works
The diagram above shows the basic structure of the
RDPGuard service. The blue parts are mainly simple Windows APIs, the yellowish ones show the simplified program parts.
The EventLog Subscription
The Service subscribes to the Windows
EventLog "Security" being notified with every new entry as long as the service is running. If the
EventLog entry is an
AudithFailure entry with a valid source IP address specified, this information is stored in the database as a
RDPGuardHit entry (containing the timestamp, the IP address and the attempted logon username - the latter out of simple curiosity of mine). The subscription is made quite easy with C# - you simply open an
EventLog by name and subscribe to the offered
_log = new EventLog("Security");
_log.EnableRaisingEvents = true;
_log.EntryWritten += EventLog_EntryWritten;
Extracting the wanted data is a bit tricky because the
Message property contains a localized
string which is poorly formatted for extracting data. But there is another property,
ReplacementStrings - it contains a
string array of the data which is used to create the localized message
string. For the
AuditFailure entry interesting for this service, the fields are the following:
So, the first check on the entry is for the field count to be
21, then the field of interest is at index
19 which is to be checked for a well formatted IP address.
The Background Thread and Data Provider
Meanwhile, a separate thread is used to periodically check on the saved
RDPGuardHit entries. Some logic in this service has been moved to the data provider for the sake of performance because a lot of it can be done in SQL much easier. I chose to use SQLite simply because I'm comfortable with it. The following steps are taken every period of the thread:
Every blocked IP is stored as a
RDPGuardBlock containing only the timestamp of the block and the blocked IP address. As the first two actions in every loop show, all collected data older than the set ban time is deleted - the attacking IPs are in a manner of speaking rehabilitated. Although there is a log written in the database, too, using the
The SQLite data provider in this service uses simple SQL commands with a few helpers methods. It is initialized when the service is started and the database file is placed in the same directory under the same name as the service executable with the ending .db3 and all necessary tables are created with the CREATE TABLE IF NOT EXISTS command.
The Firewall Rule
To block the recognized attacker's IPs, this service uses the Windows Firewall API. To access it, Visual Studio must be started with administrative rights or the COM reference
NetFwTypeLib for the firewallapi.dll won't show up at all. To make it possible to work with the code without administrative rights, I extracted the Interop-DLLs and used them instead - although debugging without administrative rights cannot work of course because these rights are needed to alter the Windows Firewall. Initializing the Interop-classes requires a bit of Googling but then it's pretty straightforward.
When the service starts, the helper class
FirewallBlockIpRule is initialized. It attempts to open the Firewall rule by name (using the one set in the settings) and if that fails, creates a new inbound block rule under that name. From then on, the usage is very simple: By setting the
RemoteAddresses property IPs to block can be set as a comma separates string and the rule can be enabled or disabled by setting the
Using the Service
If you'd like to change the source code, feel free to do so - I tried to add as much inline commenting as I could somehow still call sensible. The 64 bit project of the solution just links to all code files of the 32 bits version.
Here is an overview of the project's classes:
|Located in the file RDPGuardEntities.cs; this entity is used to model a blocked IP and can also be managed by the |
|Located in the file RDPGuardEntities.cs; this entity is used to model a recognized failed logon attempt and can also be managed by the |
|Located in the file RDPGuardEntities.cs; this entity is used to model a log entry and can also be managed by the |
abstract class, implements all tasks of a general thread which task is periodically called with a specified pause beween the executions. It also implements
Stop functions and a
|This is the central part of the service. It's an implementation of |
ABjSThread and its function is explained above.
|This is the |
DataProvider using SQLite as a backend.
|This is a helper class around the |
NetFwTypeLib classes giving access to a firewall rule.
|This is a helper class to pass any type of object with an |
|This is the main entry point of the program. It determines whether or not the service is run in command-line (or Debug) and switches the behaviour accordingly.|
|This is used to control |
RDPGuardThread when used as service.
|This is used to control the installation of the service to set name, description and running account type.|
To install a .NET Windows service in general, I usually use the installutil.exe on the command-line. It most probably is located in these folders (depending on what versions of .NET you have installed):
Depending on which version of the service (32 or 64 bit) you intend to install, you should use either the Framework folder for 32 bit or the Framework64 folder for 64 bit and then the newest version.
Change to the folder of installutil.exe and run the following command replacing
%path_to_service% with the actual path of the service:
installutil.exe /i %path_to_service%\BjSTools.RDPGuard.exe
To uninstall the service, simply use the same command, replacing the switch
The service has a configuration file in standard XML format. It contains the following options:
|A comma seperated list of white-listed IP addresses which are never blocked|
|The number of hours an IP address is blocked before the block is removed|
|The minimum number of failed logon attempts before an IP addess is blocked|
|A binary filter setting what categories of log messages are logged (see below)|
|The name of the blocking rule in the Windows firewall|
LogCategoryFilter option uses the bits of the integer - if a bit is set to
1, the according log category is logged. You can simply add the bit values of the log categories to be logged together to get the filter:
|0||1||Debug||Message is for debug purpose only|
|1||2||Information||Status or otherwise non-disturbing message|
|2||4||Warning||Disturbing information without effect on the ongoing program|
|3||8||Error||A disturbing error which does not cause the program to quit|
|4||16||Critical||A disturbing error which causes the program to quit|
The settings can be changed even when the service is already installed but will only be read when the service is starting.
The service can also be used as a command-line program. Trying to start the service without any of the following command-line options will result in an error because the service is trying to start as a service and that is only possible if the Windows service controller does it. These options are available:
|/?||Displays a help message describing the command-line options.|
|Outputs all log entries. If |
DateTime is specified with a valid
DateTime value, only log entries from that time on are loaded. The Convert.ToDateTime(string) method is used.
|/C||Start the service as a command-line tool. Log messages are also written to the command-line.|
Here are some examples for the command-line usage:
- Run service in command-line:
- Write complete log to the file log.txt:
BjSTools.RDPGuard /L > log.txt
- Show log from 2015 and newer:
- Show log from April 1st 2015 noon and newer:
When running the service in command-line mode, it can be stopped by pressing [CTRL]+[C] - it will stop the service in a managed fashion.
Points of Interest
When I started coding this service, I was prepared for some nasty invoking interop code and was then supprised how well it is implemented by the COM references. A rare: Well done Microsoft!