Reversing Doc-o-Matic
by
Maldoror
(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.
http://www.doc-o-matic.com/
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
- 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.
- 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.
- Delete or rename dom.exi - expired.
- 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.
- Change the date so that the period is over - expired. Restore the date - OK.
- 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....:-)
- 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 :-).
- 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 http://www.doc-o-matic.com/purchase.html."
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!
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