-= HCU STRAINER 1999 : CHALLENGE 1 =-
Terminate 5.0
by Spath (09/98).
Goals: 1. Extensively analyze and explain Terminate's protection scheme.
2. Create a 16 bit assembly key generator for it.
3. Design a technique to assure that your generated key will be
valid in any further version of terminate.
Tools: PCWATCH (I love this tool)
SuperTracer 2.00
SoftIce 3.0
TASM 5.0
AT FIRST LOOK
I) THE ALGORITHM
II) REVERSING THE ALGORITHM
III) OTHER CHECKS AND VERSION COMPATIBILITY
IV) THE KEY GENERATOR
FINAL NOTES
AT FIRST LOOK
Playing a little bit with PCWATCH, I find out that TERMINATE try to open
a file called TERMINAT.KEY through INT 21 (3D/3F) ; when I create a dummy
key file, 162h bytes are read 11 times and I get the message "TERMINAT.KEY
was damaged, please reinstall". By the way, some interrupt vectors are
redefined (including 00h and 3Fh), which can crash simplest debuggers.
PART ONE : THE ALGORITHM
I fire SIce 3.0 through its DOS loader : as expected, the breakpoint on
INT21/3F stops the execution at the first read attempt in TERMINAT.KEY.
Note that the number of bytes read is saved (but all files larger then
162h*11 bytes will work).
:0C22 INT 21 ; read 162h bytes
POP DX
POP DS
JB 0C2E ; if a problem occured, save error code
CMP AX,CX ; 162h bytes read ?
JZ 0C31 ; yes : go on
MOV AX,DX ; no : save number of bytes read
:0C2E MOV [24E4],AX ; flag failure (DS:[24E4] = 0 if ok)
:0C31 POP BP
RETF 0004
and later, DS:[24E4] is saved into DS:[17D6] to be checked in step 2
of the decryption algorithm.
Then, I put a BPR breakpoint on the DS:DX memory location and I land
in the algorithm. Before showing the code, I will give some explanations :
this algorithm is based on 2 layers of encryption, so that there are 2
separate checks (of course, if the first fails, the second one is not
performed). As I explained before, the datas are loaded from TERMINAT.KEY
in blocks of 162h bytes, then stored in a internal buffer before being
processed.
Two cryptographic functions are used (I call them F1 and F2) ;
F1(buffer, magic_word1, magic_word2)
This function only modify two magic words (and not the buffer). It uses
basic byte transformations (XOR, SHL, ADD, SHR, RCR) and an array of
constant datas. It is used once on the 4th block (from byte 5Bh to byte
15Dh), and once on the complete file (for each block, from byte 0 to
byte 15Dh).
F2(buffer, parameter1, parameter2, parameter3)
This function modify the bytes of the buffer from address 5Bh to address
161h and also its two first parameters. Therefore, the values you choose
for parameter1 and parameter2 are just initial values, that are only used
for byte 5Bh. This function is used 3 times in the algorithm, each call
being embeded in the previous one, so that we have
buffer = F2(F2(F2(buffer))). Note that only the 4th block's decryption
requires F2 and that the third parameter is always constant (100h).
Here's a description of the whole decryption algorithm :
0) the two magic words have initial values of (FFFFh,FFFFh).
1) compute several tests on the file (see part 3).
2) calculate F1 all over the file, and compare the resulting words
with the values at offset 10*162h+15Eh, 10*162h+160h (the magic words
of the last block). If the result is incorrect, the next tests are not
performed.
3) compute xoring with FFh on bytes 5Bh to 161h of block 4.
4) compute block4 = F2(block4, 0007h, 0000h, 0100h)
5) compute block4 = F2(block4, 325ch, 0000h, 0100h)
6) compute block4 = F2(block4, 0904h, 33eeh, 0100h)
7) with initials values of (FFFFh,FFFFh), compute F1 on bytes 5Bh to 15Dh
of block4. If the magic words are equal to bytes 15Eh and 160h of
block4, the decryption worked.
8) compute several tests on the file (see part 3).
For detailed explanations, here's the code of the F1 function:
:56B INC WORD PTR[17D2] ; increment buffer index
MOV DI,[BP+06]
LES DI,SS:[DI+FEFA]
MOV AX,ES
PUSH AX
MOV DI,[17D2] ; load index pointer
POP ES
MOV AL,ES:[DI] ; load byte from buffer
PUSH AX ; push byte read
MOV DI,[BP+06]
PUSH WORD PTR SS:[DI+FEF8] ; push magic byte 2
PUSH WORD PTR SS:[DI+FEF6] ; push magic byte 1
POP BX ; BX = magic byte 1
POP DX ; DX = magic byte 2
POP CX ; CX = byte read
PUSH DX ; push magic byte 2
PUSH BX ; push magic byte 1
:595 XOR BX,CX
XOR BH,BH ; clear upper byte
SHL BX,1
SHL BX,1
ADD BX,[E320] ; add constant (2014h)
MOV AX,[BX] ; get value from data segment
MOV CX,[BX+02]
POP BX ; BX = magic byte 1
POP DX ; DX = magic byte 1
PUSH CX ; save CX
MOV CX,08 ; initialise loop
:5AC SHR DX,1 ; play with magic bytes
RCR BX,1
LOOP 5AC ; play again
AND DX,0FF
POP CX ; restore CX
XOR AX,BX
MOV BX,CX
XOR DX,BX
MOV DI,[BP+06]
MOV SS:[DI+FEF6],AX ; save magic byte 1
MOV SS:[DI+FEF8],DX ; save magic byte 2
MOV AX,[17D2] ; test buffer index
CMP AX,[BP-04] ; 15Eh bytes processed ?
JNZ 056B ; no : continue
And here is the code of the F2 function : the 3 parameters are
respectively stored at DS:24e6, DS:24e8, SS:BP+06. The 2 first ones
are modified at each call, the third one is always equal to 100h.
:0A3C INC WORD PTR [17D2] ; increment buffer index
MOV AX,0100h ; set third parameter
PUSH AX
CALL xxxx:2053 ; calculate new parameters
MOV DX,AX
MOV DI,[BP+06]
LES DI,[17D2] ; index of the next byte to process
POP ES
MOV AL,ES:[DI] ; get byte from buffer
XOR AH,AH
XOR AX,DX ; calculate new value for this byte
MOV DL,AL
MOV DI,[BP+06]
LES DI,SS:[DI+FEFA]
MOV AX,ES
PUSH AX
MOV DI,[17D2] ; index of the processed byte
POP ES
MOV ES:[DI],DL ; put new value in the buffer
MOV AX,[17D2]
CMP AX,[BP-04] ; did we process 161h bytes ?
JNZ 0A3C ; no : continue
And here's the xxxx:2053 procedure, where the two parameters are
modified :
:2053 PUSH BP
MOV BP,SP
MOV AX,[24E6] ; get first parameter
MOV BX,[24E8] ; get second parameter
MOV CX,AX
MUL WORD PTR [12BC] ; DS:[12BC] = 8405h
SHL CX,1
SHL CX,1
SHL CX,1
ADD CH,CL
ADD DX,CX
ADD DX,BX
SHL BX,1
SHL BX,1
ADD DX,BX
ADD DH,BL
MOV CL,05
SHL BX,CL
ADD DH,BL
ADD AX,0001
ADC DX,00
MOV [24E6],AX ; modify first parameter
MOV [24E8],DX ; modify second parameter
XOR AX,AX
MOV BX,[BP+06] ; get third parameter
OR BX,BX ; avoid division by 0
JZ 2097
XCHG AX,DX
DIV BX
XCHG AX,DX
:2097 POP BP ; AX=0, DX=(second parameter /100h)
RET 0002
With the F1 function, we obtain two magic words which depend on every
byte of the key file. These words are checked versus words number 15Fh
and 160h of the last read block.
Here's the code where the result of the F1 function is checked :
:906 MOV AX,SS:[DI+FEF6] ; load magic byte 1
MOV DX,SS:[DI+FEF8] ; load magic byte 2
LES DI,SS:[DI+FEFA]
CMP DX,ES:[DI+160] ; magic byte 2 = byte 160h ,
JNZ 092A ; no : bad boy
CMP AX,ES:[DI+015E] ; magic byte 1 = byte 15Eh ,
JNZ 092A ; no : bad boy
CMP WORD PTR [17D6],00 ; no problem reading the file ,
JZ 092D ; ok : go on
:092A JMP 22A7 ; bad boys go to hell
:092D MOV DI,[BP+06]
PART TWO : REVERSING THE ALGORITHM
So the question is : can we reverse the decryption algorithm ?
Yes, if we can reverse F1 and F2 :
- for the F1 function, there's no problem at all, since the result
of the hash is stored at the end of the hashed bytes. Therefore,
we can directly use the F1 function to calculate the magic words,
then we simply write them at the end of the block.
- for the F2 function, we will use two major flaws : first, the
calculation of the two parameters is completely independant from
the contents of the buffer, i.e. the F2 function is of the form :
for (i=5Bh, i<161H, i++)
{
parameter1 = F3(parameter1, parameter2)
parameter2 = F4(parameter1, parameter2)
buffer[i] = F5(buffer[i], parameter1, parameter2)
}
Moreover, the F5 function is of the form :
F5(buffer[i]) = buffer[i] XOR F6(parameter1, parameter2)
As a result of these two weaknesses, if we get the two correct
initial parameters F2 is its own inverse !
Therefore our encryption algorithm will be :
0) fill all the buffers with random bytes.
1) with initials values of (FFFFh,FFFFh), compute F1 on bytes 5Bh to 15Dh
of lock4. Write the result at bytes 15Eh and 160h.
2) handle special tests (see part 3).
2) compute block4 = F2(block4, 0904h, 33eeh, 0100h).
3) compute block4 = F2(block4, 325ch, 0000h, 0100h).
4) compute block4 = F2(block4, 0007h, 0000h, 0100h).
5) compute xoring with FFh on bytes 5Bh to 161h of block 4.
6) with initials values of (FFFFh,FFFFh), calculate F1 all over the file
and write the resulting words at offset 15Eh and 160h of the last buffer.
PART THREE : OTHER CHECKS AND VERSION COMPATIBILITY
Apart from the algorithm, four rows of tests are performed on the file,
as described below :
1)
Buffer number Test Result
1 Buffer[0B] = 46h ('F') AND Runtime error103
Buffer[1B] = 2Fh ('/') AND (File not open)
Buffer[14] = 2Eh
2 Buffer[1] = Buffer[2] AND add 0329
Buffer[1] = Buffer[3] AND to MagicWord1
Buffer[1] = Buffer[4] AND
Buffer[1] = Buffer[5]
5 see 2) see 2)
11 (a) Buffer[12C] = Buffer[12D] and add 0192
Buffer[12C] = Buffer[12E] and to MagicWord1
Buffer[12C] = Buffer[12F] and
Buffer[12C] = Buffer[130]
11 (b) Buffer[15A] = DC and Runtime error103
Buffer[15B] = 64 and (File not open)
Buffer[15C] = D9 and
Buffer[15D] = E9
As a result of checks on buffer 2 and buffer 11 (a), a file filled with
f.i. all '0', encrypted with the correct algorithm would not work. This
means that in correct keyfiles, these parts are filled with usefull (at
least not constant) datas. This basic detection of false keyfiles is useless
since I fill the keyfile with random values.
To avoid the tests on blocks 1 and 11 (b), I could have add specific
tests in my key-generator, but generating these particular values is very
unlikely : even if it happens, you only need to re-run the key-generator.
Note that all these tests were already used in version 4, but the tested
values were slightly different.
2)
This test use a lot of code, so I tried to explain it in a "mathematical
way" to make it more understandable... I hope I succeeded. So this test use
bytes 15Eh and 160h of buffers 4 and 5. Some functions are involved, which
take these two bytes as input and return three bytes (which i will call B1,
B2 and B3).
Here is what happens :
- calculate B2 = F7(Buffer5[15E])
(B1,B3) = F8(Buffer5[160])
- after decryption of buffer 4, calculate
B2' = F9(F7(Buffer4[15E]))
(B1',B3') = F8(Buffer4[160])
- if B1=B1' and B2=B2' and B3=B3' then the result of the test
is succesfull.
Since the bytes of buffer 4 are the result of al lot of work, it
is much easier to find the correct bytes of buffer 5 to pass this
test. The condition on B1 and B3 are easy to fulfil : we only
need to have Buffer5[160] = Buffer4[160]. The other condition seems
more complicated, since we need to compensate the effects of F9 on
Buffer4[15E] (further refered to as MW1).
Here's some code "of.class" tppabs="http://fravia.org/99solu/of.class" F9 :
:141F SHR DX,1 ; SI = F7(MW1), BX=A0h=(2^4)*0Ah
RCR BX,1 ; BX = BX / 2
RCR AX,1
DEC CL
JNZ 141F ; loop
...
ADC BX,SI ; B2' = F7(MW1) + BX
So at the end, we have F9(F7(MW1)) = F7(MW1) + 2^(4-CL)*0Ah : in
other words the action of F9 is to add n*0Ah (a multiple of ten in
decimal). Let's try to compensate this effects with F7, which use
this code :
NEG DX ; invert magic word 2
NEG AX ; invert magic word 1
MOV BX,AX
...
:1667 DEC AL ; BX = NOT(magic word 1)
ADD BX,BX ; calculate B2 = 2*B2
ADC DX,DX
JNS 1667 ; loop until a negative number is obtained
We see that if we substract n to magic word 1, B2 will increase by
(2^k)*n, where k is the number of times we execute the loop. Thanks
to the author, k=4-CL, so that we have (at last) :
F9(x) = x + (2^k)*0Ah
F7(x-n) = F7(x) + (2^k)*n
=> F9(F7(Buffer4[15E])) = F7(Buffer4[15E]) + (2^k)*0Ah
= F7(Buffer4[15E] - 0Ah)
So to pass the test, a key-generator can set :
Buffer5[15E] = Buffer4[15E] - 0Ah and
Buffer5[160] = Buffer4[160].
3)
Then, some other checks are performed on the decrypted (after 3*F2) fourth
buffer to detect some people (or companies) names, like "Peter Thomsen",
"Joshua Schultz" or "Velibor Cagalj / Zeit Systeme Gmbh" (sic). These
chains are seeked at offsets 7Ah and E0h of the fourth buffer, the first
byte containing the size of the chain to compare. Here's the code :
:1070 LODSB ; load size of the chain in the buffer
MOV AH,ES:[DI] ; load size of the model chain
INC DI
MOV CL,AL
CMP CL,AH ; compare sizes
JBE 107D ; buffer size <= model size : ok
MOV CL,AH ; otherwise compare model size
:107D OR CL,CL
JZ 1087
XOR CH,CH
REPZ CMPSB ; compare chains
JNZ 1089
CMP AL,AH ; if chains are equal, set Z flag
:1089 MOV DS,DX
RET
If one of these names if found, Terminate makes an awful "beeeeeep"
before displaying the main screen ; of course, you are not registered
(maybe these guys didn't pay their key ?). This test also existed in
version 4, but the list of names was slightly different (and shorter).
4)
At last, several other tests are done on this buffer, which can cause a
funny message to appear, asking you to delete your illegal keyfile :
Buffer[158]=FA98 & Buffer[156]=12FD
Buffer[74]=4638 & Buffer[72]=2391
Buffer[150]=FE6F & Buffer[14E]=DF92
Buffer[150]=C740 & Buffer[14E]=AE99
Buffer[150]=0000 & Buffer[14E]=B37B (*)
Buffer[150]=C740 & Buffer[14E]=AE66
(*) This test has been added from version 4 to version 5 : apparently, the
Terminate 4.0 keyfile generator from sULIVIAN [UCF] always fail it. I
suppose the author analyzed the key-generator, found a flaw in the PRNG
and added this specific test.
How to design a technique to assure that my key will be valid in any
further version of terminate ?
Well, if I look how the sULIVIAN's key-generator has been invalidated,
I only need to use a good PRNG to generate keyfiles with no constant
bytes. Therefore the four rows of tests will be useless.
But it's not so easy... for instance, what if some other bytes of
the file have forbidden (or required) values : these values are avoided
(or set) by the keymaker of the author, and not tested in version 5. But
in version 6, the author add a test for these bytes, so that only the
keyfiles generated by his program are sure to pass the test. This means
in particular that without changing his key-generator, the author could
create new versions of his software and invalidate previous key-generators.
So here's the method I propose :
1) Find many original Terminate 5.0 keyfiles.
2) Decrypt them by using the algorithm used in Terminate 5.0 and described
in part one : you can either trace in SoftIce and dump the buffers or
reuse pieces of code "from.class" tppabs="http://fravia.org/99solu/from.class" the key-generator.
3) You obtain the files that the author encrypted. In each file, you can
read in plain-text all the informations about the owner of the keyfile
(name, company, town,..) : replace all these informations by 00h.
4) Compare all these files : if you are lucky, they are identical. If not,
try to figure out how the bytes that are different can be deduced
from the owner's informations (e.g. the first character before a string
is its size).
5) From these files, create a "mask" (or in worst case, code a mask-maker)
for your new key-generator : your program will overwrite only the bytes
containing informations about the owner, leaving the others bytes
unmodified.
This method will prevent your keyfile from current (and future) byte checks
for forbidden/required values and may guarantee a longer life to your
keyfile. Unfortunately, I did not find any original keyfile so that I was
unable to test it...
PART FOUR : THE KEY GENERATOR
Here's the code of my key generator, roughly commented. I used a big
buffer to store and process all the 11*162h=3894 bytes and reused the code
of F1 and F2 from Terminate. The result is (without optimisation) a 9320
bytes COM file.
;-------------------------------------------------------;
; TERMINATE 5.0 Key Generator v 0.01 ;
; ;
; coded by Spath (09/98) for +HCU strainer 1999 ;
; compile with tasm, link with tlink /t ;
;-------------------------------------------------------;
Code Segment Byte Public
Assume Cs:Code, Ds:Code, Es:Code
Org 100h
.386
Start:
mov ah,09
mov dx,offset IntroMsg
int 21h ; show intro msg
mov ax,3d00h
mov dx, offset InFileName ; open terminat.exe
int 21h
jc ErrorReadingFile
mov cx,0001h ; use constant pointer value
mov dx,0d984h
mov bx,ax
mov ax,4200h
int 21h
jc ErrorSettingPointer ; set pointer at the right position
mov ax,3f00h
mov cx,1100h
mov dx,offset TerminateDatas ; read Terminate datas
int 21h
mov ah,3Ch ; create keyfile
xor cx,cx
mov dx,offset OutFileName
INT 21h
FillAndAnalyseBuffer:
mov si,offset Buffer ; fill the buffer
mov cx,0b1h*11 ; with random values
FillBuffer:
call Random
mov [si],ax
inc si
inc si
loopne FillBuffer
;------ FIRST LAYER OF ENCRYPTION : F1 & F2 over buffers 4 and 5 --------
mov MagicWord1, 0FFFFh
mov MagicWord2, 0FFFFh
mov si, (offset Buffer) + 3*162h + 5Bh ; calculate Magic Words
mov F1Index,05Bh
call F1
mov si, (offset Buffer) + 3*162h ; focus on buffer 4
mov dx,MagicWord1 ; write magic word 1
mov word ptr [si+015eh],dx
mov dx,MagicWord2 ; write magic word 2
mov word ptr [si+0160h],dx
mov si, (offset Buffer) + 4*162h ; focus on buffer 5
mov dx,MagicWord1 ; write magic word 1
sub dx,0Ah
mov word ptr [si+015eh],dx
mov dx,MagicWord2 ; write magic word 2
mov word ptr [si+0160h],dx
mov FirstParameter,0904h ; first call to F2
mov SecondParameter,33EEh
call F2
mov FirstParameter,325Ch ; second call to F2
mov SecondParameter,0000h
call F2
mov FirstParameter,0007h ; third call to F2
mov SecondParameter,0000h
call F2
mov BufferIndex,5Bh
XORLoop: ; XOR loop
mov si,(offset Buffer) + 3*162h
add si,BufferIndex
mov al,[si] ; get byte from buffer
xor al,0FFh
mov [si],al ; put new value in the buffer
mov ax,BufferIndex
cmp ax,161h ; is it finished ?
jz SecondLayer
inc BufferIndex
jmp XORLoop ; no : continue
;------ SECOND LAYER OF ENCRYPTION : F1 all over the file ---------------
SecondLayer:
mov MagicWord1, 0FFFFh
mov MagicWord2, 0FFFFh
mov si, offset Buffer
Calculate: ; calculate magic words
mov F1Index,0 ; process 15Dh bytes
call F1
inc byte ptr F1Count ; one more buffer processed
cmp F1Count,0Bh ; is it buffer 0Bh ?
jz ConcludeF1 ; yes : conclude
add si,05 ; skip useless bytes
jmp Calculate
ConcludeF1:
mov si, (offset Buffer) + 10*162h
mov dx,MagicWord1
mov word ptr [si+015eh],dx ; write magic word 1
mov dx,MagicWord2
mov word ptr [si+0160h],dx ; write magic word 2
mov si,offset Buffer ; write to file
mov cx,162h*11
call WriteToFile
mov ah,09
mov dx,offset FileCreatedMsg
int 21h ; show end msg
jmp ExitProgram
ErrorSettingPointer: ; error in setting pointer
mov ah,09
mov dx,offset ErrorPointerMsg
int 21h ; show error msg
jmp ExitProgram
ErrorReadingFile: ; error in opening file
mov ah,09
mov dx,offset ErrorOpeningMsg
int 21h ; show error msg
ExitProgram:
int 20h ; exit
;----------- GENERAL PURPOSE PROCEDURES AND FUNCTIONS -------------------
WriteToFile PROC NEAR
push ax
push cx
push dx
push cx
OpenFile:
mov ax,3D82h ; open the file
mov dx,offset OutFileName
INT 21h
Write:
mov bx,ax
xor dx,dx
xor cx,cx ; set pointer at the end
mov ax,4202h ; of the file
INT 21h
jc end_pop_write_to_file
mov ah,40h
pop cx ; write in the file
mov dx,si
INT 21h
jc end_write_to_file
mov ah,3Eh
INT 21h ; close the file
jmp end_write_to_file
end_pop_write_to_file:
pop cx
end_write_to_file:
pop dx
pop cx
pop ax
ret
WriteTofile ENDP
; very simple PRNG
; return a number in ax
Random PROC NEAR
push cx
push dx
and ah,0fh
in al,40h ; get byte from timer
mov cx,ax
mov dx,RandSeed ; get previous randseed
shake:
mul dx
add ax,4321h
add dx,7D96h
loop shake
mov RandSeed,ax ; save new seed
pop dx
pop cx
ret
Random ENDP
;----- CRYPTOGRAPHIC PROCEDURES AND FUNCTIONS --------------------
F1 PROC NEAR
label56B:
mov al,[si] ; load byte from buffer
PUSH AX ; push byte read
PUSH WORD PTR MagicWord2 ; push magic word 2
PUSH WORD PTR MagicWord1 ; push magic word 1
POP BX ; BX = magic word 1
POP DX ; DX = magic word 2
POP CX ; CX = byte read
PUSH DX ; push magic word 2
PUSH BX ; push magic word 1
label595:
XOR BX,CX
XOR BH,BH ; clear upper byte
SHL BX,1
SHL BX,1
ADD BX,offset TerminateDatas ; use loaded datas
MOV AX,[BX] ;
MOV CX,[BX+02]
POP BX ; BX = magic word 1
POP DX ; DX = magic word 1
PUSH CX ; save CX
MOV CX,08 ; initialise loop
label5AC:
SHR DX,1 ; play with magic words
RCR BX,1
LOOP label5AC ; same player play again
AND DX,0FFh
POP CX ; restore CX
XOR AX,BX
MOV BX,CX
XOR DX,BX
MOV MagicWord1,AX ; save magic word 1
MOV MagicWord2,DX ; save magic word 2
mov ax,F1Index ; test buffer index
cmp ax,15Dh ; 15Eh bytes processed ?
jz ExitF1 ; yes : done
inc si
inc F1Index
jmp label56B ; no : continue
ExitF1:
ret
F1 ENDP
F2 PROC NEAR
mov BufferIndex,5Bh
F2Start:
CALL Proc2053 ; calculate new parameters
mov dx,ax
mov si,(offset Buffer) + 3*162h
add si,BufferIndex
MOV AL,[si] ; get byte from buffer
XOR AH,AH
XOR AX,DX ; calculate new value for this byte
MOV DL,AL
MOV [si],DL ; put new value in the buffer
MOV AX,BufferIndex
CMP AX,161h ; did we process 162h bytes ?
JZ OutF2
INC BufferIndex
JMP F2Start ; no : continue
OutF2:
RET
F2 ENDP
Proc2053 PROC NEAR
label2053:
MOV AX,FirstParameter ; get first parameter
MOV BX,SecondParameter ; get second parameter
MOV CX,AX
MUL Cst8405h ; DS:[12BC] = 8405
SHL CX,1 ; play with parameters
SHL CX,1
SHL CX,1
ADD CH,CL
ADD DX,CX
ADD DX,BX
SHL BX,1
SHL BX,1
ADD DX,BX
ADD DH,BL
MOV CL,05
SHL BX,CL
ADD DH,BL
ADD AX,0001
ADC DX,00
MOV FirstParameter,AX ; modify first parameter
MOV SecondParameter,DX ; modify second parameter
XOR AX,AX
MOV BX,0100h ; third parameter is always the same
XCHG AX,DX
DIV BX
XCHG AX,DX
label2097:
RET
Proc2053 ENDP
;------------ DATAS ----------------------------------------------------
OutFileName db "terminat.key",0
InFileName db "terminat.exe",0
IntroMsg db 13,10,'TERMINATE 5.0 Key Generator'
db 13,10,'coded by Spath in 1998'
db 13,10,'for the 1999 +HCU Strainer',13,10,'$'
ErrorOpeningMsg db 13,10,'Error: TERMINAT.EXE not found',13,10,'$'
ErrorPointerMsg db 13,10,'Error: incorrect pointer value',13,10,'$'
FileCreatedMsg db 13,10,'TERMINAT.KEY has been generated...',13,10,'$'
Buffer db 162h*11 dup (0)
MagicWord1 dw 0FFFFh
MagicWord2 dw 0FFFFh
FirstParameter dw ?
SecondParameter dw ?
BufferIndex dw 0
RandSeed dw 5A9Ch
F1Count db 00h
F1Index dw 0
TerminateDatas db 1200h dup(0)
Cst8405h dw 8405h
code ends
End Start
FINAL NOTES
I really enjoyed working on Terminate, which was for me the most
challenging part of the strainer. The author had good ideas to reject
current (and future) false keys. Yet, the decryption scheme is weak,
since F1 is useless and F2 its own inverse. As a result, the encryption
and decryption algorithms are symetricals and not really complicated.
Maybe he should have use real cryptography instead of multiple paranoid
checks.
Spath. (09/98)
---------------------------------------------------------------------------
-= HCU STRAINER 1999 : CHALLENGE 2 =-
Win32 byte patcher
by Spath (08/98).
Goal: 1. Create a Windows based 32 bit byte patcher for any target you
wish, using any programming language.
Tool: TASM 5.0
SoftIce 3.0
I) SOME EXPLANATIONS
II) THE CODE
PART ONE : SOME EXPLANATIONS
My patcher is a simple "search and replace" one ; it loads pieces of
the file in a internal buffer until it finds the byte sequence . Then
the file pointer is set to this location it found and the new sequence
is written to the file. This means in particular that the byte sequence
must be unique, otherwise only the first occurence will be replaced.
Why did I use SetFilePointer after I found the sequence ?
Indeed, if the sequence is in the buffer I just loaded, the file pointer
is already correctly set and I only need to re-write the full buffer.
Yet, I thought that a problem would appear when the byte sequence start
at the end of a buffer and continues at the begining of the next loaded
one.
For this task, I chose assembly because it is is my favourite language
and because I wanted to play with TASM 5.0 (Thanks +Aesculapius !). As
always, I also used SIce for debugging.
Well, not much to say about this challenge, so here's...
PART TWO : THE CODE
;-------------------------------------------------------;
; Simple Win32 Byte Patcher v 0.01 ;
; ;
; coded by Spath (08/98) for +HCU strainer 1999 ;
; compile: tasm32 -ml -m5 -q patcher ;
; link: tlink32 -Tpe -aa -x -c patcher ,,, import32 ;
;-------------------------------------------------------;
.386p
.model flat, stdCALL
EXTRN ReadFile:PROC ; imported functions
EXTRN WriteFile:PROC
EXTRN CloseFile:PROC
EXTRN CreateFileA:PROC
EXTRN CloseHandle:PROC
EXTRN MessageBoxA:PROC
EXTRN SetFilePointer:PROC
EXTRN ExitProcess:PROC
INCLUDE WINDOWS.INC ; the famous one
.DATA
;-- modify these datas to patch your target
Filename db "virstop.exe",0
OldChain db 0fbh,81h,21h,0cdh,35h,0feh,0b8h,50h,0a7h,26h,10h
NewChain db 00h,01h,02h,03h,04h,05h,06h,07h,08h,09h,10h
;-- internal constants
FILE_ATTRIBUTE_NORMAL equ 080h
OPEN_EXISTING equ 3
GENERIC_READ equ 80000000h
GENERIC_WRITE equ 40000000h
;-- internal variables
BufferSize equ 5000h
Buffer db BufferSize DUP (?) ; here's the buffer
FileHandle dd ?
BytesRead dd ?
CorrectBytes dd 0
ChainSize equ OFFSET NewChain - OFFSET OldChain
ChainPos dd 0
;-- strings
cptPatcher db 'Patcher32',0
msgFileNotFound db 'Error: the file was not found...',0
msgPatchSuccesfull db 'The target has been succesfully patched !',0
msgSequenceNotFound db 'Error: the byte sequence was not found...',0
msgErrorWriting db 'Error: impossible to write to file...',0
.CODE
START:
push 0
push FILE_ATTRIBUTE_NORMAL
push OPEN_EXISTING
push 0
push 0
push GENERIC_READ OR GENERIC_WRITE
push OFFSET Filename
call CreateFileA ; open the target file
cmp eax,0FFFFFFFFh ; error ?
jz ErrorOpeningFile ; yes : display error message
mov FileHandle, eax ; no : save file handle
mov esi,OFFSET OldChain ; start searching from the begining
ReadLoop:
push 0
push OFFSET BytesRead
push OFFSET BufferSize
push OFFSET Buffer
push FileHandle
call ReadFile ; fill the buffer
mov edi,OFFSET Buffer ; start searching from the begining
LoadByte:
lodsb ; load a byte from OldChain
cmp al,[edi]
je GoodByte ; one byte in common found ?
BadByte:
mov esi, OFFSET OldChain ; no : search from the begining
mov CorrectBytes,0 ; reset consecutive success counter
jmp GoOn
GoodByte: ; one correct byte was found
inc CorrectBytes ; increment consecutive success counter
cmp CorrectBytes,ChainSize ; did we find all the chain ?
jz ChainFound ; yes : go to replace part
GoOn:
inc edi ; increment Buffer pointer
cmp edi, OFFSET FileHandle ; is it the end of the buffer ?
jne LoadByte ; no : check next byte
cmp BytesRead,BufferSize ; yes : is it EOF ?
jne SequenceNotFound ; yes : sequence not found
add ChainPos, BufferSize ; adjust chain position
jmp ReadLoop ; reload from file
ChainFound:
sub edi, ChainSize - 1 ; set edi at the begining of the chain
add ChainPos, edi ; adjust chain position
mov esi, OFFSET NewChain ;
mov ecx, ChainSize
repne movsb ; write new chain over older one
sub edi, ChainSize ; adjust pointer
sub ChainPos, offset Buffer
push 0 ; set file pointer at ChainPos
push 0
push ChainPos
push FileHandle
call SetFilePointer
push 0
push offset BytesRead
push ChainSize ; ChainSize bytes are written from
push edi ; memory address edi into the file
push FileHandle
call WriteFile ; write to file
cmp eax, 1 ; write access ok ?
je Succesfull ; yes
push 0 ; no : prepare "Error writing" message
push offset cptPatcher
push offset msgErrorWriting
push 0
jmp Terminate
Succesfull:
push 0 ; prepare "Succesfull" message
push offset cptPatcher
push offset msgPatchSuccesfull
push 0
jmp Terminate
SequenceNotFound:
push 0 ; prepare "Sequence not found" message
push offset cptPatcher
push offset msgSequenceNotFound
push 0
jmp Terminate
ErrorOpeningFile:
push 0 ; prepare "File not found" message
push offset cptPatcher
push offset msgFileNotFound
push 0
Terminate:
call MessageBoxA ; display message box
push 0
push FileHandle
call CloseHandle ; close file
call ExitProcess ; quit
END START
--------------------------------------------------------------------------
Greetings: _masta_, htak and Iczelion for their good Win32 ASM tutorials.
-= HCU STRAINER 1999 : CHALLENGE 3 =-
BrainsBreaker v 2.1 (32 bits)
by Spath (08/98).
Goal: 1. Completely explain the protection scheme used by this program.
Tools: SoftIce 3.01 (no need to upgrade, my old S3 card works fine)
W32Dasm 8.9 (IDA not needed here)
heXedit 4.3 (the fastest for search&patch)
AT FIRST LOOK
I) DEMO BOXES + SIDES LIMITATION
II) TESTS OF INTEGRITY
III) THE "DEMO" WORD ON THE PICTURES
FINAL NOTES
AT FIRST LOOK
Well, this protection has at least one good point : the registration
method is part of the registration secret. The limitations include demo
boxes, a limition of the sides you can place and an awful "DEMO" word
on the nicest puzzles. I removed them all.
PART ONE : DEMO BOXES AND SIDES LIMITATION
This part will not be very exciting, because it is just a reuse of
well-known methods to remove all the annoying boxes. Since the strings of
these boxes are not visible at first look, I just wait a little bit to
let Bbrk32 load and decode them, and then I search in the data segment.
For the first box ("Warning: the program is running in evaluation mode.You
will be allowed to solve..."), the code is really straightforward :
:00444D73 push 0000002C
:00444D75 call 00420A08 ; see (1)
:00444D7A mov ebx, eax
:00444D7C test eax, eax ; should we show the box ?
:00444D7E je 00444F88 ; no => go to 444F88
:00444D84 add esp, FFFFFFFC ; yes
. . .
:00444D94 call 00465533 ; display the box
:00444D99 jmp 00444F88 ; box closed => 444F88
In the other case (the 3 first puzzles), the message displayed is
"Brainsbreaker unregistered. Please see how to register." but the idea is
the same :
:00444EFE push 0000002C
:00444F00 call 00420A08 ; see (1)
:00444F05 mov [ebp-0088], eax
:00444F0B test eax,eax ; should we show the box ?
:00444F0D jz 00444F29 ; no
Since before testing the result value, the prog always saves it somewhere
else, I chose to make a 2 lines patch:
Therefore I changed:
8BD8 mov ebx,eax into 33DB xor ebx,ebx
85C0 test eax,eax 33C0 xor eax,eax
898578FFFFFF mov [ebp-0088],eax into 33C0 xor eax,eax
85C0 test eax,eax 898578FFFFFF mov [ebp-0088],eax
For the "Since now you will be allowed to lock x sides" boxes, the author used
2 counters, counter1 and counter2 respectively incremented by n and decremented
by 2*n (1 Counter2 and you enter the "random display" zone (you also
go there after 25 sides). In this zone, boxes are displayed depending on the
clock for an average rate of. At last, if you have less then 8 sides to put,
a box is displayed every time. Here's the code :
:0044DCC7 movsx eax, word ptr [ebp+FFFFFE2E] ; read Counter1
:0044DCCE add eax, eax ;
:0044DCD0 cmp eax, dword ptr [0048CA9D] ; is (2*Counter1) > Counter2 ?
:0044DCD6 jl 0044DCE2 ; no : go to random test
:0044DCD8 cmp word ptr [ebp+FFFFFE2E], 0019 ; is Counter1 > 19 ?
:0044DCE0 jge 0044DCF4 ; yes : no box this time |
:0044DCE2 call WINMM!TimeGetTime ; random test based on clock
:0044DCE7 sub eax, dword ptr [0048CBFC]
:0044DCED cmp eax, 00007530 ;
:0044DCF2 jnb 0044DCFE ; no box this time
:0044DCF4 cmp word ptr [ebp+FFFFFE2E], 0008 ; Counter1 > 8 ?
:0044DCFC jg 0044DD68 ; yes : no box displayed
:0044DCFE call WINMM!TimeGetTime ; no : prepare for the box
:0044DD03 mov dword ptr [0048CBFC], eax
:0044DD08 push 0000002C
:0044DD0A call 00420A08 ; see (1)
:0044DD0F mov dword ptr [ebp+FFFFFE10], eax
:0044DD15 test eax, eax
:0044DD17 je 0044DD5A
... ...
:0044DD55 call 00465533 ; -= call the Message Box =-
What is really funny is that counter1 is never decremented ! Each new
value is calculated (with counter2) via a incremental loop which gets
shorter and shorter (and which contains many crazy co-processor
instructions) :
:0044DC6E inc word ptr [ebp+FFFFFE2E] ; increment counter1
:0044DC75 add si, 0002 ; increment loop counter
. . .
:0044DCA7 movsx ecx, byte ptr [edx+54]
:0044DCAB mov eax, dword ptr [ebp+FFFFFE24] ; this value is decreasing
:0044DCB1 movsx edx, word ptr [eax+2*ecx+48]
:0044DCB6 push edx
:0044DCB7 call 0045D6E2
:0044DCBC movsx ecx, si
:0044DCBF cmp eax, ecx
:0044DCC1 jg 0044DBBA ; increment again
Counter2 is incremented by this piece of code :
:0045592E push 00000002 ; push increment
:00455930 push 0048CA62 ; push base address (48CA9D - 3B)
:00455935 call 0046E8A1
. . .
:0046E8A4 mov eax, dword ptr [ebp+08] ; get base address
:0046E8A7 mov edx, dword ptr [ebp+0C] ; get increment
:0046E8AA add dword ptr [eax+3B], edx ; increment counter2
In spite of all these efforts, the crack was quite simple : I changed
:0045592E 6A02 push 02 into 6A00 push 00 ; stop counter2 increment
:0044DCD6 7C0A jl 44DCE2 into EB1C jmp 44DCF4 ; skip all the tests
(1):
Of course, I noticed that the real protection test seems to be in the
push 2C/call 420A08 (which should return EAX=0) : the best crack would
certainly have been to patch the 420A08 procedure, but this value seems
not to be enough. Indeed, I tried to put a
BPX 420a08 if @ss:(esp+4)==2c do "p ret ; r eax=0 ; g"
but unfortunately, it crashed after a few calls ; so I decided to try
to patch this way (if it had failed, I would have had to dig deeper
into 420a08).
PART TWO : TESTS OF INTEGRITY
Ok, this works fine under SoftIce, but after having modified the bytes in
Bbrk32.exe, I get a funny "Integrity check: Program seems to be altered from
his original contents". So I put a breakpoint on MessageBoxA and find this
piece of code :
:0044358A push 00000000
:0044358C call 004410AF ; call integrity check procedure
:00443591 cmp eax, 55443322 ; is it correct ?
:00443596 je 0044360A ; yes, go on ...
:00443598 push 00012010 ; display style
:0044359D push dword ptr [00486B04] ; title of the window
:004435A3 add esp, FFFFFFFC
:004435A6 mov word ptr [esp], 00BE
:004435AC call 0043A041
:004435B1 push eax ; text to display
:004435B2 push 00000000 ; an orphaned window
:004435B4 USER32!MessageBoxA ; display error box
If I patch this location as follows
:00443591 cmp eax, 55443322 into mov eax, 55443322
:00443596 je 0044360A jmp 0044360A
and start again, I still receive the same infamous message... this paranoid
author put more than one integrity test in his game. How can I find them all ?
With the title parameter, stored in DS:00486B04, which is specific to this
box (see (2)). Looking for the "push dword ptr [00486B0A]" instruction in the
disassembled listing, I find 5 other locations : 41FD9F, 4447E9, 44C9E3,
453F77, 45D033.
All these locations are patched as follows :
:0041FD8A cmp dword ptr[00485880],00012345 into mov dword ptr[00485880],00012345
:0041FD94 je 0041FE3B into jmp 0041FE3B
:004447E2 jnb 0044485F into jmp 0044485F
:0044C9D8 jnb 0044CA63 into jmp 0044CA63
:00453F70 jnb 00453FED into jmp 00453FED
:0045D02C jnb 0045D0A9 into jmp 0045D0A9
(2):
This parameter is required, but it can be
a/ read through indirect addressage : here I trust good old Borland C++
compiler for being consequent in its work.
b/ mirrored somewhere else : here I trust the author's lack of imagination.
PART THREE : THE "DEMO" WORD ON THE PICTURES
For this part, I quickly found out that in every case the picture is
displayed through a User32!UpdateWindow, which simply send a WM_PAINT message
to the window. Since I am an absolute newbie in graphics coding, I needed a
clue (4)... then I noticed that "DEMO" disappear when I click on the model
picture.
So I tried the following :
1) click on the picture and maintain the left button.
2) enter SIce and release the button.
3) "p ret" until I reach the Bbrk32 code
Then I traced a little bit and... Bingo ! This is the graphical function I
was looking for : GDI32!BitBlt. Therefore, I put a breakpoint on this
function and write down all the calling adresses when I select a "clean"
picture (f.i. the fish) from the main screen. Then I do the same thing
with a "demo" picture (f.i. the cat). As expected, this last listing shows
one more call to BitBlt ; here is a piece of the code that cause the "DEMO"
word to appear (not only on the top left corner picture, but also on the
small images and on the full-size one).
:00428915 push 00AC0744 ; see (3)
:0042891A movsx edx, word ptr [ebp-46]
:0042891E push edx ; top left corner Y value (source)
:0042891F movsx ecx, word ptr [ebp-48]
:00428923 push ecx ; top left corner X value (source)
:00428924 mov eax, dword ptr [ebp-40]
:00428927 push [eax+04] ; source handle
:0042892A movsx edx, word ptr [ebp-42]
:0042892E push edx ; height of the picture
:0042892F movsx ecx, word ptr [ebp-44]
:00428933 push ecx ; length of the picture
:00428934 push [ebp-3C] ; top left corner Y value (destination)
:00428937 push [ebp-38] ; top left corner X value (destination)
:0042893A push [esi+04] ; destination handle
:0042893D Call GDI32!BitBlt ; display the bitmap
My first idea was to change the push [edx+04] into push [esi+04], so that the
bitmap is simply overwritten by itself. In fact, it removed the "DEMO" word,
but the gray rectangle remained on the picture ; I therefore had to find
which procedure called this piece of code. Back-tracing was not needed here,
so with simple P RET and STACK commands I found these pieces of code:
for the small pictures:
:00462640 cmp dword ptr [ebp+FFFFFEC4], 00000000 ; write "DEMO" ?
:00462647 je 004627C0 ; no : skip this part
:0046264D mov eax, dword ptr [0048C918] ; yes
for the top-left picture and the full-size one:
:0045D317 push eax
:0045D318 call 0046D872
:0045D31D test eax, eax ; write "DEMO" on the picture ?
:0045D31F je 0045D427 ; no : skip this part
:0045D325 mov edi, 004877AC ; yes
And I change:
:00462647 0F8473010000 je 004627C0 into jmp 004627C0
:0045D31D 85C0 test eax,eax into 33C0 xor eax,eax
(3):
This value is the colour combination parameter, as described in the wingdi.h
file ("Ternary raster operations" paragraph) :
CONST
SRCCOPY : DWORD = 16_00CC0020; (* dest = source *)
SRCPAINT : DWORD = 16_00EE0086; (* dest = source OR dest *)
SRCAND : DWORD = 16_008800C6; (* dest = source AND dest *)
... (a total of 15 parameters are available)
By the way, the 00AC0744 value is not described in it (??)
(4):
After all this work I realized that the "DEMO" string reference in W32dasm
was immediately leading to this piece of code... :(
FINAL NOTES
Well, I enjoyed cracking this game, especially for part 3 (part 2 was quite
disapointing, though). I learned a few things about graphical interface, which
were very useful for the last challenge. Obviously, the author spent some time
on the protection, which deserve respect, even if he chose complexity instead of
precision. I think that my crack is a little bit heavy (12 patches)... I wish I
have had more time to spend on the registration part.
Here's the summary of my crack:
offset old chain new chain
Boxes: 4437A 8B D8 85 33 DB 33
44505 89 85 78 FF FF FF 85 C0 33 C0 89 85 78 FF FF FF
Integrity tests:
1F394 0F 84 A1 E9 A2 00
42B91 3D 22 33 44 55 74 B8 22 33 44 55 EB
43DE2 73 EB
4BFD8 0F 83 85 E9 86 00
53570 73 EB
5C62C 73 EB
"DEMO": 5C91D 85 33
61C47 0F 84 73 01 E9 74 01 00
Sides limitation:
54F2F 02 00
4D2D6 7C 0A EB 1C
Spath. (08/98)
-----------------------------------------------------------------------------
Greetings: - +Frog's Print, a master cracker and a nice person.
- BeLZeBuTH, Ethan, Kellogs, CyberbobJr and all the guys
on +FP's forums.
-= HCU STRAINER 1999 : ULTIMATE CHALLENGE =-
BrainsBreaker v 2.1 (32 bits)
by Spath (09/98).
Goal: The objective of this challenge is to check that:
1. The participant understands the graphical part of demo-reversing.
Tools: SoftIce 3.0
TASM 5.0
I) SOME EXPLANATIONS
II) THE CODE
PART ONE : SOME EXPLANATIONS
The little star animation of BrainsBreaker is made of 14 bitmaps, each
of these bitmaps being run-time calculated. Each bitmap is made of (at
most) 3 parts :
- a main white cross.
- 1 to 3 ellipses, each one inside the previous one, to make a gradation
of colours (the center is always white).
- some pixels and little crosses around when the star is disappearing.
The graphical functions involved are :
- MoveToEx() and LineTo() to draw the crosses.
- Ellipse() to draw the ellipses (no kidding).
- CreatePen() and CreateSolidBrush() to choose the pens and brushes colors.
The stars I create are slightly different (bigger,...) from BBrk32's : the
main reason is that I understood this challenge as a programming one, since I
did not find much to reverse-engineer. I therefore did not try to copy-paste
the disassembled code of BBrk32, but instead tried to write my own. However,
you can obtain almost the same stars if you change these parameters :
DARK_COLOR : color of largest ellipse
LIGHT_COLOR : color of middle ellipse
SIDE_CROSS_NB : number of side crosses per bitmap
ELLIPSE_GAP : distance between two ellipses
SmallCrossSize : small crosses height & width
CrossSize : main cross heights & widths
EllipseSize : main ellipse heights and widths
PART TWO : THE CODE
;-------------------------------------------------------;
; "Star Truc" v 0.01 ;
; ;
; coded by Spath (09/98) for +HCU strainer 1999 ;
; contains code from Henry S. Takeuchi (Htak) ;
; compile: tasm32 -ml -m5 -q startruc ;
; link: tlink32 -Tpe -aa -x -c startruc ,,, import32 ;
;-------------------------------------------------------;
.386
.model flat,STDCALL
MAX_BMP_HEIGHT equ 400
MAX_BMP_WIDTH equ 600
MF_END = 0080h ; end of menu template
; Define Win95 structures
;
POINT STRUC
ptX dd ?
ptY dd ?
POINT ENDS
MSG STRUC
msgWnd dd ?
msgMessage dd ?
msgWparam dd ?
msgLparam dd ?
msgTime dd ?
msgPt POINT ?
MSG ENDS
PAINTSTRUCT STRUC
psDC dd ? ; hdc
psErase dd ? ; fErase
psRect dd ? ; rcPaint
; the following reserved by Windows
psRestore dd ? ; fRestore
psIncUpdate dd ? ; fIncUpdate
psRGB db 16 dup(?) ; rgbReserved
PAINTSTRUCT ENDS
WNDCLASS STRUC
wcStyle dd ? ; style
wcWndProc dd ? ; lpfnWndProc
wcClsExtra dd ? ; cbClsExtra
wcWndExtra dd ? ; cbWndExtra
wcInstance dd ? ; hInstance
wcIcon dd ? ; hIcon
wcCursor dd ? ; hCursor
wcBackgroundBrush dd ? ; hbrBackground
wcMenuName dd ? ; lpszMenuName
wcClassName dd ? ; lpszClassName
WNDCLASS ENDS
MF_SEPARATOR = 0800h
MF_STRING = 0000h
MF_POPUP = 0010h
MF_END = 0080h ; end of menu template
;
; Window messages
;
WM_CREATE = 0001h
WM_DESTROY = 0002h
WM_PAINT = 000Fh
WM_TIMER = 0113h
WM_COMMAND = 0111h
WM_LBUTTONDOWN = 0201h
;
; Window styles
;
WS_OVERLAPPED = 0
WS_CAPTION = 00C00000h
WS_THICKFRAME = 00040000h
WS_SYSMENU = 00080000h
WS_MINIMIZEBOX = 00040000h
WS_MAXIMIZEBOX = 00020000h
WS_VISIBLE = 10000000h
WS_OVERLAPPEDWINDOW = WS_OVERLAPPED or WS_CAPTION or WS_THICKFRAME or \
WS_SYSMENU or WS_MINIMIZEBOX or WS_MAXIMIZEBOX
WS_EX_RIGHTSCROLLBAR = 0 ; scrollbar on right or bottom
WS_EX_LEFT = 0 ; left alignment
WS_EX_LTRREADING = 0 ; left-to-right reading
SRCCPY = 00CC0020h
; define prototypes (since chal2 I read NetWalker code :) )
CreateSolidBrush PROCDESC WINAPI :DWORD
CreatePen PROCDESC WINAPI :DWORD, :DWORD, :DWORD
SelectObject PROCDESC WINAPI :DWORD, :DWORD
Ellipse PROCDESC WINAPI :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
MoveToEx PROCDESC WINAPI :DWORD, :DWORD, :DWORD, :DWORD
LineTo PROCDESC WINAPI :DWORD, :DWORD, :DWORD
BitBlt PROCDESC WINAPI :DWORD, :DWORD, :DWORD, :DWORD, :DWORD, \
:DWORD, :DWORD, :DWORD, :DWORD
MessageBoxA PROCDESC WINAPI :DWORD, :DWORD, :DWORD, :DWORD
FloodFill PROCDESC WINAPI :DWORD, :DWORD, :DWORD, :DWORD
Rectangle PROCDESC WINAPI :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
PostQuitMessage PROCDESC WINAPI :DWORD
DeleteObject PROCDESC WINAPI :DWORD
DeleteDC PROCDESC WINAPI :DWORD
ExitProcess PROCDESC WINAPI :DWORD
GetDC PROCDESC WINAPI :DWORD
ReleaseDC PROCDESC WINAPI :DWORD, :DWORD
GetMessageA PROCDESC WINAPI :DWORD, :DWORD, :DWORD, :DWORD
DispatchMessageA PROCDESC WINAPI :DWORD
BeginPaint PROCDESC WINAPI :DWORD, :DWORD
EndPaint PROCDESC WINAPI :DWORD, :DWORD
LoadCursorA PROCDESC WINAPI :DWORD, :DWORD
CreateCompatibleDC PROCDESC WINAPI :DWORD
CreateCompatibleBitmap PROCDESC WINAPI :DWORD, :DWORD, :DWORD
GetModuleHandleA PROCDESC WINAPI :DWORD
RegisterClassA PROCDESC WINAPI :DWORD
LoadMenuIndirectA PROCDESC WINAPI :DWORD
; this one is clearer this way
extrn CreateWindowExA:proc
; no argument for these one
extrn GetTickCount:proc
extrn DefWindowProcA:proc
.data
; local window
;
msgbuffer MSG <>
wc WNDCLASS <0,MainWndProc,0,0,0,0,0,2,0,szClassName>
wndPaintStruct PAINTSTRUCT <>
;
; Handles
;
appInst dd 0 ; hInstance of application module
appMenu dd 0 ; hMenu of application window menu
wndDC dd 0 ; hDC of window client area
bmpDC dd 0 ; hDC of bitmap "canvas"
bmpH dd 0 ; handle to bitmap
bmpPrev dd 0 ; handle to original bitmap assigned to bmpDC
bmpBrush1 dd 0 ; handle to first brush for bitmap DC
wndColorBrush dd 0 ; handle to solid brush for window color
hWhitePen dd 0 ; handles of pens
hDarkPen dd 0
hLightPen dd 0
hBlackPen dd 0
hWhiteBrush dd 0 ; handles of brushes
hDarkBrush dd 0
hLightBrush dd 0
;
; Menu templates
;
IDM_EXIT equ 101
IDM_HELP equ 901
IDM_ABOUT equ 902
appMenuTemplate dw 0 ; menu template version
dw 0 ; offset from end of header to menu item list
dw MF_STRING or MF_POPUP
dw '&','F','i','l','e',0
dw MF_STRING or MF_END,IDM_EXIT
dw 'E','&','x','i','t',0
dw MF_STRING or MF_POPUP or MF_END
dw '&','H','e','l','p',0
dw MF_STRING,IDM_HELP
dw '&','H','e','l','p','.','.','.',0
dw MF_SEPARATOR,0
dw 0
dw MF_STRING or MF_END,IDM_ABOUT
dw '&','A','b','o','u','t','.','.','.',0
;
; auxiliary window class information
;
szClassName db 'Window03',0
; miscellaneous string data
appCaption db 'Star Truc v 0.01',0
helpCaption equ appCaption
helpText db 'Just click in the window',0Dh,0Ah
db 'to make stars...',0
aboutCaption db 'About Star Truc',0
aboutText db 'Coded by Spath for +HCU strainer 1999.',0Dh,0Ah
db 'based on code by Henri S. Takeuchi.',0
;
; graphical datas
;
DARK_COLOR equ 00B94264h ; color of largest ellipse
LIGHT_COLOR equ 00FFE010h ; color of middle ellipse
SIDE_CROSS_NB equ 2 ; number of side crosses per bitmap
ELLIPSE_GAP equ 2 ; distance between two ellipses
PreviousPoint dd 0,0 ; previous X and Y value
SmallCrossSize dd 4,4 ; small crosses height & width
; main cross height & width
CrossSize dd 5,2,8,4,12,7,16,9,20,12,16,9,12,7,8,4,5,2,0,0
; ellipse height & width values
EllipseSize dd 0,0,4,2,8,5,12,8,16,10,12,8,10,5,4,2,0,0,0,0
Index dd 0 ; pointer for main cross and main ellipse sizes
EllipseIndex dd 0 ; pointer for the two small ellipses sizes
SideCounter dd 0 ; counter of little stars
PosX dd 0 ; X value of the left-button click
PosY dd 0 ; Y value of the left-button click
.code
;--------------------------------------------------------------------------
; This is where the program starts.
_start:
call GetModuleHandleA,0 ; get hmod (in eax)
mov [appInst],eax ; HINSTANCE is the same as HMODULE in Win32
;
; Complete the WNDCLASS structure.
;
mov [wc.wcInstance],eax
call LoadCursorA, 0, 32512
mov [wc.wcCursor],eax
;
; Create and display our window.
;
call RegisterClassA, offset wc ; returns ATOM, 0 = error
call LoadMenuIndirectA, offset appMenuTemplate
mov [appMenu],eax
;
; Create colors
;
call CreatePen, 0, 1, 0 ; create black tools
mov [hBlackPen], eax
xor eax, eax
call CreateSolidBrush, eax
mov [wndColorBrush],eax
call CreatePen, 0, 1, 00FFFFFFh ; create white tools
mov [hWhitePen], eax
call CreateSolidBrush, 00FFFFFFh
mov [hWhiteBrush], eax
call CreatePen, 0, 1, DARK_COLOR ; create dark tools
mov [hDarkPen], eax
call CreateSolidBrush, DARK_COLOR
mov [hDarkBrush], eax
call CreatePen, 0, 1, LIGHT_COLOR ; create light tools
mov [hLightPen], eax
call CreateSolidBrush, LIGHT_COLOR
mov [hLightBrush], eax
;
; Create window
;
push large 0 ; lpParam
push [appInst] ; hInstance
push [appMenu] ; menu hmenu
push large 0 ; parent hwnd
push large 200 ; height
push large 200 ; width
push large 100 ; y
push large 100 ; x
push large (WS_OVERLAPPEDWINDOW or WS_VISIBLE) ; Style
push offset appCaption ; Window text (caption)
push offset szClassName ; Class name
push large (WS_EX_LEFT or WS_EX_LTRREADING \
or WS_EX_RIGHTSCROLLBAR) ; extended style
call CreateWindowExA
;
; Process messages, quit when WM_QUIT received.
;
msg_loop:
call GetMessageA, offset msgbuffer, 0, 0, 0
or eax,eax
je end_loop ; WM_QUIT message
call DispatchMessageA, offset msgbuffer
jmp msg_loop
;
; Terminate program.
;
end_loop:
call ExitProcess, [msgbuffer.msgWparam]
;----------------------------------------------------------------
; The window procedure...where messages for one class of windows
; are processed.
;
MainWndProc:
mov eax,[esp+8] ; message ID
cmp eax,WM_PAINT ; from Windows
je paint_client
cmp eax,WM_COMMAND ; from menu, accelerator, or control
je execute_command
cmp eax,WM_LBUTTONDOWN ; mouse button has been pressed
je LeftMouseDown
cmp eax,WM_CREATE ; window created, about to show it
je creating_window
cmp eax,WM_DESTROY ; about to start window destruction
je start_destroy
jmp DefWindowProcA ; delegate other message processing
;
; Process WM_COMMAND.
;
execute_command:
mov eax,[esp+12] ; wParam
and eax,large 0FFFFh ; command ID
cmp eax,IDM_EXIT ; test exit command
je exit_command
cmp eax,IDM_HELP ; test help command
je help_command
cmp eax,IDM_ABOUT ; test about command
je about_command
xor eax,eax ; none of these
ret 16
exit_command: ; exit command : quit
call PostQuitMessage, 0
xor eax,eax
ret 16
help_command: ; help command : display help box
mov eax,[esp+4]
call MessageBoxA, eax, offset helpText, offset helpCaption, 0
xor eax,eax
ret 16
about_command: ; about command : display about box
mov eax,[esp+4]
call MessageBoxA, eax, offset aboutText, offset aboutCaption, 0
xor eax,eax
ret 16
;
; Process WM_PAINT. Some part of the client area needs to be (re)painted.
;
paint_client:
mov eax,[esp+4] ; hwnd
call BeginPaint, eax, offset wndPaintStruct
call BitBlt, eax, 0, 0, MAX_BMP_WIDTH, MAX_BMP_HEIGHT, \
[bmpDC], 0, 0, SRCCPY
mov eax,[esp+4] ; hwnd
call EndPaint, eax, offset wndPaintStruct
xor eax,eax
ret 16
;- - - - - - - - - - - - - - - - - - - - - - - - - - -
; Process WM_LBUTTONDOWN. Left mouse button has been pressed.
; (here start the code which create stars)
;
LeftMouseDown:
mov esi, offset CrossSize ; start of main cross sizes array
mov edi, offset EllipseSize ; start of big ellipse size array
add esi, Index ; calculate new cross size
add edi, Index ; calculate new ellipse size
call BasicStep ; display the cross + 3 ellipses
call SmallDelay ; wait a little bit
call ClearScreen ; clear the screen
mov esi, offset CrossSize
add esi, Index
cmp esi, (offset EllipseSize)-8 ; is it finished ?
jge EndLeftMouseDown ; yes : exit
add Index, 8 ; no : prepare next step
jmp LeftMouseDown
EndLeftMouseDown:
call DisplayBitmap ; clear screen
mov Index, 0
xor eax,eax
ret 16
; ------------ Graphical Procedures and functions ---------------------
; BasicStep procedure : each step of the animation is computed here
;
BasicStep proc near
call GetDC, dword ptr [esp+8]
mov [wndDC],eax ; hDC for window
ComputeEllipses:
call SelectObject, [bmpDC], [hDarkBrush]
call SelectObject, [bmpDC], [hDarkPen]
mov EllipseIndex, 0
mov dx,[esp+22] ; HIWORD(lParam) = y
mov eax,[esp+20] ; LOWORD(lParam) = x
and edx,large 0FFFFh ; dx = y pos
and eax,large 0FFFFh ; ax = x pos
mov PosX, eax ; save origin
mov PosY, edx
call DrawEllipse ; draw dark ellipse
cmp dword ptr [edi],4 ; can we draw more ellipses
jle ComputeCross ; no
call SelectObject, [bmpDC], [hLightBrush]
call SelectObject, [bmpDC], [hDarkPen]
mov EllipseIndex, ELLIPSE_GAP
call DrawEllipse ; draw light ellipse
call SelectObject, [bmpDC], [hWhiteBrush]
call SelectObject, [bmpDC], [hWhitePen]
mov EllipseIndex, 2*ELLIPSE_GAP
call DrawEllipse ; draw white ellipse
ComputeCross:
call SelectObject, [bmpDC], [hWhitePen]
call DrawCross ; draw the cross
ComputeSideShow:
cmp Index, 48 ; should we add sideshow
jl DisplayAll ; not so soon
call SideShow ; add sideshow
DisplayAll:
call DisplayBitmap ; display the bitmap
call ReleaseDC, dword ptr [esp+8], [wndDC]
ret
BasicStep endp
; DrawCross procedure : draw a cross centered on (ax,dx) and of
; size 2*(esi+4, esi)
;
DrawCross proc near
pusha
mov eax, PosX ; get origin
mov edx, PosY
mov dword ptr [PreviousPoint], eax
mov dword ptr [PreviousPoint+4], edx
sub eax,dword ptr [esi+4]
Call MoveToEx, [bmpDC], eax, edx, 0 ; set cursor
mov eax, dword ptr[PreviousPoint]
add eax, dword ptr [esi+4]
Call LineTo, [bmpDC], eax, [PreviousPoint+4] ; draw horiz. line
mov edx, dword ptr [PreviousPoint+4]
sub edx, dword ptr [esi]
Call MoveToEx, [bmpDC], [PreviousPoint], edx, 0 ; set cursor
mov edx, dword ptr[PreviousPoint+4]
add edx, dword ptr [esi]
Call LineTo, [bmpDC], [PreviousPoint], edx ; draw vert. line
popa
ret
DrawCross endp
; DrawEllipse procedure : draw an ellipse centered on (ax,dx)
; and of size 2*(edi+4,edi)
;
DrawEllipse proc near
pusha
mov eax, PosX ; get origin
mov edx, PosY
mov ebx, eax ; save x pos
mov ecx, edx ; save y pos
add edx, dword ptr [edi]
add eax, dword ptr [edi+4]
sub eax, EllipseIndex ; down right X value
sub edx, EllipseIndex ; down right Y value
sub ecx, dword ptr [edi]
add ecx, EllipseIndex ; top left Y value
sub ebx, dword ptr [edi+4]
add ebx, EllipseIndex ; top left X value
call Ellipse, [bmpDC], ebx, ecx, eax, edx
popa
ret
DrawEllipse endp
; SideShow procedure : when the main star is disappearing, little
; stars must appear.
;
SideShow proc near
pusha
mov eax, PosX ; get origin
mov edx, PosY
sub eax, dword ptr [CrossSize+36] ; set X origin of side show
sub edx, dword ptr [CrossSize+32] ; set Y origin of side show
mov PosY, edx ; save Y origin
mov PosX, eax ; save X origin
mov dword ptr [SideCounter], SIDE_CROSS_NB
SideLoop:
xor eax,eax ; choose color
in al, 40h
cmp al, 30h
jl WhiteOne
call SelectObject, [bmpDC], [hLightPen] ; light cross
jmp GoOn
WhiteOne:
call SelectObject, [bmpDC], [hWhitePen] ; white cross
GoOn:
dec dword ptr [SideCounter]
mov ebx, PosX ; get X origin
mov edx, PosY ; get Y origin
xor eax,eax
in al,40h
and al,17h
add edx,eax ; add random value to Y
in al,40h
and al,0Fh
add eax,ebx ; add random value to X
mov PosX, eax
mov PosY, edx
mov esi, offset SmallCrossSize
call DrawCross
cmp [SideCounter], 0
jne SideLoop
popa
ret
SideShow endp
; DisplayBitmap procedure : display bmpDC in wndDC
;
DisplayBitmap proc near
call BitBlt, [wndDC], 0, 0, MAX_BMP_WIDTH, MAX_BMP_HEIGHT, \
[bmpDC], 0, 0, SRCCPY
ret
DisplayBitmap endp
; ClearScreen procedure : just paint a big black rectangle
;
ClearScreen proc near
pusha
call SelectObject, [bmpDC], [wndColorBrush] ; black pen
call SelectObject, [bmpDC], [hBlackPen] ; black brush
call Rectangle, [bmpDC], 0, 0, MAX_BMP_WIDTH-1, \
MAX_BMP_HEIGHT+25-1
popa
ret
ClearScreen endp
; SmallDelay procedure : just to wait a bit between 2 bitmaps
;
SmallDelay proc near
pusha
call GetTickCount ; get initial value
mov ebx, eax
add ebx, 0020h ; calculate end value
WaitLoop:
call GetTickCount ; wait until we reach end value
cmp eax, ebx
jle WaitLoop
popa
ret
SmallDelay endp
;-----------------------------------------------------------
; Process WM_CREATE.
;
creating_window:
call GetDC, dword ptr [esp+4+0]
mov [wndDC],eax ; hDC for window
call CreateCompatibleDC, eax ; create "canvas" DC
mov [bmpDC],eax
call CreateCompatibleBitmap, [wndDC], MAX_BMP_WIDTH, \
MAX_BMP_HEIGHT+25 ; create canvas
mov [bmpH],eax
call SelectObject, [bmpDC], eax ; bind canvas to DC
mov [bmpPrev],eax ; save original bmp (canvas)
call Rectangle, [bmpDC], 0, 0, MAX_BMP_WIDTH-1, \
MAX_BMP_HEIGHT+25-1
call CreateSolidBrush, 0
mov [wndColorBrush],eax
call SelectObject, [bmpDC], eax ; replace brush
mov [bmpBrush1],eax ; save original brush
call FloodFill, [bmpDC], 0, 0, 0 ; clear background
call FloodFill, [bmpDC], 1, 1, 0
call SelectObject, [bmpDC], [bmpBrush1] ; restore brush
call ReleaseDC, dword ptr [esp+4], [wndDC] ; release DC
xor eax,eax
ret 16
; Process WM_DESTROY.
;
start_destroy:
call SelectObject, [bmpDC], [bmpBrush1] ; restore original brush
call DeleteObject, [wndColorBrush] ; delete our brushes
call DeleteObject, [hWhiteBrush]
call DeleteObject, [hLightBrush]
call DeleteObject, [hDarkBrush]
call DeleteObject, [hWhitePen] ; delete our pens
call DeleteObject, [hBlackPen]
call DeleteObject, [hLightPen]
call DeleteObject, [hDarkPen]
call SelectObject, [bmpDC], [bmpPrev] ; restore original bitmap
call DeleteObject, eax ; delete our canvas
call DeleteDC, [bmpDC] ; delete canvas DC
call PostQuitMessage, 0
xor eax,eax
ret 16
end _start
----------------------------------------------------------------------------
Greetings : - Henry S. Takeuchi (Htak) and NetWalker, for good usage of TASM
- Earl Gray, who helped me to work so late.