ULTRAEDIT 5.00 S/N Generator
(a very funny dynamic addressing process as copy procedure)

by Aesculapius

(24 November 1997, slightly edited by reverser+)


Courtesy of reverser's page of reverse engineering
Well, Aesculapius is back, and he is pretty crossed... somebody has annoyed him. He opens with a "war declaration" against harassing protectionists... Tsch, tsch...
I like Ultraedit a lot, it's a very good program, and I'm seriously thinking to buy it (when my ALLOWED 45 days trial will be finished, of course)... the only thing that really annoys me about Uedit is the completely nut "thank-god page" of its Author Mead... man, I have never seen something so crazy, you won't believe it until you read it :-)
Anyway I like this program a lot, and therefore I have checked the situation before releasing this essay (that explains the delay, Aescu) and found out that the target is already irremediably compromised: there are at least 6 different ready made cracks for version 5.0 (this target here explained by Aesculapius to-day) all over the web and, as if those were not enough, there are hundreds of 'regged' userid/registration combinations, the most common one being the ubiquitous userID: "lxcore97" (no, I won't give you the 16-chars registration code as well... how many time d'I have to repeat it: LEARN HOW TO REVERSE YOUR TARGETS, don't use ready made code like thiefs and lusers... the whole fun is to reverse our targets, to 'earn' them somehow even if we won't use them... if you are here in order to find some software for free you have chosen a damn complicated way to get at it... you better jump over to the 'warez-world', farewell).
So I checked, and this good program is everywhere to have, regged, for free. I think therefore that Aesculapius work, far from damaging him, can actually be USEFUL to Ian Mead... here is the point he should take care of:
however, the program still has to read the registration file to 
gather its initiation values, so a bpx on readfile should be enough to find 
a fairly close entry point to it
Learn here from Aesculapius how this protection scheme works. It's interesting and the keygenerator in asm at the bottom can easily be modified for other targets. If you crack Ultraedit register it after 45 days, it deserves it (IMHO). Enjoy!

ULTRAEDIT 5.00 S/N Generator
"What has God brought"
By Aesculapius
Well, I've been retired for a while due to heavy censoring, however I think the best way to solve this problem is facing it, striking back. All the programs which protection scheme I have only cracked, will be extensively explained so even a less than 50 IQ lamer will be able to defeat them; if this harassment continues, I'll then apply some virii techniques to the cracks which will ruin any protection scheme making it totally useless, the apps will be cracked without the user even noticing it, and eventually a gorgeous computer virus will be released to strike my enemies only; after they and their customers are infected with the virus, not even Aesculapius himself, the mighty Greek God of Healing will be able to cure them and they will die under the shadow of a new breed of biological computer warfare. To successfully complete this essay, you will need: wdsm89, SoftICE 3.X, any hex editor. The program is located at www.idmcomp.com, named uedit32.exe and is less than 1MB long. This scheme is not as strong as I thought it would be, given the fact that UltraEdit's author changes the algorithm with every new version. UltraEdit has become a standard for us to write our cracking essays, so it should be worthy to unveil the scheme behind it. UltraEdit author learned a lot since +Rezident and I cracked his baby. In this new version, Windows API's use is kept at minimum to avoid possible intrusion via SoftICE breakpointing on them. Even the string lengths are calculated trough a rather old fashion custom mathematical procedure to avoid the use of any API; however, the program still has to read the registration file to gather its initiation values, so a bpx on readfile should be enough to find a fairly close entry point to it. Once we're in, you'll eventually find this: :0045BE35 56 push esi :0045BE36 8BF1 mov esi, ecx :0045BE38 E8C8FAFFFF call 0045B905 ; Copy your name to memory location :0094229C :0045BE3D 8B06 mov eax, dword ptr [esi] ; Your name location offset in EAX :0045BE3F 8B542408 mov edx, dword ptr [esp+08] ; 02H in EDX :0045BE43 8A4C240C mov cl, byte ptr [esp+0C] ; 09H in CL :0045BE47 5E pop esi :0045BE48 880C10 mov byte ptr [eax+edx], cl ; Move byte 09H to substitute the third letter of your name :0045BE4B C20800 ret 0008 ; return This piece of code prepares the address offset, where EAX points to your name, EDX is 02H so EAX+EDX points to the third character in your name, finally CL which is equal to 09H is copied over the third character of your name. In my case, Aesculapius, the first 's' letter is substituted with 09H. :00426813 E81D560300 call 0045BE35 ; Return from the previous code :00426818 A1E0014A00 mov eax, dword ptr [004A01E0]; Memory offset of your name :0042681D 8D4D0C lea ecx, dword ptr [ebp+0C] :00426820 8A4005 mov al, byte ptr [eax+05] Load fifth character of your name in AL :00426823 0C55 or al, 55 Inclusive OR of that character :00426825 50 push eax :00426826 6A05 push 00000005 :00426828 E808560300 call 0045BE35 ; We'll continue here As you can see, there are two copies of your name at :004A01E0 and :0094229C. The last location contains a copy from first one. The copy procedure is a very funny dynamic addressing process located somewhat before. As you can see, the original offset memory location of your name at :004A01E0 is stored in EAX, then at :00426820 the fifth character of your name is loaded in AL and then, the code executes an 'Inclusive or' with it. Now lets trace inside call at :004268B8; :0045BE35 56 push esi :0045BE36 8BF1 mov esi, ecx :0045BE38 E8C8FAFFFF call 0045B905 ; Checks some characters ; in your name :0045BE3D 8B06 mov eax, dword ptr [esi] ; Points to your name :0045BE3F 8B542408 mov edx, dword ptr [esp+08] ; 05H in EDX :0045BE43 8A4C240C mov cl, byte ptr [esp+0C] ; Inclusive OR result (see previous snippet) on CL :0045BE47 5E pop esi :0045BE48 880C10 mov byte ptr [eax+edx], cl ; Substitute fifth character in your name with the Inclusive OR result :0045BE4B C20800 ret 0008 The Inclusive OR calculation of the fifth character in your name is stored, then it substitutes the real fifth character in your name. Until now this is the result of the previous code: The third character in your name has been substituted with byte 09H and the fifth character after an Inclusive OR with byte 55H is copied back to its place. All these mathematical games could be there to ease the task of changing the algorithm very often, as in fact happens with this application. :0040B645 6A3C push 0000003C; push 60 bytes in stack :0040B647 8D45C0 lea eax, dword ptr [ebp-40] ; Points to the memory location where the valid code will be stored * Possible Reference to String Resource ID=00046: "Line: " | :0040B64A 6A2E push 0000002E ; Store 2E into the stack :0040B64C 50 push eax ; Push valid code offset into ; the stack :0040B64D E85E8A0300 call 004440B0 ; Lets trace inside this CALL The memory location where the valid code will be calculated is stored in EAX and then pushed to the stack. Byte 2EH '.' is also pushed. Lets now trace inside call at :0040B64D; :004440B0 8B54240C mov edx, dword ptr [esp+0C]; Load hex value 3CH equal to 60 dec. from the stack :004440B4 8B4C2404 mov ecx, dword ptr [esp+04]; Load offset of valid code memory location in ECX :004440B8 85D2 test edx, edx :004440BA 7447 je 00444103 :004440BC 33C0 xor eax, eax :004440BE 8A442408 mov al, byte ptr [esp+08]; load Hex value 2C equal to '.' decimal :004440C2 57 push edi :004440C3 8BF9 mov edi, ecx ; Load offset of valid code memory location to EDI :004440C5 83FA04 cmp edx, 4 :004440C8 722D jb 004440F7 :004440CA F7D9 neg ecx :004440CC 83E103 and ecx, 3 :004440CF 7408 je 004440D9 :004440D1 2BD1 sub edx, ecx * Referenced by a Jump at Address:004440D7(C) | :004440D3 8807 mov byte ptr [edi], al; Move 2EH to first byte of valid code memory location :004440D5 47 inc edi next byte :004440D6 49 dec ecx decrease counter from 3CH==60 bytes :004440D7 75FA jne 004440D3 Repeat cycle until ECX==0 Well, what happens now: the memory location where the final valid code will be calculated is filled with 60 bytes containing Hexadecimal value 2EH which is a dot '.' in decimal. Why? How the hell would I know? I'm a cracker not a magician! However I think this could be some sort of future expansion configuration. We later see that the valid code is only 16 characters long (10H) but the code memory location is prepared with 60 characters (3CH) instead. Probably the programmer thought to leave some additional space, so it is possible that in the future, Ultraedit valid code will be 18 or even 20 characters long, who knows. :0040B655 8D45C0 lea eax, dword ptr [ebp-40]; Loads the valid code memory offset :0040B658 56 push esi :0040B659 57 push edi :0040B65A 50 push eax :0040B65B E860860300 call 00443CC0 Moves modified name to the valid code memory location :0040B660 83C40C add esp, 0000000C :0040B663 32C0 xor al, al ; Erase AL :0040B665 33C9 xor ecx, ecx; Erase ECX :0040B667 3BF3 cmp esi, ebx; Load your name lenght in ESI :0040B669 885C35C0 mov byte ptr [ebp+esi-40], bl; Terminates your name with 00H :0040B66D 7E09 jle 0040B678 * Referenced by a Jump at Address:0040B676(C) | :0040B66F 02440DC0 add al, byte ptr [ebp+ecx-40]; Add Hex value of first byte of your name to AL :0040B673 41 inc ecx Increase counter :0040B674 3BCE cmp ecx, esi; All characters in your name added? No repeat cycle, otherwise go on :0040B676 7CF7 jl 0040B66F This is very simple. All Hex values of every character in your modified name (the name with the 09H in the third position and the Inclusive OR of the fifth character in fifth position) are added and the result is stored in AL. This is a checksum procedure. * Referenced by a Jump at Address:0040B66D(C) | :0040B678 884508 mov byte ptr [ebp+08], al; Addition Result stored in EBP+08H :0040B67B 33F6 xor esi, esi ; Erase ESI :0040B67D F65508 not [ebp+08] ; Executes complement negation of your name's checksum * Referenced by a Jump at Address:0040B6D6(C) | :0040B680 8BC6 mov eax, esi * Possible Reference to String Resource ID=00004: "*.MAC" This segment calculates the offset of the magic numbers block location using a funny dynamic addressing system: | :0040B682 6A04 push 00000004 :0040B684 99 cdq :0040B685 5F pop edi :0040B686 8D4C35C0 lea ecx, dword ptr [ebp+esi-40]; ; Offset of valid code memory location in ECX :0040B68A F7FF idiv edi :0040B68C 8BC6 mov eax, esi * Possible Reference to String Resource ID=00016: "All Files, (*.*)" | :0040B68E 6A10 push 00000010 :0040B690 5B pop ebx :0040B691 8BFA mov edi, edx :0040B693 99 cdq :0040B694 F7FB idiv ebx :0040B696 8BC2 mov eax, edx :0040B698 99 cdq :0040B699 2BC2 sub eax, edx :0040B69B 8B14BDE08B4900 mov edx, dword ptr [4*edi+00498BE0] ; First byte of magic block in EDX :0040B6A2 D1F8 sar eax, 1 :0040B6A4 8A0402 mov al, byte ptr [edx+eax]; Load first byte of magic block in AL :0040B6A7 324508 xor al, byte ptr [ebp+08]; XOR magic byte with complement NOT of your modified name result in AL :0040B6AA FEC0 inc al ; Increase result in one :0040B6AC 3001 xor byte ptr [ecx], al; XOR result with first byte of your modified name, result is stored directly in valid code memory location :0040B6AE 8A09 mov cl, byte ptr [ecx]; Load first byte of valid code in CL :0040B6B0 0FB6C1 movzx eax, cl; Load it in EAX :0040B6B3 83FE08 cmp esi, 00000008; If we reach eight character of the valid code then jump at :0040B6B7 :0040B6B6 99 cdq :0040B6B7 7D0A jge 0040B6C3 * Possible Reference to String Resource ID=00026: "Run Windows Program" | :0040B6B9 6A1A push 0000001A ; Push value 1AH into the stack :0040B6BB 59 pop ecx ; Pop it in ECX :0040B6BC F7F9 idiv ecx ; Signed divide it with EAX and remainder in DL :0040B6BE 80C241 add dl, 41 ; Add 41H to remainder :0040B6C1 EB08 jmp 0040B6CB ; Jump ahead * Referenced by a Jump at Address:0040B6B7(C) | * Possible Reference to String Resource ID=00010: "Thank you for supporting Shareware." | :0040B6C3 6A0A push 0000000A; If ESI was 8 or greater then code continues here from :0040B6B7; Push 0A into the stack :0040B6C5 59 pop ecx ; Pop it in ECX :0040B6C6 F7F9 idiv ecx ; Signed divide ECX with EAX; remainder in DL :0040B6C8 80C230 add dl, 30 ; Add 30H to remainder * Referenced by a Jump at Address:0040B6C1(U) | This piece of code follows two possible paths; from character 1 to 8 of the valid code, all result characters are letters and from 9 to 16, all characters are numbers. There are two XORing processes, the first between a magic number and the Complement NOT of the checksum from your modified name and the second one between the result from the first and one byte of your modified name. Finally the two paths will assure that the first eight characters of the valid code are letters and the last ones numbers. On very important fact is that the result from all the calculations is directly stored in the valid code memory location. Unfortunately, as you will notice, the first eight letters of the code are unprintable ASCII characters, so the author created a second valid code, which is stored in the next lines: :0040B6CB 88943540FFFFFF mov byte ptr [ebp+esi-000000C0], dl ; Store second valid code in ebp+esi-C0 :0040B6D2 46 inc esi; Increase counter :0040B6D3 83FE3C cmp esi, 0000003C ; Process 60 bytes :0040B6D6 7CA8 jl 0040B680 ; repeat cycle Now the following code fixes the final validation code: :0040B6D8 33DB xor ebx, ebx :0040B6DA 33D2 xor edx, edx * Referenced by a Jump at Address:0040B732(C) | :0040B6DC 8A4C15C0 mov cl, byte ptr [ebp+edx-40] ; Load first byte of first valid code in CL :0040B6E0 8D4415C0 lea eax, dword ptr [ebp+edx-40] ; Load offset of first valid code :0040B6E4 80F980 cmp cl, 80; Compare first byte with 80H :0040B6E7 7205 jb 0040B6EE; Jump if below :0040B6E9 80C180 add cl, 80 ; otherwise add 80H :0040B6EC 8808 mov byte ptr [eax], cl; Store it back * Referenced by a Jump at Address:0040B6E7(C) | :0040B6EE 8A08 mov cl, byte ptr [eax]; Move first corrected byte to CL :0040B6F0 80F97F cmp cl, 7F; Compare it with 7FH :0040B6F3 7C05 jl 0040B6FA; Jump if less :0040B6F5 80E97F sub cl, 7F ; otherwise subtract 7FH :0040B6F8 8808 mov byte ptr [eax], cl; Store it back * Referenced by a Jump at Address:0040B6F3(C) | :0040B6FA 8A08 mov cl, byte ptr [eax]; Move corrected first byte to CL :0040B6FC 80F921 cmp cl, 21; Compare it with 21H :0040B6FF 7F05 jg 0040B706; if greater jump :0040B701 80C121 add cl, 21 ; Add 21H otherwise :0040B704 8808 mov byte ptr [eax], cl; Store it back * Referenced by a Jump at Address:0040B6FF(C) | :0040B706 803822 cmp byte ptr [eax], 22; Compare first Corrected byte with 22H :0040B709 7503 jne 0040B70E ; if not equal jump :0040B70B C60023 mov byte ptr [eax], 23; otherwise substitute it with 23H * Referenced by a Jump at Address:0040B709(C) | :0040B70E 803827 cmp byte ptr [eax], 27; Compare first Corrected byte with 27H :0040B711 7503 jne 0040B716 :0040B713 C60028 mov byte ptr [eax], 28; substitute it with 28H * Referenced by a Jump at Address:0040B711(C) | :0040B716 80382E cmp byte ptr [eax], 2E ; Compare first corrected byte with 2EH :0040B719 7503 jne 0040B71E :0040B71B C6002F mov byte ptr [eax], 2F; If equal substitute it with 2FH * Referenced by a Jump at Address:0040B719(C) | :0040B71E 803860 cmp byte ptr [eax], 60; Compare it with 60H :0040B721 7503 jne 0040B726 :0040B723 C60061 mov byte ptr [eax], 61; If equal substitute it with 61H * Referenced by a Jump at Address:0040B721(C) | :0040B726 80387C cmp byte ptr [eax], 7C; Compare it with 7CH :0040B729 7503 jne 0040B72E :0040B72B C6006C mov byte ptr [eax], 6C; If equal substitute it with 6C * Referenced by a Jump at Address:0040B729(C) | :0040B72E 42 inc edx; Increase counter-pointer :0040B72F 83FA3C cmp edx, 0000003C ; repeat cycle for all bytes until 3CH or 60 bytes are reached :0040B732 7CA8 jl 0040B6DC ; Repeat cycle As the first 8 characters of the first valid code are filled with loads of unreadable characters, the following lines substitute bytes: 22H, 27H, 2EH, 60H AND 7CH with 23H, 28H, 2FH, 61H and 6CH. It also carries out some additional corrections to every single byte in the code. Well, as Samuel Morse said: What has God brought! There are two valid codes; the first one is partially calculated and stored in [EBP-40]; then a second valid code derivates from the first and is stored in [EBP-C0]; the code you typed is stored in [ebp-80]; finally, the first valid code is corrected to substitute the unreadable ASCII characters. As the second code is not corrected and perfectly readable in the first place, we will use it in the S/N Generator along with the partially calculated first code. The final first valid code wont be calculated in this essay, bacause although is easy to do it, since we clearly understood all the process, it is not neccessary to do it. Remember, always do what you got to do, nothing more, nothing less (who said this?). The S/N generator is finally coded: ;*************************************************************** ;* UltraEdit v. 5.00 S/N Generator * ;* Coded by Aesculapius * ;* Instructions: Compile this file as an .COM executable * ;* with TASM 4.1 and Tlink 7.1 * ;* File name should be: UEDT5KEY.COM * ;* * ;* * ;*************************************************************** .MODEL SMALL .CODE ORG 100H START: JMP BEGIN CODE1 DB 60 DUP (2EH); Prepare first code location with 2EH byte ; in all locations CODE2 DB 60 DUP (2EH); Prepare second code location with 2EH MAGIC1 DB 1 DUP (' '); Prepare location to store Complement NOT ; of your name's checksum ; These block of magic numbers were ripped with softICE MAGIC2 DB 6EH,35H,0F7H,74H,0DEH,6FH,32H,85H,0FFH,36H,0A8H,59H,0DEH,0DEH,79H,88H DB 6EH,35H,0F7H,74H,0DEH,6FH,32H,85H,0FFH,36H,0A8H,59H,0DEH,0DEH,79H,88H FINAL_CODE1 DB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,'$' ; Final partially ; calculated first ; code storage LENGHT DB 00H ; Name lenght storage CR EQU 0AH ; Carriage return definition LF EQU 0DH ; Line Feed definition BEEP EQU 07H ; Internal speaker definition ; Presentation and Ask for name message GETNAME DB CR,LF,'UltraEdit v. 5.00 S/N Generator' DB CR,LF,'Coded by Aesculapius - November 1997' DB CR,LF,'Home Page: aesculapius.home.ml.org' DB CR,LF,'Email Box: aesculapius@cryogen.com',CR,LF DB CR,LF,'Please, Type Your First and Last Name (6-21 Char): ' DB '$' ; Second final validation code will be presented with final message FINALMESS DB CR,LF DB CR,LF,'Authorization Code successfully calculated!',BEEP DB CR,LF,CR,LF,'Authorization Code: ' FINAL_CODE2 DB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,CR,LF,CR,LF,'$' ; User name storage location - no more than 21 characters long name allowed NAMESTOR DB 15H,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 GET_NAME PROC NEAR MOV AH, 09H MOV DX, OFFSET GETNAME INT 21H ; Ask for user's name MOV AH, 0AH MOV DX, OFFSET NAMESTOR INT 21H ; Store it in NAMESTOR location MOV SI, OFFSET NAMESTOR+02H; Load name lenght in SI MOV DI, OFFSET CODE1 ; First code offset in DI XOR CX, CX MOV CL, [SI-1] ; Name lenght in CL MOV BX, OFFSET LENGHT MOV BYTE PTR [BX], CL ; Store name lenght in LENGHT ; location REP MOVSB ; Move name from NAMESTOR to CODE1 XOR CX, CX ; Terminate it with 00H MOV [DI], CL ; Store 00H RET GET_NAME ENDP CODE_GEN PROC NEAR ; Calculate valid code XOR AX, AX ; Erase AX, BX, CX XOR BX, BX XOR CX, CX MOV SI, OFFSET CODE1 ; Offset of first valid code in SI MOV BYTE PTR [SI+02H], 09H ; Substitute first character ; of user's name with 09H OR BYTE PTR [SI+05H], 55H ; Inclusive OR of fifth character ; with 55H MOV DI, OFFSET LENGHT ; Load name lenght in DI MOV CL, BYTE PTR [DI] ; Load lenght in CL AGAIN1: ADD AL, BYTE PTR [SI] ; Add all characters of modified ; name to AL INC SI ; Increase pointer INC BX ; Increase counter CMP CX, BX ; Compare counter with name lenght ; if checksum is not finished ; then repeat cycle JNZ AGAIN1 ; otherwise go ahead NOT AL ; Complement NOT of MOV DI, OFFSET MAGIC1 ; Offset of magic1 storage location ; in DI MOV BYTE PTR [DI], AL ; Store Complement NOT of modified ; name checksum in MAGIC1 XOR CX, CX ; Erase CX and BX XOR BX, BX AGAIN2: MOV DI, OFFSET CODE1 ; Offset of first valid code in DI MOV SI, OFFSET MAGIC2 ;Offset of magic numbers block in SI MOV AL, BYTE PTR [SI+BX] ; Move first byte of magic block ; to AL MOV SI, OFFSET MAGIC1 ; Complement NOT checksum result in ; SI MOV AH, BYTE PTR [SI] ; First byte of magic block in AH XOR AL, AH ; Xor AH with AL; result in AL INC AL ; Increase AL in one XOR AL, BYTE PTR [DI+BX] ; XOR result with first byte of ; first valid code; result in AL MOV DI, OFFSET CODE1 ; Store result in first valid code ; memory location MOV BYTE PTR [DI+BX], AL ; Store it XOR AX, AX ; Erase AX and DX XOR DX, DX MOV AL, BYTE PTR [DI+BX] ; Load first byte of already ; partially calculated first ; valid code CMP BX, 08H ; Eight digits already? JAE NUMBER ; No then continue, otherwise JUMP MOV CL, 1AH ; Load 1AH in CL IDIV CX ; Integer divide with AL ADD DL, 41H ; Remainder in DL+41H JMP LETTER ; JUMP NUMBER: MOV CL, 0AH ; If eight digit reached execute ; this code. Load 0AH in CL IDIV CX ; Integer divide CL with AL ADD DL, 30H ; Remainder in DL+30H LETTER: MOV DI, OFFSET CODE2 ; Load offset of second valid ; code MOV BYTE PTR [DI+BX], DL ; Store remainder in it INC BX ; Increase counter CMP BX, 003CH ; 60 bytes reached? JNZ AGAIN2 ; No? then repeat cycle for ; the rest of both codes XOR CX, CX ; Erase CX MOV CX, 0010H ; Load 16 bytes in CL MOV SI, OFFSET CODE1 ; Points to CODE1 offset MOV DI, OFFSET FINAL_CODE1 ; Points to partially calculated ; first valid code REP MOVSB ; Store it in FINAL_CODE1 MOV CX, 0010H ; Do the same with second valid code MOV SI, OFFSET CODE2 MOV DI, OFFSET FINAL_CODE2 ; Store final second valid code REP MOVSB RET CODE_GEN ENDP BEGIN PROC NEAR CALL GET_NAME CALL CODE_GEN MOV AH, 09H MOV DX, OFFSET FINALMESS INT 21H ; Present final message and second ; valid code EXIT: MOV AX, 4C00H INT 21H ; Terminate program BEGIN ENDP END START
	In the S/N generator, 60 bytes of code are also calculated, even 
when only 16 bytes are used; off course, I'm thinking in the future too, 
he, he..
	
	Well, that's it; as always if you have any doubt, write me, I'm 
willing to help you understand...


					Aesculapius - November 97
					Email Box: aesculapius@cryogen.com
					Home Page: aesculapius.home.ml.org
(c) Aesculapius All rights reversed
You are deep inside reverser's page of reverse engineering, choose your way out:

redhomepage redlinks redanonymity +ORC redstudents' essays redacademy database
redtools redcocktails redantismut CGI-scripts redsearch_forms redmail_fravia
redIs reverse engineering legal?