Encryption and checksums in a small DOS utility
1) real reversers (should :-) know how reverse any operating system anyway;
2) older dos programs ('Abandoned-warez') that nobody uses anymore often hide jewels of programming (and protection :-) art inside, as +ORC always warned us;
3) disassembling and reversing a COM file is an experience 'per se', and I'm sure you'll LOVE it once you get the grasp of it...
And you'll surely get the right 'feeling' reading this very well structured essay by Red Lantern, a new contributor that I hope will send many more essays like this one: good prepared, filled with intuition and careful explanation. You'll use old (almost forgotten) debug.com... nice, succulent session for any +cracker!
One last word: reading this essay is pretty interesting per se, yet if you have a little time, fetch the target and work a little LIVE with old debug.com on it, great fun, and moreover you'll see how many surprises this old 'swiss knife' of the cracker still has to offer!
An useful essay for beginners and intermediate reversers to see how to defeat encryption and checksums using a combination of the live approach using the now almost forgotten DEBUG.COM and the "dead-listing" approach. A 90-day shareware expiration is trivial to remove after the encryption is removed.
As +OCR and Reverser constantly remind us, sometimes the best opportunities for learning come from studying small, old-fashioned DOS programs. This small little 28k DOS TSR program corrects your PC's clock. Like many shareware programs, it stops working after 90 days. Although removing the 90 day limit is trivial, we will learn about sidesteping encryption and checksums to reach the 90 day limit code.
The shareware version of this program always prints verbose startup information. The registered version allows the program to start up silently. We will look at a relatively easy way to add the silencing function to this program, even though the program does not provide for a means of registration.
Good old DOS debug.com. Basic knowledge of using debug is assumed.
A DOS disassembler (I used IDA 3.7, freeware version)
Current version 5.06 for DOS.
We start by disassembling the file. We scan through the listing for strings about registration, expiration, etc. We find no strings. Sometimes this may merely mean that the disassembler has flaked out on us. Look using a hex editor. Still no strings, of any kind. This is a definite sign of an encrypted file. Hmmmm.
Encrypted files cannot initially cracked using the "dead-listing" approach. However, the "dead- listing" approach can give us a look at the decryptor code. The encrypted program portions will disassemble into garbage. The basic approach to cracking encrypted files is to run them under a debugger until the program has decrypted itself. At this point, we stop and save the decrypted image to disk and begin the usual cracking process. Unfortunately, this is difficult to do with EXE files. With old-style COM files like rightime.com, our old friend DEBUG, now mostly forgotten, makes this easy, with its ability to write arbitrary areas of memory out to disk
We run our program through a disassembler and scan the listing, following the order of execution.. We notice something interesting at 3B3D:
seg000:3B3D seg000:3B3D loc_0_3B3D: ; CODE XREF: seg000:3B38 j seg000:3B3D mov sp, 100h seg000:3B40 mov bx, word_0_21A seg000:3B44 mov si, 1F6Bh seg000:3B47 mov cx, 1FFFh seg000:3B4A call sub_0_40F2 seg000:3B4D mov si, 3B56h seg000:3B50 mov cx, 3D6Ah seg000:3B53 call sub_0_40F2 seg000:3B53 ;------------------------------------------------------------------- seg000:3B56 db 0C0h ; + seg000:3B57 db 8Dh ; ì seg000:3B58 db 3Dh ; = seg000:3B59 db 4Ch ; L seg000:3B5A db 12h ; seg000:3B5B db 26h ; & seg000:3B5C db 5Ch ; \
At 3B53, we have a call, followed by garbage instead of executable code. Yet the program doesn't flake out if you continue to execute. This is a strong hint of encrypted code. But how is it decrypted? If I was Zen-cracking, and admittedly I wasn't, I would studied the listing further before jumping immediately to fire up debug. But I rushed to execute the program using debug ("t" for single stepping, "g" address for executing until a certain address) Executing the program until 3B53, and then to the end of the 40F2 subroutine (4104), and then executing the return brings me back to 3B56. But lo and behold, 3B56 is no longer garbage. Instead, we now have new, sensible code:
seg000:3B4D mov si, 3B56h seg000:3B50 mov cx, 3D6Ah seg000:3B53 call sub_0_40F2 seg000:3B56 mov word_0_21A, bx seg000:3B5A mov ds, ds:2Ch seg000:3B5E mov si, 0
Now, we know sub_0_40F2 is the decryption routine. and look what we have, yet another decryption call, this time with different si and cx values. After executing sub_0_40F2, we have new executable instructions at 3B56. Looking back on our most recent encryption call, we see that the values of si and cx were 3B56 and 3D6A, the beginning and end of the area that was decrypted. From this we deduce that the next call will decrypt the region 3B56-3D6A, and sure enough, it does.
By studying the encryption code, you will see that bx is the decryption key. After each decryption, bx is saved to be used as the decryption key for the next decryption.
We can save the decrypted output with debug, using the "w" command. But, we must set the ip at 100 (the start of .COM files), cx at 5300 (the length of the file), and the remaining regular registers to 0.
Now, look through the decrypted code for the shareware expiration message. What, it isn't there?! Why? The author of the program has encrypted his program section by section. As you have seen, we already executed several decryptions, the first at 3B4A, and the second at one at 3B53.
We now enter a cycle of
We could just single step our way through with debug instead of using dead-listings. But this would make us old before our time. Eventually, like tediously unwinding a ball of yarn, we have the program laid bare. There are a total of 8 encrypted regions in the program:
|Input Key||Encrypted Region||Decrypt Call From||New Key|
Of course, in real life, I did not skip directly from one encrypted region to another. Several obstacles came up:
From the listing, we find that the "corrupted program" message at is found at 20D7. Searching for 20D7, we see that at 3B8F, the program does a mov dx, 20D7. However, there is not an immediately obvious DOS print routine. With a little more sleuthing, we find that the print routine is at 36F2. A little more sleuthing reveals that the checksum is called from 3B8C, which is executed before the print routine is called to display the "corrupted program" message.
Here is the checksum routine:
seg000:05F8 ; S u b r o u t i n e seg000:05F8 seg000:05F8 sub_0_5F8 proc near ; CODE XREF: seg000:3B8C p seg000:05F8 sub cx, si seg000:05FA inc cx seg000:05FB shr cx, 1 seg000:05FD xor ax, ax seg000:05FF seg000:05FF loc_0_5FF: ; CODE XREF: sub_0_5F8+D j seg000:05FF add ax, [si] seg000:0601 add ax, si seg000:0603 inc si seg000:0604 inc si seg000:0605 loop loc_0_5FF seg000:0607 retn seg000:0607 sub_0_5F8 endp seg000:0607
Look at this routine. It "feels" like a checksum, with the loop and the addition of the contents of [si].
Notice what happens when the checksum is called:
seg000:3B8C call sub_0_5F8 seg000:3B8F mov dx, 20D7h ; corrupt program message seg000:3B92 test ax, ax seg000:3B94 jz loc_0_3B99 ; ok to proceed seg000:3B96 jmp loc_0_3679 ; beggar off, corrupt program
For now, let's put this question aside, and stick to our goal of removing the 90 day time limit. We have unmasked the decryption.
Now that the program has been laid out in unencrypted form, we can look for the 90 day crack. We look for the expiration string and find it at 4221. Searching for references to the string, we find nearby a cmp ax, 5AH (90 decimal) at 4F9B, which is where the expiration date checking is done.
seg000:4F81 ;------------------------------------------------------------------- seg000:4F81 seg000:4F81 loc_0_4F81: ; CODE XREF: seg000:4F67 j seg000:4F81 ; seg000:4F6F j seg000:4F81 ; ... seg000:4F81 mov ax, 5D06h seg000:4F84 int 21h ; DOS - 3.1+ internal - GET seg000:4F84 ; Return: CF set on error, C seg000:4F86 push ds seg000:4F87 pop es seg000:4F88 assume es:seg000 seg000:4F88 pop ds seg000:4F89 mov word ptr loc_0_184, es seg000:4F8D mov word ptr loc_0_182, si seg000:4F91 mov bx, si seg000:4F93 mov ax, es:[bx+34h] seg000:4F97 sub ax, word ptr loc_0_119+1 seg000:4F9B cmp ax, 5Ah ; compare to 90 days seg000:4F9E mov dx, 4221h ; 90 days up message seg000:4FA1 jle loc_0_4FA6 ; continue if not seg000:4FA3 jmp loc_0_3679 ; print and abort if more
We crack this by replacing the cmp ax, 5ah with xor ax,ax, which always returns a zero for the jle.
We must also disarm the decryption routines, because they would scramble our carefully unscrambled code. We also have to disarm the checksum routines.
There are several avenues to disarm the decryption.. We could simply nop out the calls (this is a .COM file and there is no problem relocation patching done by the loader). However, this may not be the safest strategy. In disarming the decryption routines, we note that the output of one decryption is used as the input of another, and that upon exiting the decryption routine, the new password is stored in bx and in the variable dw 21A. To be safe, we can replace the calls with mov bx, yy, where yy is the decryption password. That way, we mimic the return value of the encryption. Any undiscovered calls to the decryption routines (perhaps by the TSR code, which we haven't analyzed in depth) would find the appropriate value in dw 21A to decrypt other parts of the code as necessary. It is possible, but not likely, that the TSR code hides some of this. A crack using the strategy just described appears to load and run correctly. But we haven't tried all the command line options.
To disarm the checksums, we can either find where the appropriate value is added in each checksummed region to make the checksum turn out 0, or we can look for the calls to 5F8 and replace them with
xor ax,ax nop (3 bytes)
to make the following checks see the zero they want. The latter is the easier route which we take.
In my first attempt at removing the checksums, I removed all three checksum calls, which turned out to be a mistake. Two of them were used to check the integrity of data files that are written to disk. Only the one discussed above is the appropriate checksum to remove.
It is trival to remove the "This program is unregistered" message.
The registered version of the program has a command line option for suppressing the start-up messages. Although this functionality is not built into the program, we can add it. Our job is made easier because the program writes to the screen using DOS calls, not BIOS calls. Thus, it is easy, almost trivial, to changing the program's output from standard error (non-suppressable) to standard output (which can be redirected to >nul or to a file). Note not all changes are this easy. If the program writes using BIOS calls, we would have to ADD code to jump around screen writes, which is not a trivial task.
This can be done by the following change
seg000:3716 mov ah, 40h seg000:3718 mov bx, 2 ; HERE- change from 2 to 1 seg000:371B int 21h ; DOS - 2+ - WRITE TO FILE seg000:371B ; BX = file handle, CX =num
There do not appear to be any more encrypted areas, or if there are, the storage of the password values into BX in the patch is enabling them to be decrypted correctly.
That should be it for this crack.
The TSR routines that the program uses to actually correct the PC clock time seem quite interesting in themselves. For those who are interested (maybe in porting these routines to Linux?), they would make a good full-scale reversing exercise. This exercise is left for the reader. The documentation in the rightime archive on clock correction would be the place to start. Please share your discoveries with all of us.