Saturday, July 23, 2022

Daemonizing a Perl Script with Sysv

    Following up on a previous article in regards to broadcasting UDP data in Perl, here, a need to build a Linux system to run this Perl script at boot time was requested. Simple enough and as with many things in the Perl/Linux world TIMTOWTDI!

    Some background on how all this came about. A door control system was being evaluated and it was determined that it was susceptible to replay attacks. Yes, simply capturing a plaintext broadcasted packet and replaying it would cause this particular door control system to happily lock/unlock doors! Wanting to have a way to train others on how to do this, the messages needed to triggered so that students could capture the messages without requiring both a Window's machine to run the door control software and a person to trigger the controls; Enter Devuan and Perl!

    A quick installation of a minimal Devuan system was performed and then an init script was devised to start the Perl code that would randomly trigger one of four doors periodically. Initially this sounded like it would be far more difficult than it turned out to be and provided a great learning opportunity on how to leverage hash of hashes in Perl (roughly multidimensional arrays in other languages - I'm by no means an expert at why Perl doesn't really have multidimensional array but Gabor Szabo, as usual, has a article on the subject here). None the less, a single multidimensional hash was used to keep track of a number of important things for this door system.

# Four doors allowed by controller
# Status -> Zero = closed, One = open
my %Door_Data = (
  '1' => { 'open' => { 'o_code' => 'f98501', 'o_comm' => '01030303' },
          'close' => { 'c_code' => '796c01', 'c_comm' => '02030303' },
         'status' => 0, 'magic' => '04' }, 
  '2' => { 'open' => { 'o_code' => '853001', 'o_comm' => '03010303' },
          'close' => { 'c_code' => 'e73101', 'c_comm' => '03020303' },
         'status' => 0, 'magic' => '08' }, 
  '3' => { 'open' => { 'o_code' => 'bac601', 'o_comm' => '03030103' },
          'close' => { 'c_code' => 'b9e401', 'c_comm' => '03030203' },
         'status' => 0, 'magic' => '10' }, 
  '4' => { 'open' => { 'o_code' => 'e8ef01', 'o_comm' => '03030301' },
          'close' => { 'c_code' => '90ec01', 'c_comm' => '03030302' },
         'status' => 0, 'magic' => '20' }
  ); 

    So the hash contains a hash which contains a hash; Hash-ception! For anyone interested the rest of the Perl script will be posted on my github since it's not the main purpose of this article. The jist of it is the script simply obtains the network address of the eth0 interface and then randomly chooses a door, determines if the door is opened or closed according to the script (still researching how to determine this from the controller itself), and then determines the proper code and command to send to perform the lock/unlock action against the door.

    Now the process of making the init script to run on Devuan at startup. Launching a Perl program as a daemon on system start-up was not something I'd ever done before but always happy to learn something new. Since this script would rely on a number of system services, getting the dependencies correct would prove to be very important.

#! /bin/sh
### BEGIN INIT INFO
# Provides:          door_lock.pl
# Required-Start:    $local_fs $network $remote_fs $named $time
# Required-Stop:     $local_fs $network $remote_fs $named
# Should-Start:
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Launch door unlock/lock script
# Description:       Launch Perl script as system Daemon
### END INIT INFO

    While extensive testing wasn't done on the required system facilities, it was noted that $local_fs and $network were obviously necessary. The next step was to create the required actions. For this start-up script, and most of the ones I create, start|stop|status|restart are all determined to be good options.

    Each of the different actions would leverage start-stop-daemon and since the Perl script doesn't have code to fork on it's own, the --background option for start-stop-daemon will be needed when starting this process at boot (I plan to go back and utilize fork in the Perl code later though). The previous requirements led to a case statement looking like the following.

PATH='/usr/local/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin'
PIDFILE='/run/door_lockd.pid'
DAEMON='/usr/local/sbin/door_control.pl'
NAME='door_lockd'

. /lib/init/vars.sh
. /lib/lsb/init-functions

case "$1" in
  start)
	log_daemon_msg "Starting Door Control daemon" "$NAME" || true
	if start-stop-daemon --start --background --chuid 0:0 -m --pidfile $PIDFILE --exec $DAEMON; then
		log_end_msg 0 || true
	else
		log_end_msg 1 || true
	fi
	;;

  restart)
	log_daemon_msg "Restarting Door Control daemon" "$NAME" || true
	start-stop-daemon --stop --quiet --oknodo --retry TERM/3/KILL/5 --remove-pidfile --pidfile $PIDFILE
	if start-stop-daemon --start --background --chuid 0:0 -m --pidfile $PIDFILE --exec $DAEMON; then
 		log_end_msg 0 || true
	else
		log_end_msg 1 || true
	fi
	;;

  stop)
	log_daemon_msg "Stopping Door Control daemon" "$NAME" || true
	if start-stop-daemon --stop --quiet --oknodo --retry TERM/3/KILL/5 --remove-pidfile --pidfile $PIDFILE ; then
		log_end_msg 0 || true
	else
		log_end_msg 1 || true
	fi
	;;

  status)
	status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $?
	;;
  *)
	echo "Usage: door_lockd.sh [start|stop|restart|status]" >&2
	exit 3
	;;
esac
exit 0

     Thinking everything was good to go at this point, I used update-rc.d <name_of_script> defaults to install the proper symbolic links to the init script in the proper rcX.d directories and rebooted. Sadly upon reboot the Perl program wasn't running but oddly enough there was a PID file and init messages showed that there weren't any errors starting. So the init script definitely ran but there were no signs of whether the Perl program started. Some further reading about the --background argument lead me to wonder if something failed but start-stop-daemon was unable to check the status. Turned out that wasn't the case and the real issue was determined by removing --quiet from the start-stop-daemon and noticing that the Perl code indeed ran but was attempting to open the necessary socket for sending the UDP data before the network adapter was actually ready! So I modifed the Perl program to no longer die on failure to open a socket but rather warn. I also modified the program so that it wouldn't actually try to send data if the socket failed. Oddly, I was under the impression that the $network variable meant that the network adapter would be online and ready to go (ie configured with whatever protocol was needed on that adapter) before init would attempt to run this init script. None the less, the system was rebooted again and low-and-behold the init script kicked off the Perl program as a background daemon and after waiting a little while for the delay in the script to kick out a UDP message, the ether was once again presented with UDP data being broadcasted out to the world.

root@Devuan:~# tcpdump -i eth0 -nn 'port 60000'
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
19:45:17.1920 IP 192.168.1.19.35133 > 255.255.255.255.60000: UDP, length 1172
19:45:23.1926 IP 192.168.1.19.38198 > 255.255.255.255.60000: UDP, length 1172
19:45:25.1932 IP 192.168.1.19.36199 > 255.255.255.255.60000: UDP, length 1172
19:45:27.1937 IP 192.168.1.19.39466 > 255.255.255.255.60000: UDP, length 1172
19:45:32.1945 IP 192.168.1.19.53527 > 255.255.255.255.60000: UDP, length 1172

    Watching the door locks being evaluated was also entertaining as they were randomly locking and unlocking with no one interacting with the control console: Success!

No comments:

Post a Comment

Have a thought or question? Please share!