=^.^=

Installing addrwatch on FreeBSD/OPNsense

karma

addrwatch is a tool for monitoring the ARP traffic on a subnet. From its GitHub page:

This is a tool similar to arpwatch. It main purpose is to monitor network and log discovered ethernet/ip pairings.

Main features of addrwatch:

  • IPv4 and IPv6 address monitoring
  • Monitoring multiple network interfaces with one daemon
  • Monitoring of VLAN tagged (802.1Q) packets.
  • Output to stdout, plain text file, syslog, sqlite3 db, MySQL db
  • IP address usage history preserving output/logging
  • Addrwatch is extremely useful in networks with IPv6 autoconfiguration (RFC4862) enabled. It allows to track IPv6 addresses of hosts using IPv6 privacy extensions (RFC4941).

The main difference between arpwatch and addrwatch is the format of output files.

Arpwatch stores only current state of the network ethernet/ip pairings and allows to send email notification when a pairing change occurs. This is fine for small and rather static networks. In arpwatch case all the history of pairings is saved only in administrators mailbox. When arpwatch is used for monitoring dozen or more networks it becomes hard to keep track of the historic address usage information.

I chose to install addrwatch on ISP routers to monitor for:

  • ARP spoofing - clients may change their MAC to evade access control mechanisms, accounting, request new leases or otherwise obscure their identity. It is noteworthy that it is the default behaviour of modern Android and iOS devices to randomize their MAC address upon connecting to a new WiFi network; however it is the default behaviour that they will retain this association and present the same address upon reconnection.
  • ARP poisoning - the quintessential layer 2 MitM attack, allowing an adversary to fully intercept (optionally forwarding) all traffic to and from a different, legitimate host (i.e. the victim's gateway) for inspection, injection and/or exfiltration.
  • Malfunctioning devices - on occasion I have seen embedded devices go haywire, spewing broadcast storms of ARP responses with randomized source MAC addresses. This can overwhelm a router/bridge/switch's ARP table with bogus entries resulting in a DoS at worst and cache starvation at best.
  • CPE equipment changes - detecting a change in hardware at the customer's premises (i.e. a new router's installation)
  • Unauthorized IP address changes - where a client may have accidentally or intentionally forced a new layer 3 address that was not assigned to them. This could indicate a router's loss of configuration (reversion to DHCP) or the subversion of access controls, bandwidth allocation or accounting.

On OPNsense one can install addrwatch from the community repo via pkg:

pkg install addrwatch

...or compile it from source via ports:

opnsense-code ports cd /usr/ports/net/addrwatch pkg install autoconf pkgconf automake make make install

As covered in the previous article, Create an Unprivileged User Account on FreeBSD/OPNsense, it is best practice to run daemons under their own user account. To create an unprivileged user account for addrwatch run:
adduser -d /nonexistent -D -u 0 -w no -s /usr/sbin/nologin Username: addrwatch Full name: addrwatch Daemon ...

Although the man page that ships with the latest version of addrwatch indicates that it is possible to pipe logs into a standard syslog interface with the -l or --syslog flags it appears this capability has been dropped. Therefore, unless you intend to take advantage solely of the mysql or sqlite logging abilities, we will need somewhere to store our log files (-o or --output):
mkdir /var/log/addrwatch chown addrwatch:addrwatch /var/log/addrwatch

Next it's necessary to create a syshook script to ensure the daemon isEdit started at bootup. Per the syshook documentation, we will prepend the script's file name with 50-: /usr/local/etc/rc.syshook.d/start/50-addrwatch and populate it thus:
#!/bin/sh ADDRWATCH_EXEC="/usr/local/bin/addrwatch" ADDRWATCH_USER="addrwatch" # See https://foxpa.ws/unprivileged-user-on-freebsd ADDRWATCH_LOGFILE="/var/log/addrwatch/addrwatch.log" ADDRWATCH_PIDFILE="/var/run/addrwatch.pid" ADDRWATCH_RATELIMIT="-r 0" # [-r (num)] | >0 = seconds within which duplicate events shall be discarded | 0/[empty] = record all events | -1 = suppress duplicates indefinitely ADDRWATCH_LISTEN_INTERFACES="xn1" ADDRWATCH_PARAMS="" $ADDRWATCH_EXEC -d -u $ADDRWATCH_USER -o $ADDRWATCH_LOGFILE -p $ADDRWATCH_PIDFILE $ADDRWATCH_RATELIMIT $ADDRWATCH_PARAMS $ADDRWATCH_LISTEN_INTERFACES

Note that you will want to populate the ADDRWATCH_LISTEN_INTERFACES variable with a space delimited list of interfaces to listen on. In this situation, where I am listening from a gateway, I want to only hear ARP traffic from my LAN interface(s). In this instance, that is my second interface provided by the Xen paravirtualized netfront driver, xn1. Your interface name(s) will likely be different and can be obtained by running ifconfig. Leaving this variable blank will result in addrwatch listening on only the first available non-loop interface, which would be fine for example on a network analysis node or workstation in hostile territory possessing only one interface.

Let's make a stop script to mirror this configuration at /usr/local/etc/rc.syshook.d/stop/50-addrwatch
#!/bin/sh /bin/pkill -KILL addrwatch exit 0

Do not forget to set the execute bits on these scripts:
chmod +x /usr/local/etc/rc.syshook.d/start/50-addrwatch chmod +x /usr/local/etc/rc.syshook.d/stop/50-addrwatch

Optionally, we can add configd actions which can later be called up by the configctl addrwatch action command, via the WebGUI backend in a custom plugin or front end as a cron job. Create /usr/local/opnsense/service/conf/actions.d/actions_addrwatch.conf:
[start] command:/usr/local/etc/rc.syshook.d/start/50-addrwatch type:script message:starting addrwatch description:Start addrwatch [stop] command:/usr/local/etc/rc.syshook.d/stop/50-addrwatch type:script message:stopping addrwatch description:Stop addrwatch [restart] command:/usr/local/etc/rc.syshook.d/stop/50-addrwatch; /usr/local/etc/rc.syshook.d/start/50-addrwatch type:script message:restarting addrwatch description:Restart addrwatch service [reset] command:kill -9 `cat /var/run/addrwatch.pid`; rm /var/run/addrwatch.pid type:script message:resetting addrwatch description:Kills addrwatch by PID (if still running) and removes PID file [status] command:tail /var/log/addrwatch/addrwatch.log type:script_output message:displaying tail of addrwatch logfile description:Shows the last lines of addrwatch's logfile

Now it's necessary to create a newsyslog configuration to ensure that the log file is rotated, as on a busy network with ratelimiting set low or disabled we will quickly run out of storage. Create /usr/local/etc/newsyslog.conf.d/addrwatch.conf:
# configuration file for newsyslog for addrwatch # # see newsyslog.conf(5) for details # # logfilename [owner:group] mode count size when flags [/pid_file] [sig_num] /var/log/addrwatch/addrwatch.log addrwatch:addrwatch 644 7 1024 * JX /var/run/addrwatch.pid

In my situation I quickly realized that the meat-and-potatoes I was looking for would easily be drowned out with ratelimiting set too low. At the same time, with ratelimiting set too high I would be missing important details like flip-flopping. I chose to run two instances side-by-side, logging to separate locations so I can rapidly identify unusual behaviour then optionally refer to the detailed record. This was accomplished by simply duplicating the syshook script and newsyslog configuration, setting the ADDRWATCH_RATELIMIT to disabled (0) in one and indefinite (-1) in the other.

/usr/local/etc/rc.syshook.d/start/50-addrchange:
#!/bin/sh ADDRWATCH_EXEC="/usr/local/bin/addrwatch" ADDRWATCH_USER="addrwatch" # See https://foxpa.ws/unprivileged-user-on-freebsd ADDRWATCH_LOGFILE="/var/log/addrwatch/addrchange.log" ADDRWATCH_PIDFILE="/var/run/addrchange.pid" ADDRWATCH_RATELIMIT="-r -1" # [-r (num)] | >0 = seconds within which duplicate events shall be discarded | 0/[empty] = record all events | -1 = suppress duplicates indefinitely ADDRWATCH_LISTEN_INTERFACES="xn1" ADDRWATCH_PARAMS="" $ADDRWATCH_EXEC -d -u $ADDRWATCH_USER -o $ADDRWATCH_LOGFILE -p $ADDRWATCH_PIDFILE $ADDRWATCH_RATELIMIT $ADDRWATCH_PARAMS $ADDRWATCH_LISTEN_INTERFACES

It's important to ensure that the ADDRWATCH_PIDFILE is at a different location from the other instance, as newsyslog will use the PID written to this location as the target of a SIGHUP signal, which causes addrwatch to reload the output file (releasing its lock and directing output to the new inode after it has been rotated out).

/usr/local/etc/newsyslog.conf.d/addrchange.conf:
# configuration file for newsyslog for addrwatch # # see newsyslog.conf(5) for details #It's important to ensure that the ADDRWATCH_PIDFILE is at a different location, as newsyslog will use the PID written to this location as the target of a SIGHUP signal, which causes addrwatch to reload the output file (releasing its lock and directing output to the new inode after it has been rotated out). # logfilename [owner:group] mode count size when flags [/pid_file] [sig_num] /var/log/addrwatch/addrchange.log addrwatch:addrwatch 644 7 1024 * JX /var/run/addrchange.pid

Create an Unprivileged User Account on FreeBSD/OPNsense

karma

On UNIX-like systems it is considered best practice to run daemons and other automated/resident/specialized software under their own, restricted user accounts. This makes it easier to curtail unwanted or unexpected behaviors, whether due to stupidity (bugs) or malice (attack), as the fundamental unit of permissions and access control is the user account. Filesystem permissions, for instance, are determined wholesale at the user and group level. This remains true across flavours and regardless of finer-grained solutions operating at a higher level (RBACs like SELinux, AppArmour, Linux capabilities, etc.) - though they should not be disregarded where enforced.

Conventionally, daemons are assigned a UID below 1000 - where actual, human-operated UIDs begin. It is also common practice to assign no password (represented by an asterisk (*) in the password hash field of the /etc/passwd flatfile. The default post-authentication shell is typically set to /usr/sbin/nologin or /usr/bin/false. In some cases one may wish to specify a home directory where a daemon commits most of its writes to, or where a relative path might be expected to descend from. Most of the time, however, specifying a home directory is pointless and creating one simply to go unused (particularly under the default /home, where one expects to only find fully-fledged user home directories) is untidy. Therefore, by convention one will generally specify the special, virtual directory /nonexistent, which adduser will catch and decline to create a new directory for the account - contrary to its default behaviour. Alternatively, the /var/empty directory tends to be used where a chroot jail is implemented.

To line these conditions up with FreeBSD's adduser binary one may use the following command; note that unless you are providing a complete user record in colon-delimited format with the -f flag, this is an interactive operation.

adduser -d /nonexistent -D -u 0 -w no -s /usr/sbin/nologin Username: username Full name: username Daemon Uid [6]: Login group [username]: Login group is username. Invite username into other groups? []: Login class [default]: Shell (csh sh tcsh bash git-shell opnsense-shell opnsense-installer nologin) [nologin]: Home directory [/nonexistent/username]: Home directory permissions (Leave empty for default): Use password-based authentication? [no]: Lock out the account after creation? [no]: Username : username Password : <disabled> Full Name : username Daemon Uid : 6 Class : Groups : username Home : /nonexistent/username Home Mode : Shell : /usr/sbin/nologin Locked : no OK? (yes/no): yes adduser: INFO: Successfully added (username) to the user database. Add another user? (yes/no): no Goodbye!

Flags employed:

  • -d /nonexistent - home directory "partition" - create a directory of the same name as the new account under this path.
  • -D - do not create a corresponding home directory. This is obviated when specifying /nonexistent with the -d flag.
  • -u 0 - assign a UID from available numbers beginning at the specified number; the default value is 1000 which is conventionally used to delineate system/unprivileged accounts from fully-fledged, human users. Specifying 0 will select the soonest available UID starting from root.
  • -w no - disables the password function of the account, effectively rendering it usable but not login-able.
  • -s /usr/sbin/nologin - specifies the login shell, in the case of an unprivileged user either nologin or false - either immediately rejects the login if, for whatever reason or misimplementation, it became possible to log in.

Fix XFCE 4 Panel Disappears

karma

If your XFCE panel (taskbar) disappears during your session - barring any severe persistent issues - you can revive it by either leveraging an open terminal session or, where none exists, you can launch the graphical command line by pressing Alt+F2 and running xfce4-panel.

Use Windows PowerShell to Make Multiple Shortcuts Always Run their Target as Administrator

karma

To always run a shortcut's target under the Administrator account one will typically:

  • Right-click on the shortcut file in File Explorer
  • Click the Properties item in the context menu
  • Click the Advanced button
  • Check the Run as administrator checkbox

However, what is one to do when presented with a large batch of such files? Group-selecting them will reveal a Properties window of limited options, excluding the ability to mass-apply the Run as administrator option to the group.

[attachment-IzbIbr]
Illustrating how to conventionally set the Run as administrator setting on a single shortcut

We can leverage an advanced shortcut editing JScript like shortcutJS.bat from https://github.com/npocmaka/batch.scripts/blob/master/hybrids/jscript/shortcutJS.bat (Download raw script file) which will effectively hex-edit the shortcut files in-place to set the Run as administrator bit in the file's binary code. For posterity the script's source (as of March 2024) will be provided at the end of this article.

Save shortcutJS.bat somewhere within your PATH so it can be easily re-used; I chose to drop my copy into C:\Windows\System.

Now we can run a short PowerShell scriptlet that loops through the current working directory's contents where file names end in *.lnk:

$files = Get-ChildItem ./ -Include *.lnk; foreach ($f in $files) { $filename = $f.FullName shortcutJS.bat -edit $filename -adminpermissions yes }

The following is an exact reproduction of shortcutJS.bat from npocmaka / batch.scripts GitHub repository as of March 8, 2024 to ensure it can not disappear or become broken for the purposes of this article:
@if (@X)==(@Y) @end /* JScript comment @echo off cscript //E:JScript //nologo "%~f0" "%~nx0" %* exit /b %errorlevel% @if (@X)==(@Y) @end JScript comment */ var args=WScript.Arguments; var scriptName=args.Item(0); //var adminPermissions= false; var edit= false; function printHelp() { WScript.Echo(scriptName + " -linkfile link -target target [-linkarguments linkarguments] "+ " [-description description] [-iconlocation iconlocation] [-hotkey hotkey] "+ " [-windowstyle 1|3|7] [-workingdirectory workingdirectory] [-adminpermissions yes|no]"); WScript.Echo(); WScript.Echo(scriptName + " -edit link [-target target] [-linkarguments linkarguments] "+ " [-description description] [-iconlocation iconlocation] [-hotkey hotkey] "+ " [-windowstyle 1|3|7] [-workingdirectory workingdirectory] [-adminpermissions yes|no]"); WScript.Echo(); WScript.Echo(scriptName + " -examine link"); WScript.Echo(); WScript.Echo(" More info: http://msdn.microsoft.com/en-us/library/xk6kst2k%28v=vs.84%29.aspx "); } // reads the given .lnk file as a char array function getlnkChars(lnkPath) { // :: http://www.dostips.com/forum/viewtopic.php?f=3&t=3855&start=15&p=28898 :: var ado = WScript.CreateObject("ADODB.Stream"); ado.Type = 2; // adTypeText = 2 ado.CharSet = "iso-8859-1"; // code page with minimum adjustments for input ado.Open(); ado.LoadFromFile(lnkPath); var adjustment = "\u20AC\u0081\u201A\u0192\u201E\u2026\u2020\u2021" + "\u02C6\u2030\u0160\u2039\u0152\u008D\u017D\u008F" + "\u0090\u2018\u2019\u201C\u201D\u2022\u2013\u2014" + "\u02DC\u2122\u0161\u203A\u0153\u009D\u017E\u0178" ; var fs = new ActiveXObject("Scripting.FileSystemObject"); var size = (fs.getFile(lnkPath)).size; var lnkBytes = ado.ReadText(size); ado.Close(); var lnkChars=lnkBytes.split(''); for (var indx=0;indx<size;indx++) { if ( lnkChars[indx].charCodeAt(0) > 255 ) { lnkChars[indx] = String.fromCharCode(128 + adjustment.indexOf(lnkChars[indx])); } } return lnkChars; } function hasAdminPermissions(lnkPath) { return (getlnkChars(lnkPath))[21].charCodeAt(0) == 32 ; } function setAdminPermissions(lnkPath , flag) { lnkChars=getlnkChars(lnkPath); var ado = WScript.CreateObject("ADODB.Stream"); ado.Type = 2; // adTypeText = 2 ado.CharSet = "iso-8859-1"; // right code page for output (no adjustments) //ado.Mode=2; ado.Open(); // setting the 22th byte to 32 if (flag) { lnkChars[21]=String.fromCharCode(32); } else { lnkChars[21]=String.fromCharCode(0); } ado.WriteText(lnkChars.join("")); ado.SaveToFile(lnkPath, 2); ado.Close(); } function examine(lnkPath) { var fs = new ActiveXObject("Scripting.FileSystemObject"); if (!fs.FileExists(lnkPath)) { WScript.Echo("File " + lnkPath + " does not exist"); WScript.Quit(2); } var oWS = new ActiveXObject("WScript.Shell"); var oLink = oWS.CreateShortcut(lnkPath); WScript.Echo(""); WScript.Echo(lnkPath + " properties:"); WScript.Echo(""); WScript.Echo("Target:" + oLink.TargetPath); WScript.Echo("Icon Location:" + oLink.IconLocation); WScript.Echo("Description:" + oLink.Description); WScript.Echo("Hotkey:" + oLink.HotKey ); WScript.Echo("Working Directory:" + oLink.WorkingDirectory); WScript.Echo("Window style:" + oLink.WindowStyle); WScript.Echo("Admin Permissions:" + hasAdminPermissions(lnkPath)); WScript.Quit(0); } if (WScript.Arguments.Length==1 || args.Item(1).toLowerCase() == "-help" || args.Item(1).toLowerCase() == "-h" ) { printHelp(); WScript.Quit(0); } if (WScript.Arguments.Length % 2 == 0 ) { WScript.Echo("Illegal arguments "); printHelp(); WScript.Quit(1); } if ( args.Item(1).toLowerCase() == "-examine" ) { var linkfile = args.Item(2); examine(linkfile); } if ( args.Item(1).toLowerCase() == "-edit" ) { var linkfile = args.Item(2); edit=true; } if(!edit) { for (var arg = 1;arg<5;arg=arg+2) { if ( args.Item(arg).toLowerCase() == "-linkfile" ) { var linkfile = args.Item(arg+1); } if (args.Item(arg).toLowerCase() == "-target") { var target = args.Item(arg+1); } } } if (typeof linkfile === 'undefined') { WScript.Echo("Link file not defined"); printHelp(); WScript.Quit(2); } if (typeof target === 'undefined' && !edit) { WScript.Echo("Target not defined"); printHelp(); WScript.Quit(3); } var oWS = new ActiveXObject("WScript.Shell"); var oLink = oWS.CreateShortcut(linkfile); if(typeof target === 'undefined') { var startIndex=3; } else { var startIndex=5; oLink.TargetPath = target; } for (var arg = startIndex ; arg<args.Length;arg=arg+2) { if (args.Item(arg).toLowerCase() == "-linkarguments") { oLink.Arguments = args.Item(arg+1); } if (args.Item(arg).toLowerCase() == "-description") { oLink.Description = args.Item(arg+1); } if (args.Item(arg).toLowerCase() == "-hotkey") { oLink.HotKey = args.Item(arg+1); } if (args.Item(arg).toLowerCase() == "-iconlocation") { oLink.IconLocation = args.Item(arg+1); } if (args.Item(arg).toLowerCase() == "-windowstyle") { oLink.WindowStyle = args.Item(arg+1); } if (args.Item(arg).toLowerCase() == "-workingdirectory" || args.Item(arg).toLowerCase() == "-workdir") { oLink.WorkingDirectory = args.Item(arg+1); } if (args.Item(arg).toLowerCase() == "-adminpermissions") { if(args.Item(arg+1).toLowerCase() == "yes") { var adminPermissions= true; } else if(args.Item(arg+1).toLowerCase() == "no") { var adminPermissions= false; } else { WScript.Echo("Illegal arguments (admin permissions)"); WScript.Quit(55); } } } oLink.Save(); if (!(typeof adminPermissions === 'undefined')) { setAdminPermissions(linkfile ,adminPermissions); }

Use Windows PowerShell to Make Multiple Executables Always Run as Administrator (Compatability Mode)

karma
[attachment-zFCOoa]
Application Compatibility Tab

To always run an executable under the Administrator account one will typically:

  • Right-click on the application file in File Explorer
  • Click the Properties item in the context menu
  • Navigate to the Compatibility tab
  • Check the Run this program as an administrator checkbox
  • Click the Apply button at the bottom of the Properties window

However, what is one to do when presented with a large batch of such files? Group-selecting them will reveal a Properties window of limited options, excluding the ability to mass-apply the Run this program as an administrator option to the group.

Counterintuitively, this property is not actually attached to the file itself. It is in fact set in the Windows Registry under the path HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers by making a new value for each path to an individual executable file and assigning it the String value RUNASADMIN

[attachment-NCcq3C]
The Windows Registry after running the following PowerShell snippet on the SysInternals Suite

We can perform this action in bulk with a very terse little PowerShell scriptlet which you can paste directly into your session (running as Administrator), simply chdir (cd) to the desired directory to use unmodified:

$files = Get-ChildItem ./ -Include *.exe; foreach ($f in $files) { $filename = $f.FullName reg.exe ADD "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" /v $filename /t REG_SZ /d "RUNASADMIN" /f }