Protecting AppleScripts Compiled as Run-Only Applications

by Julio Sancho, more familiarly “jj”

Can code saved as run-only be uncompiled?

Yes, but it isn’t worth the effort. It would take too much time for a regular programmer.

Run-only code is not encrypted in any form, it is simply compiled in a different way so it can only be read by the AppleScript interpreter, but not by any of the script editors. Nonetheless, your data is still in there in semi-readable form.

What is this relevant data I should worry about?

Everything you write will be stored in the run-only script. For example, if you compile the number “65535”, a cracker can find the hex digits “0x0000FFFF” (which is “65535” in hex) in your run-only script. Strings are compiled literally. If you compile the string “foo” (with quotes), the cracker will find “foo” (in ASCII form, of course) in the file. Anything involving a variable name (handler, script object, variable) will be also compiled as ASCII-readable. For example, if you compile this:

script robert
	on blah()
		set myName to "foo"
	end blah
end script

When the cracker reads the run-only code, (s)he will find all of these terms “robert”, “blah”, “myName” and “foo”.

So you can see that if you are especially concerned about securing in the registration methods of your handy shareware applications, you should consider the possibility of using “tricky” names for the objects in it. Instead of “validateRegistration()”, use as handler name a single character (eg, “p()”), use a “wrong” name (eg, “search_replace()”) or make use of pipes to introduce high-ascii (eg, “||()”, where  is the ASCII character numbered 240)

The same advice applies to parameters in handlers and variable names. Consider the differences:

to validateRegistrationCode(username, usermail, serialNumber)
	if ((count usermail) + (count username)) is serialNumber then
		return "All is OK!!!"
		return "Wrong serial!!!"
	end if
end validateRegistrationCode

Against this:

to search_replace(srchTrm, rplcTrm, txtToRplc)
	if ((count srchTrm) + (count rplcTrm)) is txtToRplc then
		return 77
		return 1926
	end if
end search_replace

Or, even better, this:

to ||(|�|, |^|, |ˇ|)
	((count |�|) + (count |^|)) is |ˇ|
end ||

These last two methods won’t give explicit hints to casual crackers about where to find the validation code. You can use one of these methods or make up a more elaborate one of your own …

Obviously, you should never use literal strings with relevant data either. Eg:

set regData to "APP-200-" & blah

A cracker would discover that the registration info must start with “APP-200-”. While this may be or not useful info, the less hints you give him or her, the better.

The same applies for any kind of data. If you must use the number 65535 in a sensitive part of your code, you can hide it better by using:

5 * (((256 ^ 2) - 1) as integer)

Than by what you would normally write:

5 * 65535

To recompose the sensitive data (the number 65535), the cracker should first guess that (s)he must use the “^” operator applied to “256” and followed by “2”, and then subtract “1”; which is nearly impossible because the operations are not in clear ASCII. Using the direct way (using 65535 as is), he or she could find this “65535” and use it for “experiments”. Remember that all cracking is experimental.

More relevant data? What else?

Yes, there is more relevant data a cracker can use to crack your app…

It’s not just the names you use for your sensitive code (variables, strings, etc.), but the names you don’t choose. The AppleScript compiler translates certain pieces of your plain text code to raw apple events, classes, etc. These can be read by a cracker familiar with them as well. For example, if you compile “beep 5”, you will find “sysobeep” in the run-only code. If you compile “count x”, you will find “corecnte”. These are the apple event equivalents to “beep” and “count”. If you compile “x’s length”, you will find “leng” (four-char code equivalent to the property “length” of a text).

So, let’s imagine you use a labyrinthine algorithm as your registration code. Eg:

to ||(|�|, |\||, |^|)
	"" & (count |�|) & (count |\||)
	if ("" & result & (ASCII character 45) & (result as number) * (offset of (ASCII character 64) in |\||)) is |^| then
		return true
		return false
	end if
end ||

You’re confusing the cracker with those “corecnte”, “shor” (stored numbers), “sysontoc” (ASCII character command), etc. But later you’re giving out a single very good bet: the return of a true or false result. This is compiled as “boovtrue” or “boovfals”. If I was the cracker, I would try changing this “boovfals” to “boovtrue”. So, our executing code would be interpreted by AppleScript as:

if [blah] then
	return true
	return true
end if

This way, the registration algorithm would allways return a “true” and, consequently, whatever registration info would be valid to pass your registration test.

It would be better then, in this example, to use something like:

return [blah]

Where “blah” is the comparison (and we won’t compile “boovtrue” nor “boovfals”). Or, better yet:

if [blah] then
	error number -875643
end if

Which you can evaluate later as:

	||(|�|, |\||, |^|) --> call "cryptic" validation handler
	--> no error means that validation doesn't match
on error number |6|
	if |6| is -875643 then
		--> match!
		--> doesn't match!
	end if
end try

Here’s an example to try in your Script Editor:

 -- Well away from the test, we mask an important constant
set |+| to (41 * (2 ^ (2 ^ 3) - (3 ^ ((3 ^ 2) - 1))) div 3)

-- Then the registration code does this:
set userName to text returned of (display dialog "Please enter the name you used to purchase this application:" default answer "John Doe")
set userMail to text returned of (display dialog "Please enter your registration email address:" default answer "")
set sn to text returned of (display dialog "Please enter the serial number you received via email:" default answer "") 

(* To try this out, load this code into your Script Editor by clicking the
link above, and then do these two tests:
1) leave the defaults (just press OK) and enter any guess (or none) to see the failure mode.
2) leave the defaults and enter "818-4090" (without the quotes) for the serial number to see the success mode. *)

-- The test of the entries:
	||(userName, userMail, sn)
	display dialog "SORRY, WRONG NUMBER!" with icon stop
on error e number |@|
	if |@| is |+| then display dialog "THANKS FOR REGISTERING!" with icon note
end try

-- The handler, buried in the code, to compute the test result
to ||(|�|, |\||, |^|)
	"" & (count |�|) & (count |\||)
	if ("" & result & (ASCII character 45) & (result as number) * �
		(offset of (ASCII character 64) in |\||)) is |^| then error number my |+|
end ||

Anything more regarding AppleScript?

Not as far as I know. There are still some more considerations, but you must do it yourself. For example, you can use “hidden” files to track the installation date (and establish a demo period), you can use a key generator based on single machines, eg, using the machine’s MAC Address found this way in Tiger:

set MACaddr to primary Ethernet address of (system info)

Instead of a plain registration code, you can also “phone home” for internet activation using curl, etc. You can ask about these methods in any forum for mac developers (including our own BBS).

Cheating with demo dates by setting the computer clock back is easily avoided, too. See this thread, started by Jon Nathan, in MacScripter’s Code Exchange: Check if clock is correct for more information. The basic idea there, of course, is that the correct time is always available on the internet - you don’t have to rely on the computer’s clock, and very few modern users are willing to disconnect from their internet service provider just to use a piece of inexpensive software for free.

Don’t panic!

  • There are not a lot of people with the knowledge required to crack compiled AppleScript code, and most of them are honest programmers or Apple employees (or both).
  • You can write your code using “easy” names, such as “regCode”, and just make a quick search-replace with obfuscating names before saving as run-only.

Other Considerations

There is a bug in run-only scripts. Constructors (a script object inside a handler) are not compiled as run-only, and they can be read using certain tools.

See this thread in the BBS: as well