Tracking a Mac laptop over the internet (locating a stolen laptop)

This is an example of what you can achieve with just a little work, this is meant to provide more of an idea than an actual production piece of code (however it seems like a good place to start). So the problem I want to address is the ability to track Mac laptops in the wild, via the internet. I want to know where in the entire world and when my laptops are on line, even be able for them to fetch commands (future post) based on their status. This is ment to be simple I threw this idea together in about two hours (from start to finish) so not polished, but effective. This tutorial has a couple of distinct pieces you need:

  1. Client computers running os x, 10.6 in this case, but 10.5 should also work.
  2. A web server that is accessible over the internet and able to run PHP server side scripts.
  3. A mysql server accessible by the web server.

Lets begin by examining the client bash script that will send data to our web server, there are a few things to configure in it.

#!/bin/bash

# Author: Joseph J. Viscomi    E-Mail: jjviscomi [at] gmail [dot] com || jviscomi [at] brehm [dot] org
# Website: http://www.theobfuscated.org
# Date: 4/6/2011
# Description: This script when run will send information about itself to a webserver.
#              It can be used to track computers in the wild, on the internet. This is
#              the first of two files that will need to be installed on the clients.
#              For full functionality you will also need the server side code to handle
#              this script.
#              I have provided a basic verson of this for educational purposes only.
#              This is an attempt to show you what can be possible, this can be extended
#              to relay any information back to a server, and it can even be used for
#              two-way communications (post to follow).

# STEP 1.
#       CREATE A DIRECTORY ON THE CLIENT MACHINES: sudo mkdir /etc/.callHome/
#       CHANGE THE HOMEADDRESS TO POINT TO THE CORRECT DIRECTORY ON YOUR WEBSERVER WHERE THE callHome.php IS LOCATED
#       SET THE UNIQUE LOCATIONID - USE ONLY UPPERCASE AND NUMBERS, UPTO 64 CHARS
#       SAVE AND SET THE FILE TO EXECUTABLE, sudo chmod +x callHome.sh
#       PLACE THIS FILE IN /etc/.callHome/

# STEP 2.
#       PLACE THE org.theObfuscated.callHome.plist FILE IN THE /Library/LaunchDaemons/ DIRECTORY
#       IT IS SET TO CALL IN EVERY 10 MIN, THIS CAN BE CHANGED.

# STEP 3.
#       launchctl load /Library/LaunchDaemons/org.theObfuscated.callHome.plist

#THIS IS THE ADDRESS THE COMPUTER CALLS HOME TO, IP OR FQDN
# - CHANGE THIS TO REFLECT WHERE THE SERVER SIDE SCRIPT RESIDES
HOMEADDRESS="www.theobfuscated.org/callHome/"
#THIS IS THE NAME OF THE SERVER SIDE SCRIPT TO HANDLE THE REQUEST
SCRIPTNAME="checkin.php"

#THIS IS LIKE A SITE PASSWORD, JUST A BASIC SECURITY MEASURE - MAKE SURE IT MATCHES WHAT YOU HAVE IN THE DB
#EACH SITE SHOULD HAVE A UNIQUE LOCATIONID
LOCATIONID="SDJELK45689DSK"

#GET THE MAC ADDRESS OF THE MACHINE THAT IS CALLING HOME
LOCALMAC=`(/sbin/ifconfig en0 | awk '/ether/ { gsub(":", ""); print $2 }')`
#GET THE PUBLIC IP ADDRESS OF WHERE THIS MACHINE IS CURRENTLY CONNECTED TO THE INTERNET
EXTERNALIP=`(curl -s http://checkip.dyndns.org | awk '{print $6}' | awk ' BEGIN { FS = "<" } { print $1 } ')`
#GET THE SERIAL NUMBER OF THIS MACHINE
SERIALNUMBER=`ioreg -l | grep IOPlatformSerialNumber | awk '{ print $4 }' | sed "s/^\([\"']\)\(.*\)\1\$/\2/g"`
#GET THE TIME IN DAYS SINCE THE LAST RESTART
UPTIMEDAYS=`(uptime | awk '{ print $3 }')`
#GET THE USERNAME OF THE CURRENT USER
CURRENTUSER=`whoami`
#GET THE CURRENT HOSTNAME
HOSTNAME=`hostname`

#CHECKIN SITE
HOMESITE="http://"$HOMEADDRESS"/"$SCRIPTNAME"?mac="$LOCALMAC"&hostname="$HOSTNAME"&user="$CURRENTUSER"&days="$UPTIMEDAYS"&ip="$EXTERNALIP"&serial="$SERIALNUMBER"&loc="$LOCATIONID

#GET THE RESPONSE FROM THE SERVER
HOMERESPONSE=`(curl -s $HOMESITE)`

#RECORD IN THE LOCAL LOG
echo "$HOMERESPONSE" >> /Library/Logs/callHome.log

The above script can be download here, for simplicity call it what we do, callHome.sh. The first thing you will need to edit the script at line # 32
HOMEADDRESS="www.theobfuscated.org/callHome/"
It should point to the directory on your web server where the server side script will reside. It is important to exclude the http:// but to include the trailing slash. Now depending on if you modify the name of the file on the server (I recommend you do not) you might need to change line # 34, this needs to be the file that will accept the client communications.

Line # 38 is the next important line, this is here for some simple security. This is a randomly generated “Location ID”, randomly type some upper case & numeric characters here (up to a maximum length of 64). However you have to write this down for a later step, we will refer to this as the LOCATIONID, this should not be shared with anyone else.

This is all the editing you have to do in this script. Save it to the following location: /etc/.callHome/callHome.sh
Make sure this script is executable (i.e. sudo chmod +x /etc/.callHome/callHome.sh). Just because we have this script now on the client computers doesn’t mean we are finished yet.

We want to make sure it will regularly contact our web server and register with it, and to do that we will run it as a Launch Deamon. To simplify this process I have created a plist file that will tell launchd to run this script every 10 minuets, it is displayed below:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>org.theObfuscated.callHome</string>
    <key>OnDemand</key>
    <true/>
    <key>StartInterval</key>
    <integer>600</integer>
    <key>ProgramArguments</key>
    <array>
      <string>/private/etc/.callHome/callHome.sh</string>
    </array>
  </dict>
</plist>

Line # 10 specifies how often the script should be run, in this case every 600 seconds. Feel free to change this, but I would suggest to wait until after you confirm this is working. This file should be called org.theObfuscated.callHome.plist , down load it here. This file needs to be saved in the following location: /Library/LaunchDaemons/org.theObfuscated.callHome.plist and then we just need to tell launchd to load it, however this should wait until we have our servers configured properly first.

 

Configuring the Server side PHP Recording Script & MySQL DB.

Copy  this file to the location you desire on your web server, it should be the same location and name as indicated earlier when configuring the client script.

<?php
/**
 * User: Joe Viscomi | jjviscomi [at] gmail [dot] com | www.theobfuscated.org
 * Date: 4/6/11
 * Time: 8:29 PM
 * Description: Simple PHP Script to handle reporting from clients.
 */

    //#CHANGE THESE SETTING TO REFLECT YOUR DATABASE
    $DBHOST="localhost";
    $DBUSER="root";
    $DBPASSWORD="root";
    $DBNAME="call_home";

    //#GET KEY VAULES TO AUTHENTICATE THE REMOTE COMPUTER
    $location_hash = $_GET['loc'];
    $mac_address = $_GET['mac'];
    $computer_serial = $_GET['serial'];

    //#CONNECT TO THE DATABASE
    $db_link = mysql_connect($DBHOST,$DBUSER,$DBPASSWORD);
    mysql_select_db($DBNAME, $db_link);

    //#THIS IS THE AUTHENTICATION QUERY
    $sql  = "SELECT `LOCATION`.`LOCATION_id`, `LOCATION`.`LOCATION_name`, `COMPUTER`.`COMPUTER_mac`, ";
    $sql .= "`STATUS`.`STATUS_name`, `STATUS`.`STATUS_id`, `COMPUTER`.`COMPUTER_id` ";
    $sql .= "FROM `LOCATION`, `COMPUTER`, `STATUS` ";
    $sql .= "WHERE `LOCATION`.`LOCATION_active` = '1' AND `LOCATION`.`LOCATION_hash` = '".$location_hash;
    $sql .= "' AND `COMPUTER`.`COMPUTER_mac` = '". $mac_address ."' AND ";
    $sql .= "`COMPUTER`.`LOCATION_id` = `LOCATION`.`LOCATION_id` AND ";
    $sql .= "`COMPUTER`.`COMPUTER_serial` = '". $computer_serial ."' AND `COMPUTER`.`COMPUTER_active` = '1' AND ";
    $sql .= "`COMPUTER`.`STATUS_id` = `STATUS`.`STATUS_id`";

    //#QUERY THE DB TO SEE IF THIS COMPUTER SHOULD EVEN CALL HOME
    $result = mysql_query($sql, $db_link);
    $computer = mysql_fetch_array($result);
    mysql_free_result($result);

    //IF $COMPUTER HAS A SIZE OF 12 THEN A RECORD EXISTS FOR THE REMOTE COMPUTER
    if(sizeof($computer) === 12){

        //GET LAST CONTACT TIME
        $sql  = "SELECT MAX(`CONTACT-LOG`.`CONTACT-LOG_doc`) FROM `CONTACT-LOG` WHERE `CONTACT-LOG`.`COMPUTER_id` = '";
        $sql .= $computer['COMPUTER_id']."' AND `CONTACT-LOG_active` = '1'";
        $result = mysql_query($sql, $db_link);
        $last = mysql_fetch_array($result);
        mysql_free_result($result);

        //LOG THE REMOTE CONTACT
        $sql  = "INSERT INTO `call_home`.`CONTACT-LOG` (`CONTACT-LOG_id`, `COMPUTER_id`, `STATUS_id`, ";
        $sql .= "`CONTACT-LOG_hostname`, `CONTACT-LOG_user`, `CONTACT-LOG_uptime`, `CONTACT-LOG_ip`, ";
        $sql .= "`CONTACT-LOG_client`, `CONTACT-LOG_doc`, `CONTACT-LOG_active`) VALUES (NULL, ";
        $sql .= "'" . $computer['COMPUTER_id'] . "', '" . $computer['STATUS_id'] . "', '" . $_GET['hostname'];
        $sql .= "', '".$_GET['user']."', '".$_GET['days']."', '".$_GET['ip']."', '" .$_SERVER['REMOTE_ADDR'];
        $sql .= "', CURRENT_TIMESTAMP, '1')";
        mysql_query($sql);

        //GET LAST CONTACT TIME - NOW
        $sql  = "SELECT MAX(`CONTACT-LOG`.`CONTACT-LOG_doc`) FROM `CONTACT-LOG` WHERE `CONTACT-LOG`.`COMPUTER_id` = '";
        $sql .= $computer['COMPUTER_id']."' AND `CONTACT-LOG_active` = '1'";
        $result = mysql_query($sql, $db_link);
        $current_date = mysql_fetch_array($result);
        mysql_free_result($result);

        //COMPUTER HAS ACCOUNT
        echo("[" . $_SERVER['SERVER_ADDR'] ."][" .$current_date['MAX(`CONTACT-LOG`.`CONTACT-LOG_doc`)'] . "]:" .
             "Computer Account Found\r\n");
        echo("[" . $_SERVER['SERVER_ADDR'] ."][" .$current_date['MAX(`CONTACT-LOG`.`CONTACT-LOG_doc`)'] . "][" .
             "STATUS]: " . $computer['STATUS_name'] . "\r\n");
        echo("[" . $_SERVER['SERVER_ADDR'] ."][" .$current_date['MAX(`CONTACT-LOG`.`CONTACT-LOG_doc`)'] . "][" .
             "TIME OF LAST CONTACT]: " . $last['MAX(`CONTACT-LOG`.`CONTACT-LOG_doc`)']);

    }else{
        echo("[" . $_SERVER['SERVER_ADDR'] ."][" .$current_date['MAX(`CONTACT-LOG`.`CONTACT-LOG_doc`)'] . "]:" .
             "Computer Account Not Found\r\n");
    }

    mysql_close($db_link);

?>

Lines # 11-14 are the only lines you need to change here, they need to reflect the setting to access your mysql database server. Then save this file as stated earlier in the proper location in you web server this can be downloaded here in zip format.

The last step is to setup the MySQL server DB and table structure and populate it with the inventory of the computers it is suposed to keep track of. You can download the sql file that will create all the necessary tables and default values for you here. It is also displayed below for you to read if you wish, however the detail is not that important.

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;

--
-- Database: `call_home`
--

-- --------------------------------------------------------

--
-- Table structure for table `COMPUTER`
--

CREATE TABLE `COMPUTER` (
  `COMPUTER_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `LOCATION_id` int(10) unsigned NOT NULL,
  `COMPUTER_serial` varchar(64) NOT NULL,
  `COMPUTER_mac` varchar(64) NOT NULL,
  `STATUS_id` smallint(5) unsigned NOT NULL DEFAULT '1',
  `COMPUTER_active` smallint(5) unsigned NOT NULL DEFAULT '1',
  PRIMARY KEY (`COMPUTER_id`),
  UNIQUE KEY `COMPUTER_serial` (`COMPUTER_serial`,`COMPUTER_mac`),
  KEY `COMPUTER_active` (`COMPUTER_active`),
  KEY `LOCATION_id` (`LOCATION_id`),
  KEY `STATUS_id` (`STATUS_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

--
-- Dumping data for table `COMPUTER`
--

-- --------------------------------------------------------

--
-- Table structure for table `CONTACT-LOG`
--

CREATE TABLE `CONTACT-LOG` (
  `CONTACT-LOG_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `COMPUTER_id` int(10) unsigned NOT NULL,
  `STATUS_id` smallint(5) unsigned NOT NULL,
  `CONTACT-LOG_hostname` varchar(128) NOT NULL,
  `CONTACT-LOG_user` varchar(128) NOT NULL,
  `CONTACT-LOG_uptime` smallint(5) unsigned NOT NULL,
  `CONTACT-LOG_ip` varchar(64) NOT NULL,
  `CONTACT-LOG_client` varchar(64) NOT NULL,
  `CONTACT-LOG_doc` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `CONTACT-LOG_active` smallint(5) unsigned NOT NULL DEFAULT '1',
  PRIMARY KEY (`CONTACT-LOG_id`),
  KEY `CONTACT-LOG_hostname` (`CONTACT-LOG_hostname`,`CONTACT-LOG_user`,`CONTACT-LOG_uptime`,`CONTACT-LOG_ip`,`CONTACT-LOG_client`),
  KEY `CONTACT-LOG_active` (`CONTACT-LOG_active`),
  KEY `COMPUTER_id` (`COMPUTER_id`),
  KEY `STATUS_id` (`STATUS_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

--
-- Dumping data for table `CONTACT-LOG`
--

-- --------------------------------------------------------

--
-- Table structure for table `LOCATION`
--

CREATE TABLE `LOCATION` (
  `LOCATION_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `LOCATION_hash` varchar(64) NOT NULL,
  `LOCATION_name` varchar(128) NOT NULL,
  `LOCATION_active` smallint(6) NOT NULL DEFAULT '1',
  PRIMARY KEY (`LOCATION_id`),
  UNIQUE KEY `LOCATION_hash` (`LOCATION_hash`),
  KEY `LOCATION_active` (`LOCATION_active`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

--
-- Dumping data for table `LOCATION`
--

-- --------------------------------------------------------

--
-- Table structure for table `STATUS`
--

CREATE TABLE `STATUS` (
  `STATUS_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  `STATUS_name` varchar(64) NOT NULL,
  `STATUS_active` smallint(6) NOT NULL DEFAULT '1',
  PRIMARY KEY (`STATUS_id`),
  KEY `STATUS_name` (`STATUS_name`,`STATUS_active`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=6 ;

--
-- Dumping data for table `STATUS`
--

INSERT INTO `STATUS` VALUES(1, 'NORMAL', 1);
INSERT INTO `STATUS` VALUES(2, 'UPDATES REQUIRED', 1);
INSERT INTO `STATUS` VALUES(3, 'CRITICAL ERRORS', 1);
INSERT INTO `STATUS` VALUES(4, 'FORCE SHUTDOWN', 1);
INSERT INTO `STATUS` VALUES(5, 'STOLEN', 1);

Just make sure you create a database with the same name that is configured in the php file above (i.e. $DBNAME=”call_home”;). I suggest using a tool like phpMyAdmin to import the sql file and to make the following inserts, we will not cover this in this article, I will assume you can do/ figure this out:

INSERT INTO  `call_home`.`LOCATION` (
`LOCATION_id` ,
`LOCATION_hash` ,
`LOCATION_name` ,
`LOCATION_active`
)
VALUES (
NULL ,  'SDFA78ASDFA98F',  'MY HOME',  '1'
);

The insert above registers your LOCATIONID that was set on the client script, so replace SDFA78ASDFA98F with what ever you set earlier. Also Replace MY HOME with a description that is meaningful to your site location. Next we need to insert inventory of all the client computers this server can expect to hear from:

INSERT INTO  `call_home`.`COMPUTER` (
`COMPUTER_id` ,
`LOCATION_id` ,
`COMPUTER_serial` ,
`COMPUTER_mac` ,
`STATUS_id` ,
`COMPUTER_active`
)
VALUES (
NULL ,  '1',  'SERIALNUMBER',  'MACADDRESS',  '1',  '1'
);

Please note that you need to insert the serial number and the mac address for each Mac you want to track, you can get that by running the following commands respectively, then simply copy and paste it in place of  ’SERIALNUMBER’ and ‘MACADDRESS’:

ioreg -l | grep IOPlatformSerialNumber | awk '{ print $4 }' | sed "s/^\([\"']\)\(.*\)\1\$/\2/g"
/sbin/ifconfig en0 | awk '/ether/ { gsub(":", ""); print $2 }'

That is all we have to do to prepare the MySQL Server to record the clients announcements. It is importiant that if you do not insert the correct values then the computer calls will NOT be recorded by the server.

You can view the results in phpMyAdmin or you can include this server side script to display a simple table of the latest requests from all registered clients (I know this is sloppy but, again it is just a proof of concept):

<?php
/**
 * User: Joe Viscomi | jjviscomi [at] gmail [dot] com | www.theobfuscated.org
 * Date: 4/6/11
 * Time: 8:41 PM
 * Description: To Display the latest results from all registered clients!
 */

    //#CHANGE THESE SETTING TO REFLECT YOUR DATABASE
    $DBHOST="localhost";
    $DBUSER="root";
    $DBPASSWORD="root";
    $DBNAME="call_home";

    $db_link = mysql_connect($DBHOST,$DBUSER,$DBPASSWORD);
    mysql_select_db($DBNAME, $db_link);

    $sql  = "SELECT MAX(`CONTACT-LOG`.`CONTACT-LOG_doc`), `CONTACT-LOG`.`CONTACT-LOG_user`, `CONTACT-LOG`.`CONTACT-LOG_ip`, ";
    $sql .= "`CONTACT-LOG`.`CONTACT-LOG_client`, `CONTACT-LOG`.`CONTACT-LOG_hostname`, `CONTACT-LOG`.`CONTACT-LOG_uptime` FROM `CONTACT-LOG` ";
    $sql .= "WHERE EXISTS (SELECT * FROM COMPUTER WHERE `CONTACT-LOG`.COMPUTER_id = COMPUTER.COMPUTER_id ";
    $sql .= "AND COMPUTER_active = 1)";

    $result = mysql_query($sql,$db_link);

    $lastContactPage  = "<table class=\"sample\">\n";
    $lastContactPage .= "<tr>";
    $lastContactPage .= " <th>HOSTNAME</th>\n";
    $lastContactPage .= " <th>PUBLIC IP</th>\n";
    $lastContactPage .= " <th>CLIENT IP</th>\n";
    $lastContactPage .= " <th>USER</th>\n";
    $lastContactPage .= " <th>UP TIME (DAYS)</th>\n";
    $lastContactPage .= " <th>TIME OF LAST CONTACT</th>\n";
    $lastContactPage .= "</tr>";

    while($row = mysql_fetch_array($result)){
        $lastContactPage .= "<tr>\n";
        $lastContactPage .= "  <td>" . $row['CONTACT-LOG_hostname'] . "</td>\n";
        $lastContactPage .= "  <td>" . $row['CONTACT-LOG_ip'] . "</td>\n";
        $lastContactPage .= "  <td>" . $row['CONTACT-LOG_client'] . "</td>\n";
        $lastContactPage .= "  <td>" . $row['CONTACT-LOG_user'] . "</td>\n";
        $lastContactPage .= "  <td>" . $row['CONTACT-LOG_uptime'] . "</td>\n";
        $lastContactPage .= "  <td>" . $row['MAX(`CONTACT-LOG`.`CONTACT-LOG_doc`)'] . "</td>\n";
        $lastContactPage .= "</tr>\n";
    }

    $lastContactPage .= "</table>\n";

    mysql_free_result($result);
    mysql_close($db_link);

    echo "<html><head><title>Client Up Time</title>";
    echo "<style type=\"text/css\">" .
         "  table.sample {" .
	     "    border-width: 0px;" .
	     "    border-spacing: 2px;" .
	     "    border-style: none;" .
	     "    border-color: gray;" .
	     "    border-collapse: separate;" .
	     "    background-color: white;" .
         "  }" .
         "  table.sample th {" .
	     "    border-width: 0px;" .
	     "    padding: 2px;" .
	     "    border-style: inset;" .
	     "    border-color: gray;" .
	     "    background-color: rgb(250, 240, 230);" .
	     "    -moz-border-radius: ;" .
         "  }" .
         "  table.sample td {" .
	     "    border-width: 0px;" .
	     "    padding: 2px;" .
	     "    border-style: inset;" .
	     "    border-color: gray;" .
	     "    background-color: rgb(250, 240, 230);" .
	     "   -moz-border-radius: ;" .
         "  }" .
         "</style>";
    echo "</head>";
    echo "  <body>".$lastContactPage."</body>";
    echo "</html>";

That is it all we have to do now is enable the clients, or load our script to launchd. So on each of the clients run the following command:

launchctl load /Library/LaunchDaemons/org.theObfuscated.callHome.plist

That should be it … I have tested this, but in my hurry to put this together I might have included some errors in this post, please let me know so that I can correct them! I hope this provides a clear and clean example of how this all could work. Eventually I might post a more polished packaged solution that can be used by anyone.

Feel free to extend this or clean it up, and let me know so I can put it on the site.

  1. Dear Joseph,

    My Mac has just been stolen but I have the Serial number (don’t know the Mac address unless it is unique and given with the product when sold?) or my ISP provider I have been using may be able to get it for me.

    I do not have access to the machine now – can I still use your software to track it?

    BW

    Deric Cambridge UK

    • Sorry Deric, the software needs to be installed and setup on the computer ahead of time. There is not much you can do now. Mac addresses are unique but they are at level 2 which means that you cannot really locate it remotely using it.

      It really sucks when stuff like that happens.

Leave a Comment


NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Performance Optimization WordPress Plugins by W3 EDGE