Seguridad informática para paranoicos: Crypto-Port-Knocking

Bajo la premisa: «No es que yo sea paranoico, es que me están siguiendo…» alguien ha creado un par de scripts que hacen a tu pc ridículamente segura y hasta le ha dado nombre (y apellido); Crypto Port Knocking 0 Single packet authentication. –o ambas cosas juntas, en realidad

Sencillamente genial.

¿Creíste que tu Linux con SELinux, ACL, Port knocking, Fail2ban, Snort, OSSEC, sin puertos abiertos, con autenticación SSH basada en llaves, con el usuario root deshabilitado y todos los servicios enjaulados, los DNS y la tabla ARP estática era seguro?

No… No es seguro. Al menos no para el creador de esta obra maestra de la seguridad que ha llevado la cosa a tal extremo que raya con el absurdo.

Si te interesan estos temas, después del salto viene lo bueno. Es fundamental tener una noción básica de bash y entender un poco de inglés para echarlo a andar:  

Yendo por partes: El port knocking tradicional, es vulnerable a ataques de tipo replay. Alguien en el medio puede capturar toda la trama y usarla para repetir la secuencia de knocks, algo parecido pasa con el otro método, single packet authentication, y de allí debe venir la idea original: Prevalecer en donde todo lo demás es vulnerable.

Conceptualmente, la idea general es: Al igual que port knocking, un paquete de datos puntual, que viaja encriptado y golpea en un puerto aleatorio. Detrás del firewall hay un tcpdump escuchando. Cuando se recibe el paquete el mismo script que controla tcpdump abre un puerto -aleatorio de nuevo- durante apenas unos pocos segundos, los suficientes para que la conexión se establezca y luego lo cierra. Este puerto de allí en adelante figurará -de cara a internet- como cerrado.

En este caso puntual los scripts trabajan controlando los puertos 21 y 22 tcp, SSH o FTP, pero bien se podría adaptar facilmente para que funcione con cualquier otro servicio o dispare tal o cual acción en concreto en el servidor a la escucha.

Todo el sistema se basa en 4 scripts de bash configurables y funciona siempre y cuando haya instalados en el servidor:

  • tcpdump – Será el encargado de escuchar en busca del paquete «gatillo» que inicia todo el proceso.
  • openssl – Desencriptará el paquete gatillo.

Y en el cliente:

  • openssl – Será el encargado de encriptar el primer paquete que hará las veces de gatillo.
  • hping – Usado para construir el paquete gatillo y hacerlo llegar hasta el servidor.

En el servidor corren dos scripts de bash:

  • hydrophone que es el que pone a funcionar tcpdump y lo mantiene a la escucha del paquete gatillo.
  • airlock que es el que controla a iptables. Abre un puerto aleatorio durante un período de tiempo configurable y luego lo cierra cuando es llamado por hydrophone luego de recibir el paquete gatillo.

El cliente por otro lado consta de dos scripts:

  • ramius, que tiene que ser accesible y ejecutable por todos los usuarios escalando privilegios con su o sudo para que hping pueda funcionar y va copiado en /usr/bin o cualquier otro lugar dentro del path.
  • ramius_agent,  que es una extensión del sript anterior y puede ser configurado de manera diferente para las necesidades de cada usuario en cuestión. Ubicarlo en ~/.ramius

~/.ramius/ramius.conf:

# ~/.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

/usr/bin/ramius:

El script principal que hace funcionar al cliente.

#!/bin/bash

# ~/bin/ramius
# John Brendler
# 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

~/.ramius/ramius_agent:

#!/bin/bash
# ~/.ramius/ramius_agent
# John Brendler
# 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

/usr/bin/hydrophone:

#!/bin/bash

# hydrophone
# John Brendler
# 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

/usr/bin/airlock:

#!/bin/bash
# /root/bin/hydrophone/airlock
# John Brendler
# 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

Se debe tener especial cuidado en este último script y no implementarlo a la ligera, net_dnat y net2fw son nomenclaturas de dos cadenas de iptables que usa Shorewall y que no necesariamente vas a encontrar en tu firewall.

Todo el crédito a John Brendler, alias BoneKracker, que lo posteó en los foros de Gentoo en este artículo original, en inglés y mucho mejor comentariado.

6 comentarios

    1. Señor canibal, no falta absolutamente nada mas que usted se tome la molestia de leer el artículo completo antes de comentarlo, para que se dé cuenta de que se menciona la fuente y al autor del artículo original.

      Leo, luego existo, luego comento.

      Saludos!

      1. No veo que DrLecter sugiera que «no se menciona la fuente y al autor del artículo original», sólo dice que falta la extensión «.html» en el enlace al original, lo cual es cierto.

        Leo, luego existo?, ENTIENDO, ahora si existo, luego respondo comentarios.

        Saludos!!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *