adding "srcon" protocol, and new cvars:
authordivverent <divverent@d7cf8633-e32d-0410-b094-e92efae38249>
Thu, 9 Apr 2009 07:30:53 +0000 (07:30 +0000)
committerdivverent <divverent@d7cf8633-e32d-0410-b094-e92efae38249>
Thu, 9 Apr 2009 07:30:53 +0000 (07:30 +0000)
rcon_secure - if enabled, client sends srcon requests instead of rcon, and server ONLY accepts secure (HMAC-MD4) rcon.
rcon_secure_maxdiff - maximum clock difference between srcon client and server in seconds (is used to protect against replay attacks)

In srcon, the command is included into the hashing, so one can still sniff the command and its replies - but what an attacker can't do in srcon is sniff the password, or change the command over the wire. There is still limited replaying potential (within 15 seconds, the command can be replayed), but I don't think this can cause much harm.

TODO: add some way to FORCE rcon_secure to be 1, as currently a malicious server could stuffcmd("rcon_secure 0\nrcon ...") and then issue a rcon command to get the password.

git-svn-id: svn://svn.icculus.org/twilight/trunk/darkplaces@8886 d7cf8633-e32d-0410-b094-e92efae38249

hmac.c
host_cmd.c
netconn.c

diff --git a/hmac.c b/hmac.c
index 372921d..de44162 100644 (file)
--- a/hmac.c
+++ b/hmac.c
@@ -8,10 +8,10 @@ void hmac(
        unsigned char *key, int k
 )
 {
-       unsigned char hashbuf[32];
-       unsigned char k_xor_ipad[128];
-       unsigned char k_xor_opad[128];
-       unsigned char catbuf[256];
+       static unsigned char hashbuf[32];
+       static unsigned char k_xor_ipad[128];
+       static unsigned char k_xor_opad[128];
+       static unsigned char catbuf[4096];
        int i;
 
        if(sizeof(hashbuf) < (size_t) hlen)
index 6e3a2ab..c8c7889 100644 (file)
@@ -22,12 +22,18 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 #include "sv_demo.h"
 #include "image.h"
 
+// for secure rcon authentication
+#include "hmac.h"
+#include "mdfour.h"
+#include <time.h>
+
 int current_skill;
 cvar_t sv_cheats = {0, "sv_cheats", "0", "enables cheat commands in any game, and cheat impulses in dpmod"};
 cvar_t sv_adminnick = {CVAR_SAVE, "sv_adminnick", "", "nick name to use for admin messages instead of host name"};
 cvar_t sv_status_privacy = {CVAR_SAVE, "sv_status_privacy", "0", "do not show IP addresses in 'status' replies to clients"};
 cvar_t sv_status_show_qcstatus = {CVAR_SAVE, "sv_status_show_qcstatus", "0", "show the 'qcstatus' field in status replies, not the 'frags' field. Turn this on if your mod uses this field, and the 'frags' field on the other hand has no meaningful value."};
 cvar_t rcon_password = {CVAR_PRIVATE, "rcon_password", "", "password to authenticate rcon commands"};
+cvar_t rcon_secure = {0, "rcon_secure", "1", "force secure rcon authentication"};
 cvar_t rcon_address = {0, "rcon_address", "", "server address to send rcon commands to (when not connected to a server)"};
 cvar_t team = {CVAR_USERINFO | CVAR_SAVE, "team", "none", "QW team (4 character limit, example: blue)"};
 cvar_t skin = {CVAR_USERINFO | CVAR_SAVE, "skin", "", "QW player skin name (example: base)"};
@@ -2378,7 +2384,21 @@ void Host_Rcon_f (void) // credit: taken from QuakeWorld
        if (mysocket)
        {
                // simply put together the rcon packet and send it
-               NetConn_WriteString(mysocket, va("\377\377\377\377rcon %s %s", rcon_password.string, Cmd_Args()), &to);
+               if(rcon_secure.integer)
+               {
+                       char buf[1500];
+                       char argbuf[1500];
+                       dpsnprintf(argbuf, sizeof(argbuf), "%ld %s", (long) time(NULL), Cmd_Args());
+                       memcpy(buf, "\377\377\377\377srcon HMAC-MD4 TIME ", 24);
+                       HMAC_MDFOUR_16BYTES((unsigned char *) (buf + 24), (unsigned char *) argbuf, strlen(argbuf), (unsigned char *) rcon_password.string, strlen(rcon_password.string));
+                       buf[40] = ' ';
+                       strlcpy(buf + 41, argbuf, sizeof(buf) - 41);
+                       NetConn_Write(mysocket, buf, 41 + strlen(buf + 41), &to);
+               }
+               else
+               {
+                       NetConn_WriteString(mysocket, va("\377\377\377\377rcon %s %s", rcon_password.string, Cmd_Args()), &to);
+               }
        }
 }
 
@@ -2756,6 +2776,7 @@ void Host_InitCommands (void)
 
        Cvar_RegisterVariable (&rcon_password);
        Cvar_RegisterVariable (&rcon_address);
+       Cvar_RegisterVariable (&rcon_secure);
        Cmd_AddCommand ("rcon", Host_Rcon_f, "sends a command to the server console (if your rcon_password matches the server's rcon_password), or to the address specified by rcon_address when not connected (again rcon_password must match the server's)");
        Cmd_AddCommand ("user", Host_User_f, "prints additional information about a player number or name on the scoreboard");
        Cmd_AddCommand ("users", Host_Users_f, "prints additional information about all players on the scoreboard");
index 77fb60c..93199c1 100755 (executable)
--- a/netconn.c
+++ b/netconn.c
@@ -23,6 +23,11 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 #include "quakedef.h"
 #include "lhnet.h"
 
+// for secure rcon authentication
+#include "hmac.h"
+#include "mdfour.h"
+#include <time.h>
+
 #define QWMASTER_PORT 27000
 #define DPMASTER_PORT 27950
 
@@ -80,6 +85,8 @@ static cvar_t net_slist_favorites = {CVAR_SAVE, "net_slist_favorites", "", "cont
 static cvar_t gameversion = {0, "gameversion", "0", "version of game data (mod-specific), when client and server gameversion mismatch in the server browser the server is shown as incompatible"};
 static cvar_t rcon_restricted_password = {CVAR_PRIVATE, "rcon_restricted_password", "", "password to authenticate rcon commands in restricted mode"};
 static cvar_t rcon_restricted_commands = {0, "rcon_restricted_commands", "", "allowed commands for rcon when the restricted mode password was used"};
+static cvar_t rcon_secure_maxdiff = {0, "rcon_secure_maxdiff", "5", "maximum time difference between rcon request and server system clock (to protect against replay attack)"};
+extern cvar_t rcon_secure;
 
 /* statistic counters */
 static int packetsSent = 0;
@@ -2173,16 +2180,37 @@ void NetConn_ClearConnectFlood(lhnetaddress_t *peeraddress)
        }
 }
 
+typedef qboolean (*rcon_matchfunc_t) (const char *password, const char *hash, const char *s, int slen);
+
+qboolean hmac_mdfour_matching(const char *password, const char *hash, const char *s, int slen)
+{
+       char mdfourbuf[16];
+       long t1, t2;
+
+       t1 = (long) time(NULL);
+       t2 = strtol(s, NULL, 0);
+       if(abs(t1 - t2) > rcon_secure_maxdiff.integer)
+               return false;
+
+       HMAC_MDFOUR_16BYTES((unsigned char *) mdfourbuf, (unsigned char *) s, slen, (unsigned char *) password, strlen(password));
+       return !memcmp(mdfourbuf, hash, 16);
+}
+
+qboolean plaintext_matching(const char *password, const char *hash, const char *s, int slen)
+{
+       return !strcmp(password, hash);
+}
+
 // returns a string describing the user level, or NULL for auth failure
-const char *RCon_Authenticate(const char *password, const char *s, const char *endpos)
+const char *RCon_Authenticate(const char *password, const char *s, const char *endpos, rcon_matchfunc_t comparator, const char *cs, int cslen)
 {
        const char *text;
        qboolean hasquotes;
 
-       if(!strcmp(rcon_password.string, password))
+       if(comparator(rcon_password.string, password, cs, cslen))
                return "rcon";
        
-       if(strcmp(rcon_restricted_password.string, password))
+       if(!comparator(rcon_restricted_password.string, password, cs, cslen))
                return NULL;
 
        for(text = s; text != endpos; ++text)
@@ -2230,6 +2258,44 @@ match:
        return "restricted rcon";
 }
 
+void RCon_Execute(lhnetsocket_t *mysocket, lhnetaddress_t *peeraddress, const char *addressstring2, const char *userlevel, const char *s, const char *endpos)
+{
+       if(userlevel)
+       {
+               // looks like a legitimate rcon command with the correct password
+               const char *s_ptr = s;
+               Con_Printf("server received %s command from %s: ", userlevel, host_client ? host_client->name : addressstring2);
+               while(s_ptr != endpos)
+               {
+                       size_t l = strlen(s_ptr);
+                       if(l)
+                               Con_Printf(" %s;", s_ptr);
+                       s_ptr += l + 1;
+               }
+               Con_Printf("\n");
+
+               if (!host_client || !host_client->netconnection || LHNETADDRESS_GetAddressType(&host_client->netconnection->peeraddress) != LHNETADDRESSTYPE_LOOP)
+                       Con_Rcon_Redirect_Init(mysocket, peeraddress);
+               while(s != endpos)
+               {
+                       size_t l = strlen(s);
+                       if(l)
+                       {
+                               client_t *host_client_save = host_client;
+                               Cmd_ExecuteString(s, src_command);
+                               host_client = host_client_save;
+                               // in case it is a command that changes host_client (like restart)
+                       }
+                       s += l + 1;
+               }
+               Con_Rcon_Redirect_End();
+       }
+       else
+       {
+               Con_Printf("server denied rcon access to %s\n", host_client ? host_client->name : addressstring2);
+       }
+}
+
 extern void SV_SendServerinfo (client_t *client);
 static int NetConn_ServerParsePacket(lhnetsocket_t *mysocket, unsigned char *data, int length, lhnetaddress_t *peeraddress)
 {
@@ -2408,12 +2474,31 @@ static int NetConn_ServerParsePacket(lhnetsocket_t *mysocket, unsigned char *dat
                        }
                        return true;
                }
+               if (length >= 37 && !memcmp(string, "srcon HMAC-MD4 TIME ", 20))
+               {
+                       char *password = string + 20;
+                       char *timeval = string + 37;
+                       char *s = strchr(timeval, ' ');
+                       char *endpos = string + length + 1; // one behind the NUL, so adding strlen+1 will eventually reach it
+                       const char *userlevel;
+                       if(!s)
+                               return true; // invalid packet
+                       ++s;
+
+                       userlevel = RCon_Authenticate(password, s, endpos, hmac_mdfour_matching, timeval, endpos - timeval - 1); // not including the appended \0 into the HMAC
+                       RCon_Execute(mysocket, peeraddress, addressstring2, userlevel, s, endpos);
+                       return true;
+               }
                if (length >= 5 && !memcmp(string, "rcon ", 5))
                {
                        int i;
                        char *s = string + 5;
                        char *endpos = string + length + 1; // one behind the NUL, so adding strlen+1 will eventually reach it
                        char password[64];
+
+                       if(rcon_secure.integer)
+                               return true;
+
                        for (i = 0;!ISWHITESPACE(*s);s++)
                                if (i < (int)sizeof(password) - 1)
                                        password[i++] = *s;
@@ -2422,41 +2507,8 @@ static int NetConn_ServerParsePacket(lhnetsocket_t *mysocket, unsigned char *dat
                        password[i] = 0;
                        if (!ISWHITESPACE(password[0]))
                        {
-                               const char *userlevel = RCon_Authenticate(password, s, endpos);
-                               if(userlevel)
-                               {
-                                       // looks like a legitimate rcon command with the correct password
-                                       char *s_ptr = s;
-                                       Con_Printf("server received %s command from %s: ", userlevel, host_client ? host_client->name : addressstring2);
-                                       while(s_ptr != endpos)
-                                       {
-                                               size_t l = strlen(s_ptr);
-                                               if(l)
-                                                       Con_Printf(" %s;", s_ptr);
-                                               s_ptr += l + 1;
-                                       }
-                                       Con_Printf("\n");
-
-                                       if (!host_client || !host_client->netconnection || LHNETADDRESS_GetAddressType(&host_client->netconnection->peeraddress) != LHNETADDRESSTYPE_LOOP)
-                                               Con_Rcon_Redirect_Init(mysocket, peeraddress);
-                                       while(s != endpos)
-                                       {
-                                               size_t l = strlen(s);
-                                               if(l)
-                                               {
-                                                       client_t *host_client_save = host_client;
-                                                       Cmd_ExecuteString(s, src_command);
-                                                       host_client = host_client_save;
-                                                       // in case it is a command that changes host_client (like restart)
-                                               }
-                                               s += l + 1;
-                                       }
-                                       Con_Rcon_Redirect_End();
-                               }
-                               else
-                               {
-                                       Con_Printf("server denied rcon access to %s\n", host_client ? host_client->name : addressstring2);
-                               }
+                               const char *userlevel = RCon_Authenticate(password, s, endpos, plaintext_matching, NULL, 0);
+                               RCon_Execute(mysocket, peeraddress, addressstring2, userlevel, s, endpos);
                        }
                        return true;
                }
@@ -2971,6 +3023,7 @@ void NetConn_Init(void)
        Cmd_AddCommand("heartbeat", Net_Heartbeat_f, "send a heartbeat to the master server (updates your server information)");
        Cvar_RegisterVariable(&rcon_restricted_password);
        Cvar_RegisterVariable(&rcon_restricted_commands);
+       Cvar_RegisterVariable(&rcon_secure_maxdiff);
        Cvar_RegisterVariable(&net_slist_queriespersecond);
        Cvar_RegisterVariable(&net_slist_queriesperframe);
        Cvar_RegisterVariable(&net_slist_timeout);