1. To protect the code the AHK script has to be compiled to an executable with ahk2exe, which is part of the AHK distribution. To prevent de-compilation a (good) password has to be provided. This way the program runs normally, but an attacker would have difficulties modifying the program or getting out secrets (keys).
2. Collect some data specific to the target machine. For simplicity we use the following environment variables:
COMPUTERNAME
HOMEPATH
USERNAME
PROCESSOR_ARCHITECTURE
PROCESSOR_IDENTIFIER
PROCESSOR_LEVEL
PROCESSOR_REVISION
AHK built in variables:
A_OSVersion
A_OSType
These should not change at OS updates or changing the network environment.
In addition one can use disk volume identifiers by running the consol command VOL > info.txt:
Volume in drive C has no label.
Volume Serial Number is 24B5-2345
The MAC (Media Access Controller) address of the network cards can be used, too, by Getmac /NH >> info.txt:
00-20-30-F0-F1-F2 Media disconnected
…
(To be safe only the first 17 characters should be used, the MAC of the first network card.)
Instead of saving this data in a temporary file, one can use CMDret - return output from console programs [DLL version]
3. Having the machine/user specific data, we hash them all (Message Authentication Code = MAC with key 0), so no confidential data can be retrieved from it (for privacy concerns) and everybody can verify that no user secret is hidden in there. This is called the PC fingerprint. We ask the user to register the program via email, with the PC fingerprint and with a username and valid return email address, where we would send the unlock code.
4. We generate an unlock code from the data and send the whole SafeSW.ini file back to the user. S/he saves it into the script's working directory. At startup the program verifies if the user info, the PC fingerprint and the unlock code match. If not, it asks for registering the SW, otherwise it starts working.
Fingerprint = MAC(0_key; PC_data)
Registration_data = Username, Email, Fingerprint → emailed for registration
Authentication_data = Username, Email, MAC(secret_key, Username, Email, Fingerprint) ← emailed back from registration site
5. Operation
- If no or invalid format Authentication_data: Request registration, Exit
- Retrieve PC_data, Username, Email (from stored Authentication_data)
- Compute MAC(secret_key; Username, Email, Fingerprint)
- If invalid Authentication_data in the SafeSW.ini file: Request registration, Exit
- Else: display Username, Email; Start working
- Periodically re-compute Fingerprint, at change: Exit.
6. SafeSW.ini
It contains the following information:
[Registration]
User = Laszlo Hars
Email = <!-- e --><a href="mailto:[email protected]">[email protected]</a><!-- e -->
UnlockCode = d0e8fd5f48edbc00
7. Notes
A malicious user could change the environment variables before the program starts, but it prevents normal operation, so he has to change it back. The periodic check of the PC fingerprint should make this attack more difficult.
MAC was used with key 0 for the Fingerprint, so users could verify the information sent for registration does not contain their secrets. A general, un-keyed hash is safer, as it is harder in the wrong PC to modify just one environmental variable to get a desired Fingerprint. We used MAC for simplicity.
There is more information that can be included in the machine specific data, like the physical serial number of the primary disk, the BIOS version, checksum, etc. You can make the sample script arbitrarily complex.
Some random, secret info can be written at installation somewhere hidden on the disk (e.g. in the registry) and it could be read back. (It is easy to catch the writing action, so this does not look very secure.) This random number could modify the executable itself, but it is complicated by the need of re-encrypting the code.
It is still possible to disassemble the program, and read the secret keys, but it is hard. Stronger protection is possible, but things would soon get very complicated.
; AutoHotkey Version: 1.0.39+ ; Language: English ; Platform: Win2000/XP ; Author: Laszlo Hars <www.Hars.US> ; Function: SW copy protection k0 = 0x11111111 ; 128-bit secret key (example) k1 = 0x22222222 k2 = 0x33333333 k3 = 0x44444444 l0 = 0x12345678 ; 64- bit 2nd secret key (example) l1 = 0x12345678 m0 = 0x87654321 ; 64- bit 3rd secret key (example) m1 = 0x87654321 IniFile = SafeSW.ini GoSub CheckAuth SetTimer CheckAuth,1000 MsgBox,,,This SW is registered to`n%User% at %Email%,4 MsgBox OK ; add your code here Sleep 10000 ; add your code here ExitApp ;---- End autoexecute secsion ----; CheckAuth: IniRead User, %IniFile%, Registration, User IniRead Email,%IniFile%, Registration, Email IniRead Code, %IniFile%, Registration, UnlockCode PCdata = %COMPUTERNAME%%HOMEPATH%%USERNAME%%PROCESSOR_ARCHITECTURE%%PROCESSOR_IDENTIFIER% PCdata = %PCdata%%PROCESSOR_LEVEL%%PROCESSOR_REVISION%%A_OSType%%A_OSVersion%%Language% Fingerprint := XCBC(Hex(PCdata,StrLen(PCdata)), 0,0, 0,0,0,0, 1,1, 2,2) Together = %User%%Email%%Fingerprint% AuthData := XCBC(Hex(Together,StrLen(Together)), 0,0, k0,k1,k2,k3, l0,l1, m0,m1) If (User="Error" || Email="Error" || Code <> AuthData) { S = ( LTrim To: [email protected] Username = <enter your full name here> Your email address = <where you want the unlock code sent> PC Fingerprint = %Fingerprint% ) ClipBoard = %S% MsgBox Please Register! Email the following information`n`n%S%`n`n(it has been copied to the ClipBoard) ExitApp } Return ;---- Crypto functions ----; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; TEA cipher ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Block encryption with the TEA cipher ; [y,z] = 64-bit I/0 block ; [k0,k1,k2,k3] = 128-bit key ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; TEA(ByRef y,ByRef z, k0,k1,k2,k3) { ; need SetFormat Integer, D s = 0 d = 0x9E3779B9 Loop 32 ; could be reduced to 8 for speed { k := "k" . s & 3 ; indexing the key y := 0xFFFFFFFF & (y + ((z << 4 ^ z >> 5) + z ^ s + %k%)) s := 0xFFFFFFFF & (s + d) ; simulate 32 bit operations k := "k" . s >> 11 & 3 z := 0xFFFFFFFF & (z + ((y << 4 ^ y >> 5) + y ^ s + %k%)) } } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; XCBC-MAC ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; x = long hex string input ; [u,v] = 64-bit initial value (0,0) ; [k0,k1,k2,k3] = 128-bit key ; [l0,l1] = 64-bit key for not padded last block ; [m0,m1] = 64-bit key for padded last block ; Return 16 hex digits (64 bits) digest ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; XCBC(x, u,v, k0,k1,k2,k3, l0,l1, m0,m1) { Loop % Ceil(StrLen(x)/16)-1 ; full length intermediate message blocks XCBCstep(u, v, x, k0,k1,k2,k3) If (StrLen(x) = 16) ; full length last message block { u := u ^ l0 ; l-key modifies last state v := v ^ l1 XCBCstep(u, v, x, k0,k1,k2,k3) } Else { ; padded last message block u := u ^ m0 ; m-key modifies last state v := v ^ m1 x = %x%100000000000000 XCBCstep(u, v, x, k0,k1,k2,k3) } Return Hex8(u) . Hex8(v) ; 16 hex digits returned } XCBCstep(ByRef u, ByRef v, ByRef x, k0,k1,k2,k3) { StringLeft p, x, 8 ; Msg blocks StringMid q, x, 9, 8 StringTrimLeft x, x, 16 p = 0x%p% q = 0x%q% u := u ^ p v := v ^ q TEA(u,v,k0,k1,k2,k3) } Hex8(i) ; 32-bit integer -> 8 hex digits { format = %A_FormatInteger% ; save original integer format SetFormat Integer, Hex i += 0x100000000 ; convert to hex, set MS bit StringTrimLeft i, i, 3 ; remove leading 0x1 SetFormat Integer, %format% ; restore original format Return i } Hex(ByRef b, n=0) ; n bytes data -> stream of 2-digit hex { ; n = 0: all (SetCapacity can be larger than used!) format = %A_FormatInteger% ; save original integer format SetFormat Integer, Hex ; for converting bytes to hex m := VarSetCapacity(b) If (n < 1 or n > m) n := m Loop %n% { x := 256 + *(&b+A_Index-1) ; get byte in hex, set 17th bit StringTrimLeft x, x, 3 ; remove 0x1 h = %h%%x% } SetFormat Integer, %format% ; restore original format Return h }