# Single-Packet Authentication (Crypto-Port-Knocking) in BASH

## Bones McCracker

Here is a small set of BASH scripts you can take and use to set up your own single-packet authentication, and then customize it to your heart's content.  I believe it is leaner and more secure the the available packaged solutions.

[Edit: note that the port-randomization concepts demonstrated in this post have been incorporated recently into fwknop as a command-line option, and the author has kindly credited this thread for the idea.]

Traditional port-knocking is vulnerable to replay attacks.  While good single-packet authentication tools are not vulnerable to replay, they ARE vulnerable to piggy-back exploits.  Some port-knocking tools install thousands of lines of code and dozens of perl modules and other dependencies.  A couple hundred lines of BASH can do the job, give you complete flexibility, and reduce your vulnerability to scripts designed to attack common configurations.  Analyzing and customizing this script for your needs should also be a good intermediate-level BASH learning exercise (please share your improvements).

The client script uses:

openssl - used to encrypt a tiny unique one-time message

hping - used to construct and send a packet containing the encrypted message

The "daemon" script uses:

tcpdump - listens (outside the firewall) for the very special packet, and captures it

openssl - decrypts the packet's payload

What's different about this approach is that it knocks on a random port every time (one of about 28,000) and makes the service connection on a random port every time.  Some SPA solutions (not fwknop, any more) currently knock on one port -- the same port every time.  That makes it pretty easy to monitor.  And they do not randomize the ports used for service connections, which makes it very easy to simply piggy-back in on the open connection simply by spoofing the connecting client's address and banging away on the predictable port until it opens.

Aside from handling that randomization, this tool more or less imitates what is done by similar open source and commercially-available tools.  The script uses encrypted information in the knock packet to decide how to insert or append iptables rules, opening a firewall port for a specific IP address only, connecting to a specific service behind the firewall, for a period of a few seconds only for a connection to be established, and then closing said port.  The ongoing connection is maintained automatically by netfilter connection tracking and the port continuously appears to be closed to all other addresses, even while our client is connecting.

Please feel free to tell me how terrible my script is, as long as you tell me what I need to do make it better.  : )

I call the client "Ramius", because like the submarine captain of that name in "Hunt for Red October", it sends "one ping only" (you must have seen the movie to appreciate this).  Correspondingly, the daemon is called "hydrophone" because it listens for the ping.

The client consists of two scripts:

ramius: the executable script shared system-wide among any users on the machine (run using su or sudo so hping can access a raw port); this belongs in /usr/loca/bin or someplace like that

ramius_agent: a per-user extension script that runs with user privileges to initiate the actual service connections using the desired interface (e.g., ssh via shell or nautilus).  Each user gets their own copy of ramius_agent so they can customize if desired, and a per-user configuration file; these go in ~/.ramius.

Similarly, the daemon consists of two scripts:

hydrophone: basically a loop that listens for a packet using tcpdump, then parses the packet, then dispatches the airlock script to take action.  Hydrophone is run as a daemon, launched by an initscript.

airlock: instantiated by hydrophone to handle connections once a packet has been accepted and parsed.  Airlock instances are run in parallel to hydrophone, so that multiple connections can be handled in rapid sequence if necessary.  Airlock opens the port (inserts IPTables rules) waits a configurable period of time, and then closes it (removes IPTables rules).

It's easiest to understand by seeing the client first, so you understand what the daemon is decrypting and parsing.

First, a quick peek at the per-user config file.  You could easily make any of these items command-line parameters if you prefer:

```
# ~/.ramius/ramius.conf

# This is the user configuration file for ramius port-knocking script

# your Linux username on target host (the machine you want to log onto)

LOGIN="bonekracker"

# the address of the firewall

FW="bone.yard.com"

# location of the hping binary (or a symlink to it) on this machine:

HPING="/usr/sbin/hping"

# temporary file for outbound packet

TMP="/var/tmp/.ramius_payload"

## the following configurables must match the firewall's settings

# encryption passphrase clause (See OPENSSL(1): PASS PHRASE ARGUMENTS):

PAS="pass:Tr1bbl3s!E@t!Qu@dr0tr1tic@le"

#PAS="file:~/.ramius_kfile"

# firewall's "local port range"

FW_LO=32768

FW_HI=61000
```

This is the main client script.  It sources the config file (above) of the user who runs the script (using su or sudo), and as noted above, it also calls the small per-user extension script "ramius_agent".

```

#!/bin/bash

# ~/bin/ramius

# BoneKracker

# Rev. 13 April 2008

# Purpose:

# Cryptographic port-knocking client (single-packet authentication)

# for secure connection via the hydrophone port-knocking daemon.

# Note: Run using sudo. See USAGE below for command-line options. 

# Installation.  See README.txt

#-----------------------------------------------------------------------------

##### CONFIGURATION

# identify real user and source their configuration file

USER=$(ps -p $PPID -o ruser=)

source /home/${USER}/.ramius/ramius.conf

##### USAGE FUNCTION

f_Usage() {

cat <<EOF

 == USAGE ====================================================================

|  ramius [-[s|r][h|f|w][c|n|x]]                                              |

|                                                                             |

|  This script accepts up to thee single-letter options preceded by a         |

|  single hyphen (such as "ramius" or "ramius -rfn" ).                        |

|                                                                             |

|  You may indicate up to one TARGET option (default = s)                     |

|  -s   "server"                                                              |

|  -r   "router"                                                              |

|                                                                             |

|  You may indicate up to one SERVICE option (default = h)                    |

|  -h   "ssh"         : open firewall port to target for one ssh session      |

|  -f   "ftp"         : open firewall port to target for one ftp session      |

|  -w   "wake-target" : ask firewall to send Wake-On-LAN packet to target     |

|                                                                             |

|  You may indicate up to one UI option (default = c)                         |

|  -c   "cli"         : auto-start service in the shell (command-line)        |

|  -n   "nautilus"    : auto-start service in nautilus (for ssh or ftp)       |

|  -x   "noauto"      : do not auto-start a service (you will do it manually) |

|                                                                             |

 =============================================================================

EOF

exit $E_PARAMS

}

##### EXIT CODES:

E_PARAMS=3              # invalid or wrong number of arguments

E_CRYPT=4               # openssl encryption of the message failed

E_NET=5                 # hping of the firewall failed

##### OPTION PROCESSING:

while getopts ":srhfwcnx" OPTION; do

        case $OPTION in

                s ) [ -z "$TGT" ] && TGT="s" || f_Usage;;

                r ) [ -z "$TGT" ] && TGT="r" || f_Usage;;

                h ) [ -z "$SVC" ] && SVC="h" || f_Usage;;

                f ) [ -z "$SVC" ] && SVC="f" || f_Usage;;

                w ) [ -z "$SVC" ] && SVC="w" || f_Usage;;

                c ) [ -z "$UI" ]  && UI="c"  || f_Usage;;

                n ) [ -z "$UI" ]  && UI="n"  || f_Usage;;

                x ) [ -z "$UI" ]  && UI="x"  || f_Usage;;

                * ) f_Usage;;

        esac

done

shift $(($OPTIND - 1))

# assign default options

TGT=${TGT:="s"} # server

SVC=${SVC:="h"} # ssh

UI=${UI:="c"}   # shell

## catch invalid option combinations

[ "$TGT" == "r" ] && [ "$SVC" == "w" ] && echo "Router is always awake." && exit 0

[ "$TGT" == "s" ] && [ "$SVC" == "w" ] && echo "Server does not currently sleep." && exit 0

##### MAIN LOGIC

## select random ports for the knock and the service connection

# get local "local port range"

LOC_LO=$(awk {'print $1'} /proc/sys/net/ipv4/ip_local_port_range)

LOC_HI=$(awk {'print $2'} /proc/sys/net/ipv4/ip_local_port_range)

# use only ports within local port range of both machines

[ "$LOC_LO" -gt "$FW_LO" ] && U_LO=$LOC_LO || U_LO=$FW_LO

[ "$LOC_HI" -lt "$FW_HI" ] && U_HI=$LOC_HI || U_HI=$FW_HI

# in that resulting set, select a pseudo-random port on which to knock

(( PING_PORT = $RANDOM % (U_HI - U_LO) + U_LO ))

# and select a pseudo-random port for the service connection

(( SVC_PORT = $RANDOM % (U_HI - U_LO) + U_LO ))

## generate 16 random bytes to make this packet extra-unique

NONCE=$(openssl rand -base64 12)

## generate timestamp

TSTAMP=$(date -u +%s)

## concatenate those elements of the message

MSG="${NONCE}:${TSTAMP}:${TGT}:${SVC}:${SVC_PORT}"

## append a cryptographic hash of the message

MSG="${MSG}:$(echo $MSG | openssl md5)"

## encrypt that payload string and temporarily save it

echo $MSG | openssl enc -aes256 -salt -pass $PAS -e -a -A > $TMP || exit $E_CRYPT

## get payload size (in bytes)

SIZE=$(stat -c%s "$TMP")

# send message in a single innocuous ping (a la Hunt for Red October)

echo "Give me a ping, Vasili... one ping only, please."

$HPING -1 -c 1 -y -P -q -d $SIZE -p $PING_PORT -E $TMP $FW &>/dev/null

## clean up

rm $TMP

#### Initate service connection interfaces (with user privileges):

# real user's ramius_agent script

USER_AGENT="/home/${USER}/.ramius/ramius_agent"

# call user agent to initiate the connection via chosen interface

if [ -x $USER_AGENT ]; then

        su ${USER} -c "$USER_AGENT $FW $LOGIN $SVC $SVC_PORT $UI"

else

        echo "Unable to execute ramius_agent script." && exit $E_PARAMS

fi
```

Note: this proof-of-concept version uses a symmetric 256-bit AES-cbc cipher to simplify authentication and produce a relatively small encrypted payload. To avoid packet fragmentation, overall packet size must be smaller than the path MTU.  The code here creates a conservatively small 128-byte payload (156-byte packet), which is well below path MTU in most scenarios.  Although this aes256-based implementation provides a very high degree of security, mature implementations might use larger asymmetric ciphers (e.g. X.509 certs or GPG with keys as large as 1024-2048 bits) while remaining useful in PMTU >=576 settings.  A network of at least ethernet grade under unified control will typically have an MTU of 1500 bytes, but I see no use for SPA payloads that large.  So what you see here works, and it's secure, but you can "bump it up" if you want.

This is the small extension script, found in the user's home directory (~/.ramius/ramius_agent):

```
#!/bin/bash

# ~/.ramius/ramius_agent

# BoneKracker

# Rev. 13 April 2008

# Purpose: extend ramius port-knocking client with user-privileged

# functionality.  Once the root-privileged ramius script has opened a

# firewall port via single-packet authentication, it calls this script

# as the real user to initiate the actual service connection (e.g. ssh,

# ftp, etc.) with user privileges and via the user's chosen interface

# (e.g., shell, nautilus, etc.).

# This file should be in the ~/.ramius directory and executable by the user.

# receive parameters from ramius script

FW=$1

LOGIN=$2

SVC=$3

SVC_PORT=$4

UI=$5

# used to keep redundant host entries from cluttering ~/.ssh/known_hosts

[ -w ~/.ramius/known_hosts.tmp ] && echo "" > ~/.ramius/known_hosts.tmp

## initiate the selected connection (or prompt user to do so)

case $UI in

        c )

                case $SVC in

                        h ) ssh -p ${SVC_PORT} -l ${LOGIN} ${FW} 2>/dev/null;;

                        f ) ftp -p ${FW}:${SVC_PORT};;

                esac

        ;;

        n )

                case $SVC in

                        h ) nautilus ssh://${LOGIN}@${FW}:${SVC_PORT}&;;

                        f ) nautilus ftp://${FW}:${SVC_PORT}&;;

               esac

        ;;

        x )     echo "Okay, manually connect now to ${FIREWALL} on port ${SVC_PORT}."

        ;;

esac

exit 0
```

On the server side: this is the daemon script, which I have located in /root/bin but should probably be in /usr/local/bin or someplace like that:

```
#!/bin/bash

# hydrophone

# BoneKracker

# Rev. 8 May 2008

# Purpose:

# A tcpdump wrapper that serves as a cryptographic port-knocking daemon

# (i.e., "single-packet authentication"). Uses the "airlock" extension script.

##### CONFIGURATION

# interface on which to listen (external, probably)

IFACE="eth0"

# maximum acceptable age of incoming packet (seconds)

AGE_LIMIT="10"

# how long opened ports should remain open (seconds)

WAIT="20"

# encryption passphrase clause (re: OPENSSL(1): PASS PHRASE ARGUMENTS)

#PASSARG="file:/root/.hydrophone_key"

PASSARG='pass:Tr1bbl3s!E@t!Qu@dr0tr1tic@le'

# location of action scripts

AIRLOCK="/root/bin/hydrophone/airlock"

WAKER="/root/bin/hydrophone/wake"

# temporary file used as buffer for incoming packets

BUFFER="/var/tmp/hydrophone/packet_buffer"

# directory to contain persistent variable data files

DATA="/var/lib/hydrophone/"

# compare the last <HIST_SIZE> packets to new ones (to prevent replay attack)

HIST_SIZE="1500"

# pid file to create when started

PIDFILE="/var/run/hydrophone.pid"

# tcpdump expression used to discriminate SPA packets from noise

FILTER='icmp[icmptype] = icmp-echo and (greater 128 and less 576)'

##### EXIT CODES:

E_PARAMS=3              # invalid or wrong number of arguments

E_EXEC=4                # not executable (e.g., permission, not installed)

E_NET=5                 # tcpdump packet capture failed

E_CRYPT=6               # decryption of the payload failed

##### INITIALIZATION:

# verify tools are accessible (and register full paths with shell)

TOOLS="iptables logger openssl tcpdump"

for TOOL in $TOOLS; do

        hash $TOOL || exit $E_EXEC

done

# create directories if missing

[ -d ${BUFFER%/*} ] || mkdir -pm 0770 ${BUFFER%/*}

[ -d ${DATA%/*} ] || mkdir -pm 0770 ${DATA%/*}

# advertise the hydrophone service's running status

echo $$ > $PIDFILE

##### Packet Decomposition Function:

f_decompose() {

## extract tcpdump capture time and source IP address

TIME_CAPTURED=$(sed -n -e '1s/\..*//p' $BUFFER)

IP=$(cut -d " " -s -f 3 $BUFFER)

## extract, decrypt and parse the packet's data into an array

payload=( $(sed -n -r -e '1d' -e 's/.+\.+//g' -e $'p' $BUFFER \

        | openssl enc -aes256 -salt -pass $PASSARG -d -a -A \

        | xargs -d :) ) || exit $E_CRYPT

# assign the array elements to their respective variables,

FIELDS="TIMESTAMP TARGET SERVICE PORT HASH"

i=1     # ignore the Nonce, ${payload[0]}

for FIELD in $FIELDS; do

        eval $FIELD=${payload[i++]}

done

## verify message is timely 

AGE=$(($TIME_CAPTURED - $TIMESTAMP))

[ "${AGE}" -lt "0" ] && AGE=$((0 - $AGE))

if [ "${AGE}" -gt "${AGE_LIMIT}" ]; then

        AGE_STATUS="POSSIBLE REPLAY!"

        REJECT="Y"

else

        AGE_STATUS="okay"

        REJECT="N"

fi

## verify message is original

if [ grep $HASH ${DATA}/history.* &>/dev/null ]; then

        HASH_STATUS="REPLAY!"

        REJECT="Y"

else

        HASH_STATUS="okay"

        echo $HASH >> ${DATA}/history.0

        REJECT="N"

fi

## If the packet is acceptable, dispatch a subshell to take the

## appropriate action while the main process returns to listening.

## stdout/stderr must be redirected or the main process will wait.

if [ "$REJECT" = "N" ]; then

        echo "**Packet Accepted.**"

        case $SERVICE in

                "h" | "f" ) $AIRLOCK $TARGET $IP $SERVICE $PORT $WAIT 2>&1 | logger -t hydrophone: & ;;

                "w"       ) $WAKER $TARGET 2>&1 | logger -t hydrophone: & ;;

                *         ) echo "Invalid Service: ${SERVICE}" && exit $E_PARAMS;;

        esac

else

        echo "**PACKET REJECTED!**"

fi

## announce (log) the packet

echo "Source:    ${IP}

  Timestamp: ${TIMESTAMP}

  Target:    ${TARGET}

  Service:   ${SERVICE}

  Port:      ${PORT}

  Age:       ${AGE} (${AGE_STATUS})

  Hash:      ${HASH} (${HASH_STATUS})"

        

## rotate packet history data files if necessary

if [ "$(wc -l < ${DATA}/history.0)" -gt "$((${HIST_SIZE}/3))" ]; then

        [ -f ${DATA}/history.1 ] && mv ${DATA}/history.1 ${DATA}/history.2

        mv ${DATA}/history.0 ${DATA}/history.1

fi

}

##### Main Loop (Listen & Packet Capture)

while true; do

        tcpdump -c 1 -AttnnNp -i ${IFACE} -s 0 ${FILTER} > ${BUFFER} || exit $E_NET

        f_decompose

done

exit 0
```

And this is the companion module that the daemon dispatches instances of to take action in parallel while it returns to listening.  If multiple packets are accepted in a short period of time, there will be multiple instances of this running in parallel.

Note: You should not try to implement this blindly -- this code here must fit your iptables rules and you will need to customize it.  Specifically, make sure the rules are going into the right chains (I have it set up below to insert into the chains that are created by shorewall -- while the tables should be the same, if you don't use shorewall, you won't have these particular chains.)  Where it says, "/sbin/iptables -t nat -${ACTION} net_dnat -s ${IP} -p tcp -m tcp ", you will have something else instead of "net_dnat".  And where it says, "sbin/iptables -t filter -${!ACTION} net2fw ${RULE} -s ${IP}", you will have something else instead of "net2fw".  The phrases "net_dnat" and "net2fw" are the names of two of my iptables chains.  You will have the names of your iptables chains within the nat and filter tables respectively into which the rules should be inserted or appended. 

```
#!/bin/bash

# /root/bin/hydrophone/airlock

# BoneKracker

# Rev. 16 March 2008

# Purpose: used by hydrophone to open and close ports by appending ("A")

# or inserting ("I") iptables rules, and then deleting ("D") them 

# after waiting a prescribed time.

##### Configuration

SERVER_IP="192.168.1.10"

##### Parameters

# Cannot simply inherit these as a subshell, because their value

# in main script may change during this script instance's "wait" event.

TARGET=$1               # a code for the host (behind our firewall) the client wants access to 

IP=$2                   # the IP address of the requesting client

SERVICE=$3              # a code for the host's service the client wants access to

PORT=$4                 # the external port the client will connect to

HOLD=$5                 # number of seconds to wait before closing opened port

##### airlock-event function

# IPTables uses actions "-A", "-D", and "-I" (i.e. Append, Delete, Insert)

# In this file, we will indirectly substitute "I" for "A" when 

# the rule should be inserted instead of appended

#   - where "append" is appropriate use the parameter: ACTION

#   - where "insert" is appropriate use the parameter: !ACTION

# (If this confuses you see the BASH man page and search for "indirection".)

A="I"; D="D"

## map services to internal ports

# Note: because this script causes all connections to be rewritten by DNAT or

# REDIRECT, all hosts -- including the firewall -- can use the same ports.

# (If that doesn't suit you, add a $TARGET layer to the case block.)

case $SERVICE in

        "f" ) INT_PORT="21";;

        "h" ) INT_PORT="22";;

esac

airlock-event() {

    # RULENUM: we want to insert *after* "related,established accept"; and we

    # don't want "${RULE}" in the delete command).

    [ $ACTION == "A" ] && RULE=" 2" || RULE=""

    case $TARGET in

        r ) # Target: Router (REDIRECT)

            echo "Airlock Event: Router (REDIRECT) ->${ACTION})"

            /sbin/iptables -t nat -${ACTION} net_dnat -s ${IP} -p tcp -m tcp --dport ${PORT} -j LOG --log-prefix "Hydrophone:net_dnat:REDIRECT:" --log-level 6

            /sbin/iptables -t nat -${ACTION} net_dnat -s ${IP} -p tcp -m tcp --dport ${PORT} -j REDIRECT --to-ports ${INT_PORT}

            /sbin/iptables -t filter -${!ACTION} net2fw ${RULE} -s ${IP} -p tcp -m tcp --dport ${INT_PORT} -j ACCEPT

            ;;

        s ) # Target: Server (DNAT)

            echo "Airlock Event: Server (DNAT) ->${ACTION})"

            /sbin/iptables -t nat -${ACTION} net_dnat -s ${IP} -p tcp -m tcp --dport ${PORT} -j LOG --log-prefix "Hydrophone:net_dnat:DNAT:" --log-level 6

            /sbin/iptables -t nat -${ACTION} net_dnat -s ${IP} -p tcp -m tcp --dport ${PORT} -j DNAT --to-destination ${SERVER_IP}:${INT_PORT}

            /sbin/iptables -t filter -${!ACTION} net2dmz ${RULE} -s ${IP} -d ${SERVER_IP}/32 -p tcp -m tcp --dport ${INT_PORT} -j ACCEPT

            ;;

    esac

}

##### main logic

ACTION="A"

airlock-event

sleep $HOLD

ACTION="D"

airlock-event

exit 0
```

And from the README file that I use with the client, this explains the use of an entry in ~/.ssh/config to avoid having the use of random ports for ssh connections fill up your known_hosts file:

```
Installation:

 1. Save this folder (ramius) in your home directory as .ramius/

 2. Set ownership and permissions on the "ramius" file: 

      'cd ~/.ramius'

      'sudo chown root:root ramius'

      'sudo chmod 0750 ramius'

 3. Set (verify) ownership and permissions on the "ramius_agent" file:

      'chown <you>:<you> ramius_agent'Installation:

 4. Put ramius file somewhere on root's path accessible to users (this

    is optional, you can put it wherever you want).

       'sudo mv ramius /usr/local/bin'

 5. Because ramius connects with a different port each time, we ask the

     ssh client not treat each new port as a new host it must track.  We do

     this with an entry in ~/.ssh/config for each firewall you knock on.

   Host <the.firewall.com>

      CheckHostIP no

      StrictHostKeyChecking no

      UserKnownHostsFile ~/.ramius/known_hosts.tmp

If you don't do this, you will be prompted every time to press 'y' to accept

the remote host's key. Because of the large range of ports this script uses,

a single remote host could theoretically have over 28,000 keys consuming 

10.8 MiB.  Refer to SSH_CONFIG(5) for more information.

This sacrifices SSH's host verification logic, which I believe is a small loss relative

to the security against piggy-backing gained by randomizing ports, especially given

the use of public key authentication for the SSH connection, which somewhat 

obviates host verification.

```

Oh, and of course we need an init script to launch the "daemon".  The hydrophone script is not a proper daemon, so this is a bit of a hack.  Suggestions are welcome with regard to making the script more properly "daemonized":

```
#!/sbin/runscript

depend() {

        need firewall

}

start() {

        ebegin "Starting ${SVCNAME}"

        /root/bin/hydrophone/hydrophone 2>&1 | logger -t hydrophone: &

        eend $?

}

stop() {

        ebegin "Stopping ${SVCNAME}"

        kill $(pgrep hydrophone)

        kill $(pgrep tcpdump)

        [ -f /var/run/hydrophone.pid ] && rm /var/run/hydrophone.pid

        eend $?

}
```

Last edited by Bones McCracker on Tue Jun 03, 2008 5:45 am; edited 12 times in total

----------

## UberPinguin

Thanks very much for coming up with this - it addresses a number of concerns I've had about port knocking for a while.

I finally got around to trying to implement your scripts today, but I keep getting this error:

```
ramius_scripting/ramius-orig: line 97: [: : integer expression expected

ramius_scripting/ramius-orig: line 98: [: : integer expression expected

ramius_scripting/ramius-orig: line 101: ((: PING_PORT = 17509 % (U_HI - U_LO) + U_LO : division by 0 (error token is "+ U_LO ")

ramius_scripting/ramius-orig: line 104: ((: SVC_PORT = 18889 % (U_HI - U_LO) + U_LO : division by 0 (error token is "+ U_LO ")

Give me a ping, Vasili... one ping only, please.
```

I'm really not sure how to resolve this.  Do you have any ideas? 

EDIT: It seems that there are some variables that aren't being set: FW_LO, and FW_HI.  How are these configured on your system?

EDIT2: I finally got this beast working.  I added the FW_LO and FW_HI variables to ramius.conf, I had to remove the -a flag from hping, I had to patch hping to work with my wireless card, I had to remove the ${RULE} logic from the airlock script and change A to I to insert the rule at the beginning of the chain instead of trying to append it, and I had to change the ${!ACTION} to $ACTION in the airlock script to get the filter rule set properly.

----------

## Bones McCracker

Sorry I didn't respond in time to be of help.  Thanks for trying this, and I'm glad it works for you.

It appears that I did indeed fail to copy the entire ramius.conf file when I cut-and-pasted it in here, and these lines were missing:

```
# firewall's "local port range"

FW_LO=32768

FW_HI=61000
```

I have added them into the original post.

As I noted, this is a proof-of-concept you can build upon.  Now that you've troubled yourself to look at the code, I'd be interested to hear of any variations you make (and any other problems you encounter -- especially if you fix them like this one).

 :Smile: 

 *Quote:*   

> I had to remove the -a flag from hping

 

Yeah, spoofing won't work because the server-side script opens the firewall only for the IP from which it thinks the packet came.  That's why -a is not in the options I used.

 *Quote:*   

> , I had to patch hping to work with my wireless card, I had to remove the ${RULE} logic from the airlock script and change A to I to insert the rule at the beginning of the chain instead of trying to append it, and I had to change the ${!ACTION} to $ACTION in the airlock script to get the filter rule set properly.

 

Hping seems like a bit of a hack, and I'd rather use something else if anybody has a good idea.  This seemed like the simplest, but I'm not so sure about its quality/documentation, etc.  For example, there's a persistent and meaningless error so it always exits with a non-zero status, which is why I had to ">/dev/null" its output and did not perform any test to see if it succeeded.

One of the reasons I put the "airlock" stuff in a separate script was because it's the portion that is most likely to require customization.  Congratulations on working it out.  You have to know a bit about IPTables to be able to do that.  The !ACTION vs. ACTION thing was supposed to be an easy way to toggle between "appending" vs. "inserting", but indirect substitution is pretty confusing, so whatever works for you and your rule set is good.

Right now I'm trying to figure out why mine is failing intermittently after a rebuild of my desktop (moved to ~x86).   It looks like the packet is no longer getting properly decomposed.  It works for a while and then tells me it can't do math on the timestamp if the timestamp is " ".  So I suppose it's possible that some tool or encryption changed.  I'll have to take a look at the tempfile on the sending and receiving sides to see if I need to modify some bit of code (sed, awk, cut, etc.).  Either that or go back to "freeballing it".   :Razz: 

----------

## UberPinguin

 *BoneKracker wrote:*   

>  *UberPinguin wrote:*   I had to remove the -a flag from hping 
> 
> Yeah, spoofing won't work because the server-side script opens the firewall only for the IP from which it thinks the packet came.  That's why -a is not in the options I used.

 Hmm. Then I'm not sure how it got into mine in the first place, as I copied and pasted it verbatim from this forum post. *shrug*.  *BoneKracker wrote:*   

> Right now I'm trying to figure out why mine is failing intermittently after a rebuild of my desktop (moved to ~x86).   It looks like the packet is no longer getting properly decomposed.  It works for a while and then tells me it can't do math on the timestamp if the timestamp is " ".  So I suppose it's possible that some tool or encryption changed.  I'll have to take a look at the tempfile on the sending and receiving sides to see if I need to modify some bit of code (sed, awk, cut, etc.).  Either that or go back to "freeballing it".  

 I've noticed this a couple of times too.  Usually a second or third attempt gets through, so I'm not sure exactly what's going wrong.  I may put my debugging echoes back in on both ends to see what's being sent, and what's being received.

As far as the hackiness of hping, I agree after looking at its code.  An example being that it determines which physical link layer header size to use based upon the name of the interface (eth/ppp/br/wlan, etc).  However, I'm not aware of another way to assemble custom packets like this and I don't really want to go back to knockd  :Wink: 

I forgot to mention yesterday - the lines dealing with $BUFFER and $HISTORY in hydrophone needed some tweaking.  Specifically, 

```
[ -d ${BUFFER%/*} ] || mkdir -pm 0770 ${BUFFER%/*}

[ -d ${HISTORY%/*} ] || mkdir -pm 0770 ${HISTORY%/*} 
```

 The rest of the script treats $BUFFER and $HISTORY as files, but this portion of the script creates a directory named /var/tmp/hydrophone/packet_buffer%/*/ and one named /var/lib/hydrophone/packet_history%/*/.  Those aren't wildcards - those are actually directories named "*".  I'm not sure what the original intention of these lines was, but I changed them to 

```
[ -f "$BUFFER" ] || touch "$BUFFER"

[ -f "$HISTORY" ] || touch "$HISTORY"
```

 and everything worked like a charm.

With regard to changes and tweaks, later today I'll be working on a version of ramius that uses the same static port (443) for both PING_PORT and SVC_PORT.  The motivation is use behind a very strict http proxy that won't allow outgoing packets on just any port.  I'll still have the security of passphrase-encrypted random packets and replay protection, so the loss of random ports isn't particularly troublesome to me.

----------

## Bones McCracker

No, -a (spoof) was never one of the flags I used.

What this does is simply check to see if the directories exist, and if they don't, creates them.  It shouldn't have caused an error:

```
[ -d ${BUFFER%/*} ] || mkdir -pm 0770 ${BUFFER%/*}

[ -d ${HISTORY%/*} ] || mkdir -pm 0770 ${HISTORY%/*} 
```

This has the same effect as the code below, but avoids use of the external tool 'dirname' (and is therefore a hair faster and not dependent upon the tool's availability).  For that reason, I try to avoid the use of external tool when a built-in can do the job:

```
[ -d $(dirname ${BUFFER}) ] || mkdir -pm 0770 $(dirname ${BUFFER})

[ -d $(dirname ${HISTORY}) ] || mkdir -pm 0770 $(dirname ${HISTORY}) 
```

If you want to demonstrate this to yourself try the following at the BASH prompt:

```
TESTFILE="/var/tmp/testfile"

echo $TESTFILE

echo ${TESTFILE%/*}
```

What you did with 'touch' is fine.  But since the server script is run by root, the directories can be assumed writable if they exist, so the time needed to write the file can be avoided.

With regard to using static ports (in ramius):

I had considered putting in a configuration option where the user can choose a static port in the config file (and if the port is not defined, it assumed that one should be randomly chosen).  While the port randomization adds security, it has the down-side you mentioned as well as the fact that SSH was designed with the assumption of static ports ("known_hosts" are tracked by hostname/address as well as port number).  

So if you do decide to switch it to static ports, you might want to consider the approach of making it a configurable option.  Although, if you're going to use static ports, you might want to just use one of the available packages.

Edit: Oh, and if you do go to a static port for ssh, you can get rid of the ~/.ssh/config file I suggested.Last edited by Bones McCracker on Tue Jun 03, 2008 5:47 am; edited 2 times in total

----------

## Bones McCracker

Regarding the intermittent failure:

The vast majority of the time, the captured packet text looks like this:

```

1210045667.704504 IP 192.168.2.101 > 192.168.2.100: ICMP echo request, id 64793, seq 0, length 138

E.....@.@..W...e...d...,....U2FsdGVkX18JnCaBWsjoBeAtUvuSZJSK47rDR3YcMa8J4xk+97hhKweXufQml5ni

NTgKeujS09+gpJH+F0qzyWKvC7LIg/8KHs3PJLl/XD8y81luJ061atXoCADJP38Z

1210045731.829688 IP 192.168.2.101 > 192.168.2.100: ICMP echo request, id 3866, seq 0, length 138

E...j.@.@.I....e...d..P5....U2FsdGVkX19F9iX9183AOIQn7Jz++PrGDaMZJKf3LGPTVPR8mUCBxeErFlm+V8o2

cEnG28O8lACUJDFiUUnm2ZpZ16W9jtSbowgfo0cVUNcO1LxFJcZDibHV2SrfmZqI

1210045744.825528 IP 192.168.2.101 > 192.168.2.100: ICMP echo request, id 8474, seq 0, length 138

E....n@.@.)....e...d....!...U2FsdGVkX1+xFGMA+jF7gVKfwBZ76MvIaXpwomQgmKWItWostS9SqHUM5Fgakt7k

L+QaW3DGYNTEmISY7tfo8/A0Qlw3kw4tV3elnlkURcFauZGqxoOiEH+tOsOA3XRA
```

The encrypted message string is everything after the last dots.  In the final example above, the encrypted message string is:

```

U2FsdGVkX1+xFGMA+jF7gVKfwBZ76MvIaXpwomQgmKWItWostS9SqHUM5Fgakt7k

L+QaW3DGYNTEmISY7tfo8/A0Qlw3kw4tV3elnlkURcFauZGqxoOiEH+tOsOA3XRA
```

We need the program to isolate the encrypted message string from the rest of the packet text.  The way I have been doing that is this:

```

sed -n -r -e '2s/.+\.+//gp' -e '3p'
```

That says, "on line 2, erase all occurrences of characters followed by dots and keep the rest along with line 3.

As you can see, that would work fine in the cases above.

Here is the problem.  For some reason I do not understand, tcpdump occasionally outputs the packet text looking like this:

```
1210041305.322932 IP 192.168.2.101 > 192.168.2.100: ICMP echo request, id 46360, seq 0, length 138

E....;@.@..

...e...d........U2FsdGVkX18X50k8jc2yXezgPxZb2EulAws8tjnE5qAZpgrC+IYH6Y70cArjtesA

rKByU085UQbCKmkDygErEK56N/j3AXukSAAD0NC2IL8a/gAQTGCNC3iED4rTbmX8
```

Note the extra line break.

So I am rewriting that sed statement to eliminate literal line number references.  Here is what I'm currently trying, although it seems to be intermittently causing its own mangling, resulting in a different openssl error ('bad magic number' instead of 'error reading input file'):

```
--- hydrophone   2008-05-05 23:32:31.927239604 -0400

+++ hydrophone.new   2008-05-06 02:19:28.247668328 -0400

@@ -72,9 +72,9 @@

 

 ## extract, decrypt and parse the packet's data into an array

 

-payload=( $(sed -n -r -e '2s/.+\.+//gp' -e '3p' $BUFFER \

-   | openssl enc -aes256 -salt -pass $PASSARG -d -a \

-   | xargs -d :) ) || exit $E_CRYPT

+payload=( $(sed -n -r -e '1d' -e $!'s/.+\.+//g' -e $'p' $BUFFER \

+        | openssl enc -aes256 -salt -pass $PASSARG -d -a \

+        | xargs -d :) ) || exit $E_CRYPT

 

 # assign the array elements to their respective variables,
```

That says, "trash the first line, then within what's left (without touching the last line) eliminate all occurrences of some char(s) followed by dot(s), then output what's left including the last line."  I suck with sed - learning as I go.

I'd like to avoid extracting the string based on its specific position within the overall packet text and without relying on the fixed prefix that it begins with.  These would change if someone chooses to use a different encryption algorithm or a different packet type.  So I need to simply eliminate everything but the encrypted string (where the dots end), leaving the rest on its natural two lines.  

Apparently my current attempt is somehow altering the string's contents itself; while I have eliminated the original error "error reading input file" caused by the extra line break, I am now getting another openssl decrypt error "bad magic number" (which is similar in that you see it when you feed openssl something it can't decrypt).

Plus, the old error is still there in the rare event that the extra line break is preceded immediately by a character and not a dot:

```
1210057088.498493 IP 192.168.2.101 > 192.168.2.100: ICMP echo request, id 14623, seq 0, length 138

E...<

@.@.x;...e...d...N9...U2FsdGVkX1+igjS04BGx49NHesKCoZFjI98KSeX64gFZyDqTK2hGzZjbLOFSLr3r

HwErGCU2/Wwbvq2CkM7qB0XGo2GmwSvbmbHDWIie9lops/fXAAGZp6xjYlZXXp9M
```

I'm tempted to just extract it based on character position or the encryption string prefix.

More tweaking required.    :Confused: 

----------

## UberPinguin

I'm also getting the "bad magic number" error on occasion, so I don't think it's due to your sed-fu.  I'm afraid I won't be much help with sed, as my regex is not very good at all.

In the current implementation, if the packet is somehow damaged (bad magic number or something else that causes it to be incompletely parsed), hydrophone crashes completely and renders the box remotely inaccessible.  I think we need to come up with a way to protect against that to make the script more robust, but I haven't come up with a workable solution yet.

----------

## Bones McCracker

I probably need to split that complex statement out into its three parts instead of using pipes.  Then I could just log the error and jump out of the function on a non-zero exit code.  That would keep the server-side script from crashing.

Notifying the client of the failure would make it something other than "single-packet" authentication and would require a listener on the client side.  I don't think we want to go there.  The client's connection attempts should perhaps have a limited time-out parameter if that can be included as a command-line option.

My intuition tells me the "bad magic number" problem I am having now is not caused by transmission error (i.e., it's not the occasional bad packet somehow mangled in transit).  I was using this basic setup successfully for a month or so without that problem.  Then, suddenly it's experiencing this.  Although it could be a bug in hping or something, its more likely my own bad programming.

Right now, after implementing the revised sed statement, a half dozen trials show it seems to succeed on the first connection every time, and then fail (bad magic number) on the subsequent connection every time.  At that point I went to bed and haven't got back to it yet.  That might be something simple like bad programming -- a variable that must be reset each time or something like that.

If you are actually interested in putting some thought into this, in addition to this problem here are two other pretty core-level items that I could use somebody else's brain on:

1.  I am running tcpdump one packet at a time.  It's really designed to run and run.  An alternative approach would be to run tcpdump continuously side-by-side with another process that receives the captures packets and then spawns the packet decomposition function in a separate thread for each packet captured.  I was afraid to do that because I didn't want to end up with multiple processes trying to implement changes to the IPTables at the same time.  Theoretically though, this kind of approach should work because of locking mechanisms internal to netfilter.  

Tcpdump could write out to a FIFO, and then a separate loop (a peer process, if you will) could read the FIFO and fork subprocesses to perform packet decomposition (in other words, move the decomposition function into what is now the "airlock" script).  In one of my very early prototypes, I think I was running tcpdump on a continuous basis like that, but I changed to a single-packet approach at some point for reasons I no longer recall.

2.  The "packet history", which is checked to see if the incoming packet's hash has been received before, is currently just a log that grows indefinitely.  It needs to be limited to a certain size or time period, maybe with the oldest packets being removed as new ones arrive.  One idea that occurred to me is a ring-buffer of sorts, where a new file is created daily or weekly and the files are rotated.  Maybe there's a better way.  I wonder if a tiny database of some sort would be more efficient.  The simplest approach would be to occasionally check the length of the file (as in wc -l) and delete the first 10 lines or whatever when it gets too long.  I just haven't got round to giving this any thought yet.

----------

## Bones McCracker

Okay.  Problem fixed.  As far as a couple-dozen rapid-fire connections show, it now works consistently.

It was a combination of things.  These are the changes I made:

1.  Added the -A argument to the 'openssl enc' command to keep its output to a single line (which simplifies the task of parsing it from the surrounding packet text).

2.  Modified the sed statement accordingly.

3.  Put the value of $PASSARG in single-quotes instead of double-quotes.

The last one is a bit mysterious.  I discovered the need to do this by interactively passing an extracted message string to the openssl decryption command used in the server script.  It gave errors when the passarg was only in single quotes, apparently becoming confused by the exclamation point I had chosen to use in the actual passphrase.  I thought this was probably just due to the fact that I was trying it in an interactive shell, but when I made the same change to the actual script, the behavior of "subsequent connections" failing disappeared.

So I will add the changes to the original post.  Note that this change to the sed statement supersedes the one highlighted in the diff I provided above (that one can be ignored).  For your convenience, here is a diff:

```
--- hydrophone.old   2008-05-07 22:17:05.553928684 -0400

+++ hydrophone   2008-05-08 00:18:22.169929563 -0400

@@ -2,7 +2,7 @@

 

 # hydrophone

 # BoneKracker

-# Rev. 26 March 2008

+# Rev. 8 May 2008

 

 # Purpose:

 # A tcpdump wrapper that serves as a cryptographic port-knocking daemon

@@ -21,7 +21,7 @@

 

 # encryption passphrase clause (re: OPENSSL(1): PASS PHRASE ARGUMENTS)

 #PASSARG="file:/root/.hydrophone_key"

-PASSARG="pass:Tr1bbl3s!E@t!Qu@dr0tr1tic@le"

+PASSARG='pass:Tr1bbl3s!E@t!Qu@dr0tr1tic@le'

 

 # location of action scripts

 AIRLOCK="/root/bin/hydrophone/airlock"

@@ -74,8 +74,8 @@

 

 ## extract, decrypt and parse the packet's data into an array

 

-payload=( $(sed -n -r -e '1d' -e $!'s/.+\.+//g' -e $'p' $BUFFER \

-        | openssl enc -aes256 -salt -pass $PASSARG -d -a \

+payload=( $(sed -n -r -e '1d' -e 's/.+\.+//g' -e $'p' $BUFFER \

+        | openssl enc -aes256 -salt -pass $PASSARG -d -a -A \

         | xargs -d :) ) || exit $E_CRYPT

 

 # assign the array elements to their respective variables,
```

----------

## UberPinguin

This change broke hydrophone for me.  Removing the -A from the openssl command restored functionality.  The error was a rather vague "error reading input file".

----------

## Bones McCracker

Did you add the '-A' option to both sides (client and server)?  It needs to go in the 'ssl enc' command in both the hydrophone script and the ramius script.

I suppose I probably should have mentioned that.   :Confused: 

----------

## UberPinguin

Ah, sure enough that was the issue.  Adding -A on both sides made it work  :Smile: 

----------

## Bones McCracker

And here is the fix for the endlessly-growing packet history file.  Be forewarned I was not in a position to test this, but it's fairly trivial so I hope it's ok.  Let me know if you have problems.

Instead of a $HISTORY file, we now have a $DATA directory (to accomodate any future modifications that might also require persistent storage).

In the $DATA directory (i.e. /var/lib/hydrophone), there will be three history files, which will be rotated:

 history.0

 history.1

 history.2

The hash of an incoming packet is appended to history.0.

When history.0 line count exceeds a threshold, the files are rotated like:

  mv history.1 history.2

  mv history.0 history.1

The incoming packets are compared to ${DATA}/history.* for uniqueness.

That "threshold" is one-third of $HIST_SIZE, which is configurable (the total number of records to keep).  I have no idea what to set this to.  I set it conservatively low (1500), but I suspect you could have tens or hundreds of thousands of records without any real performance problem (checking is a simple grep, and on the rare occasion that rotation is required, it will occur in parallel with the "airlock" script (the IPTables manipulation)).  I imagine though that in reality most replay attacks would attempt to use a packet while fresh, probably even instantaneously upon receipt, so it might be quite realistic to set this number much lower, like 100 or so.

Here's the diff (it looks big because I changed a variable name and also moved a block of code).  This is also in the original post if you want to just grab that:

```
--- hydrophone.20080507   2008-05-08 22:11:26.331893329 -0400

+++ hydrophone   2008-05-08 23:06:01.480887153 -0400

@@ -27,11 +27,14 @@

 AIRLOCK="/root/bin/hydrophone/airlock"

 WAKER="/root/bin/hydrophone/wake"

 

-# location for temporary file

+# temporary file used as buffer for incoming packets

 BUFFER="/var/tmp/hydrophone/packet_buffer"

 

-# location for packet history file (suggest rotation at 1 MiB +)

-HISTORY="/var/lib/hydrophone/packet_history"

+# directory to contain persistent variable data files

+DATA="/var/lib/hydrophone/"

+

+# compare the last <HIST_SIZE> packets to new ones (to prevent replay attack)

+HIST_SIZE="1500"

 

 # pid file to create when started

 PIDFILE="/var/run/hydrophone.pid"

@@ -55,7 +58,7 @@

 

 # create directories if missing

 [ -d ${BUFFER%/*} ] || mkdir -pm 0770 ${BUFFER%/*}

-[ -d ${HISTORY%/*} ] || mkdir -pm 0770 ${HISTORY%/*}

+[ -d ${DATA%/*} ] || mkdir -pm 0770 ${DATA%/*}

 

 # advertise the hydrophone service's running status

 echo $$ > $PIDFILE

@@ -100,25 +103,15 @@

 

 ## verify message is original

 

-if [ grep $HASH $HISTORY &>/dev/null ]; then

+if [ grep $HASH ${DATA}/history.* &>/dev/null ]; then

    HASH_STATUS="REPLAY!"

    REJECT="Y"

 else

    HASH_STATUS="okay"

-   echo $HASH >> $HISTORY

+   echo $HASH >> ${DATA}/history.0

    REJECT="N"

 fi

 

-## announce (log) the packet

-

-echo "Source:    ${IP}

-  Timestamp: ${TIMESTAMP}

-  Target:    ${TARGET}

-  Service:   ${SERVICE}

-  Port:      ${PORT}

-  Age:       ${AGE} (${AGE_STATUS})

-  Hash:      ${HASH} (${HASH_STATUS})"

-   

 

 ## If the packet is acceptable, dispatch a subshell to take the

 ## appropriate action while the main process returns to listening.

@@ -135,6 +128,25 @@

    echo "**PACKET REJECTED!**"

 fi

 

+

+## announce (log) the packet

+

+echo "Source:    ${IP}

+  Timestamp: ${TIMESTAMP}

+  Target:    ${TARGET}

+  Service:   ${SERVICE}

+  Port:      ${PORT}

+  Age:       ${AGE} (${AGE_STATUS})

+  Hash:      ${HASH} (${HASH_STATUS})"

+

+   

+## rotate packet history data files if necessary

+

+if [ "$(wc -l < ${DATA}/history.0)" -gt "$((${HIST_SIZE}/3))" ]; then

+   [ -f ${DATA}/history.1 ] && mv ${DATA}/history.1 ${DATA}/history.2

+   mv ${DATA}/history.0 ${DATA}/history.1

+fi

+

 }

 

 ##### Main Loop (Listen & Packet Capture)
```

----------

## UberPinguin

I was still getting intermittent failures on the payload function, so I took a closer look at it and asked for some help from a friend whose bash-fu is far greater than my own.  This is what we came up with, as a substitute for all the sed work and piping:

```
## extract, decrypt and parse the packet's data into an array

data=$(<"$BUFFER")

payload=( $(echo ${data##*.} | openssl enc -aes256 -salt -pass $PASSARG -d -a -A | xargs -d :) )|| exit $E_CRYPT
```

 I've tested it several times, and it seems to work more consistently for me.  Your thoughts?

Also, I still think that we need a way to protect against garbage packets - right now if someone were to send a mangled hping (wrong passphrase or some other cause), it would (and does) crash hydrophone immediately and completely.  I /think/ that's what the || exit $E_CRYPT above is supposed to accomplish, but if so something is going wrong.  Any ideas on this?

[EDIT]I also have a modified version of /etc/init.d/hydrophone:

```
#!/sbin/runscript

depend() {

        need iptables

}

start() {

        ebegin "Starting ${SVCNAME}"

        start-stop-daemon --start --quiet --exec /usr/local/sbin/hydrophone 2>&1 | logger -t hydrophone &

        eend $?

}

stop() {

        ebegin "Stopping ${SVCNAME}"

        start-stop-daemon --stop --quiet --exec /usr/local/sbin/hydrophone

        if  pgrep -fx "logger -t hydrophone" >/dev/null; then kill -s 9 $(pgrep -fx "logger -t hydrophone"); fi

        eend $?

}
```

----------

## Bones McCracker

 *UberPinguin wrote:*   

> I was still getting intermittent failures on the payload function, so I took a closer look at it and asked for some help from a friend whose bash-fu is far greater than my own.  This is what we came up with, as a substitute for all the sed work and piping:
> 
> ```
> ## extract, decrypt and parse the packet's data into an array
> 
> ...

 

Excellent!  Thanks for the help -- and thank your friend too.

I had thought about using ${foo##bar} parameter expansion instead of sed, but didn't think it would work because of the line break at the end of the first line of the packet text that tcpdump outputs.  If it works, let's use that that since it's more readable and also uses only built-ins instead of external tools.

 *UberPinguin wrote:*   

> Also, I still think that we need a way to protect against garbage packets - right now if someone were to send a mangled hping (wrong passphrase or some other cause), it would (and does) crash hydrophone immediately and completely.  I /think/ that's what the || exit $E_CRYPT above is supposed to accomplish, but if so something is going wrong.  Any ideas on this?

 

Yes, I agree.  Although I am no longer having this problem since revising that statement (the -A change), it is definitely too fragile, and your problem demonstrates the need.

The statement your friend enhanced was originally three statements.  I piped them together (which violates readability/simplicity) because I thought that run faster than the original three statements (which must othersie pass the data as either a variable or a tempfile or fd).  The $E_CRYPT is just one of several exit codes assigned centrally in another location for debugging convenience.  $E_CRYPT was originally at the end of the openssl statement when it was alone on its own line.  Its still there because my intuition told me the openssl statement needed some kind of error trapping, and I intended to follow up later.

When I debugged that complex statement the other day, I broke it back out into the three individual statements.  I think what we really want to happen on a "bad packet" is merely return (terminating the decompose function and returning to listening, maybe logging a "bad packet" error).  Although your friend left all the pipes in place, I think we may need to break it back out to an extent.  So, in short, what do you think of the following:

```

## extract, decrypt and parse the packet's data into an array

# read the tcpdump file

PACKET=$(<"$BUFFER")

#extract the payload text and decrypt it (on failure return to listening and log bad packet)

PAYLOAD= $(echo "${PACKET##*.}" | openssl enc -aes256 -salt -pass $PASSARG -d -a -A)

if [ "${$?}" -gt "0" ]; then

     echo "Bad Packet!!"

     echo $PACKET

     return $E_CRYPT

fi

#parse the payload (a colon-delimited string) into an array of values

values=( $( echo "${PAYLOAD} | xargs -d :) )

# assign the values to their respective variables,

FIELDS="TIMESTAMP TARGET SERVICE PORT HASH"

i=1     # ignore the Nonce, ${payload[0]}

for FIELD in $FIELDS; do

        eval $FIELD=${values[i++]}

done

## verify message is timely 

```

 *UberPinguin wrote:*   

> [EDIT]I also have a modified version of /etc/init.d/hydrophone:
> 
> ```
> #!/sbin/runscript
> 
> ...

 

This is great;  thank you.  I was hoping somebody with a better understanding of gentoo's init scripts would do something with this.  I had originally created an initscript using the normal "start-stop-daemon" that looked quite similar, but I had problems making it work.  I figured this was because the hydrophone script itself was lacking some of the normal behavior of a "daemon".  I tried to add some of this (publishing the PID), but I think it's still missing things like error-trapping on the event of an "exit" or in the case of a "crash", that returns control to the parent process (the initscript).  I gave up and went with the crude initscript I originally included.  Do you think anything else needs to be added to the hydrophone script to make it more of a proper "daemon"?  For example, I am vaguely aware there should be some kind of error trapping that occurs when it crashes so the parent process knows and can set the service status accordingly.

Have you tested the initscript to the extent that I should replace the one in the original post?

----------

## avx

Nice one, altough it has some bash-ish - yeah, I know, bash is a dep on gentoo, but I still avoid it whenever possible. I'm trying to get this to perl if you don't mind. Till then, thanks.

----------

## Bones McCracker

 *ph030 wrote:*   

> Nice one, altough it has some bash-ish - yeah, I know, bash is a dep on gentoo, but I still avoid it whenever possible. I'm trying to get this to perl if you don't mind. Till then, thanks.

 

Heck no, I don't mind.   :Smile: 

Like I've said, this was a proof of concept, to demonstrate that people can create their own home-grown SPA solutions.

I'll be very interested to see what you come up with, which I hope you'll share.

----------

## michaelrash

Hi -

The original post regarding the idea to send SPA packets over random ports as well as creating NAT rules for incoming connections

over random ports is a good idea.  I have implemented these ideas within the 1.9.4 release of fwknop, and credited BoneKracker with

the ideas:

http://www.cipherdyne.org/blog/2008/06/single-packet-authorization-with-port-randomization.html

Thanks,

----------

## Bones McCracker

 *michaelrash wrote:*   

> Hi -
> 
> The original post regarding the idea to send SPA packets over random ports as well as creating NAT rules for incoming connections
> 
> over random ports is a good idea.  I have implemented these ideas within the 1.9.4 release of fwknop, and credited BoneKracker with
> ...

 

I'm flattered.   :Smile: 

The port randomization is really just an additive measure I felt would reduce the likelihood of detection, sniffing and a resulting piggy-back intrusion.  Most networks can be sniffed, and it is trivial to perform packet capture/analysis and spoofing.  So the vulnerability is real, if somewhat obscure and only accessible to a hacker with at least some minimal skill and a real desire to break into your particular firewall.

While the randomization does not technically eliminate such risk, it makes it statistically very unlikely as well as enormously more difficult to perform.  While a firewall is (hopefully) just one layer in your security "stack", it is better if there's not a hole in that layer.

I should add that it's my opinion that fwknop is a great tool and the obvious choice for linux users (besides anyone with an oddball interest in creating a "grow your own" solution).

I did find one drawback to port randomization (specific to ssh) worth noting:

Ssh is not designed with idea in mind of randomizing the connection port.  Some of its security features assume a connection from a given client will always originate from the same port.  One work-around is demonstrated in the client-side "ramius connector" extension script and it's installation notes (i.e., the use of a ramius-specific ~/.ssh/config file prevents the ~/.ssh/known_hosts file from receiving a new entry for every new connection and becoming unnecessarily large).

----------

## michaelrash

 *BoneKracker wrote:*   

> 
> 
> The port randomization is really just an additive measure I felt would reduce the likelihood of detection, sniffing and a resulting piggy-back intrusion.  Most networks can be sniffed, and it is trivial to perform packet capture/analysis and spoofing.  So the vulnerability is real, if somewhat obscure and only accessible to a hacker with at least some minimal skill and a real desire to break into your particular firewall.
> 
> While the randomization does not technically eliminate such risk, it makes it statistically very unlikely as well as enormously more difficult to perform.  While a firewall is (hopefully) just one layer in your security "stack", it is better if there's not a hole in that layer.

 

If the concern is about an attacker that can sniff the network, why does randomizing the destination port for the follow-on connection (say, SSH) make a piggy-back attack more difficult to perform?  If the attacker can sniff traffic, it is trivial to watch for the outbound SSH connection over *any* port, and it doesn't matter if SPA is used (except of course that an attack against a userspace vulnerability in the remote SSH daemon can only be accomplished over an established TCP connection, which the attacker cannot establish when spoofing a SYN packet - hence SPA is still useful).  If you mean that the port randomization feature makes it harder for those attackers that are not expecting SSH connections over anything but port 22 and are therefore just not watching for this, then sure I agree.  IMHO, the main benefit of port randomization is that it adds an element of unpredictability for those who are analyzing networks with certain assumptions in mind, but watching for SSH connections over a non-standard port is easy.

 *BoneKracker wrote:*   

> 
> 
> I should add that it's my opinion that fwknop is a great tool and the obvious choice for linux users (besides anyone with an oddball interest in creating a "grow your own" solution).
> 
> 

 

Thanks.

 *BoneKracker wrote:*   

> 
> 
> I did find one drawback to port randomization (specific to ssh) worth noting:
> 
> Ssh is not designed with idea in mind of randomizing the connection port.  Some of its security features assume a connection from a given client will always originate from the same port.  One work-around is demonstrated in the client-side "ramius connector" extension script and it's installation notes (i.e., the use of a ramius-specific ~/.ssh/config file prevents the ~/.ssh/known_hosts file from receiving a new entry for every new connection and becoming unnecessarily large).

 

Agreed, although this seems like it might be a candidate for using the -o option on the ssh client command line.  Also, a modified version of my OpenSSH/fwknop integration patch could be developed without the need for a custom config:

http://trac.cipherdyne.org/trac/fwknop/browser/fwknop/trunk/patches/openssh-4.3p2_SPA.patch?rev=1114

----------

## Bones McCracker

 *michaelrash wrote:*   

> If the concern is about an attacker that can sniff the network, why does randomizing the destination port for the follow-on connection (say, SSH) make a piggy-back attack more difficult to perform?  If the attacker can sniff traffic, it is trivial to watch for the outbound SSH connection over *any* port, and it doesn't matter if SPA is used (except of course that an attack against a userspace vulnerability in the remote SSH daemon can only be accomplished over an established TCP connection, which the attacker cannot establish when spoofing a SYN packet - hence SPA is still useful).  If you mean that the port randomization feature makes it harder for those attackers that are not expecting SSH connections over anything but port 22 and are therefore just not watching for this, then sure I agree.  IMHO, the main benefit of port randomization is that it adds an element of unpredictability for those who are analyzing networks with certain assumptions in mind, but watching for SSH connections over a non-standard port is easy.

 

Yes, that's the essence of it.  But also, I think it's a necessary multiplier of the value of using a restricted time window.  I'm going somewhat on instinct here so forgive me for not explaining my (possibly invalid) thinking more clearly earlier.  Please do correct me where I'm wrong.

The value of only opening a port for a brief window of time (e.g. 30 seconds) is that it's not enough time for most people to identify the connection, identify the address of the connecting client, set up to spoof the address of the connecting client, interdict the client (if necessary), and connect.  However, if the port never changes, this is somewhat invalidated.

In the fixed service port scenario, once the attacker has detected and monitored one connection, they know a lot of useful information.  They know the source and destination addresses, they know the ports.  With this information they can:

- attempt a simple* piggy-back attack any time they detect a connection simply by spoofing the address:port (which they know)

- efficiently capture future SPA packets themselves, which they can then analyze to attempt to identify the algorithm in use

- zero in on subsequent connections to footprint the operating systems and select a sequence number prediction algorithm

Now, granted, in the case of SSH in particular, a single connection attempt (or even a 30-second window) is of limited value.  But the SPA should be able to serve a wide variety of protocols, many of which may be less secured.  And as I've said, having multiple layers doesn't mean a hole in one is acceptable.

*Also granted, a properly-constructed, stateful firewall will drop the spoofed connection's SYN attempt if it arrives after the legitimate client's.  But if it arrives first, the attacker will have successfully interdicted the client.

To arrive first, the attacker would require an automated attack set up on a hair trigger to launch when the SPA packet is detected.  They don't have time to filter thousands of packets across thousands of ports, identify the SPA connection, capture the packet, extract the port number, configure to spoof the address and port, and launch a connection.  Even if automated, this will take enough time to make a difference.

But, if they know all that information in advance, I think they **do** have time.  When the first packet -- any packet -- passes the sniffer with that address and port, the sniffer automatically triggers a burst of connection attempts.  They are at least as likely as the legitimate client to successfully pass through the firewall.

Furthermore, the same time constraints make a more sophisticated attack involving interdiction via sequence number prediction more difficult as well.

 *michaelrash wrote:*   

> 
> 
> Agreed, although this seems like it might be a candidate for using the -o option on the ssh client command line.  Also, a modified version of my OpenSSH/fwknop integration patch could be developed without the need for a custom config:
> 
> http://trac.cipherdyne.org/trac/fwknop/browser/fwknop/trunk/patches/openssh-4.3p2_SPA.patch?rev=1114

 

Yes.  That's the answer I came up with.  The "connector scripts" which are user-customizable extensions, use SSH's -o option to "turn off" strict host checking (which avoids the user having to acknowledge/accept a new known_hosts entry for each connection) and to specify a temporary known_hosts file which is deleted after the script is run (which avoids the endless bloat of the known_hosts file).  The drawback is that this bypasses SSH's host-checking algorithm, which is also a means to avoid spoofing-style attacks.  So there is a trade-off at play here.

----------

## UberPinguin

Finally, with some help from SteveL, I managed to crack the stability issue when malformed packets or packets with the wrong password come in to the hydrophone script.  Previously, this would crash the entire script, essentially creating a DoS.

Around line 81, I changed the payload= assignment statement to the following: 

```
pingCrypt() { (($#>1)) || { echo -E "Bad use of $FUNCNAME">&2; exit 1;}; local pass=$1; shift; printf '%s\n' "$@" | openssl enc -aes256 -salt -pass "$pass" -d -a -A | xargs -d :; return "${PIPESTATUS[1]}"; }

if payload=( $(pingCrypt "$PASSARG" "${data##*.}") ); then
```

  The rest of f_decompose() proceeds as before, ending with: 

```
else

        echo "Bad packet. Exit code $E_CRYPT"

fi

}
```

Now, if a bad packet comes in an error is logged and the script goes back to listening.  I've tested it pretty thoroughly here.

I'd post a diff, but I've lost my original copy of hydrophone, and there isn't a complete copy posted anywhere in this thread that patches cleanly for me.

----------

## Bones McCracker

Awesome!  I have not been having any problems, but that may be circumstantial. 

Regarding this code: I like the use of PIPESTATUS (a builtin I didn't know about) as an alternative to breaking out the statements (so we can check the return code of the openssl enc statement.  I'd like to use that.  The alternative is breaking up that series of pipes and checking exit status of the openssl command (and 'return' if it's bad, to abort the f_decompose function).

However, other than the use of PIPESTATUS (and possibly printf) this function seems to introduce some inefficiency.  Both of those can be used inline without nesting an additional function.  Theoretically at least, the additional function call introduces overhead, as does its argument-handling.  I don't follow the need to check the argument count, since the function call is intrinsic to the script (there will always be the right number of args).  I'm not sure why printf is preferable to echo, but that could be substituted in the code as it stands without a function.  I realize the rationale here may be over my head, so don't hesitate to educate me.    :Smile: 

Maybe it would be more efficient to simply use PIPESTATUS like this:

```
data=$(<"$BUFFER")

payload=( $(echo ${data##*.} | openssl enc -aes256 -salt -pass $PASSARG -d -a -A | xargs -d :) )

[ ${PIPESTATUS[1]} -gt 0 ] && return 1

  .

  .

  .

##### Main Loop (Listen & Packet Capture)

while true; do

        tcpdump -c 1 -AttnnNp -i ${IFACE} -s 0 ${FILTER} > ${BUFFER} || exit $E_NET

        f_decompose || echo "Packet Decryption Failed" | logger -t hydrophone

done

```

Once we decide on this, why don't you make whatever changes are called for (if any) to your copy and post it here (since you've been actively working on it).  I'll copy that and we'll be on the same page.    :Smile: 

----------

## .yankee

Seems like a great set of scripts, BoneKracker!

Though this makes little sense:

```

[ "$TGT" == "r" ] && [ "$SVC" == "w" ] && echo "Router is always awake." && exit 0 

[ "$TGT" == "s" ] && [ "$SVC" == "w" ] && echo "Server does not currently sleep." && exit 0

```

It actually leaves the -w option unusable. But I guess you just forgot to add support for WOL and left it that way to be modified sometime in the future...

----------

## Bones McCracker

 *.yankee wrote:*   

> Seems like a great set of scripts, BoneKracker!
> 
> Though this makes little sense:
> 
> ```
> ...

 

Yes, that's right.  They are just place-holders.  I didn't have a need for it, but I wanted to make it clear that you could use a single packet as an event trigger for basically anything (sleep was one thing that came to mind, although you could have it toggle a service on or off, run a job, percolate your coffee, trigger the Self-Destruct Sequence, etc.).

I'm still running this, by the way.  It's a hack, but it works.

[Edit: I didn't include script for WOL, but it's trivial.  Emerge the package and then do something like:

```

network_address="X.X.X.255"

mac_address="XX:XX:XX:XX:XX:XX"

wakeonlan -i $network_address $mac_address  1>/dev/null
```

----------

