hashing known_hosts

Robert Drake rdrake at sprint.net
Tue Mar 4 12:19:39 EST 2003


Scenario:

I have access to a semi-public (about 30 users) server where I keep my 
webpage.  Occasionally, especially if I'm on the road.  I use this as a 
bounce point to get to "secured" systems which only allow ssh from 
certian IP's.  (Ignoring the discussion on spoofing, since we have host 
keys)

But host keys are the problem.  If anyone gets root on this hypothetical 
public server they have 30 known_hosts files they can look through to find 
new hosts to go after.  I didn't like that, so my first instinct was to 
delete my known_hosts file on logout, but then I lose all the benifits of 
host keys.

So how about this?

We should hash the <host>,<ip> pair and store that in the known_hosts file.  
Then someone has to know the hash to figure out what host key goes to what.

Drawbacks:

If a host is only known by IP (no hostname) the attacker only has to check a 
maximum of 4 billion hashes.  I'm running through these now to see how long
it takes and how big the dictionary file is.

You could get around this by using salt, but since you have no reference you 
have to check every hash in the known_host file, which could be expensive if
you have 300 host keys.

Alternatively, you can use DNS everywhere (which you should be doing anyway)
since that gives you a hostname,xx.xx.xx.xx hash message, which makes 
dictionary attacks much less feasible.  

Another drawback is that it makes key management a bitch, since you can't 
search you're known_hosts file by hostname.  I consider this a problem that 
can be addressed by ethier an external program, or by adding it into ssh.  
Personally I think ssh should prompt you for more options when a key comes 
up as invalid, something like "Host key not found.  Do you want to accept 
new key?"  Of course, I'd also like the ability to sign old host keys just 
to make the trust model safer.

I also think this should be an optional configuration option.  Something 
like "HashKnownHosts yes"

Test code..

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <openssl/evp.h>
#include <openssl/sha.h>

int
uuencode(u_char *src, u_int srclength,
    char *target, size_t targsize)
{
        return b64_ntop(src, srclength, target, targsize);
}

static char *pt(unsigned char *md)
{
        int i;
        static char buf[80];

        for (i=0; i<SHA_DIGEST_LENGTH; i++)
                sprintf(&(buf[i*2]),"%02x",md[i]);
        return(buf);
}


int main(void) {
   
    int a,b,c,d;
    char ip[16];
    unsigned char md[SHA_DIGEST_LENGTH];
    char uu[SHA_DIGEST_LENGTH*2];
   
/*    for (a=1; a<=233;a++) { */
     for (a=1; a<=5;a++) {
        for(b=0;b<=255;b++) {
          for(c=0;c<=255;c++) {
            for(d=0;d<=255;d++) {
               sprintf(ip, "%d.%d.%d.%d", a,b,c,d); 
               SHA1(ip,strlen(ip),md);
               uuencode(md, SHA_DIGEST_LENGTH, uu, SHA_DIGEST_LENGTH*2);
               printf("%s %s\n", ip, uu);
            }
          }
        }
    }

    return 0;
} 


I'm only doing the first 5 "class A" ranges of IPs.  We can use this to 
calculate how long it would take to look at all 4 billion IPs.

revolution:~$ time ./test1 > /dev/null

real    43m44.440s
user    19m20.701s
sys     0m3.442s

revolution:~$ time ./test1 > test-dict.out

real    47m53.129s
user    20m23.721s
sys     1m8.202s

revolution:~$ ls -l test-dict.out 
-rw-r--r--   1 rdrake   other    3498967040 Aug 20 11:42 test-dict.out

revolution:~$ ls -l dict.gz 
-rw-r--r--   1 rdrake   other    2044302219 Aug 20 13:42 dict.gz

So, for 2% we have 2 gigs of disk space for the dictionary.  A dictionary 
attack is obviously not too practicle, but definately feasible (especially 
if they can download the known_hosts file to their machine)


Problem:  It's hard to remove old hosts because you don't know which ones 
have changed.

Solution: timestamp each entry with a "last accessed time".  Any host that 
hasen't been logged into in over xxx time is later removed (or processed in
some other way)

Of course, if you wanted effective host management, you could always store 
the hostname/sha hash pairs in a public-key encrypted file.  Then the user
would type their passphrase and have access to the hostnames.  Adds a whole
asston of complications to ssh key management, but alot of it could be in an
external program.  

I wrote this initial email about 6 months ago and finally got around
to making a patch.  Please let me know if this would be an interesting
addition, or if I'm just a retard.

Thanks,
Robert

-- 
Robert Drake
-------------- next part --------------
Common subdirectories: openssh-3.4p1/autom4te-2.53.cache and openssh-3.4p1-hash/autom4te-2.53.cache
Common subdirectories: openssh-3.4p1/contrib and openssh-3.4p1-hash/contrib
diff -u -p openssh-3.4p1/hostfile.c openssh-3.4p1-hash/hostfile.c
--- openssh-3.4p1/hostfile.c	Thu Dec 20 20:47:09 2001
+++ openssh-3.4p1-hash/hostfile.c	Mon Mar  3 17:28:25 2003
@@ -135,8 +135,13 @@ check_host_in_hostfile(const char *filen
 			;
 
 		/* Check if the host name matches. */
+#ifdef HASH_KNOWN_HOSTS
+                if (match_hashed_hostname(host, cp, (u_int) (cp2 - cp)) != 1)
+                        continue;
+#else
 		if (match_hostname(host, cp, (u_int) (cp2 - cp)) != 1)
 			continue;
+#endif
 
 		/* Got a match.  Skip host name. */
 		cp = cp2;
diff -u -p openssh-3.4p1/match.c openssh-3.4p1-hash/match.c
--- openssh-3.4p1/match.c	Mon Mar  4 20:42:43 2002
+++ openssh-3.4p1-hash/match.c	Mon Mar  3 17:32:17 2003
@@ -176,6 +176,14 @@ match_hostname(const char *host, const c
 	return match_pattern_list(host, pattern, len, 1);
 }
 
+#ifdef HASH_KNOWN_HOSTS
+int
+match_hashed_hostname(const char *host, const char *pattern, u_int len)
+{
+        return  match_pattern_list(host, pattern, len, 0);
+}
+#endif
+
 /*
  * returns 0 if we get a negative match for the hostname or the ip
  * or if we get no match at all.  returns 1 otherwise.
diff -u -p openssh-3.4p1/match.h openssh-3.4p1-hash/match.h
--- openssh-3.4p1/match.h	Mon Mar  4 20:42:43 2002
+++ openssh-3.4p1-hash/match.h	Mon Mar  3 17:30:44 2003
@@ -20,5 +20,8 @@ int	 match_hostname(const char *, const 
 int	 match_host_and_ip(const char *, const char *, const char *);
 int	 match_user(const char *, const char *, const char *, const char *);
 char	*match_list(const char *, const char *, u_int *);
+#ifdef HASH_KNOWN_HOSTS
+int      match_hashed_hostname(const char *, const char *, u_int);
+#endif
 
 #endif
Common subdirectories: openssh-3.4p1/openbsd-compat and openssh-3.4p1-hash/openbsd-compat
diff -u -p openssh-3.4p1/readconf.c openssh-3.4p1-hash/readconf.c
--- openssh-3.4p1/readconf.c	Thu Jun 20 20:41:52 2002
+++ openssh-3.4p1-hash/readconf.c	Mon Mar  3 17:33:00 2003
@@ -114,6 +114,9 @@ typedef enum {
 	oDynamicForward, oPreferredAuthentications, oHostbasedAuthentication,
 	oHostKeyAlgorithms, oBindAddress, oSmartcardDevice,
 	oClearAllForwardings, oNoHostAuthenticationForLocalhost,
+#ifdef HASH_KNOWN_HOSTS
+        oHashKnownHosts,
+#endif
 	oDeprecated
 } OpCodes;
 
@@ -186,6 +189,9 @@ static struct {
 	{ "smartcarddevice", oSmartcardDevice },
 	{ "clearallforwardings", oClearAllForwardings },
 	{ "nohostauthenticationforlocalhost", oNoHostAuthenticationForLocalhost },
+#ifdef HASH_KNOWN_HOSTS
+        { "hashknownhosts", oHashKnownHosts },
+#endif
 	{ NULL, oBadOption }
 };
 
@@ -380,6 +386,12 @@ parse_flag:
 		intptr = &options->check_host_ip;
 		goto parse_flag;
 
+#ifdef HASH_KNOWN_HOSTS
+        case oHashKnownHosts:
+                intptr = &options->hash_known_hosts;
+                goto parse_flag;
+#endif
+
 	case oStrictHostKeyChecking:
 		intptr = &options->strict_host_key_checking;
 		arg = strdelim(&s);
@@ -793,6 +805,9 @@ initialize_options(Options * options)
 	options->bind_address = NULL;
 	options->smartcard_device = NULL;
 	options->no_host_authentication_for_localhost = - 1;
+#ifdef HASH_KNOWN_HOSTS
+        options->hash_known_hosts = -1;
+#endif
 }
 
 /*
@@ -907,6 +922,10 @@ fill_default_options(Options * options)
 		clear_forwardings(options);
 	if (options->no_host_authentication_for_localhost == - 1)
 		options->no_host_authentication_for_localhost = 0;
+#ifdef HASH_KNOWN_HOSTS
+        if (options->hash_known_hosts == -1)
+                options->hash_known_hosts = 0;
+#endif
 	/* options->proxy_command should not be set by default */
 	/* options->user will be set in the main program if appropriate */
 	/* options->hostname will be set in the main program if appropriate */
diff -u -p openssh-3.4p1/readconf.h openssh-3.4p1-hash/readconf.h
--- openssh-3.4p1/readconf.h	Sun Jun  9 16:04:03 2002
+++ openssh-3.4p1-hash/readconf.h	Wed Aug 21 11:44:59 2002
@@ -100,6 +100,9 @@ typedef struct {
 	Forward remote_forwards[SSH_MAX_FORWARDS_PER_DIRECTION];
 	int	clear_forwardings;
 	int	no_host_authentication_for_localhost;
+#ifdef HASH_KNOWN_HOSTS
+        int     hash_known_hosts;
+#endif
 }       Options;
 
 
Common subdirectories: openssh-3.4p1/regress and openssh-3.4p1-hash/regress
Common subdirectories: openssh-3.4p1/scard and openssh-3.4p1-hash/scard
diff -u -p openssh-3.4p1/sshconnect.c openssh-3.4p1-hash/sshconnect.c
--- openssh-3.4p1/sshconnect.c	Sun Jun 23 17:23:20 2002
+++ openssh-3.4p1-hash/sshconnect.c	Mon Mar  3 17:34:04 2003
@@ -17,6 +17,11 @@ RCSID("$OpenBSD: sshconnect.c,v 1.126 20
 
 #include <openssl/bn.h>
 
+#ifdef HASH_KNOWN_HOSTS
+#include <openssl/sha.h>
+#include "uuencode.h"
+#endif
+
 #include "ssh.h"
 #include "xmalloc.h"
 #include "rsa.h"
@@ -505,6 +510,11 @@ check_host_key(char *host, struct sockad
 	char msg[1024];
 	int len, host_line, ip_line;
 	const char *host_file = NULL, *ip_file = NULL;
+#ifdef HASH_KNOWN_HOSTS
+        unsigned char md[SHA_DIGEST_LENGTH];
+        char uu[SHA_DIGEST_LENGTH*2];
+#endif
+
 
 	/*
 	 * Force accepting of the host key for loopback/localhost. The
@@ -579,6 +589,26 @@ check_host_key(char *host, struct sockad
 	 * hosts or in the systemwide list.
 	 */
 	host_file = user_hostfile;
+
+#ifdef HASH_KNOWN_HOSTS
+        if (options.hash_known_hosts) {
+               /* 
+                * turn off host ip checking because we take care of it 
+                */
+               options.check_host_ip = 0;
+                              
+               snprintf(hostline, sizeof(hostline), "%s,%s", host,ip);
+               SHA1(hostline, strlen(hostline), md);
+               uuencode(md, SHA_DIGEST_LENGTH, uu, SHA_DIGEST_LENGTH*2);
+               host_status = check_host_in_hostfile(host_file, uu, host_key,
+                   file_key, &host_line);
+               if (host_status == HOST_NEW) { 
+                       host_file = system_hostfile;
+                       host_status = check_host_in_hostfile(host_file, uu, 
+                           host_key, file_key, &host_line);
+               }
+        } else {
+#endif
 	host_status = check_host_in_hostfile(host_file, host, host_key,
 	    file_key, &host_line);
 	if (host_status == HOST_NEW) {
@@ -586,6 +616,10 @@ check_host_key(char *host, struct sockad
 		host_status = check_host_in_hostfile(host_file, host, host_key,
 		    file_key, &host_line);
 	}
+
+#ifdef HASH_KNOWN_HOSTS
+        } /* end if options.hash_known_hosts */
+#endif
 	/*
 	 * Also perform check for the ip address, skip the check if we are
 	 * localhost or the hostname was an ip address to begin with
@@ -662,6 +696,10 @@ check_host_key(char *host, struct sockad
 		if (options.check_host_ip && ip_status == HOST_NEW) {
 			snprintf(hostline, sizeof(hostline), "%s,%s", host, ip);
 			hostp = hostline;
+#ifdef HASH_KNOWN_HOSTS
+                } else if (options.hash_known_hosts) {
+                        hostp = uu;
+#endif
 		} else
 			hostp = host;
 


More information about the openssh-unix-dev mailing list