
This article attempts to describe in detail the study of one entertaining crackme. It will be interesting first of all for beginners in reverse engineering (whom I am and on which the article is mainly designed), but perhaps more serious experts will find the technique used in it interesting. To understand what is happening will require basic knowledge of the assembler and WinAPI.
Foreword
The university assembler course awakened a dormant couple of years the spirit of the researcher, which originated in high school. This spirit was quickly sacrificed for some unpretentious crackme, but soon there was a more interesting copy on the hard disk. Unfortunately, over the years, the original source of this crackme could not be found, so I uploaded the
archive to Google Drive.
Tools used:- Debugger (I used OllyDbg )
- A utility for getting window handles of running programs (for example, Zero Dump or supplied with Visual Studio Spy ++)
- Some files included in the fasm archive
First look at the enemy

The appearance of the main window of the experimental is very simple: two edit'a and a few buttons. Moreover, the first input field is intended to give the opportunity to write to the second, which, judging by the Check button located next to it, will be the main obstacle to the registration of cracks.
The About window contains one interesting inscription
“Patching is allowed, but where where” , preceding the most interesting. The whole essence of this inscription will be revealed in the process of research, but for now we will not spoil.
By the way, if someone has already seen this crackme and found a way to patch it (well, or if it seemed very simple to someone and he did it with one left hand in the process of reading the article), then I ask you to tell everyone about success, preferably with comments.
Attempts to cause at least some reaction of the program are not crowned with success: no reaction should be made to pressing the Enable and Check buttons. However, it is striking that in the active input field, you can only write in Latin letters and put a minus sign. We notice this fact and begin work.
To battle!
To win this crackme, we need to deal with two input fields.
')
First edit and Enable button
After loading the program in OllyDbg, we immediately notice an interesting
GetProcAddress near the entry point:
00401086 |. 68 56504000 PUSH OFFSET 00405056; / Procname = "" 10, "& 7", 14, "* - ', 4", 0F, ", - $", 02
0040108B |. FF35 9C504000 PUSH [DWORD DS: 40509C]; | hModule = NULL
00401091 |. E8 C0020000 CALL <JMP. & KERNEL32. GetProcAddress>; \ KERNEL32.GetProcAddress
To see what is being transmitted to it, set the bryak directly on the call to GetProcAddress. Stopping here, we see that the SetWindowLongA function was “drawn” in
Procname , and the user32.dll descriptor was “drawn” in hModule:
00401086 |. 68 56504000 PUSH OFFSET 00405056; / Procname = "SetWindowLongA"
0040108B |. FF35 9C504000 PUSH [DWORD DS: 40509C]; | hModule = 767F0000 ('USER32')
00401091 |. E8 C0020000 CALL <JMP. & KERNEL32. GetProcAddress>; \ KERNEL32.GetProcAddress
Calling this function through GetProcAddress hints that they wanted to hide it from us and something important is happening here. To find out what exactly is happening there, let's look at the parameters passed to this function. This can be done very quickly by placing a bryak on call eax, located a few teams below.
Why call eax?By convention, the
stdcall call used in WinAPI functions, an integer value (i.e., the address of the desired function), which GetProcAddress returns, must be in the eax register.
After stopping at call eax, you can take a closer look at what went on the stack. Olly kindly offers us a decoding of the transmitted values:
0018FC04 / 00020876; | hWnd = 00020876, class = Edit
0018FC08 | FFFFFFFC; | Index = GWL_WNDPROC
0018FC0C | 0040302B; \ NewValue = Chiwaka1.40302B
A quick look at the description of the function and it becomes clear that there is a replacement window procedure of one of the edit'ov. The instinct tells you that this is an active edit, but to be sure, you need to use the Zero Dump utility (the Spy ++ analog that is on hand). Dragging the Finder Tool sight to active edit, we find that its descriptor matches the one passed to SetWindowLongA.
When you restart the crack, this value will change, so the check should be done in one go.Now it becomes clear where the legs grow from filtering input, which we found earlier. Let's see what else the new window procedure does. Let's go to the address (Go To -> Expression or Ctrl + G in OllyDbg), which was passed by the NewValue parameter (40302b):
0040302B 55 PUSH EBP
0040302C 8BEC MOV EBP, ESP
0040302E 817D 0C 0201000 CMP [DWORD SS: EBP + 0C], 102
00403035 75 61 JNE SHORT 00403098
00403037 8B45 10 MOV EAX, [DWORD SS: EBP + 10]
The constant 102, with which a comparison is made on 40302E, is
WM_CHAR . I looked at the list of all constants denoting the type of message in the
%fasm_directory%\INCLUDE\EQUATES\USER32.INC
(link to the archive with the fasm in the list of used tools), since the Internet was not at hand, but the fasm in
gentlemen’s student suite was available.
We proceed further along the code, as if the next JNE jump is not being executed. The code after the JNE will be executed if the WM_CHAR message arrives, which is sent to the window after pressing (more precisely, after the WM_KEYDOWN message is processed) of the key, in accordance with which the ASCII code is set. In this message, we are interested in wParam, which contains the character code of the key pressed. Next, this value is written to EAX and a series of checks begins, preventing entry of anything other than Latin and minus.
0040303A 3C 41 CMP AL, 41
0040303C 72 04 JB SHORT 00403042
0040303E 3C 5A CMP AL, 5A
00403040 76 10 JBE SHORT 00403052
00403042 3C 61 CMP AL, 61
00403044 72 04 JB SHORT 0040304A
00403046 3C 7A CMP AL, 7A
00403048 76 08 JBE SHORT 00403052
0040304A 3C 08 CMP AL, 8
0040304C 74 04 JE SHORT 00403052
0040304E 3C 2D CMP AL, 2D
00403050 75 61 JNE SHORT 004030B3
00403052 3C 08 CMP AL, 8
00403054 75 12 JNE SHORT 00403068
00403056 833D D5504000 0 CMP [DWORD DS: 4050D5], 0
0040305D 74 09 JE SHORT 00403068
0040305F 832D D5504000 0 SUB [DWORD DS: 4050D5], 1
An interesting reaction to the backspace (code 8 in ASCII): at 403056, some value in the memory is checked for equality to zero and the value of this value is one if it is not zero. This behavior means that the length of the entered string is being counted. And yes, it does happen a little further (last command):
00403069 8B0D D5504000 MOV ECX, [DWORD DS: 4050D5]
0040306F 8881 70504000 MOV [BYTE DS: ECX + 405070], AL
00403075 8305 D5504000 0 ADD [DWORD DS: 4050D5], 1
Also here it is clearly seen that the entered character is written into memory at the end of the already entered sequence. In calculating the place to write a character, the same address appears (4050D5), the value at which is the length of the entered string.
Conclusion: the search for a suitable GetDlgItemText will not help to find the place where the input data is being checked (as it will not be here), but it will be helped by the memory stick that is being written to.
So, set the breakpoint to read a few bytes, starting at address 405070. It also makes sense to be safe and to set breakpoint to read the number of characters entered (it was previously established that it is 4050D5). The second is best done immediately before clicking Check, so as not to be distracted by stopping while entering a line. Now run the program, enter something in the first edit and click on Check.
Hidden textThe bytes located at the point where the breakpoint is triggered (as well as many more where there is a crack) go in such a sequence that it can be interpreted differently. If you try to move higher on the commands, then the place where the breakpoint is triggered will be reanalysed and will look different. To fix this, it is enough to go to the address of the breakpoint with the Go to -> Expression command (Ctrl + G).
We see that the stop occurred on the record of the number of characters entered in EAX, and the next command compares the 11th character (405070 + A) of the entered string with a minus (2D is the minus code in ASCII).
00402007 A1 D5504000 MOV EAX, [DWORD DS: 4050D5] <- memory breakpoint when reading 4050D5
0040200C 803D 7A504000 2 CMP [BYTE DS: 40507A], 2D
00402013 75 1E JNE SHORT 00402033
00402015 33C9 XOR ECX, ECX
00402017 33DB XOR EBX, EBX
00402019 80B9 70504000 0 CMP [BYTE DS: ECX + 405070], 0
00402020 74 11 JE SHORT 00402033
00402022 8A99 70504000 MOV BL, [BYTE DS: ECX + 405070]
00402028 03C3 ADD EAX, EBX
0040202A 83C1 03 ADD ECX, 3
0040202D EB EA JMP SHORT 00402019
0040202F 33C0 XOR EAX, EAX
00402031 5E POP ESI
00402032 58 POP EAX
00402033 C605 7A504000 0 MOV [BYTE DS: 40507A], 0
0040203A C3 RETN
It is clearly noticeable that after the comparison we do not need a jump, since then (from 402019) a certain cycle is started, which we hardly want to miss. Let us check this assumption and falsify the result of comparing the 11th character with a minus by modifying the ZF flag (or just restart the check with a suitable string for this comparison).
In this cycle, every third character entered is checked to the first zero and the codes of these characters are added to the EAX register, which, as we remember, is the length of the entered string. On 4020300 instead of minus zero is recorded and the test is completed.
We follow on RETN.
004010F8 /. 3D 0A030000 CMP EAX, 30A
004010FD |. 0F85 C6000000 JNE 004011C9
00401103 |. FF75 08 PUSH [DWORD SS: EBP + 8]
00401106 |. E8 380F0000 CALL 00402043
0040110B |. C605 A4504000 MOV [BYTE DS: 4050A4], 1
00401112 |. 6812504000 PUSH OFFSET 00405012; / Text = "Well done! Keep going ;-)"
00401117 |. 6A 65 PUSH 65; | ControlID = 101.
00401119 |. FF75 08 PUSH [DWORD SS: EBP + 8]; | hDialog => [ARG.EBP + 8]
0040111C |. E8 23020000 CALL <JMP. & USER32. SetDlgItemTextA>; \ USER32.SetDlgItemTextA
The first team can see the comparison of the value of EAX c 30A and jump somewhere in case of inequality. Further, the desired
“Well done! Keep going ;-) » . Now you need to see how to get there. Next to the cherished SetDlgItemTextA noticeable call, in which we definitely need to go. To get there, you must pass the test at 30A. The easiest way to do this is to modify the ZF flag before jumping the JNE.
Having executed Call, we see the following:
00402043 55 PUSH EBP
00402044 8BEC MOV EBP, ESP
00402046 53 PUSH EBX
00402047 68 70504000 PUSH OFFSET 00405070; start of the entered string
0040204C FF35 9C504000 PUSH [DWORD DS: 40509C]
00402052 E8 FFF2FFFF CALL <JMP. & KERNEL32. GetProcAddress>; Jump to kernel32.GetProcAddress
00402057 6A 66 PUSH 66
00402059 FF75 08 PUSH [DWORD SS: EBP + 8]
0040205C FFD0 CALL EAX
0040205E 50 PUSH EAX
0040205F 68 7B504000 PUSH OFFSET 0040507B; 12th character of the entered string
00402064 FF35 9C504000 PUSH [DWORD DS: 40509C]
0040206A E8 E7F2FFFF CALL <JMP. & KERNEL32. GetProcAddress>; Jump to kernel32.GetProcAddress
0040206F 5B POP EBX
00402070 53 PUSH EBX
00402071 FFD0 CALL EAX
00402073 68 70504000 PUSH OFFSET 00405070; start of the entered string
00402078 FF35 9C504000 PUSH [DWORD DS: 40509C]
0040207E E8 D3F2FFFF CALL <JMP. & KERNEL32. GetProcAddress>; Jump to kernel32.GetProcAddress
00402083 68 E8030000 PUSH 3E8
00402088 FF75 08 PUSH [DWORD SS: EBP + 8]
0040208B FFD0 CALL EAX
0040208D 50 PUSH EAX
0040208E 68 7B504000 PUSH OFFSET 0040507B; 12th character of the entered string
00402093 FF35 9C504000 PUSH [DWORD DS: 40509C]
00402099 E8 B8F2FFFF CALL <JMP. & KERNEL32. GetProcAddress>; Jump to kernel32.GetProcAddress
0040209E 5B POP EBX
0040209F 6A 00 PUSH 0
004020A1 53 PUSH EBX
004020A2 FFD0 CALL EAX
Four GetProcAddress, in which parts of the string entered by us are transferred! The first time is the address of the beginning of the line, the second is the address of 12 characters, and then again the beginning and again the 12th character. As we remember, in place of 11 characters (which should be a minus) zero is written. Now it is clear that this is necessary in order to pass a string to GetProcAddress null-terminated.
Now you can set a well-defined task: find two functions from user32.dll (this is indicated by the handle passed to GetProcAddress), each of which has two parameters (the number of push'e before the call eax), and the first has a length of ten characters.
Let's narrow down the search by looking at the transmitted parameters and comparing them with what should happen after clicking the Enable button. We have already seen SetDlgItemTextA with a cheering inscription, and there is a possibility that it will appear in the first input field, since we still need the second one. And the second field should be made active, we after all still click on the Enable button!
To quickly look at all the functions having a length of 10 characters from user32.dll, I used the search in the built-in spotlight editor for the file
%fasm_directory%\INCLUDE\API\USER32.INC
. For the search applied regexp
'\s\i{10}\,'
. It was possible to further select from the functions found those that I have two parameters, but this was not necessary, since
GetDlgItem ideally fits the
meaning . A confirmation of this thought is the “magic constant” 66h, which is the ID of the second, as yet inactive input field (this can be seen with the help of zDump or a similar utility). This value is the first parameter of GetDlgItem.
It remains only to activate the second input field. For two parameters, this is perfectly able to do
EnableWindow .
Check the found string
GetDlgItem-EnableWindow and ... success!
With the first input field sorted out, go to the "main dish".
Second edit and decisive blow to the Check button
Since the replacement of the window procedure was done only for one edit'a, the text from the second should (in any case there is reason to hope for it) be extracted in a more trivial way. Use the command Search for -> All intermodular calls and see what can be used to get the text from the second input field.
All intermodular calls 0040103C CALL <JMP. & USER32.DialogBoxParamA> 7684CB0C USER32.DialogBoxParamA TemplateName = 1, hParent = NULL, DialogProc = Chiwaka1.401061, InitParam = 0
004011B2 CALL <JMP. & USER32.DialogBoxParamA> 7684CB0C USER32.DialogBoxParamA TemplateName = 2, hParent = NULL, DialogProc = Chiwaka1.401206, InitParam = 0
004011C4 CALL <JMP. & USER32.EndDialog> 7682B99C USER32.EndDialog Result = 0
00401214 CALL <JMP. & USER32. EndDialog> 7682B99C USER32. EndDialog Result = 1
0040123D CALL <JMP. & USER32.EndDialog> 7682B99C USER32.EndDialog Result = 1
0040104C CALL <JMP. & KERNEL32.ExitProcess> 765779C8 kernel32.ExitProcess
00401072 CALL <JMP. & USER32. GetDlgItem> 7682F1BA USER32. GetDlgItem ItemID = 101.
00401002 CALL <JMP. & KERNEL32. GetModuleHandleA> 76571245 kernel32. GetModuleHandleA ModuleName = NULL
00401091 CALL <JMP. & KERNEL32. GetProcAddress> 76571222 kernel32. GetProcAddress Procname = "" 10, "& 7", 14, "* - ', 4", 0F, ", - $", 02
00401168 CALL <JMP. & KERNEL32. GetProcAddress> 76571222 kernel32. GetProcAddress Procname = "" 04, "& 7", 07, "/ $", LF, "7 &.", 17, "&; 7", 02
00401268 CALL <JMP. & KERNEL32. GetProcAddress> 76571222 kernel32. GetProcAddress
004010C4 CALL <JMP. & USER32.SendMessageA> 7681612E USER32.SendMessageA Msg = WM_COMMAND
0040111C CALL <JMP. & USER32. SetDlgItemTextA> 7681C4D6 USER32. SetDlgItemTextA ControlID = 101., Text = "Well done! Keep going ;-)"
004011DC CALL <JMP. & USER32.SetDlgItemTextA> 7681C4D6 USER32.SetDlgItemTextA ControlID = 101., Text = "Careful now !!"
The most suitable candidate for the function, the text from the second input field, seems to be one of GetProcAddress, which probably gets the address
GetDlgItemTextA . To verify this, run the program, unlock the second field with the found string, and then set the breakpoints on all three GetProcAddress. Now you can click on Check and make sure that the predictions are correct.
0040115D |> \ 68 46504000 PUSH OFFSET 00405046; / Procname = "GetDlgItemTextA"
00401162 |. FF35 9C504000 PUSH [DWORD DS: 40509C]; | hModule = 767F0000 ('USER32')
00401168 |. E8 E9010000 CALL <JMP. & KERNEL32. GetProcAddress>; \ KERNEL32.GetProcAddress
0040116D |. 6A 40 PUSH 40
0040116F |. 68 70504000 PUSH OFFSET 00405070
00401174 |. 6A 66 PUSH 66
00401176 |. FF75 08 PUSH [DWORD SS: EBP + 8]
00401179 |. FFD0 CALL EAX
Next, it checks the number of characters entered (GetDlgItemText returns it to EAX), writes this value to memory, and calls four functions. Most likely, it is in these four functions that some actions take place with the string, so we examine them in turn.
Having glanced in the first call, we observe some frauds with characters in the string, performed in a loop until the detection of the first zero (402126):
CALL 00402101 00402101 8D05 70504000 LEA EAX, [405070]; string entered
00402107 EB 1D JMP SHORT 00402126
00402109 8038 40 CMP [BYTE DS: EAX], 40; 40h = @ (the last character in ASCII before the uppercase Latin letter)
0040210C 76 0A JBE SHORT 00402118
0040210E 8038 5B CMP [BYTE DS: EAX], 5B; 5Bh = [(first character after uppercase latin)
00402111 73 05 JAE SHORT 00402118
00402113 8000 20 ADD [BYTE DS: EAX], 20; 20h - the difference between letters in different registers
00402116 EB 0D JMP SHORT 00402125
00402118 8038 60 CMP [BYTE DS: EAX], 60; 60h = '(the last character before the Latin in lowercase)
0040211B 76 08 JBE SHORT 00402125
0040211D 8038 7B ​​CMP [BYTE DS: EAX], 7B; 7Bh = {(the first character after the Latin in lowercase)
00402120 73 03 JAE SHORT 00402125
00402122 8028 20 SUB [BYTE DS: EAX], 20; 20h - the difference between letters in different registers
00402125 40 INC EAX
00402126 8038 00 CMP [BYTE DS: EAX], 0
00402129 75 DE JNE SHORT 00402109
0040212B C3 RETN
Looking closely at the constants that are encountered here, and then to the ASCII table, it is easy to see that this call inverts the register of each of the entered characters. It was all very simple.
CALL 00402133 00402133 53 PUSH EBX
00402134 33DB XOR EBX, EBX
00402136 33D2 XOR EDX, EDX
00402138 8D05 70504000 LEA EAX, [405070]; string entered
0040213E 50 PUSH EAX
0040213F 8B0D A5504000 MOV ECX, [DWORD DS: 4050A5]
00402145 51 PUSH ECX
00402146 49 DEC ECX
00402147 8A1401 MOV DL, [BYTE DS: EAX + ECX]
0040214A 8893 A9504000 MOV [BYTE DS: EBX + 4050A9], DL
00402150 43 INC EBX
00402151 83F9 00 CMP ECX, 0
00402154 ^ 75 F0 JNE SHORT 00402146
00402156 59 POP ECX
00402157 58 POP EAX
00402158 49 DEC ECX
00402159 33DB XOR EBX, EBX
0040215B 33D2 XOR EDX, EDX
0040215D 33C0 XOR EAX, EAX
0040215F EB 1A JMP SHORT 0040217B
00402161 8A99 70504000 MOV BL, [BYTE DS: ECX + 405070]; string entered
00402167 8A90 70504000 MOV DL, [BYTE DS: EAX + 405070]; string entered
0040216D 8891 70504000 MOV [BYTE DS: ECX + 405070], DL
00402173 8898 70504000 MOV [BYTE DS: EAX + 405070], BL
00402179 49 DEC ECX
0040217A 40 INC EAX
0040217B 83F8 05 CMP EAX, 5
0040217E ^ 72 E1 JB SHORT 00402161
00402180 C605 7A504000 4 MOV [BYTE DS: 40507A], 41
00402187 5B POP EBX
00402188 C3 RETN
The second call is a little bigger, and several actions are performed in it. The first thing is to copy the entered string back to another place in memory using the following cycle:
00402146 49 DEC ECX
00402147 8A1401 MOV DL, [BYTE DS: EAX + ECX]
0040214A 8893 A9504000 MOV [BYTE DS: EBX + 4050A9], DL
00402150 43 INC EBX
00402151 83F9 00 CMP ECX, 0
00402154 ^ 75 F0 JNE SHORT 00402146
Next, the same thing happens with the source string, but only 10 characters are inverted. This may indicate that in the future only they will be used, but it is too early to make plans.
Finally, the 11th position of the original string of the character 'A' is written:
00402180 C605 7A504000 4 MOV [BYTE DS: 40507A], 41
Remembering what technique has already been used in this crack, it can be assumed that there will be something similar here, but the WinAPI function will not be introduced in a ready-made form.
CALL 00401285 brings us one more call:
CALL 004012C4 004012C4 / $ 8D05 BD504000 LEA EAX, [4050BD]
004012CA |. 33D2 XOR EDX, EDX
004012CC |. 8A15 F0204000 MOV DL, [BYTE DS: 4020F0]
004012D2 |. 8810 MOV [BYTE DS: EAX], DL
004012D4 |. 8A15 31214000 MOV DL, [BYTE DS: 402131]
004012DA |. 8850 01 MOV [BYTE DS: EAX + 1], DL
004012DD |. 8A15 AE204000 MOV DL, [BYTE DS: 4020AE]
004012E3 |. 8850 02 MOV [BYTE DS: EAX + 2], DL
004012E6 |. 8A15 CF204000 MOV DL, [BYTE DS: 4020CF]
004012EC |. 8850 03 MOV [BYTE DS: EAX + 3], DL
004012EF |. 8A15 41204000 MOV DL, [BYTE DS: 402041]
004012F5 |. 8850 04 MOV [BYTE DS: EAX + 4], DL
004012F8 |. 8A15 40204000 MOV DL, [BYTE DS: 402040]
004012FE |. 8850 05 MOV [BYTE DS: EAX + 5], DL
00401301 |. 8A15 31214000 MOV DL, [BYTE DS: 402131]
00401307 |. 8850 06 MOV [BYTE DS: EAX + 6], DL
0040130A |. 8A15 05204000 MOV DL, [BYTE DS: 402005]
00401310 |. 8850 07 MOV [BYTE DS: EAX + 7], DL
00401313 |. 8A15 83124000 MOV DL, [BYTE DS: 401283]
00401319 |. 8850 08 MOV [BYTE DS: EAX + 8], DL
0040131C |. 8A15 5B124000 MOV DL, [BYTE DS: 40125B]
00401322 |. 8850 09 MOV [BYTE DS: EAX + 9], DL
00401325 \. C3 RETN
Here there are manipulations with memory, which is not connected either with the source line or with its copy, so without going into the details of what is happening, let's see what happens after the return. After the return, a copy of the entered string is processed using xor'a. Most likely, one of these two lines should end up being a WinAPI function, and something should be a message about successful registration.
Everything will clarify the last call.
CALL 0040125D 0040125D / $ 68 70504000 PUSH OFFSET 00405070; / Procname = "the entered string backwards with an inverted register and an 'A' at the end"
00401262 |. FF35 9C504000 PUSH [DWORD DS: 40509C]; | hModule = 767F0000 ('USER32')
00401268 |. E8 E9000000 CALL <JMP. & KERNEL32. GetProcAddress>; \ KERNEL32.GetProcAddress
0040126D |. 6A 30 PUSH 30
0040126F |. 68 BB124000 PUSH 004012BB; ASCII "API API"
00401274 |. 68 A9504000 PUSH OFFSET 004050A9; ASCII "entered string backwards with inverted register, to which xor is applied"
00401279 |. 6A 00 PUSH 0
0040127B |. FFD0 CALL EAX
0040127D \. C3 RETN
It remains quite a bit: again to solve the puzzle to find a suitable WinAPI function. Here is what we know about her:
- the length of the name is 11 characters; the letter A is at the end
- is in user32.dll
- four parameters are passed, two of them are strings
To search for a suitable function, I used the
%fasm_directory%\INCLUDE\PCOUNT\USER32.INC
using the following regular expression
'^\i{10}\%\s\=\s\s4'
. The ANSI versions of functions are not duplicated in this file, so a large A at the end cannot be specified. There were not so many suitable functions, but only one is perfect in its meaning:
MessageBoxA .
Remembering what operations are performed with it before passing it to GetProcAddress, we transform accordingly: MessageBox - (case inversion) - mESSAGEbOX - (character order inversion) -
XObEGASSEm .
Let's enter the treasured line in the second input field and get a winning MessageBox with a window title that reminds of adventures!
Instead of conclusion
Crackmy examined demonstrates an interesting way to firmly bind protection to the work of the program itself. The mechanism used makes it very difficult to patch. It is hard for me to imagine the use of such a method in commercial protection (but I will be happy if someone talks about examples), but for a puzzle for a couple of days it is very good, especially for beginners.