Let's go straight to the works...
- Choose 'Register NetTerm...' option from Help menu
- Enter something as your name and the registration number, and before you hit 'OK' button, CTRL-D to SoftICE...
- Let's define a simple macro that will display parameters on the stack when a breakpoint occurs. We'll call it 'PARAMS':
macro PARAMS = "dd ss:esp+4"
Now we can execute the macro simply by typing its name like any other command. But we
defined it for another purpose...
bpx GetDlgItemTextA DO "PARAMS"
return to the NetTerm and hit 'OK'. The ICE will pop up immediately at the beginning of
GetDlgItemTextA function and our little macro will dump the function's parameters on
stack in the data window when the break occurs. Now, remember
definition: the pointer to the buffer that receives the text is the third parameter on
stack and therefore the third DWORD on the stack (it's the fourth DWORD actually, but we
skipped the first one - the return address; thus ESP+4). Note here that we could make a
more usefull macro that could immediately dump the contents of the buffer in the data
macro GETTEXT = "db (ss:esp+4)->8"So, the breakpoint would look like this:
bpx GetDlgItemTextA DO "GETTEXT"What have we done here? We took esp+4 (the address of the first parameter on stack) as the base address of the parameter array and using C-like syntax, we reffered to the contents of the third DWORD in the array (which starts at offset 8). This third dword happens to be the address of the buffer that receives the text from the edit box, so the above command 'db' will display the contents of the memory that begins at that address.
Ok...so, at the first break you should get the address of the NAME buffer. Exit the
function with F11 and the buffer will be filled with entered name. Put a bpr on the name
string and resume the program. It will break again at another
GetDlgItemTextA. This one
reads in the registration number. When you exit the function with F11, the buffer will fill
up with the reg# you entered. Put a bpr on that string as well and resume the program again.
Smack! You land in the middle of KERNEL32!lstrcmp function that is called from here:
:4644ED 680C804700 push 47800C ;ptr.to "33413953" :4644F2 8B4510 mov eax, [ebp+10] ;ptr.to the entered reg# :4644F5 50 push eax :4644F6 FF151C924700 call [KERNEL32!lstrcmp] ;compare two strings :4644FC 85C0 test eax, eax ;do they match? :4644FE 0F8507000000 jnz 46450B ;jump if not!Hmmm....it can't be that easy...Let's try it! Type 33413953 as the registration number and hit 'OK'. Nope! That's not it. That was too easy anyway. "Back to the drawing board..." as Wile E. Coyote would say ;]]] So, let's do all of the above steps again, but this time proceed after lstrcmp. We'll land in the middle of KERNEL32!lstrlen function that is called from here:
:464764 8B450C mov eax,[ebp+0C] ;ptr. to the entered reg# :464767 50 push eax :464768 FF1528924700 call [KERNEL32!lstrlen] ;get length of the reg# string :44476E 8945F4 mov [ebp-0C],eax ;store lengthOK, let's go further. The next break occurs just a few instructions later:
:4647A2 8B45EC mov eax, dword ptr [ebp-14] ;let's assume this is kind of an offset :4647A5 8B4D0C mov ecx, dword ptr [ebp+0C] ;ptr.to entered reg# :4647A8 8A0408 mov al, byte ptr [eax+ecx] ;get a char. from reg# string :4647AB 50 push eax ;push it as the 2nd parameter :4647AC 8B4508 mov eax, dword ptr [ebp+08] ;ptr.to a table (this is interesting ;) :4647AF 50 push eax ;push it as the 1st parameter :4647B0 E851000000 call 464806 ;let's call this a translation routine :4647B5 8845FC mov byte ptr [ebp-04], al ;store translated digit :4647B8 8B45EC mov eax, dword ptr [ebp-14] ;get offset :4647BB 8B4D0C mov ecx, dword ptr [ebp+0C] ;get ptr. to entered reg# :4647BE 8A440801 mov al, byte ptr [eax+ecx+01];get the next char. :4647C2 50 push eax ;push it as the 2nd parameter :4647C3 8B4508 mov eax, dword ptr [ebp+08] ;get a ptr. to the translation table :4647C6 50 push eax ;push it as the 1st parameter :4647C7 E83A000000 call 464806 ;translate character :4647CC 8845F8 mov byte ptr [ebp-08], al ;store translated digit :4647CF 33C0 xor eax, eax ;clear work register :4647D1 8A45FC mov al, byte ptr [ebp-04] ;get 1st converted digit :4647D4 C1E004 shl eax, 04 ;move it to high nibble of AL :4647D7 33C9 xor ecx, ecx ;clear work register :4647D9 8A4DF8 mov cl, byte ptr [ebp-08] ;get 2nd converted digit :4647DC 0BC1 or eax, ecx ;combine it with the 1st digit into a number :4647DE 8945F0 mov dword ptr [ebp-10], eax ;store fully translated number :4647E1 8A45F0 mov al, byte ptr [ebp-10] ;get translated number :4647E4 8B4DE8 mov ecx, dword ptr [ebp-18] ;get the buffer offset :4647E7 8B5510 mov edx, dword ptr [ebp+10] ;get the ptr.to the buffer :4647EA 880411 mov byte ptr [ecx+edx], al ;store the translated number into the buffer :4647ED FF45E8 inc [ebp-18] ;insrease buffer offset (and a loop counter) :4647F0 E996FFFFFF jmp 46478B ;back to the looptopThe translation table looks like this:
:44E9A8 61 31 32 33 34 35 36 37 38 39 63 66 69 6B 75 71 a123456789cfikuq :44E9B8 33 41 39 53 3A9SNow...Hey! Wait a minute! What's that in the table? That 33 41 39 53 looks just like...Yes, that's the reg# we tried the first time, remember ? Only now it is written as an integer array, not as ASCII string. This, of course, doesn't have to mean anything. Still, I sense something fishy..
The return parameter from this routine is the length of the array of converted bytes.
Next, we land here:
:46456B E8EB010000 call 46475B ;WE CAME FROM HERE :464570 8D85FCFCFFFF lea eax, dword ptr [ebp-0304] ;ptr. to the array of converted bytes :464576 50 push eax ;push it as the 2nd parameter :464577 8D85FCFDFFFF lea eax, dword ptr [ebp-0204] ;ptr. to destination buffer :46457D 50 push eax ;push it as the 1st parameter :46457E FF15E091011C Call dword ptr [KERNEL32!lstrcpyA] ;copy the array :464584 8D85FCFCFFFF lea eax, dword ptr [ebp-0304] ;ptr.to the array :46458A 50 push eax :46458B 8D85FCFDFFFF lea eax, dword ptr [ebp-0204] ;ptr.to the array copy :464591 50 push eax :464592 8B450C mov eax, dword ptr [ebp+0C] ;ptr.to the name string :464595 50 push eax :464596 8B4508 mov eax, dword ptr [ebp+08] ;array length :464599 50 push eax :46459A E839000000 call 4645D8 ;THE KEYGEN ROUTINE :46459F 8D8500FFFFFF lea eax, dword ptr [ebp-0100] ptr.to string "3A9S" ...remember :4645A5 50 push eax ;what that is ? :4645A6 8D85FCFCFFFF lea eax, dword ptr [ebp-0304] ;ptr.to the generated string :4645AC 50 push eax :4645AD FF151C92011C Call dword ptr [KERNEL32!lstrcmpA] ;compare strings :4645B3 85C0 test eax, eax ;do they match? :4645B5 0F850F000000 jne 4B45CA ;if not, beggar off !Ok, so, there we are... If we experimet with different combinations of the name and reg#, we'll see that "3A9S" string always remains the same while the other one is always different. So, if we think about it and look at the parameters passed to the keygen routine, we can conclude that the entered name and reg# are combined together to form some string (or byte array, call it whatever you like). This array must match our magic string/array "3A9S" (33 41 39 53). Now it's clear what that number we encountered before was for....
Ok, if we know what's going on, can we regg it ? Hmmm...look tough - we would have to analyse the keygen algorythm in order to reverse the keygen process: if a combination of the name and the correct reg# produces "3A9S" string, then we must make the algorythm that would produce the correct reg# by combining the name and the magic string. Before we start tracing through the keygen code, we might consider what I like to call The Empirical Method, briefly described as "Try it and see if it works" method - a handy alternative for those who are not yet able to use zen ;]]] What does the above process look to you? My immediate thought was that it looks like a big xor operation, and we know that xor operations are reversibile:
A XOR B = C , and then C XOR B = AWhat am I trying to say here? Well, if we pass the name and the correct reg# to the keygen routine to produce the magic string, then perhaps we could pass the name and the magic string to the same routine to produce the correct registration number. Now, the easiest way to enter the magic string instead of the reg# is to type it in AS the reg# in the 'Register NetTerm' dialog box. The magic array 33h 41h 39h 53h , is translated to the ASCII using the same translation table as before. So we get "33413953" string,enter it as our reg#, hit 'OK' , but then we just get 'Invalid key' message. Breakpoints set around the keygen routine did not fire. Why? ....Remember that first lstrcmp ? Well, that's it. Programmers anticipated that someone will try this, so they placed a countermeasure, which is stupid btw, considering the fact that we can edit the reg# buffer directly in ICE. So, let's go back to the code immediately after the translation routine, specifficaly to :464570. There's LEA that loads the ptr.to the translated reg# into EAX, so we can take that address here and edit its contents:
eb [array_addr] 33 41 39 53That should do it... We'll trace quickly over the calls until we get to the code after the keygen routine. Now let's take a look at our generated array. It requires translation to ASCII string using the translation table. Now that we translated it , we can enter it as the reg#. Disable the breakpoints, enter the reg#, hit 'OK' and.....and......... .... .... and YES! It works! Now it's clear that the same algo is used for both generating the reg# and verifying it (this means we can use this very routine in our key generator). For those who didn't understand how we translated the 4-byte array into the 8-chr string, here's the example: I took my handle - iNCuBuS++ and from keygen routine got the array 9Ah,16h,3Ch,E0h. Look at the translation table:
HEX DIGIT 0 1 2 3 4 5 6 7 8 9 A B C D E F ASCII CHR 'a' '1' '2' '3' '4' '5' '6' '7' '8' '9' 'c' 'f' 'i' 'k' 'u' 'q'So, the translation goes:
ARRAY 9A 16 3C E0 / \ / \ / \ / \ HEX DIGITS 09h 0Ah 01h 06h 03h 0Ch 0Eh 00h | | | | | | | | ASCII CHRS '9' 'c' '1' '6' '3' 'i' 'u' 'a'And there we have it. Registration number for me is 9c163iua . Simple, isn't it?
In order to make a key generator for this proggie, we now must analyse the keygen algo and implement it in our code. We'll have to decide whether we'll just rip the entire keygen routine and put it in our key generator or we'll reimplement it. This decision depends on the keygen code itself. If it requires too much source editing or the keygen branches into many subroutines, maybe it would be better to reimplement the algorythm. In our case we'll reimplement the algo, although we could've just ripped it, as you'll see yourself. To the keygen algo ! We'll skip less important parts and go straight to the main keygen loop. Grab a pencil and a piece of paper and write down every step of the algorythm.
:464635 FF45EC inc [ebp-14] ;Increase offset into the array. :464638 8B4508 mov eax, dword ptr [ebp+08] ;Get the length of the array. :46463B 3945EC cmp dword ptr [ebp-14], eax ;Did offset reach the end of array? :46463E 0F8D41000000 jnl 464685 ;If it did, we're done, so jump! :464644 C745E800000000 mov [ebp-18], 00000000 ;Clear inner loop's counter. :46464B E903000000 jmp 464653 ;Jump to the beginning of the loop. :464650 FF45E8 inc [ebp-18] ;Increase inner loop's counter. :464653 8B45E8 mov eax, dword ptr [ebp-18] ;Get the current counter value. :464656 3945F0 cmp dword ptr [ebp-10], eax ;Did the counter reach the namelen ? :464659 0F8E21000000 jle 464680 ;If it did, the loop is over. :46465F 8B45F4 mov eax, dword ptr [ebp-0C] ;Get the name buffer offset (I guess :464662 50 push eax ;it is always 0). :464663 8B45F8 mov eax, dword ptr [ebp-08] ;Get length of the array. :464666 50 push eax :464667 8B45FC mov eax, dword ptr [ebp-04] ;Get ptr.to the name buffer. :46466A 50 push eax :46466B 8B45EC mov eax, dword ptr [ebp-14] ;Get the offset into the array. :46466E 8D8405E8FDFFFF lea eax, dword ptr [ebp+eax-0218];Get ptr.to the current byte :464675 50 push eax ;of the array. :464676 E822000000 call 46469D ;Do the math stuff with the byte. :46467B E9D0FFFFFF jmp 464650 ;Go back to the inner loop's top. :464680 E9B0FFFFFF jmp 464635 ;Go back to the outer loop's top. :464685 8D85E8FDFFFF lea eax, dword ptr [ebp-0218] :46468B 50 push eax :46468C 8B4514 mov eax, dword ptr [ebp+14] :46468F 50 push eax :464690 FF15E091011C Call dword ptr [KERNEL32!lstrcpyA] :464696 5F pop edi :464697 5E pop esi :464698 5B pop ebx :464699 C9 leave :46469A C21000 ret 0010Now, let's see what's actually happening with entered reg#:
:46469D 55 push ebp :46469E 8BEC mov ebp, esp :4646A0 53 push ebx :4646A1 56 push esi :4646A2 57 push edi :4646A3 8B4514 mov eax, dword ptr [ebp+14] ;Get the name buffer offset. :4646A6 8B4D0C mov ecx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :4646A9 0FBE0408 movsx eax, byte ptr [eax+ecx] ;Get a byte from the name buffer. :4646AD 8B4D0C mov ecx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :4646B0 0FBE09 movsx ecx, byte ptr [ecx] ;Get the 1st byte from the name buff. :4646B3 0FAF4D14 imul ecx, dword ptr [ebp+14] ;1st byte * name buff offset. :4646B7 33C1 xor eax, ecx ;Taken_byte XOR (1st byte*nb_offset). :4646B9 8B4D08 mov ecx, dword ptr [ebp+08] ;Get ptr. to the array. :4646BC 3201 xor al, byte ptr [ecx] ;Previous_result XOR byte_from_array. :4646BE 8B4D08 mov ecx, dword ptr [ebp+08] ;Get ptr. to the array. :4646C1 8801 mov byte ptr [ecx], al ;Replace byte_from_array with the result. :4646C3 8B4510 mov eax, dword ptr [ebp+10] ;Get the array length. :4646C6 48 dec eax ;Decrease it. :4646C7 3B4514 cmp eax, dword ptr [ebp+14] ;Did it reach the name buffer offset? :4646CA 0F8F20000000 jg 4646F0 ;If it is greater, skip next code. :4646D0 8B4514 mov eax, dword ptr [ebp+14] ;Get the name buffer offset. :4646D3 8B4D0C mov ecx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :4646D6 0FBE0408 movsx eax, byte ptr [eax+ecx] ;Get a byte from the name buffer. :4646DA 8B4D0C mov ecx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :4646DD 0FBE09 movsx ecx, byte ptr [ecx] ;Get the 1st byte from the name buff. :4646E0 03C1 add eax, ecx ;Add 2 bytes. :4646E2 8B4D14 mov ecx, dword ptr [ebp+14] ;Get the name buffer offset. :4646E5 8B550C mov edx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :4646E8 880411 mov byte ptr [ecx+edx], al ;Store the result of adding. :4646EB E920000000 jmp 464710 :4646F0 8B4514 mov eax, dword ptr [ebp+14] ;Get the name buffer offset. :4646F3 8B4D0C mov ecx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :4646F6 0FBE440801 movsx eax, byte ptr [eax+ecx+01] ;Get the next byte from the name buff. :4646FB 8B4D14 mov ecx, dword ptr [ebp+14] ;Get the name buffer offset. :4646FE 8B550C mov edx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :464701 0FBE0C11 movsx ecx, byte ptr [ecx+edx] ;Get the current byte from the name buff. :464705 03C1 add eax, ecx ;Add 2 bytes. :464707 8B4D14 mov ecx, dword ptr [ebp+14] ;Get the name buffer offset. :46470A 8B550C mov edx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :46470D 880411 mov byte ptr [ecx+edx], al ;Replace the curr.byte with the result. :464710 8B4514 mov eax, dword ptr [ebp+14] ;Get the name buffer offset. :464713 8B4D0C mov ecx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :464716 0FBE0408 movsx eax, byte ptr [eax+ecx] ;Get the result. :46471A 85C0 test eax, eax ;Is it 0? :46471C 0F8514000000 jne 464736 ;If not, don't adjust. :464722 8B4514 mov eax, dword ptr [ebp+14] ;Get the name buffer offset. :464725 8B4D0C mov ecx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :464728 0FBE0408 movsx eax, byte ptr [eax+ecx] ;Get the result. :46472C 40 inc eax ;Adjust the result. :46472D 8B4D14 mov ecx, dword ptr [ebp+14] ;Get the name buffer offset. :464730 8B550C mov edx, dword ptr [ebp+0C] ;Get ptr. to the name buffer. :464733 880411 mov byte ptr [ecx+edx], al ;Store adjusted result. :464736 FF4514 inc [ebp+14] ;Increase the name buffer offset. :464739 8B4510 mov eax, dword ptr [ebp+10] ;Get the array length. :46473C 394514 cmp dword ptr [ebp+14], eax ;Does name buff offset exceed the length? :46473F 0F8C07000000 jl 46474C ;If not, skip. :464745 C7451400000000 mov [ebp+14], 00000000 ;Clear the name buff offset. :46474C 8B4514 mov eax, dword ptr [ebp+14] ;Get the name buffer offset. :46474F E900000000 jmp 464754 ;STUPID! :464754 5F pop edi :464755 5E pop esi :464756 5B pop ebx :464757 C9 leave :464758 C21000 ret 0010So, what do we have here? We have 2 nested loops doing the whole thing. The outer loop's purpose is to pass the current byte of the array to the inner loop for processing. Once the inner loop has finished, the outer loop moves to the next byte of the array. The outer loop ends when all the bytes from the array have been processed. The inner loop's role is to take the current byte, pass it to the subroutine that does the math stuff with it and pass the resulting byte to the next iteration. The number of iterations equals the length of the name. The byte processing is done directly in the array.
Here's the complete algo:
.---------------------- WHILE ARRAY_OFFSET <= ARRAY_LENGTH DO | | LOOP_COUNTER = 0 | | .-------------- WHILE LOOP_COUNTER < NAME_LENGTH DO | | OUTER INNER CURRENT_ARRAY_BYTE = 1ST_BYTE_FROM_NAME XOR CURRENT_ARRAY_BYTE LOOP LOOP 1ST_BYTE_FROM_NAME = 1ST_BYTE_FROM_NAME + 2ND_BYTE_FROM_NAME | | LOOP_COUNTER = LOOP_COUNTER + 1 | | | `-------------- END INNER_LOOP | | ARRAY_OFFSET = ARRAY_OFFSET + 1; | `---------------------- END OUTER_LOOPIt's easy as that... Again, I have to stress out that this algo layout is valid only if we assume that name buffer offset is always 0 (as I think it is).
And now...the keygenerator ! You should write it yourself, of course, but I give the full source code of my keygen as an example (Otherwise, I might sue you for stealing my code ;)))) It is written for NASM and won't assemble with other assemblers.
;************************************************************************ ;* * ;* NETTERM KEY GENERATOR program by iNCuBuS++ [MiB] 1998 * ;* * ;************************************************************************ ; BITS 16 ORG 0x100 start: push cs pop ds ; DS = CS mov ax,0003h ;video mode=text 80x25 int 10h ;SET VIDEO MODE mov dx,logo call print ;PRINT STRING request_input: mov dx,input call print ;REQUEST USER INPUT mov dx,buffer ;pointer to buffer mov ah,0Ah ; int 21h ;BUFFERED INPUT mov dx,eol call print lea si,[buffer+1] lodsb ;get length cmp al, 2 ;string less than 2 chrs.? jge keygen ;if NOT, proceed with convert mov dx,wrong call print jmp request_input keygen: lea di, [initkey] ;ptr. to the magic number mov bx, di ;save ptr. movzx bp, al ;save name length mov dx,4 ;set the outer loop counter main_loop: mov cx, bp ;set the inner loop counter kg_loop: mov al,[si] ; get 1ST_BYTE_FROM_NAME xor [di],al ; CURRENT_ARRAY_BYTE XOR 1ST_BYTE_FROM_NAME mov al,[si+1] ; get 2ND_BYTE_FROM_NAME add [si],al ; 1ST_BYTE_FROM_NAME + 2ND_BYTE_FROM_NAME loop kg_loop ; END INNER_LOOP inc di ;next array byte dec dx ;decrement outer loop's counter jnz main_loop ;END OUTER_LOOP lea si, [conv_tbl] ;ptr.to translation table lea di, [buffer] ;ptr.to the output buffer mov cx, 4 ;set the loop counter convert: movzx bp, byte [bx] ;get the byte from the array mov dx, bp ;save it and bp, 00F0h ;keep high nibble and dx, 000Fh ;keep low nibble shr bp, 4 ;move high nibble to its place mov al, [si+bp] ;get the corresponding chr.from table stosb ;write it to the output buffer mov bp, dx ; mov al, [si+bp] ;get the corresponding chr.from table stosb ;write it to the output buffer inc bx ;next array byte loop convert ;back to looptop mov al,'$' ;mark the end of the string stosb mov dx,output call print ;PRINT STRING mov dx,buffer call print mov dx,eol call print exit: mov ax,4c00h int 21h ;Exit to DOS with exit code ; print: mov ah,09 int 21h ;PRINT STRING ret ; ; ; buffer: db 32 times 33 db 0 ; ; input: db "Enter your name: $" output: db "Your registration number is: $" wrong: db "ERROR! The name must be AT LEAST 2 characters !!!",0Dh,0Ah,0Dh,0Ah,"$" conv_tbl: db "a123456789cfikuq" initkey: db 033h, 041h, 039h, 053h ; ; logo: db '°ÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÂ°',0Dh,0Ah db '°³ Net Term V4.2.7 ³°',0Dh,0Ah db '°³ Cracked by iNCuBuS++ [MiB] ³°',0Dh,0Ah db '°ÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÁ°',0Dh,0Ah eol: db 0Dh,0Ah,'$'
Copyright © MiB 1998. All rights reversed.