Reversing Doc-o-Matic

(Chant#1: The rebellion of mediocrity)

published by +tsehp June 2001

   This essay is exclusively aimed at surveying the software protection scheme of the program Doc-o-matic 
in the greatest possible detail. This means that it does not contain any explicit instructions for particular 
changes in the program to bring about a fully-functioning program copy. Perhaps the kind reader is aware that a 
substantial difference exists between cracking and RCE. In cracking, the purpose is to make a patch of a program 
so that its protection is overridden (and eventually to publish this program patch in a warez site). 
Having in mind the "qualities" of a large part of the protections existing today, this often 
happens to be a rather easy task - sometimes you even have to do nothing more but patch just a few 
instructions.(I viewed the site of a prominent cracking group recently. I was impressed to read that 
a single person from this group had released 51 cracks for a 3 month period, making a maximum of 9 
cracks per day - WOW:-)). The most important thing is that too often the logic underlying the protection 
is totally ignored by the cracker - the buddy is completely satisfied that the program works due 
to his "competent" intervention.RCE is quite a different activity. 
The purpose is to read the mind and make out the thinking (or the lack of thinking) of the person, 
who has made the effort to invent the particular protection - to figure out the whole protection 
system to its most detailed entirety. In this sense, I have sought to write an essay on RCE. 
The kind reader will have the last word whether I have made it or not.
Target's URL/FTP

What is Doc-O-Matic?
From the authors point of view:
"Doc-O-Matic is the brand new, revolutionary solution for all source code documentation needs. 
Doc-O-Matic generates documentation right from your source code, it understands naturally formed 
in-source comments.
Doc-O-Matic is the most flexible and user friendly documentation system available." etc. etc.

This is a program written in Delphi 5. The version we are talking about is version 1.1.
Here is an explanation of the protection found on the site:
"The trial version is not feature limited nor is it crippled in any way. It only generates a 
copyright and distribution notice on every generated documentation page. The trial version will work 
30 days after you have installed Doc-O-Matic."

This doesn't sound very interesting. But let's keep in mind that our goal is to reverse the protection in 
the maximum (depending on my reversing capabilities :-))) extent. The protection did not seem to be very 
hard and I found it suitable for my goal. 

Every survey should normally progress from the peripheral (the empirical) 
towards the essence. That's why we'll start with some facts about the program behaviour, which we'll try to explain later.

Empirical facts
  1. Run the installation (without running the program itself). Let's see what happened: Registry changes(I use the Regmonitor) When searching the registry for the string "toolsfactory" we find two keys: -HKCU\Software\Toolsfactory\Doc-O-Matic - nothing interesting here -HKLM\Software\Toolsfactory\Doc-O-Matic - the interesting (suspicious) thing here is a binary value named "Info". It contains 48 bytes. We will pay special attention to them later. Installed files Let's take a look at the folder where the program was installed (in my case C:\Program Files\Doc-O-Matic). (There is nothing interesting in the subfolders.) We see: dom.exe, dmcc.exe, dom.exi, idom.dll, (Unwise.exe, Install.log, Readme.txt) and dom.idb - hmm, is this a part of the system....:-)). dom.exe is the main file. dmcc.exe: According to the help: "The command line compiler (dmcc.exe) takes a Doc-O-Matic project file (*.dox) as input and generates documentation according to the settings in the project." Well, this is a command line compiler and we will not take it into account for now. dom.exi: If we open this file in a hex editor (or just look at the file size in the explorer) we will see that this file contains 48 bytes. Hmmm. We remember the "Info" value in the registry and compare the bytes. Well, as it was expected, they are the same :-)) idom.dll: I don't know what the purpose of this dll is. IMHO this dll is not a part of the protection. I performed the following tests: Firstly, the dll is not imported in dom.exe. Secondly, I used Softice to trace all calls to LoadLibraryA and LoadLibraryExA by dom.exe - this library was not loaded. Then I deleted the dll and nothing wrong seemed to happen.
  2. Run the program. There is an Information window containing Getting Started info. It also says that our demo will expire on ......(expiry date here). We also note that nothing is changed in the folders and the registry.
  3. Delete or rename dom.exi - expired.
  4. Delete or rename the registry key "Info" - expired. Note that neither dom.exi nor the registry key are recovered by the program. When the file and the reg key are restored back the things are OK.
  5. Change the date so that the period is over - expired. Restore the date - OK.
  6. Using a hex editor I changed the contents of dom.exi (i.e. fill with zeroes). What do you expect to have happened? It worked....:-)
  7. Uninstall program. Everything is uninstalled except the "Info" key from the registry and in the case of automatic uninstallation dom.exi. If you delete the reg key your trial period starts over :-).
  8. Change the name of dom.exe - expired.
Obviously this is not a malicious protection. I like Fravia friendly protections! So let's go ahead! Analysis I think it becomes clear from the above that the "Info" key in the registry could be a good starting point for analysis. If you decide to use IDAPro to disassemble dom.exe it is good idea to download Delhi 5 FLIRT File from here. I don't know the reason why IDA is not able to apply any Delphi signature automatically. I applied the downloaded d5vcl signature manually. Then I found that there are two string references: at address 004BC182 and 004BC1A8. Both references are in one function at addr 004BC0B8, which I named "ProcessRegInfo". ProcessRegInfo is called from a function at 004BC42C. I named it ReadRegistrationData. I propose to start the analysis of the protection with this function. Let's take a closer look. 004BC42C ReadRegistrationData proc near ; CODE XREF: sub_4BC688+43.p 004BC42C 004BC42C Revision = byte ptr -20h 004BC42C Version = byte ptr -1Fh 004BC42C RegDay = word ptr -1Eh 004BC42C RegMonth = word ptr -1Ch 004BC42C RegYear = word ptr -1Ah 004BC42C PathToExe = dword ptr -18h 004BC42C IdxOfAddrOfYear = dword ptr -14h 004BC42C IdxOfAddrOfMonth= dword ptr -10h 004BC42C IdxOfAddrOfDay = dword ptr -0Ch 004BC42C IdxOfAddrOfVersion= dword ptr -8 004BC42C IdxOfAddrOfRevision= dword ptr -4 004BC42C 004BC42C push ebp 004BC42D mov ebp, esp 004BC42F add esp, 0FFFFFFE0h 004BC432 push ebx 004BC433 push esi 004BC434 push edi 004BC435 mov [ebp+PathToExe], eax <-- Store the path to exe in a local variable. (This is Delphi. Some parameters to functions are passed through registers.) 004BC438 mov eax, [ebp+PathToExe]<--Parameter for next function in eax 004BC43B call sub_404228 <--LStrAddRef 004BC440 mov edi, offset addressOfList 004BC445 xor eax, eax 004BC447 push ebp 004BC448 push offset loc_4BC608 004BC44D push dword ptr fs:[eax] 004BC450 mov fs:[eax], esp 004BC453 call @Randomize <--Init the random generator 004BC458 mov dl, 1 004BC45A mov eax, ds:off_40F868 <--Where to place the pointer to TList instance 004BC45F call @TObject@$bctr ; TObject::`...' 004BC464 mov [edi], eax <-- addressOfList = eax 004BC466 mov eax, 1F4h <--500 : The parameter for RandInt 004BC46B call RandInt <--Get a random number from 0 to 499 004BC470 add eax, 64h <--Add 100dec to the result 004BC473 mov esi, eax <--Store in esi I can't understand what the hell is happening in the next 4 lines. In esi we have a number between 100 and 599. Let's say 100 for convenience. Than we get 99 and test for negative.... 004BC475 dec esi 004BC476 test esi, esi 004BC478 jl short loc_4BC4A1 jump if negative 004BC47A inc esi 004BC47B Below is a loop until esi(the random number) becomes 0 004BC47B loop: ; CODE XREF: sub_4BC42C+73.j 004BC47B mov eax, 2 004BC480 call GetMem GetMem simply calls SysGetMem in the VCL. Here is an excerpt from the VCL Reference: "extern PACKAGE void * __fastcall SysGetMem(int Size); Allocates the specified number of bytes and returns a pointer to it." Hence the above code allocates 2 bytes and returns a pointer to them in eax 004BC485 mov ebx, eax <---The returned pointer in ebx 004BC487 mov edx, ebx <---The returned pointer in edx. This is input parameter for TList::Add 004BC489 mov eax, [edi] 004BC48B call @TList@Add ; TList::Add <--Store the pointer in the list 004BC490 mov eax, 1Fh ;31 decimal 004BC495 call RandInt <--Get a random number from 0 to 30 004BC49A inc eax <--Convert in interval 1 - 31 004BC49B mov [ebx], ax <--Initialize the stored pointer with the random value 004BC49E dec esi <--dec the loop parameter 004BC49F jnz short loop OK. After this loop we have a list of random number of pointers to 2 bytes initialized with random values between 1 and 31 004BC4A1 004BC4A1 loc_4BC4A1: ; CODE XREF: sub_4BC42C+4C.j 004BC4A1 lea eax, [ebp+Version] 004BC4A4 push eax <--Here you can see that 2 arguments to function which I called ProcessRegInfo are pushed into the stack, and another 3 arguments are passed through ecx, edx and eax 004BC4A5 lea eax, [ebp+Revision] 004BC4A8 push eax 004BC4A9 lea ecx, [ebp+RegDay] <--I renamed the local variables with these names. Later you will see that names are not arbitrary :-) 004BC4AC lea edx, [ebp+RegMonth] 004BC4AF lea eax, [ebp+RegYear] 004BC4B2 call ProcessRegInfo 004BC4B7 mov eax, [ebp+PathToExe] 004BC4BA push eax <--Push the parameter for CheckDOM_EXI. 004BC4BB lea ecx, [ebp+RegDay] <--One can think that these are other parameters for CheckDOM_EXI. May be they are declared as parameters but actually they are not used. 004BC4BE lea edx, [ebp+RegMonth] 004BC4C1 lea eax, [ebp+RegYear] 004BC4C4 call CheckDOM_EXI 004BC4C9 test al, al 004BC4CB jnz short loc_4BC4E7 <--OK 004BC4CD mov [ebp+RegYear], 0 <--If dom.exi is not present fill previously read data with 0 004BC4D3 mov [ebp+RegMonth], 0 004BC4D9 mov [ebp+RegDay], 0 004BC4DF mov [ebp+Version], 0 004BC4E3 mov [ebp+Revision], 0 004BC4E7 004BC4E7 loc_4BC4E7: ; CODE XREF: sub_4BC42C+9F.j I am going to explain briefly what happened up to now: 1. The setup has created a coded info containing the date and version of registration 2. This function has read and decoded the data and stored the results in corresponding local variables 3. Meanwhile the protection checked the existence of the file dom.exi 4. The data have to be stored somewhere in memory for further use - this is performed by the next lines. How? Remember that the protector(s) paid special attention to create an array with addresses. In the next lines 5 random addresses are fetched from the array and the date and version are stored in the returned locations. Obviously the protector thought that the play with these random numbers will make the protection very strong. As we see here all these efforts are in vain and rather ridiculous:-) 004BC4E7 mov eax, [edi] 004BC4E9 mov eax, [eax+8] <--[eax+8] is the cnt of addresses stored in the list 004BC4EC sub eax, 0Ah <--last 10 addresses are not available. See below why. 004BC4EF call RandInt <--Get a random index in the list 004BC4F4 mov [ebp+IdxOfAddrOfYear], eax <--Store the result in a local variable 004BC4F7 mov eax, [edi] 004BC4F9 mov eax, [eax+8] 004BC4FC sub eax, 0Ah 004BC4FF call RandInt 004BC504 mov [ebp+IdxOfAddrOfMonth], eax 004BC507 mov eax, [edi] 004BC509 mov eax, [eax+8] 004BC50C sub eax, 0Ah 004BC50F call RandInt 004BC514 mov [ebp+IdxOfAddrOfDay], eax 004BC517 mov eax, [edi] 004BC519 mov eax, [eax+8] 004BC51C sub eax, 0Ah 004BC51F call RandInt 004BC524 mov [ebp+IdxOfAddrOfVersion], eax 004BC527 mov eax, [edi] 004BC529 mov eax, [eax+8] 004BC52C sub eax, 0Ah 004BC52F call RandInt 004BC534 mov [ebp+IdxOfAddrOfRevision], eax Now we have 5 random indexes. The next lines ensure that the indexes are unique. (This was a bug in version 1.0) 004BC537 push ebp 004BC538 mov eax, 1 004BC53D call ChangeIndexIfNeeded 004BC542 pop ecx 004BC543 push ebp 004BC544 mov eax, 2 004BC549 call ChangeIndexIfNeeded 004BC54E pop ecx 004BC54F push ebp 004BC550 mov eax, 3 004BC555 call ChangeIndexIfNeeded 004BC55A pop ecx 004BC55B push ebp 004BC55C mov eax, 4 004BC561 call ChangeIndexIfNeeded 004BC566 pop ecx 004BC567 mov edx, [ebp+IdxOfAddrOfYear] 004BC56A mov eax, [edi] 004BC56C call @TList@Get <--Get the address where we will store reg year 004BC571 mov ds:PtrToRegYear, eax <--Move the address in memory location I called PtrToRegYear 004BC576 mov eax, ds:PtrToRegYear <--The same address in eax 004BC57B mov dx, [ebp+RegYear] <--The year in dx 004BC57F mov [eax], dx <--The year in memory pointed by eax 004BC582 mov edx, [ebp+IdxOfAddrOfMonth] <--Do the same with other data 004BC585 mov eax, [edi] 004BC587 call @TList@Get ; TList::Get 004BC58C mov ds:PtrToRegMonth, eax 004BC591 mov eax, ds:PtrToRegMonth 004BC596 mov dx, [ebp+RegMonth] 004BC59A mov [eax], dx 004BC59D mov edx, [ebp+IdxOfAddrOfDay] 004BC5A0 mov eax, [edi] 004BC5A2 call @TList@Get ; TList::Get 004BC5A7 mov ds:PtrToRegDay, eax 004BC5AC mov eax, ds:PtrToRegDay 004BC5B1 mov dx, [ebp+RegDay] 004BC5B5 mov [eax], dx 004BC5B8 mov edx, [ebp+IdxOfAddrOfVersion] 004BC5BB mov eax, [edi] 004BC5BD call @TList@Get ; TList::Get 004BC5C2 mov ds:PtrToVersion, eax 004BC5C7 xor eax, eax 004BC5C9 mov al, [ebp+Version] 004BC5CC mov edx, ds:PtrToVersion 004BC5D2 mov [edx], ax 004BC5D5 mov edx, [ebp+IdxOfAddrOfRevision] 004BC5D8 mov eax, [edi] 004BC5DA call @TList@Get ; TList::Get 004BC5DF mov ds:PtrToRevision, eax 004BC5E4 xor eax, eax 004BC5E6 mov al, [ebp+Revision] 004BC5E9 mov edx, ds:PtrToRevision 004BC5EF mov [edx], ax 004BC5F2 xor eax, eax 004BC5F4 pop edx 004BC5F5 pop ecx 004BC5F6 pop ecx 004BC5F7 mov fs:[eax], edx 004BC5FA push offset loc_4BC60F 004BC5FF 004BC5FF loc_4BC5FF: ; CODE XREF: CODE:004BC60D.j 004BC5FF lea eax, [ebp+PathToExe] 004BC602 call LStrClr 004BC607 retn 004BC607 sub_4BC42C endp ; sp = -40h 004BC607 004BC608 ; --------------------------------------------------------------------------- 004BC608 004BC608 loc_4BC608: ; DATA XREF: sub_4BC42C+1C.o 004BC608 jmp loc_4037EC 004BC60D ; --------------------------------------------------------------------------- 004BC60D jmp short loc_4BC5FF 004BC60F ; --------------------------------------------------------------------------- 004BC60F 004BC60F loc_4BC60F: ; DATA XREF: sub_4BC42C+1CE.o 004BC60F pop edi 004BC610 pop esi 004BC611 pop ebx 004BC612 mov esp, ebp 004BC614 pop ebp 004BC615 retn //-------------------------------------------------------------------------------------------------- //-------------------------------------------------------------------------------------------------- 004BC0B8 ProcessRegInfo proc near ; CODE XREF: sub_4BC42C+86.p 004BC0B8 004BC0B8 InfoBufferAddr = dword ptr -20h 004BC0B8 RegDataType = byte ptr -1Ch 004BC0B8 RegDataSize = dword ptr -18h 004BC0B8 var_14 = dword ptr -14h 004BC0B8 RegDayAddr = byte ptr -10h 004BC0B8 RegMonthAddr = dword ptr -0Ch 004BC0B8 RegYearAddr = dword ptr -8 004BC0B8 InfoBufferAddrCopy = dword ptr -4 004BC0B8 RevisionAddr = dword ptr 8 004BC0B8 VersionAddr = dword ptr 0Ch 004BC0B8 004BC0B8 push ebp 004BC0B9 mov ebp, esp 004BC0BB add esp, 0FFFFFFE0h 004BC0BE push ebx 004BC0BF push esi 004BC0C0 push edi 004BC0C1 mov dword ptr [ebp+RegDayAddr], ecx 004BC0C4 mov [ebp+RegMonthAddr], edx 004BC0C7 mov [ebp+RegYearAddr], eax 004BC0CA mov edi, [ebp+RevisionAddr] 004BC0CD mov esi, [ebp+VersionAddr] 004BC0D0 mov eax, [ebp+RegYearAddr] Initialization of data below 004BC0D3 mov word ptr [eax], 0 004BC0D8 mov eax, [ebp+RegMonthAddr] 004BC0DB mov word ptr [eax], 0 004BC0E0 mov eax, dword ptr [ebp+RegDayAddr] 004BC0E3 mov word ptr [eax], 0 004BC0E8 mov byte ptr [esi], 1 <--Version 1 004BC0EB mov byte ptr [edi], 0 <--Revision 0 004BC0EE xor ecx, ecx 004BC0F0 mov dl, 1 004BC0F2 mov eax, ds:off_47A3CC 004BC0F7 call unknown_libname_684 004BC0FC mov [ebp+var_14], eax 004BC0FF xor edx, edx 004BC101 push ebp 004BC102 push offset loc_4BC2AB 004BC107 push dword ptr fs:[edx] 004BC10A mov fs:[edx], esp 004BC10D mov edx, 80000002h 004BC112 mov eax, [ebp+var_14] 004BC115 call @TRegistry@SetRootKey ; TRegistry::SetRootKey 004BC11A xor ecx, ecx 004BC11C mov edx, offset aSoftwareTool_1 ; "Software\\toolsfactory\\Doc-O-Matic" 004BC121 mov eax, [ebp+var_14] 004BC124 call @TRegistry@OpenKey ; TRegistry::OpenKey 004BC129 test al, al 004BC12B jz loc_4BC295 004BC131 xor edx, edx 004BC133 push ebp 004BC134 push offset loc_4BC28E 004BC139 push dword ptr fs:[edx] 004BC13C mov fs:[edx], esp 004BC13F mov eax, 30h 004BC144 call GetMem <--Allocate 48 bytes buffer 004BC149 mov [ebp+InfoBufferAddr], eax <--Store the addres in InfoBufferAddr var. 004BC14C xor edx, edx 004BC14E push ebp 004BC14F push offset loc_4BC271 004BC154 push dword ptr fs:[edx] 004BC157 mov fs:[edx], esp 004BC15A xor ebx, ebx 004BC15C xor edx, edx 004BC15E push ebp 004BC15F push offset loc_4BC22B 004BC164 push dword ptr fs:[edx] 004BC167 mov fs:[edx], esp 004BC16A mov eax, [ebp+InfoBufferAddr] 004BC16D mov [ebp+InfoBufferAddrCopy], eax <--Later it is shown why they need this copy. 004BC170 mov eax, [ebp+InfoBufferAddr] 004BC173 xor ecx, ecx 004BC175 mov edx, 30h 004BC17A call FillChar <-- Fill buffer with zeroes. 004BC17F lea ecx, [ebp+RegDataType] 004BC182 mov edx, offset aInfo 004BC187 mov eax, [ebp+var_14] 004BC18A call @TRegistry@GetDataInfo ; TRegistry::GetDataInfo The above call to TRegistry::GetDataInfo retrieves the number of bytes in the registry key 'Info' The variable 'RegDataSize' contains this value 004BC18F test al, al <--Test TRegistry::GetDataInfo return value 004BC191 jz loc_4BC221 004BC197 mov eax, [ebp+RegDataSize] 004BC19A cmp eax, 30h <--'Info' key contains 48 bytes? 004BC19D jz short loc_4BC1A4 004BC19F cmp eax, 20h <--'Info' key contains 32 bytes? We know that the 'Info' key contains 48 bytes. The above check sounds strange. Fortunately my work started with version 1.0. In v.1.0 the 'Info' key contained 32 bytes. Obviously the protector added another 16 bytes in order to get 'stronger' protection. The version info is stored there. 004BC1A2 jnz short loc_4BC221 <--Exit 004BC1A4 004BC1A4 loc_4BC1A4: ; CODE XREF: ProcessRegInfo+E5.j 004BC1A4 push eax <--Push number of bytes to read 004BC1A5 mov ecx, [ebp+InfoBufferAddr] <--Ptr to buffer 004BC1A8 mov edx, offset aInfo <--Key name 004BC1AD mov eax, [ebp+var_14] 004BC1B0 call @TRegistry@ReadBinaryData ; TRegistry::ReadBinaryData OK. Now we have the 48 bytes in memory. Let's divide these bytes in 6 consecutive octets, for example: 1E 95 62 87 96 06 B5 7F 9C 76 F2 E4 86 FA 92 B7 17 1A 0E 2F E9 18 D6 57 44 4F A9 D9 B5 C2 2C 9C 9F 55 2C B3 E0 5D 48 7B E8 AF 89 80 44 24 FA 93 Look at the calls to function DecodeOctetAndGetAByte below. There are 6 calls to this function. It is a good idea to take a look at its description before we proceed. 004BC1B5 push ebp 004BC1B6 call DecodeOctetAndGetAByte <--process the first 8 bytes 004BC1BB pop ecx 004BC1BC and eax, 0FFh <--In our example the function returned 0x14 == 20Dec in eax 004BC1C1 mov edx, [ebp+RegYearAddr] <--Move address of reg year in edx 004BC1C4 mov [edx], ax <--Move 20dec in memory pointed by edx 004BC1C7 push ebp 004BC1C8 call DecodeOctetAndGetAByte 004BC1CD pop ecx 004BC1CE and eax, 0FFh <--In our example the function returned 0x1 in eax 004BC1D3 mov edx, [ebp+RegYearAddr] <--[edx] is the result from first ProcessInfo(20dec) 004BC1D6 imul dx, [edx], 64h <--20*100 = 2000, stored in dx 004BC1DA add ax, dx add the offset from year 2000, we get 2001 004BC1DD mov edx, [ebp+RegYearAddr] 004BC1E0 mov [edx], ax 2001 in [edx] 004BC1E3 push ebp 004BC1E4 call DecodeOctetAndGetAByte 004BC1E9 pop ecx 004BC1EA and eax, 0FFh <--we have 4 in eax. This stands for April 004BC1EF mov edx, [ebp+RegMonthAddr] 004BC1F2 mov [edx], ax 004BC1F5 push ebp 004BC1F6 call DecodeOctetAndGetAByte 004BC1FB pop ecx 004BC1FC and eax, 0FFh <--we have 0x14 in eax 004BC201 mov edx, dword ptr [ebp+RegDayAddr] 004BC204 mov [edx], ax 004BC207 cmp [ebp+RegDataSize], 30h Do you remember that in ver 1.0 we have 20h bytes in registry and in version 1.1 - 30h 004BC20B jnz short loc_4BC21F Use default settings, look 004BC0E8 004BC20D push ebp 004BC20E call DecodeOctetAndGetAByte 004BC213 pop ecx 004BC214 mov [esi], al <--al == 1 004BC216 push ebp 004BC217 call DecodeOctetAndGetAByte 004BC21C pop ecx 004BC21D mov [edi], al <--al == 1 004BC21F 004BC21F loc_4BC21F: ; CODE XREF: ProcessRegInfo+153.j 004BC21F mov bl, 1 .......Some lines skipped here....... //-------------------------------------------------------------------------------------------------- DecodeOctetAndGetAByte This function reads 8 bytes from the buffer with 48 bytes, performs decoding and returns the result in al. Increments the pointer in variable InfoBufferAddrCopy above, to point to the next octet. For example. let's get the first octet: 1E 95 62 87 96 06 B5 7F. This stands for year 2000, so if you install the program on your computer you will have the same first octet. //-------------------------------------------------------------------------------------------------- 004BC040 DecodeOctetAndGetAByte proc near ; CODE XREF: ProcessRegInfo+FE.p 004BC040 ; ProcessRegInfo+110.p ... 004BC040 004BC040 var_6 = byte ptr -6 004BC040 var_5 = byte ptr -5 004BC040 var_4 = byte ptr -4 004BC040 var_3 = byte ptr -3 004BC040 var_2 = byte ptr -2 004BC040 var_1 = byte ptr -1 004BC040 arg_0 = dword ptr 8 <--This argument is the ebp of ProcessRegInfo 004BC040 004BC040 push ebp 004BC041 mov ebp, esp 004BC043 add esp, 0FFFFFFF8h 004BC046 push ebx 004BC047 push esi 004BC048 mov esi, [ebp+arg_0] <-- esi contains the ebp of ProcessRegInfo 004BC04B add esi, 0FFFFFFFCh <--0FFFFFFFCh == -4, esi = (ebp of ProcessRegInfo) - 4. Now look at 004BC16D above. [ebp of ProcessRegInfo - 4] == &(Buffer with the data). 004BC04E mov eax, [esi] <--Move address of the buffer in eax 004BC050 mov bl, [eax] <--Get first byte in bl(bl = 1E) 004BC052 inc dword ptr [esi] <--[esi] = addr of next byte in the octet. This is the reason why they need a second copy of the address of the buffer in ProcessRegInfo 004BC054 mov eax, [esi] 004BC056 mov al, [eax] <--2nd(95) byte in al 004BC058 mov [ebp+var_1], al <--var_1 = 0x95 004BC05B inc dword ptr [esi] 004BC05D mov eax, [esi] 004BC05F mov al, [eax] 004BC061 mov [ebp+var_2], al <--var_2 = 0x62 004BC064 inc dword ptr [esi] 004BC066 mov eax, [esi] 004BC068 mov al, [eax] 004BC06A mov [ebp+var_3], al <--var_3 = 0x87 004BC06D inc dword ptr [esi] 004BC06F mov eax, [esi] 004BC071 mov al, [eax] 004BC073 mov [ebp+var_4], al <--var_4 = 0x96 004BC076 inc dword ptr [esi] 004BC078 mov eax, [esi] 004BC07A mov al, [eax] 004BC07C mov [ebp+var_5], al <--var_5 = 0x06 004BC07F inc dword ptr [esi] 004BC081 mov eax, [esi] 004BC083 mov al, [eax] 004BC085 mov [ebp+var_6], al <--var_4 = 0xb5 004BC088 inc dword ptr [esi] <--[esi] = addr of last byte in the octet 004BC08A mov al, [ebp+var_3] 004BC08D push eax <--Prepare arguments for next function. 004BC08E mov al, [ebp+var_4] 004BC091 push eax 004BC092 mov al, [ebp+var_5] 004BC095 push eax 004BC096 mov al, [ebp+var_6] 004BC099 push eax 004BC09A mov cl, [ebp+var_2] <--var_2 is passed throuh ecx 004BC09D mov dl, [ebp+var_1] <--<--var_1 is passed throuh edx 004BC0A0 mov eax, ebx <--bl = 1E, the first byte is passed throuh eax 004BC0A2 call SumFisrtSeven 004BC0A7 mov edx, [esi] 004BC0A9 xor al, [edx] <--xor the result with last number in the octet(7F) 004BC0AB and al, 0FFh <--Convert to byte. In our example we get 0x14 004BC0AD inc dword ptr [esi] <--Move the ptr to next octet 004BC0AF pop esi 004BC0B0 pop ebx 004BC0B1 pop ecx 004BC0B2 pop ecx 004BC0B3 pop ebp 004BC0B4 retn 004BC0B4 DecodeOctetAndGetAByte endp //-------------------------------------------------------------------------------------------------- SumFisrtSeven This function is very simple: it sums up its seven arguments, the result is divided by 7 and the byte converted quotient is returned. //-------------------------------------------------------------------------------------------------- 004BBF98 ; Attributes: bp-based frame 004BBF98 004BBF98 SumFisrtSeven proc near ; CODE XREF: DecodeOctetAndGetAByte+62.p 004BBF98 004BBF98 arg_0 = byte ptr 8 004BBF98 arg_4 = byte ptr 0Ch 004BBF98 arg_8 = byte ptr 10h 004BBF98 arg_C = byte ptr 14h 004BBF98 004BBF98 push ebp 004BBF99 mov ebp, esp 004BBF9B and eax, 0FFh <--Convert arguments in eax and edx to byte 004BBFA0 and edx, 0FFh 004BBFA6 add eax, edx <--Add first two 004BBFA8 xor edx, edx 004BBFAA mov dl, cl 004BBFAC add eax, edx <--Add the byte passed through ecx 004BBFAE xor edx, edx 004BBFB0 mov dl, [ebp+arg_C] <--Then add the rest of arguments passed through stack 004BBFB3 add eax, edx 004BBFB5 xor edx, edx 004BBFB7 mov dl, [ebp+arg_8] 004BBFBA add eax, edx 004BBFBC xor edx, edx 004BBFBE mov dl, [ebp+arg_4] 004BBFC1 add eax, edx 004BBFC3 xor edx, edx 004BBFC5 mov dl, [ebp+arg_0] 004BBFC8 add eax, edx 004BBFCA mov ecx, eax 004BBFCC mov eax, ecx <--Strange compiler...:-) 004BBFCE mov ecx, 7 004BBFD3 cdq 004BBFD4 idiv ecx <--Divide the sum in accumulator by 7 004BBFD6 mov ecx, eax 004BBFD8 mov eax, ecx 004BBFDA and al, 0FFh <--convert result to byte. 004BBFDC pop ebp 004BBFDD retn 10h 004BBFDD SumFisrtSeven endp //-------------------------------------------------------------------------------------------------- CheckDOM_EXI This function deals with the dom.exi file. I renamed the arg_0 to FullPathToExe. This is the only input parameter of this function.(i.e. C:\Program Files\Doc-O-Matic\dom.exe). It returns 1 if the file C:\Program Files\Doc-O-Matic\dom.exi exists and 0 otherwise. IMHO this function is a shame for the author :-) Let's take a look! //-------------------------------------------------------------------------------------------------- 004BC2F8 CheckDOM_EXI proc near ; CODE XREF: sub_4BC42C+98.p 004BC2F8 var_C = dword ptr -0Ch 004BC2F8 var_8 = dword ptr -8 004BC2F8 result = byte ptr -1 004BC2F8 FullPathToExe = dword ptr 8 004BC2F8 004BC2F8 push ebp 004BC2F9 mov ebp, esp 004BC2FB add esp, 0FFFFFFF4h 004BC2FE push ebx 004BC2FF push esi 004BC300 push edi 004BC301 xor ebx, ebx 004BC303 mov [ebp+var_C], ebx 004BC306 mov eax, [ebp+FullPathToExe] 004BC309 call LStrAddRef 004BC30E xor eax, eax 004BC310 push ebp 004BC311 push offset loc_4BC3C0 004BC316 push dword ptr fs:[eax] 004BC319 mov fs:[eax], esp 004BC31C mov [ebp+result], 0 <--Initialize the result with 0. It will be changed to 1 when OK. 004BC320 lea edx, [ebp+var_C] 004BC323 mov eax, [ebp+FullPathToExe] 004BC326 call ChangeExt <-- Change C:\Program Files\Doc-O-Matic\dom.exe to C:\Program Files\Doc-O-Matic\dom.exi 004BC32B mov eax, [ebp+var_C] 004BC32E call @FileExists <--Test file existence 004BC333 test al, al 004BC335 jz short loc_4BC3A2 <--Jump if file doesn't exist. The result is still 0. This explains the empirical facts #3 and #8. 004BC337 mov dl, 1 004BC339 mov eax, ds:off_410040 004BC33E call @TObject@$bctr ; TObject::`...' <--Preparing a stream object to read the contents of C:\Program Files\Doc-O-Matic\dom.exi 004BC343 mov [ebp+var_8], eax 004BC346 xor eax, eax 004BC348 push ebp 004BC349 push offset loc_4BC39B 004BC34E push dword ptr fs:[eax] 004BC351 mov fs:[eax], esp 004BC354 xor eax, eax 004BC356 push ebp 004BC357 push offset loc_4BC37B 004BC35C push dword ptr fs:[eax] 004BC35F mov fs:[eax], esp 004BC362 mov edx, [ebp+var_C] 004BC365 mov eax, [ebp+var_8] 004BC368 call @TIBXSQLVAR@LoadFromFile ; TIBXSQLVAR::LoadFromFile <--Read the contents in memory. 004BC36D mov [ebp+result], 1 <--After this we suppose that things are OK. Note that down to the end of the function the value is not changed. Here I will try to explain the behaviour of the program described in experimental fact #6. It states that the contents of dom.exi is never used. Let's experiment. Fill dom.exi with the string 'I love Microsoft'. I suppose it is unlikely to find this string in memory :-). After the above call we search the memory and find the string at some location. Place a bpm there and press Ctrl-D. SoftIce pops and after a few F12 we find out .........that the memory is accessed by a call to TObject::Free (take a look a few lines below). I think this proves the contents of dom.exi is never used. 004BC371 xor eax, eax 004BC373 pop edx 004BC374 pop ecx 004BC375 pop ecx 004BC376 mov fs:[eax], edx 004BC379 jmp short loc_4BC385 004BC37B ; --------------------------------------------------------------------------- 004BC37B 004BC37B loc_4BC37B: ; DATA XREF: CheckDOM_EXI+5F.o 004BC37B jmp @@HandleAnyException ; __linkproc__ HandleAnyException 004BC380 ; --------------------------------------------------------------------------- 004BC380 call DoneExcept 004BC385 004BC385 loc_4BC385: ; CODE XREF: CheckDOM_EXI+81.j 004BC385 xor eax, eax 004BC387 pop edx 004BC388 pop ecx 004BC389 pop ecx 004BC38A mov fs:[eax], edx 004BC38D push offset loc_4BC3A2 004BC392 004BC392 loc_4BC392: ; CODE XREF: CheckDOM_EXI+A8.j 004BC392 mov eax, [ebp+var_8] 004BC395 call @TObject@Free ; TObject::Free <--This line frees the stream :-). The string 'I love Microsoft' dissapeared. 004BC39A retn <--This is just a jmp to loc_4BC3A2 as a result of the above push offset loc_4BC3A2 004BC39B ; --------------------------------------------------------------------------- 004BC39B 004BC39B loc_4BC39B: ; DATA XREF: CheckDOM_EXI+51.o 004BC39B jmp loc_4037EC 004BC3A0 ; --------------------------------------------------------------------------- 004BC3A0 jmp short loc_4BC392 004BC3A2 ; --------------------------------------------------------------------------- 004BC3A2 004BC3A2 loc_4BC3A2: ; CODE XREF: CheckDOM_EXI+3D.j 004BC3A2 ; DATA XREF: CheckDOM_EXI+95.o 004BC3A2 xor eax, eax 004BC3A4 pop edx 004BC3A5 pop ecx 004BC3A6 pop ecx 004BC3A7 mov fs:[eax], edx 004BC3AA push offset loc_4BC3C7 004BC3AF 004BC3AF loc_4BC3AF: ; CODE XREF: CODE:004BC3C5.j 004BC3AF lea eax, [ebp+var_C] 004BC3B2 call LStrClr 004BC3B7 lea eax, [ebp+FullPathToExe] 004BC3BA call LStrClr 004BC3BF retn <--This is just a jmp to loc_4BC3C7 as a result of the above push offset loc_4BC3C7 004BC3BF CheckDOM_EXI endp ; sp = -20h 004BC3BF 004BC3C0 ; --------------------------------------------------------------------------- 004BC3C0 004BC3C0 loc_4BC3C0: ; DATA XREF: CheckDOM_EXI+19.o 004BC3C0 jmp loc_4037EC 004BC3C5 ; --------------------------------------------------------------------------- 004BC3C5 jmp short loc_4BC3AF 004BC3C7 ; --------------------------------------------------------------------------- 004BC3C7 004BC3C7 loc_4BC3C7: ; DATA XREF: CheckDOM_EXI+B2.o 004BC3C7 mov al, [ebp+result] 004BC3CA pop edi 004BC3CB pop esi 004BC3CC pop ebx 004BC3CD mov esp, ebp 004BC3CF pop ebp 004BC3D0 retn 4 //-------------------------------------------------------------------------------------------------- ChangeIndexIfNeeded This function helps the authors to solve the VERY difficult task to ensure that five random generated numbers are not equal. Let's see what they have invented: Let's have 5 numbers stored in an array int A[5]. The assembler code below could be reversed like this (I don't state that if you compile my functions you will get the same assembly code. They just demonstrate the logic of their algorithm): void ChangeIndexIfNeeded(int* array, int skipIdx) { while (IsDuplicated(array, skipIdx) == 0) array[skipIdx]++; } IsDuplicated returns 0 if duplicated and 1 otherwise; int IsDuplicated(int* array, int skipIdx) { for (int i = 0; i < 5; i++) { if (i != skipIdx && array[i] == array[skipIdx]) return 0; } return 1; } (Homework: Try to find a more ineffective algorithm!) //-------------------------------------------------------------------------------------------------- 004BC408 ChangeIndexIfNeeded proc near ; CODE XREF: sub_4BC42C+111.p 004BC408 ; sub_4BC42C+11D.p ... 004BC408 004BC408 arg_0 = dword ptr 8 <--arg_0 is the ebp of the caller 004BC408 004BC408 push ebp 004BC409 mov ebp, esp 004BC40B push ebx 004BC40C mov ebx, eax 004BC40E jmp short loc_4BC417 004BC410 ; --------------------------------------------------------------------------- 004BC410 004BC410 loc_4BC410: ; CODE XREF: ChangeIndexIfNeeded+1D.j 004BC410 mov eax, [ebp+arg_0] 004BC413 inc dword ptr [eax+ebx*4-14h] 004BC417 004BC417 loc_4BC417: ; CODE XREF: ChangeIndexIfNeeded+6.j 004BC417 mov eax, [ebp+arg_0] 004BC41A push eax 004BC41B mov eax, ebx 004BC41D call IsDuplicated 004BC422 pop ecx 004BC423 test al, al 004BC425 jz short loc_4BC410 004BC427 pop ebx 004BC428 pop ebp 004BC429 retn 004BC429 ChangeIndexIfNeeded endp //-------------------------------------------------------------------------------------------------- IsDuplicated //-------------------------------------------------------------------------------------------------- 004BC3D4 IsDuplicated proc near ; CODE XREF: ChangeIndexIfNeeded+15.p 004BC3D4 004BC3D4 arg_0 = dword ptr 8 004BC3D4 004BC3D4 push ebp 004BC3D5 mov ebp, esp 004BC3D7 push ebx 004BC3D8 push esi 004BC3D9 mov esi, eax 004BC3DB mov al, 1 004BC3DD xor ecx, ecx 004BC3DF mov edx, [ebp+arg_0] 004BC3E2 add edx, 0FFFFFFECh 004BC3E5 004BC3E5 loc_4BC3E5: ; CODE XREF: IsDuplicated+2B.j 004BC3E5 cmp ecx, esi 004BC3E7 jz short loc_4BC3F8 004BC3E9 mov ebx, [ebp+arg_0] 004BC3EC mov ebx, [ebx+esi*4-14h] 004BC3F0 cmp ebx, [edx] 004BC3F2 jnz short loc_4BC3F8 004BC3F4 xor eax, eax 004BC3F6 jmp short loc_4BC401 004BC3F8 ; --------------------------------------------------------------------------- 004BC3F8 004BC3F8 loc_4BC3F8: ; CODE XREF: IsDuplicated+13.j 004BC3F8 ; IsDuplicated+1E.j 004BC3F8 inc ecx 004BC3F9 add edx, 4 004BC3FC cmp ecx, 5 004BC3FF jnz short loc_4BC3E5 004BC401 004BC401 loc_4BC401: ; CODE XREF: IsDuplicated+22.j 004BC401 pop esi 004BC402 pop ebx 004BC403 pop ebp 004BC404 retn 004BC404 IsDuplicated endp When the ReadRegistrationData finish, we have the day, month, year and version info in memory. For example PtrToRegYear is stored at address 005349F8. Two more questions could arise here: How the above function is called and when? Let's trace the call sequence in IDA. ReadRegistrationData is called only from sub_4BC688. Next we have the offset of sub_4BC688 stored at 0052F414. It is not difficult to see that this is a table with function offsets. The table starts at 0052F074 with a dword value(0xD9) that I suppose is the length of the table, followed by a ptr to the beginning of the table itself. As it can be seen in the IDA listing, the size of the table is used in the InitExe function: 0052F744 public start 0052F744 start: 0052F744 push ebp 0052F745 mov ebp, esp 0052F747 add esp, 0FFFFFFF4h 0052F74A push ebx 0052F74B mov eax, offset dword_52F074 <---Here 0052F750 call InitExe 0052F755 mov ebx, ds:off_532E5C 0052F75B mov eax, [ebx] 0052F75D call unknown_libname_430 0052F762 mov eax, [ebx] 0052F764 mov byte ptr [eax+4Bh], 0 0052F768 mov eax, [ebx] 0052F76A mov edx, offset dword_52F7B0 0052F76F call @TApplication@SetTitle ; TApplication::SetTitle 0052F774 mov ecx, ds:off_532670 0052F77A mov eax, [ebx] 0052F77C mov edx, ds:off_496928 0052F782 call @TApplication@CreateForm ; TApplication::CreateForm 0052F787 mov ecx, ds:off_532D04 0052F78D mov eax, [ebx] 0052F78F mov edx, ds:off_508450 0052F795 call @TApplication@CreateForm ; TApplication::CreateForm 0052F79A mov eax, [ebx] 0052F79C call @TApplication@Run ; TApplication::Run ..............Skiped lines here Tracing InitExe with SoftIce we land onto this function: 00403ACC ; System::_16605 00403ACC ; Attributes: library function bp-based frame 00403ACC 00403ACC @System@_16605 proc near ; CODE XREF: StartExe+29.p 00403ACC push ebp 00403ACD mov ebp, esp 00403ACF push ebx 00403AD0 push esi 00403AD1 push edi 00403AD2 mov eax, ds:hModule1 00403AD7 test eax, eax <--eax contains the address of table length 00403AD9 jz short loc_403B26 00403ADB mov esi, [eax] <--Move the table length in esi 00403ADD xor ebx, ebx <--ebx will be the parameter of a loop 00403ADF mov edi, [eax+4] <--Move the address of the first function in edi 00403AE2 xor edx, edx 00403AE4 push ebp 00403AE5 push offset loc_403B12 00403AEA push dword ptr fs:[edx] 00403AED mov fs:[edx], esp 00403AF0 cmp esi, ebx <--If there are no functions to call, return 00403AF2 jle short loc_403B08 00403AF4 00403AF4 loc_403AF4: ; CODE XREF: System::_16605+3A.j 00403AF4 mov eax, [edi+ebx*8] <--Move the address of the first function in eax 00403AF7 inc ebx <--Move to next function 00403AF8 mov ds:dword_5344AC, ebx 00403AFE test eax, eax <--Is the address 0? 00403B00 jz short loc_403B04 00403B02 call eax <--Call the function 00403B04 00403B04 loc_403B04: ; CODE XREF: System::_16605+34.j 00403B04 cmp esi, ebx <--Are there more functions to call 00403B06 jg short loc_403AF4 00403B08 00403B08 loc_403B08: ; CODE XREF: System::_16605+26.j 00403B08 xor eax, eax 00403B0A pop edx 00403B0B pop ecx 00403B0C pop ecx 00403B0D mov fs:[eax], edx 00403B10 jmp short loc_403B26 ..............Skiped lines here >From the above it follows that ReadRegistrationData is called only once - during the program initialization. Our next goal is to see where the reg year and the other stored data is used. Now look at the references to PtrToRegYear. IDA gives 3 references. The first two are in the function ReadRegistrationData. We already know what is happening there. The third reference is more interesting: 005327AC PtrPtrRegYear dd offset PtrToRegYear ; DATA XREF: sub_4E6AD4+1A.r 005327AC ; sub_4E6D3C+1A.r ... Yes, to keep things complicated and uncrackable, the protector uses a pointer to pointer to registration data. There are 34 data references to this pointer. Fortunately all functions that refer to this pointer are uniform. For example: 004E6AD4 sub_4E6AD4 proc near ; CODE XREF: sub_4E6B14+6.p 004E6AD4 004E6AD4 var_8 = qword ptr -8 004E6AD4 004E6AD4 push ebp 004E6AD5 mov ebp, esp 004E6AD7 add esp, 0FFFFFFF8h 004E6ADA mov eax, ds:PtrPtrRegDay 004E6ADF mov eax, [eax] 004E6AE1 mov cx, [eax] <--Reg day in cx 004E6AE4 mov eax, ds:PtrPtrRegMonth 004E6AE9 mov eax, [eax] <--Reg month in cx 004E6AEB mov dx, [eax] 004E6AEE mov eax, ds:PtrPtrRegYear 004E6AF3 mov eax, [eax] 004E6AF5 mov ax, [eax] <--Reg year in cx 004E6AF8 call @EncodeDate <--Call to VCL function EncodeDate: Excerpt: "extern PACKAGE System::TDateTime __fastcall EncodeDate(Word Year, Word Month, Word Day); Returns a TDateTime object for a specified Year, Month, and Day." This TDateTime object is just a double. It is stored in st(0). 004E6AFD fadd ds:flt_4E6B10 <--Add 30.(days) to the result 004E6B03 fstp [ebp+var_8] <--Store the result in var_8 and pop the FPU register stack. 004E6B06 wait <--Give the processor a chance to handle FP exceptions 004E6B07 fld [ebp+var_8] <--Load var_8 in the st(0). This is the return value. 004E6B0A pop ecx 004E6B0B pop ecx 004E6B0C pop ebp 004E6B0D retn 004E6B0D sub_4E6AD4 endp 004E6B0D ; ----------------------------------------------------------------------------------------- 004E6B0E align 4 004E6B10 flt_4E6B10 dd 3.0e1 ; DATA XREF: sub_4E6AD4+29.r ; -------------------------------------------------------------------------------------------------- Obviously the above function is used to create the expiry date. In most cases (actually in all cases except one - the case when the expiry string which is to be shown to the user is constructed) the instances like the function above are called from functions like the one described below (sub_4E6B14). I think sub_4E6B14-like functions are crucial for the protection. I will analyse sub_4E6B14 in details. For those of you who are not comfortable with floating point instructions I recommend downloading the "Intel Architecture Software Developerís Manual Volume 2: Instruction Set Reference" from the Intel site. I will try to give as much explanations as needed. If you definitely don't want to mess with floating point arithmetic you could skip this assembly listing and look at the reversed pascal code immediately after the sub_4E6B14. 004E6B14 sub_4E6B14 proc near ; CODE XREF: sub_4E6BCC.p 004E6B14 004E6B14 var_2C = qword ptr -2Ch 004E6B14 var_24 = tbyte ptr -24h 004E6B14 var_18 = qword ptr -18h 004E6B14 var_10 = qword ptr -10h 004E6B14 var_8 = qword ptr -8 004E6B14 004E6B14 push ebp 004E6B15 mov ebp, esp 004E6B17 add esp, 0FFFFFFD4h 004E6B1A call sub_4E6AD4 <--Get expiry date in st(0) 004E6B1F fstp [ebp+var_8] <--Store exp date in var_8 and pop 004E6B22 wait 004E6B23 call @Date <--Get today date in st(0) 004E6B28 fstp [ebp+var_10] <-- Store it in var_10 and pop 004E6B2B wait 004E6B2C fld [ebp+var_8] <--Load exp date in st(0) 004E6B2F fsub [ebp+var_10] <--Subtract today 004E6B32 fabs <--Get the absolute value 004E6B34 fcomp ds:const_30 <--Compare with 30 004E6B3A fnstsw ax <--Store the FPU status word(the result from the comparison) in ax 004E6B3C sahf <--store ah into eflags register 004E6B3D jbe short loc_4E6B4B <--if (expdate - todaydate < 30) jmp loc_4E6B4B 004E6B3F mov eax, dword ptr [ebp+var_8] <--Else var_10 = var_8 (note that var_8 and var_10 are qwords) 004E6B42 mov dword ptr [ebp+var_10], eax 004E6B45 mov eax, dword ptr [ebp+var_8+4] 004E6B48 mov dword ptr [ebp+var_10+4], eax 004E6B4B 004E6B4B loc_4E6B4B: ; CODE XREF: sub_4E6B14+29.j 004E6B4B fld [ebp+var_8] <--Load exp date in st(0) 004E6B4E fsub [ebp+var_10] <--Subtract today, result in st(0) 004E6B51 fabs 004E6B53 fld [ebp+var_8] 004E6B56 fsub [ebp+var_10] <--the same as above, but without fabs. The result is pushed in st(0), hence the previous difference is in st(1) 004E6B59 faddp st(1), st <--Add the two differences, result in st(0) 004E6B5B fdiv ds:const_2 <--Divide by 2. 004E6B61 fstp [ebp+var_8] <--Store the result in var_8. FPU stack is now empty. Let's name the quotient Q. 004E6B64 wait 004E6B65 fld [ebp+var_10] <--Load today in st(0) 004E6B68 call TRUNC <--This function converts the value in st(0) to integer. The result is stored in eax, edx (edx is usually == 0) 004E6B6D mov dword ptr [ebp+var_18], eax <--today as integer in eax 004E6B70 mov dword ptr [ebp+var_18+4], edx 004E6B73 fild [ebp+var_18] <--Load today in st(0) 004E6B76 fstp [ebp+var_24] <--Copy st(0) in var_24 004E6B79 wait 004E6B7A fld [ebp+var_8] <--Load Q in st(0) 004E6B7D call TRUNC 004E6B82 mov dword ptr [ebp+var_2C], eax <--Convert to integer and store in var_2C 004E6B85 mov dword ptr [ebp+var_2C+4], edx 004E6B88 fild [ebp+var_2C] 004E6B8B fld [ebp+var_24] 004E6B8E fdivrp st(1), st <--st(1) = st(0)/st(1). Then pop FPU stack, so the result is in st(0) 004E6B90 call TRUNC 004E6B95 mov edx, eax 004E6B97 fld [ebp+var_8] <--Load Q in st(0) 004E6B9A fcomp ds:const_0 <--Compare Q with 0 004E6BA0 fnstsw ax 004E6BA2 sahf 004E6BA3 jnz short loc_4E6BB8 <--Q is not 0, function can return 004E6BA5 xor ecx, ecx 004E6BA7 mov dl, 1 004E6BA9 mov eax, ds:off_407F2C 004E6BAE call sub_40C168 004E6BB3 call RaiseExcept <--Generate exception 004E6BB8 004E6BB8 loc_4E6BB8: ; CODE XREF: sub_4E6B14+8F.j 004E6BB8 mov eax, edx 004E6BBA mov esp, ebp 004E6BBC pop ebp 004E6BBD retn 004E6BBD sub_4E6B14 endp 004E6BBD ; --------------------------------------------------------------------------- 004E6BBE align 4 004E6BC0 const_30 dd 3.0e1 ; DATA XREF: sub_4E6B14+20.r 004E6BC4 const_2 dd 2.0 ; DATA XREF: sub_4E6B14+47.r 004E6BC8 const_0 dd 0.0 ; DATA XREF: sub_4E6B14+86.r //-------------------------------------------------------------------------------------------------- Well, I have to say that functions involving floating point arithmetic are more difficult to understand than those without it. I prefer to explain what happens in the function above in more readable way. The function sub_4E6B14_Reversed below is a pascal function that IMHO is very close to the original implementation of sub_4E6B14. I compiled this function with Delphi 5. The IDA listing was very close to the above. (If you decide to try it by yourself keep in mind that the project settings for code generation lead up to differently generated code (of course :-)). "Stack frames" must be checked. You can also play with "Optimization" and "Aligned record fields".). Note: I am not an experienced Pascal programmer! function sub_4E6B14_Reversed : Integer; var expDate: Real; today : Real; truncExpDate: Real; truncToday: Real; begin expDate := GetExpDate; <--sub_4E6AD4 today := Date; <--the function from SysUtils if (Abs(expDate-today) > 30) then today := expDate; expDate := (Abs(expDate-today) + (expDate-today))/2; What are the possible values for the above expDate? 1. If Abs(expDate-today) > 30 then expDate is 0 2. If today >= expDate then expDate is 0 3. Otherwise expDate contains the remaining days truncToday := Trunc(today); <--I don't know why they need these truncations here truncExpDate := Trunc(expDate); CheckDate := Trunc(truncToday/truncExpDate); Now look here! What do you think will happen in cases 1 and 2 described above? Of course the above line will generate a floating point exception. This is not a bug my friends, this is the main idea of this protection :-)) I didn't find any place that the return value is used for any reasonable purpose. if (expDate = 0)then <--Hm...why the hell... Raise EZeroDivide.Create('Msg string here'); end; There are 33 functions like the above sub_4E6B14. These functions check the evaluation period and generate an exception if the period is expired. The protection calls different instances of this function in different situations. Then if an exception is caught the corresponding actions are performed. For example: 1. The call to TApplication::CrateForm (look at 0052F795 in function "start") calls the function sub_50C904. IMO this function serves for creation of the Views pane in the program. It contains 8 buttons. There are 8 calls to different instances of the protection function (I suppose one call for each button). There is something wrong with this function: ... 0050CA72 push offset ExceptionHandler 0050CA77 push dword ptr fs:[edx] 0050CA7A mov fs:[edx], esp 0050CA7D call _CheckExpired1 0050CA82 mov [ebp+var_10], eax <--the result is stored in var_10 0050CA85 fild [ebp+var_10] <--Then loaded in the FPU stack 0050CA88 mov eax, [ebp+var_4] 0050CA8B fstp qword ptr [eax+708h]<--And stored in the memory 0050CA91 wait ... 0050CB47 call _CheckExpired2 0050CB4C mov [ebp+var_10], eax 0050CB4F fild [ebp+var_10] 0050CB52 mov eax, [ebp+var_4] 0050CB55 fadd qword ptr [eax+708h] <--Add this result to the result of _CheckExpired1 0050CB5B mov eax, [ebp+var_4] 0050CB5E fstp qword ptr [eax+708h]<--store back in memory 0050CB64 wait .... This code appears 8 times. All results from 8 protection functions are summed and stored in the memory. Of course I placed a bpm there but the memory was not touched. What could this mean? Either I don't understand the idea or the protection is still under development (do you remember that they don't use the content of dom.exi?). 2. Every click on Symbols or QA button, or in the corresponding view result in a call to sub_4E9038. 3. Pressing the export project button calls sub_50E4AC and sub_51FF0C. I don't know where the remaining 22 instances are used. This could be bad from a cracker's point of view but IMHO it doesn't mean less knowledge about the underlying ideas of this protection. Protection strings My initial idea was to deal not with the strings indicating that the trial version has expired and other like this. Afterwards I found out that the protector has paid special attention to this. Hence the essay will not be complete without covering this topic. 1. When the trial period is not over. 1.1. In every documentation page generated with the program the following text appears: "This documentation has been created with a demo version of Doc-O-Matic. This version is supplied for evaluation purposes only, do not redistribute this documentation. To obtain a commercial license please see" 1.2. The text "This demo will expire on ...." is placed at the bottom of Information window. 2. When the trial period is over 2.1. There is a message in the Information window: "Trial version expired" 2.2. The contents of the Information window is changed (how to obtain license etc.) 3. In both cases there is a "We wish to thank... message" in the about dialog. Let's consider the case 1.1. I looked for this string everywhere - in the IDA string references, with a resource editor, in the registry and so on, but it was not there. Then I concluded that the string was somehow protected and was constructed dynamically. Unfortunately (as far as I know) SoftIce doesn't offer a way to break when a string first appears in memory. The strings in case 2 are also protected and they appear in the "Information" window when it appears on the screen. It follows that the strings decoding is performed at the program startup. This hypothesis turned out to be true. The string 1.1. appeared in the memory after the call to InitExe. We already know that InitExe calls the function System::_16605 that calls functions placed in a table. Here is a code snippet: ... 00403AF4 loc_403AF4: ; CODE XREF: System::_16605+3A.j 00403AF4 mov eax, [edi+ebx*8] <--Move the address of first function in eax 00403AF7 inc ebx <--Move to next function 00403AF8 mov ds:dword_5344AC, ebx 00403AFE test eax, eax <--Is the address 0? 00403B00 jz short loc_403B04 00403B02 call eax <--Call the function ... But how to find out the value in ebx for which the strings are decoded? The above call eax is executed 217 times!!! Unfortunately I don't have an elegant solution of this problem. I used binary search in the table with functions until I found out that the string appears in the memory when ebx == 0xAB. I traced the program for a few minutes and found that the string appears in the memory after the call to a function at address 004BBD70 which I named "DecodeString". Prior to starting with the analysis of decoding routines I will explain in a few words the underlying idea of strings protection. There are 10 coded strings. They reside in the dom.exe in the following format: Example: 01 64 CA 9E A2 A3 B9 EA AE.... The first two bytes indicate the length of the string. In the above example the length is 0x164. The third byte is an xor mask. As you can see in the function below, the table with the strings begins at file offset 0xBA81C. The function DecodeString has two arguments: a reference to pointer to the decoded string and an index (form 0 to 9) of the string that has to be decoded. (You can use the breakpoint "bpx DecodeString do "p ret; d edx"" in order to see the decoded strings.) 004BBD70 DecodeString proc near ; CODE XREF: DecodeAllStrings+27.p 004BBD70 ; DecodeAllStrings+3E.p ... 004BBD70 004BBD70 DecodedStringAddr = dword ptr -8 004BBD70 CodedStringsAddr = dword ptr -4 004BBD70 004BBD70 push ebp 004BBD71 mov ebp, esp 004BBD73 add esp, 0FFFFFFF8h 004BBD76 push ebx 004BBD77 push esi 004BBD78 push edi 004BBD79 xor ecx, ecx 004BBD7B mov [ebp+DecodedStringAddr], ecx 004BBD7E mov edi, edx <--edx is a parameter (an address where the pointer to buffer will be stored) 004BBD80 mov esi, eax <--eax is a parameter (index of the string) 004BBD82 xor eax, eax 004BBD84 push ebp 004BBD85 push offset loc_4BBDE7 004BBD8A push dword ptr fs:[eax] 004BBD8D mov fs:[eax], esp 004BBD90 mov eax, edi 004BBD92 call LStrClr 004BBD97 or ebx, 0FFFFFFFFh <--ebx = -1 004BBD9A mov [ebp+CodedStringsAddr], offset CodedStrings 004BBDA1 lea eax, [ebp+DecodedStringAddr] 004BBDA4 call LStrClr 004BBDA9 004BBDA9 loc_4BBDA9: ; CODE XREF: DecodeString+51.j 004BBDA9 inc ebx <--ebx = 0 004BBDAA cmp esi, ebx <--esi contains the index of the string to be decoded. Here we have a loop from 0 to index of the desired string. Note that dl == 1 only when the desired index is reached. This flag is an indicator for the function PerformDecoding that it has to perform the actual decoding of the string. Every previous call to this function results in increasing of the CodedStringsAddr with the length of previous string. 004BBDAC setz dl 004BBDAF lea ecx, [ebp+DecodedStringAddr] 004BBDB2 lea eax, [ebp+CodedStringsAddr] 004BBDB5 call PerformDecoding 004BBDBA cmp ebx, 9 004BBDBD jge short loc_4BBDC3 004BBDBF cmp esi, ebx 004BBDC1 jnz short loc_4BBDA9 004BBDC3 004BBDC3 loc_4BBDC3: ; CODE XREF: DecodeString+4D.j 004BBDC3 cmp esi, ebx 004BBDC5 jnz short loc_4BBDD1 004BBDC7 mov edx, edi 004BBDC9 mov eax, [ebp+DecodedStringAddr] 004BBDCC call sub_4BBCF4 This function moves the result in edi ..... ---------------------------------------------------------------------------------------------------- 004BBB84 PerformDecoding proc near ; CODE XREF: DecodeString+45.p 004BBB84 004BBB84 var_8 = dword ptr -8 004BBB84 XORMask = byte ptr -1 004BBB84 004BBB84 push ebp 004BBB85 mov ebp, esp 004BBB87 add esp, 0FFFFFFF8h 004BBB8A push ebx 004BBB8B push esi 004BBB8C push edi 004BBB8D xor ebx, ebx 004BBB8F mov [ebp+var_8], ebx 004BBB92 mov edi, ecx <--the address for result 004BBB94 mov ebx, eax <--coded str addr 004BBB96 xor eax, eax 004BBB98 push ebp 004BBB99 push offset loc_4BBC07 004BBB9E push dword ptr fs:[eax] 004BBBA1 mov fs:[eax], esp 004BBBA4 mov eax, [ebx] 004BBBA6 movzx esi, byte ptr [eax] <--first byte in esi 004BBBA9 shl esi, 8 004BBBAC inc dword ptr [ebx] 004BBBAE mov eax, [ebx] 004BBBB0 movzx eax, byte ptr [eax] <--second byte in eax 004BBBB3 add esi, eax <--number of bytes to decode in esi 004BBBB5 inc dword ptr [ebx] 004BBBB7 mov eax, [ebx] 004BBBB9 mov al, [eax] 004BBBBB mov [ebp+XORMask], al <--xor mask 004BBBBE inc dword ptr [ebx] 004BBBC0 test dl, dl <--should decode? 004BBBC2 jz short loc_4BBBEF 004BBBC4 mov eax, edi 004BBBC6 call LStrClr 004BBBCB test esi, esi 004BBBCD jle short loc_4BBBF1 004BBBCF 004BBBCF loc_4BBBCF: ; CODE XREF: PerformDecoding+67.j This is the loop where the actual decoding is performed. 004BBBCF lea eax, [ebp+var_8] 004BBBD2 mov edx, [ebx] 004BBBD4 mov dl, [edx] 004BBBD6 xor dl, [ebp+XORMask] 004BBBD9 call unknown_libname_13 004BBBDE mov edx, [ebp+var_8] 004BBBE1 mov eax, edi 004BBBE3 call LStrCat <--this allocates the buffer 004BBBE8 inc dword ptr [ebx] 004BBBEA dec esi 004BBBEB jnz short loc_4BBBCF 004BBBED jmp short loc_4BBBF1 004BBBEF ; --------------------------------------------------------------------------- 004BBBEF 004BBBEF loc_4BBBEF: ; CODE XREF: PerformDecoding+3E.j 004BBBEF add [ebx], esi <--Skip the current string and move to next ... OK. This essay grew very large, so I am not going to analyse the calls to DecodeString in details. I will first explain briefly what strings are decoded for different input indexes. 1. The string with index 0 is the string described in 1.1. above (when the trial period is not over.) 2. For index 1 the case 2.1 was already covered. 3. For index 2 the case 2.2 was already covered. 4. For index 9 the case 3 was already covered. 2. I don't know the meanings of decoded strings with indexes 3, 4, 5, 6, 7 and 8. I patched the exe so that some of these strings were filled with zeroes and I didn't notice incorrect program behavior. Maybe I had to test for a longer period of time :-))) Following the references to DecodeString it is easy to find out the following function: 00502D80 FillInformationView proc near ; CODE XREF: FillExpiredInfoView+3A.p 00502D80 ; sub_50324C+5C.p .... 00502E4D cmp byte ptr [ebx+2F4h], 0 <--Here is a flag that is set by the caller. If it is true this means that the function is called when the trial period is expired. 00502E54 jnz loc_50302A 00502E5A push ebp 00502E5B call CreateEndDateStr <--See the case 1.2. above. The expiration date string is prepared here. It is used a few lines below to create the message "This demo will expire on...". ... 0050302A loc_50302A: ; CODE XREF: FillInformationView+D4.j 0050302A lea edx, [ebp+var_C] 0050302D mov eax, 2 <--The parameter for DecodeString. 00503032 call DecodeString <--Decode: The Doc-o-matic period is over. To further... ... ---------------------------------------------------------------------------------------------------- 00503124 FillExpiredInfoView proc near ; CODE XREF: sub_50C904+81C.p This function is called when the trial period is over. As you may remember in this case the protection generates an exception. The call to this function can be found in the exception handler.(look at 0050D120) 00503124 00503124 var_4 = dword ptr -4 00503124 00503124 push ebp 00503125 mov ebp, esp 00503127 push 0 00503129 push ebx 0050312A mov ebx, eax 0050312C xor eax, eax 0050312E push ebp 0050312F push offset loc_503179 00503134 push dword ptr fs:[eax] 00503137 mov fs:[eax], esp 0050313A lea edx, [ebp+var_4] 0050313D mov eax, 1 <--The parameter for DecodeString 00503142 call DecodeString <--Decode "Trial version expired". 00503147 mov edx, [ebp+var_4] 0050314A mov eax, [ebx+2D8h] 00503150 call @TControl@SetText ; TControl::SetText 00503155 mov byte ptr [ebx+2F4h], 1 <--A flag that denotes what strings to be created in Info View. True means expired. 0050315C mov eax, ebx 0050315E call FillInformationView ... That is all folks :-)
>From a protector's point of view, there is nothing interesting in this protection. >From a Fravia's point of view, I could say just a few things: Protections that use floating point calculations are much harder to reverse than the other type. The time spent for reversing even a trivial protection is much more than the time to crack it. Most of us share the belief that software protection is an impossible dream. The work on this protection increased my personal belief that the complete reversing of software protections is an impossible dream too.

Some general conclusions.

Both software protectors and reverse engineers (crackers) cause the other group's existence - none of them would exist without the other. If there were no crackers, software protections would have been rather simple, and vice versa - if software protections were too easy to crack, there would have been much fewer reverse engineers and crackers. The correlation between these two agent groups is ambivalent: on the one hand, this relation is a synergism(better protections, better Fravias); on the other hand, it is antagonistic. In other words, a secret duel takes place on the battlefield of software protection. Unfortunately this swordfight often happens to be rather imbalanced. While the reverse engineer is well-acquainted with most of the mechanisms and tools used for the creation of a software protection, the protector regards his "enemy" as a "phantom menace" (in most cases - just like the one described above).This is the reason why the protector's ideas are often rather laughable. The protector is anxious about his opponent because he doesn't know how his opponent would attack him. Then his anxiety drives him to undertake panicky actions - he writes a huge amount of useless code, spends lots of bucks for dongles, protection shells, packers etc. And here is the paradox: he knows he's going to lose this fight, but nonetheless he stays rebellious. As we all know, this is a hopeless resistance, which I would call "The rebellion of mediocrity". This is my first essay! Mercy! P.S. Finally my special thanks go to Julia and Eva!
Ob Duh
I won't even bother to explain to you that you should BUY this target program if you intend to use it for a longer period than the allowed one. Should you want to STEAL this software instead, you don't need to crack its protection scheme at all: you'll find it on most Warez sites, complete and already regged, farewell, don't come back.
(c) April, 2001 Maldoror All rights reversed