Introduction
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 EntryWritten
event:
_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:
SubjectSecurityID
SubjectAccountName
SubjectAccountDomain
SubjectLogonID
AccountSecurityID
AccountAccountName
AccountAccountDomain
Status
FailureReason
SubStatus
LogonType
LogonProcess
AuthenticationPackage
SourceWorkstationName
TransitedServices
PackageName
KeyLength
CallerProcessID
CallerProcessName
SourceNetworkAddress
SourcePort
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 RDPGuardLog
entries.
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 Enabled
property.
Using the Service
Source Code
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:
Class name | Description |
RDPGuardBlock | Located in the file RDPGuardEntities.cs; this entity is used to model a blocked IP and can also be managed by the DataProvider . |
RDPGuardHit | Located in the file RDPGuardEntities.cs; this entity is used to model a recognized failed logon attempt and can also be managed by the DataProvider . |
RDPGuardLog | Located in the file RDPGuardEntities.cs; this entity is used to model a log entry and can also be managed by the DataProvider . |
ABjSThread | An abstract class, implements all tasks of a general thread which task is periodically called with a specified pause beween the executions. It also implements Start and Stop functions and a StatusMessage event. |
RDPGuardThread | This is the central part of the service. It's an implementation of ABjSThread and its function is explained above. |
RDPGuardDao | This is the DataProvider using SQLite as a backend. |
FirewallBlockIpRule | This is a helper class around the NetFwTypeLib classes giving access to a firewall rule. |
GenericEventArgs<<font color="#000">T</font>> | This is a helper class to pass any type of object with an EventHandler . |
Program | 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. |
Service1 | This is used to control RDPGuardThread when used as service. |
ServiceInstaller1 | This is used to control the installation of the service to set name, description and running account type. |
Install
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):
- C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe
- C:\Windows\Microsoft.NET\Framework64\v2.0.50727\InstallUtil.exe
- C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe
- C:\Windows\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe
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 /i
with /u
.
Configuration
The service has a configuration file in standard XML format. It contains the following options:
Option name | Type | Description |
WhiteList | string | A comma seperated list of white-listed IP addresses which are never blocked |
BlockSpanHours | int | The number of hours an IP address is blocked before the block is removed |
BlockHitCount | int | The minimum number of failed logon attempts before an IP addess is blocked |
LogCategoryFilter | int | A binary filter setting what categories of log messages are logged (see below) |
FirewallRuleName | string | The name of the blocking rule in the Windows firewall |
The 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:
Bit | Bit value | Category | Description |
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.
Command-line Usage
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:
Option | Description |
/? | Displays a help message describing the command-line options. |
/L[=DateTime ] | 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:
BjSTools.RDPGuard /C
- Write complete log to the file log.txt:
BjSTools.RDPGuard /L > log.txt
- Show log from 2015 and newer:
BjSTools.RDPGuard /L=2015-01-01
- Show log from April 1st 2015 noon and newer:
BjSTools.RDPGuard /L=2015-04-01T12:00:00
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!