The first idea was to work out some 'magic value' from the password (for example, sum all characters in the password) and use that as the key for an XOR loader. But that is not secure enough, and can in fact be cracked quite easily. Some byte combinations are almost certainly going to appear in the original code (which is encrypted in the COM file). Some examples are
CD21 INT 21 B8004C MOV AX, 4C00 CD13 INT 13
Knowledge of what the program does can help. For example if the program scans for keyboard input, that might be done through INT 16 (CD16). Let's suppose that we pick CD21 to break the encryption. The all you need to do is XOR the entire file with the bytes CD21 (you will need to do this twice because you are trying to match it with existing CD21's in the code), save the new file, and then find duplicate bytes in the new file. For example:
90 XOR CD = 5D 2D XOR 21 = 0C E9 XOR CD = 24 <-- Bingo! Our XOR key may be 24 05 XOR 21 = 24 <-- 90 XOR 24 = B4 2D XOR 24 = 09 B409 MOV AH, 09 <-- Yes! E9 XOR 24 = CD 05 XOR 24 = 21 CD21 INT 21 <-- Yes!
All such duplicates which turn up are tested on the entire file until one of them produces data which disassembles into valid code.
Essentially this means that, without too much effort, people can access the decrypted code without knowing the password.
My solution was to stay with the XOR encryption method but change it so that each byte is XORed with a different value. The main difference over the old method is that there are now two keys instead of one, and these two keys are used to generate a sequence of keys to encrypt the code with. The code for the encryption function is given below:
; Encrypt the bytes referenced by si using ax, bx as keys. The same ; two keys can be used to decrypt the same data. ; The number of bytes is given by cx. Encrypt PROC NEAR push ax push cx push si next_encrypt: test cx, cx ; check if done (cx = 0) jz done_encrypt xor byte ptr [si], al ; encrypt ror ax, 1 add ax, bx ; change encryption key dec cx inc si jmp next_encrypt done_encrypt: pop si pop cx pop ax ret Encrypt ENDP
The two instructions
ror ax, 1 add ax, bx
change the key between each byte. Now all that needs to be done is to get these two "keys" from a user-supplied password.
One way to check if the password is correct is to do a string comparision (CMPSB) between the typed in password and the correct password. Of course, doing this allows anyone with a debugger to snoop around in your code and fish out the correct password. So we can't store the original password (too easy to crack), but we can store some sort of checksum. Then, if you compare the checksum of the typed in password, and the previously saved checksum, you can expect the password to be correct if the checksums match.
Thus we need three keys. Here is my ReadPasswd function. It returns three keys in AX, BX and CX. Note that the password is not read into memory (so no need to reset the computer to clear the memory after you type in the password).
; Read in a password and return two 'magic' numbers based on some calculation ; of the password. ; The two values are returned in ax and bx. ; A shifted sum/xor of all the characters is returned in cx, useful for a ; checksum to print out "wrong password". ReadPasswd PROC NEAR push dx xor bx, bx xor cx, cx xor dx, dx next_char: xor ah, ah ; read character int 16h ; from keyboard cmp al, 0dh ; enter/return -> done jz read_passwd_done xor ah, ah ; discard scancode add ch, al ror ch, 1 ; ch stores shifted sum xor cl, al ; cl stores xor of password add dx, ax ; dx stores sum imul dl mov bx, ax ; bx stores product of sums jmp next_char read_passwd_done: mov ax, dx ; ax now stores sum pop dx ret ReadPasswd ENDP
You may have noticed that checksum (CX) is not unique. This basically means