Check if clock is correct

For many shareware developers, when an application is first run, you want to check the date and then set a timeout based on that first date. But what if the user sets their clock ahead a year before the first run? If you have a 7 day trial period and you are hard coded to that first run date, if the user resets their clock when subsequently running your application, their trial period is effectively 1 year + 7 days. To avoid this, you can use the following code to determine if their clock is correct. It requires an internet connection but you could use the code in such a way that if there is no connection and you are unable to verify the clock, then tell your app to quit without marking the first run.

The code allows you to return the adjusted correct date or to simply return a boolean if the system clock is within an allowable offset (in the code below this offset is 2 hours: if the system time is within 2 hours (on either side) of the correct date, the clock is deemed to be correct).

Here’s the code:

--NIST time servers: <http://tf.nist.gov/service/time-servers.html>
property time_servers : {"time-a.nist.gov", "time-b.nist.gov", "time-a.timefreq.bldrdoc.gov", "time-b.timefreq.bldrdoc.gov"}
property allowable_offset : (2 * hours) --if the clock is within correct time ± allowable_offset, return true
property the_timeout : 30 --seconds

my clock_is_correct(true)
-->date "Tuesday, November 29, 2005 5:31:39 PM"

my clock_is_correct(false)
-->true

on clock_is_correct(return_adjusted_date)
	set e to "could not determine"
	repeat with i from 1 to (count time_servers)
		set the_address to (item i of time_servers)
		try
			with timeout of the_timeout seconds
				if (my ping_address(the_address)) then
					set the_offset to (do shell script "ntpdate -q " & the_address)'s paragraph -1
					set the_offset to (text ((offset of "offset" in the_offset) + 7) thru -5 of the_offset) as real
					if return_adjusted_date then return (current date) + the_offset
					return ((the_offset < allowable_offset) and (the_offset > (allowable_offset * -1)))
				end if
			end timeout
		on error e
		end try
	end repeat
	return e
end clock_is_correct

on ping_address(the_address)
	if the_address does not contain "://" then set the_address to "http://" & the_address
	((the_address as URL)'s host & {dotted decimal form:""})'s dotted decimal form ≠ ""
end ping_address

Jon

Hi, Jon. I’m wondering if it might be easier to make use the “first run” date, with an updatable “most recent run” date that started off equal to “first run” and worked its way toward the expiry date. If the machine date ever preceded the “most recent” date by more than a certain amount, a cheat could reasonably be suspected and the app could self-destruct. If it were only allowed to operate between the “most recent” and expiry dates, the advance of the former would gradually squeeze out the opportunities for cheating.

Hi Nigel,

I have implemented something similar to this in an upcoming app of mine. What I do is note the first run date and hard code the trial period timeout date to that date plus the trial days. On subsequent runs, if the time between the current date and the hard timeout date (based on the first run date + the trial period) is greater than the raw trial period in days (not linked to a date), the app refuses to launch. To clarify (or not…), it goes something like this:

property trial_days : (7 * days)
property first_run_date : missing value

set first_run_date to my get_first_run_date()
if first_run_date = false then set first_run_date to my save_first_run_date()
set trial_over_date to first_run_date + trial_days
if ((current date) > trial_over_date) or ((trial_over_date - (current date)) > trial_days) then
	display dialog "Trial period over!"
	quit
end if
--"continue starting up..."

on get_first_run_date()
	try
		--code I'm not divulging...
		--if it suceeds, return the date but for now, in this example, 
		--just return a date that is further in the past than the trial period allows:
		return (current date) - trial_days - 1
	on error
		--if it fails:
		return false
	end try
end get_first_run_date

on save_first_run_date()
	set the_date to current date
	--save this somewhere for get_first_run_date() to retrieve later...
	return the_date
end save_first_run_date

Further, in my app, the subroutines are very obfuscated and none of the values are coded in a way that editing the binary could circumvent them. Well, it could be edited, I guess, but it’d have to be a very talented person. For instance, someone who could reverse engineer this from the binary deserves the app for free (this is nothing like my actual code but you get the idea):

property no_relation_name : missing value --trial_days: ultimately it gets to 604800 or (7 * days)

try
	set random_1 to ((21 + 5) * 2)
	set random_2 to (((5 ^ 2) * 2) + 1)
	set random_3 to {32, 17, 114, 131, 101, 44, 116, 32, 99, 2, 97, 11, 114, 56, 97, 84, 104, 46, 99, 22, 32, 91, 73, 46, 73, 73, 67, 18, 83, 21, 65}
	set random_4 to {}
	repeat with i from 1 to (count random_3) by 2
		set end of random_4 to ASCII character (item i of random_3)
	end repeat
	set random_1 to run script ((reverse of random_4) & random_1) as Unicode text
	set random_2 to run script ((reverse of random_4) & random_2) as Unicode text
	set no_relation_name to ((random_1 + random_2) * (60 * 60 * 24))
on error
	--somebody messed with my binary...
	display dialog "Don't tamper with me!"
	quit
end try

Hope that was sufficiently unclear! I only run this check if a legitimate registration code is not found but, if it isn’t, I run the check for the trial period dates periodically throughout the use of my app, not just when it’s launched.

Now, if someone could figure out where you’re storing the first run date (necessarily in an external but local location though equally obfuscated – steganography anyone?) then that could be a problem so you further encrypt and add a checksum to the data (so they can’t just modify it) and you don’t let the app launch if the first run data is missing entirely but a prefs file for the app exists. In this way they’d have to start over with the prefs each time they launched, and, for my app, that would be a huge pain (over 120 defaults that need to be set for it to work).

Oh the lengths to which we go to get a $10 registration fee…

Jon

Love the notion of “steganography”. SteganIcon anyone?

You’re kidding, right? Anybody can catch each and EVERY file your application has open.

losf -p (pid of your app)

Eventually, it WILL catch what file you have open. If you launch another process to open a file, I can catch that too (and then catch what file it opens). I know you don’t dare mess with OS files (unless you have scads of liability insurance - think “Sony”).

jonn8, what if someone trashes the hidden file, and copied the prefs file to the desktop, trashes the prefs file, starts your app up again, and quits, then dumps the old prefs back into the new file (via a text editor or otherwise)? Easy work around for all your efforts. If you store any odd or unique number there, I can copy & paste that back - plists are nicely editable. Any unique number in your plist I can determine by setting comparing identical preferences settings. If you try a random number * some setting, you still need to store the random number somewhere. Your whole manner of protection just evaporated.

You must REALLY want that $10 - but not more than I want to break it.

anaxamander;

That’s lsof, btw, and someone sufficiently facile with Darwin (or any *nix, come to that) can always break any protection methodology eventually - happens every day the world around. The idea in Jon’s and many other schemes is to make it sufficiently difficult that it takes a lot of work by someone with the appropriate skills to do it, and that the method required is not trivial to explain to fellow hackers, but requires that they have the same level of skills as the discoverer of the hack.

In the meantime, the developer gets $10 (or whatever) from everyone else (all the honest folk).

I pay for every shareware app I actually use (and trash it if its demo mode is so broken that I never can discover exactly what it does - authors can be astonishingly thick about how they hobble demos), and I often donate as well if I like a piece of donationware - as I did, for example for CCC.

You say to Jon “You must REALLY want that $10 - but not more than I want to break it.”. I say, of course he wants the $10, it’s part of his income. If folks hack his protection, it’s no different that shoplifting. I know how to get by most anti-shoplifting technology too - doesn’t mean I do it.

Most people wouldn’t try to circumvent any protections - but some just won’t stop. And most people who want to circumvent the protections will know about lsof (thank you for the correction). It’s hardly far out of the mind of a curious end-user - any “hacker” worth using the term knows that command, so it’s hardly an advanced technique. I was just using lsof to get at whether iTunes was using an internet connection the other (and to HELP someone out), so it was on my mind - and outside of the context of breaking an application’s protection scheme.

Clearly, a lot of thought & effort went into developing jonn8’s protection method - I was tying to point out a flaw in the logic. If you think 1 command and some file switcheroo is some advanced hacker method, then this protection is probably enough then (I doubt it however).

And the people who send the $10 - they don’t send it because they are honest (is there a subtle implication there… ), they send it they find it USEFULL. Make crap & get crap.

It is completely unthinkable (apparently) to change the whole perspective on the subject around: you pay for a service. In this scenario - the people are just as honest as in your scenario NovaScotian, but this time the source to jonn8’s application is available as well. Oh, dear… but that’s not about to happen. That’s why its unthinkable. Did having the source make people less honest? The $10 fee could be the fee for finding the application useful & to further develop & fix bugs as they appear (not letting them fester). That way there won’t be a need for intrusive protection schemes (which waste developer time) & developers would be paid. It seems that an enforced honor system is preferable to an obligatory honor system. That’s the beauty of releasing the source and asking for a fee - it tells you end-users that the developer has nothing to hide, and might even encourage enhancements & faster bug removal.

Releasing the code doesn’t make a difference to the bottomline: the “honest” people will still pay if you say “If you find this program useful…”

And now I’ve gotten way, way off topic.

This is the Code Exchange forum, so let’s all try to stay focused on the code.

I’m sorry but a felt a follow-up was in order…

I could be wrong, but I don’t believe just reading a file with AS (read file “path:to:file”) actually opens the file for lsof to catch. That said, you are right, there are ways to circumvent the methods I describe (though, of course, their are additional steps I didn’t and won’t describe but suffice it to say that there are ways to make it much, much more difficult than simply replacing the prefs file after resetting the trial period) but they are sufficiently onerous that, for the effort, they’re not worth it for most shareware apps. The point isn’t to make it impossible (because no protection is invulnerable) but to make it just inconvenient enough that paying the nominal fee is preferable (if the app is valuable to the user in the first place).

As to the open source argument (release everything, ask for fee), I’ve tried that. I have an open source utility that has been downloaded 150,000+ times (probably closer to twice that – the name of the app is a common phrase and searching for that phrase on Google returns my app as the number 1 hit; it’s popular). It has been lauded in Macworld (4-mice) and many other magazines & sites and is generally regarded as a good tool and valuable to users. I have spent about 200 hours developing, testing, and supporting it. It contains no copy protections and a simple checkbox for disabling the plea for contributions. Total contributions: just over $1,000 (from about 150 users). $5/hour is painful.

I have released many other open source apps/example code and ask for contributions for those as well. Total: about $100. For one of my shareware apps, I have spent 600+ hours on development, testing, & support. About 5 hours of that was spent on devising and implementing a trivial protection scheme. I’ve got over 1,000 paid users for this app over the last 18 months. This has been much more profitable than the much more popular open source project.

Jon

That’s OK. I figured you’d have something to say about all this.

I just want to ensure that this doesn’t turn into an attack on people who try to make money from software (even if it’s a $5 alarm clock).

It does.

set the_path to “volume:Users:YOU_ARE_HERE:Desktop:”
set file_spec to (the_path & “Text.txt”) as file specification
set ref_num to (open for access file_spec)

repeat with a from 1 to 10000
set ref_num to (open for access file_spec)
close access ref_num
end repeat

or

repeat with a from 1 to 10000
set b to read file “volume:Users:YOU_ARE_HERE:Desktop:Text.txt”
end repeat

Access to ~/Text.txt can be seen directly with:

lsof -p ps -axw | grep YOUR_PROG_HERE | grep -v grep | awk '{print $1}'

use lsof with its -r feature or in a loop, and that file will be seen:

TEST.app 1382 me 14r VREG 14,20 5 6409186 /Users/me/Desktop/Text.txt

I did a similar test to what you’ve done. I created this script and saved it as a standalone script application on my desktop named “lsof Tester.app”:

set the_file to ((path to temporary items) as Unicode text) & ".an_invisible_file"
set the_posix_file to quoted form of POSIX path of the_file
do shell script "echo 'some text' > " & the_posix_file
try
	repeat 100000 times
		set the_text to (read file the_file)'s paragraph 1
	end repeat
	--hit command-. to cancel out of this when running		
end try

I then ran this script in Script Editor:

set file_name to "an_invisible_file"
set app_name to "lsof Tester"
tell application "System Events" to set open_apps to name of processes
if open_apps does not contain app_name then
	set app_path to ((path to desktop) as Unicode text) & app_name & ".app"
	do shell script "open " & quoted form of POSIX path of app_path
	delay 1
	activate
end if
set the_pid to (do shell script ("ps -auxwww | grep " & quoted form of app_name & " | grep -v grep | awk '{print $2}' "))
if the_pid = "" then
	return app_name & "'s PID wasn't found"
else
	repeat 10000 times
		try
			return do shell script ("lsof -p " & the_pid & " | grep " & quoted form of file_name)
		end try
	end repeat
	return file_name & " wasn't found"
end if

In the Event Log, I get this:

	...
	do shell script "ps -auxwww | grep 'lsof Tester' | grep -v grep | awk '{print $2}' "
		"19576"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
	do shell script "lsof -p 19576 | grep 'an_invisible_file'"
		"lsof\\x20T 19576  jon   14r    VREG       14,9       10  3919056 /private/var/tmp/folders.501/TemporaryItems/.an_invisible_file"
	...

So, yes, it does appear that simply reading a file is enough to trigger the file’s access by an app in lsof. However, to get this to work, lsof has to hit it at just the right moment that the file is being read (we significantly increased these chances by reading it over and over). Also, without knowing what to look for, you’d have to run lsof in a loop, piping the output to a log file for the entire time an app is run and, you’d have to get lucky that one of the few times that the app (possibly only one time) accessed the file, you were running lsof. You’d then have to parse the output file and look for a suspicious file (which, if the coder were smart, would not necessarily look suspicious…). You’d have to do this whole process over and over launching and quitting the app and if the file is small enough and the read so early on in the launch, this might happen before your lsof script can even determine the PID. So, while possible, this would appear to be an unlikely way to crack this type of scheme.

Again, that said, this is academic as there are ways for one to observe even the smallest change to any file in the file system (either a new file or a modification to an existing file). So the point is, yes, things can be cracked (though, also again, the hidden file isn’t necessarily the only thing for which the app is looking) but you have to be determined and the effort has to be worth more than the legitimate registration and the moral satisfaction of paying for a valuable product. Also, if the coder is vigilant, he will monitor certain sites looking for news of a crack and adjust the code accordingly and release a new version.

The original post was to try and thwart casual users from trying to bypass a date-related trial period, it won’t stop determined crackers.

Jon

Thanks for your comments, Jon. I’ve never had to bother with this subject before, but I’m currently trying to get some ideas together. Your demos and the discussion with anaxamander have been very interesting.

With regard to the lsof “problem”, is there any mileage in duplicating the invisible file to another location under another name, reading the duplicate instead, and then zapping or overwriting it? Would that make the original any harder to find? The Finder doesn’t handle invisible files, and System Events’s own ‘duplicate’ command doesn’t appear to work, but presumably there’s a Unix command that does something similar.