FRAVIA'S HOW TO PROTECT BETTER
(SOFTWARE REVERSING LAB)
A good protection is NOT an impossible dream!
How to (try to) protect software effectively
(Last update: October 1999)
an impossible dream?
I would like to start, with this section of my site, a good "special" reversing
laboratory for software protection. As my readers know, we have already tried this
some time ago with the Our Protections section. Yet that
section didn't seem to florish, so I'm now re-organizing various 'threads' into a
coeherent "software protection" project. I hope that this will
help reversers and protectors alike... no cracker should underestimate the
difficulty (and the beauty) of writing a good protection, exactly as no protector
should underestimate the difficulty (and the beauty) of reversing a difficult
snippet of software. Cracking and protecting are two very important sciences in
their own right, but I have noticed that the development of good protections suffers
a 'psychological' penalty. "There's no point in protecting or overprotecting: a determined
cracker can crack any protection!"; "nothing can guarantee absolute security";
"Any protection scheme can be defeated"; "protection measures can be easily worked around",
and so on and so on... stalk the protectors on usenet and you'll see this kind
of typical whining.
Well, any protection scheme can be cracked... may be. Yet let's devise schemes that
will make things a lot HARDER for the typical cracker to mess with...
and who better than some good +crackers can teach protectors how to
It's almost winter, snow is in the air! C'mon: let's
push another snowball down
an impossible dream?
All the advices you may need
Mark's famous 14 protector's commandments
- 1 Never use meaningful file or procedure names such as
IsValidSerialNum (duh.) If you do use functions for
checking purposes, place at least some required code
that your program really needs, in such a function.
When the cracker disables the function, the program
will produce incorrect results.
- 2 Don't warn the user right after a violation is made.
Wait later, maybe until the next day or two (crackers hate that).
- 3 Use checksums in DLL's and in the EXE. Have them check each other.
Not perfect but it just makes it harder to crack.
- 4 Pause a second or two after a password entry to make brute
force cracking unfeasible. Simple to do, but rarely done.
- 5 Self-heal your software. You know, error correction like modems
and hard drives use. The technology has been around for years,
and no one uses it on their software? The best thing about this
is that if the cracker used a decompiler, they may be looking at
a listing that is no longer valid.
- 6 Patch your own software. Change your code to call different
validation routines each time. Beat us at our own game.
- 7 Store serial numbers in unlikely places, like as a property
of a database field.
- 8 Store serial numbers in several places
- 9 Don't rely on the system date. Get the date of several files,
like SYSTEM.DAT, SYSTEM,DA0 and BOOTLOG.TXT and compare them to
the system date. Require that the time be greater than the last
- A Don't use literal strings that tell the user that their time is
expired. These are the first things to look for. Build strings
dynamically or use encryption.
- B Flood the cracker with bogus calls and hard-coded strings. Decoys
- C Don't use a validation function. Every time you validate the user,
write your validation code inline with the current process. That
just makes more cracking for the cracker.
- D When using hard-coded keys or passwords, make them look like program
code or function calls (i.e., "73AF" or "GetWindowText"). This
actually works very well and confuses some decompilers.
- E Finally, never reveal your best protection secrets :-)
Tidbit's 'common sense' rules
- 1. No nagscreens. Making enemies among your customers is very stupid, indeed.
- 2. Create important menus and dialog boxes dynamically, whether in something
like turbo-vision, xforms or m$windoze. Use your own format of rcdata like borland
did in delphi if you don't want to write too much code.
- 3. If your program doesn't save data in "crapware" edition, don't include a "grayed"
menu item. No saving means no saving. That's it.
- 4. The only way to tell user that he is unregistered should be in the "about" dialog.
The latter should also be created dynamically.
- 5. Avoid using any kind of string resources like "this crap is unregistered, blah blah"
- 6. Link statically unless your program is a complex one (at least of m$word 2.0
- 7. Don't loose time on writing anything that will kill disassemblers or debuggers.
Take my word on it - doing it is worthless, people who made them or HCU'ers :-)
will soon find the way around, so shift your interest to more important stuff.
- 8. Use your language of choice fully. Even if it's visual basic, learn it thoroughly first.
Using some advanced constructs it is possible to make debugging a nightmare.
- 9. Use run time library fully when writing the beta versions, in final release rewrite
some functions at least to make crackers life harder.
- 9a) Example: many ocx'es for vbasic take PLENTY of space, and they are really
easy to hook to. Use only ones which you need, usually much of ocx crap can be
rewritten to use plain windows api.
- 10. Take SOME time to THINK about protecting your software. Is it worth the
protection? Wouldn't it be better to IMPROVE YOUR SOFTWARE, rather than
- 11. Try to embed at least part of the protection inside the data manipulation.
Data structures can take ages to understand basing only on disassembly listing,
they also are more error-prone for cracker. Crackers usually take notes on loose
sheets of paper, and not everyone reads his own handwriting 100% accurately.
- 12. A protection that mangles data is a good one usually.
- 12a) Example: Got a charting program. The crapware shouldn't print. Disabling
printing and later on enabling it basing on some registration# is the most often
committed suicide. Let your thingo print. When creating data structures for
printing, mangle them in some way. Unmangle them just before printing, using
reg# or something other for that purpose. Even more, make this mangling subtle.
Assume that you've got a pie chart to print. Don't alter anything, but add some
not too big random numbers to values of data series - this is mangling then.
The chart will look "not that bad", but will be otherwise unuseable (if the changes
are random and on the order of 20%, for example). Finding such protection, if
its connection with reg# is not self-evident can take much time. One has to delve
inside your data structures and find that dreaded mangling and unmangling code. Not
too easy, especially if you program high level, not in asm.
- 13. When mangling data, use some tricks so that it is not self-evident where that
reg# comes from into unmangling code.
- 13a) Example: I did it for a dos platform once. The reg# (in a "go/no-go" protection)
was specified as a command line argument. The command line parser was clean and
it was the only place where command line was ever referenced. In some other place
the "irrevelant" code was using a "bad" version of borland pascal move routine from
rtl (move copies a range of data from here to there). The routine in rtl was altered
to copy a few bytes more and to wrap around the segment boundary. The "irrevelant" code
called copy with parameters set so that it would copy some useless crap together with
command line somewhere else, and that "somewhere" was left unused for some time. It was
evaluated in the wholly other part of program. Athough setting bpx on memory range would
find that, it was not THAT evident that one should place the "enable#" on command line
(it was nowhere mentioned since I needed to protect from my coworkers only).
- 14. Be economically inclined. Calculate whether you need to write your "to be protected"
program at all. Isn't there (albeit big) a commercial, widely available package that
does the same thing? If you need that tiny program for yourself only, in 99% of cases
it's cheaper to use existing package that supports scritping (to tailor it to your needs).
If you want to sell it, check your market. The problem of protecting software vanishes
if no one will use your software. Don't overestimate your work's "importance to the world".
Search the net. There's nothing like you want to do? Search more, then. Start work only
when you're sure there's need.
- 15. Always explore your language of choice on the level proposed (or, more often these
times, pushed) by the language developers. If you're real programmer that doesn't want
his "hello world" app to be 2megs+ big, shift down to the api level asap - ie. after
exploring all the nice "visualties" the commercial types might be pushing you to use.
- 15a) Example: This pertains mostly to things like borland c++, delphi and m$ vc++ -
write your app using the existing user interface crap (foundation classes etc). When
the innards are debugged (hah, they never are, but if you at least don't find any bugs
in the first 20 minutes of post-build testing), rewrite your user interface to use
windows api directly. Do it cleverly, think first, think a lot, and you'll usually get
an app that is at most 1/4 of the original size, and that works much faster. Do you
believe that corel draw 3 was written using nearly pure api? Such a HUGE app? - you
say it's impossible. No, it quite is, ever more in the 32bit world when you don't need
to spend so much time moving data around the 16bit segment boundary (corel 3.0 was for
windows 3.x - it was 16 bit).
- 16. What does p.15 have to do with protections, you think. Oh well, it does have much.
Since your code doesn't use the "prepackaged" stuff, it's more personal, more custom.
Since each programmer's style of programming is different, it take more time for a
potential cracker to find out what's going inside your app. Alas, all this is useless
if cracker doesn't have to know the innards to deprotect. So always protect in a way
that is tied in MANY PLACES to the innards (data structures etc).
- 17. A monolithic app that doesn't reference to much more than kernel or device drivers
is usually harder to crack. There are less dependencies to get from watching the .exe
and .dll's with quickview for example. Try to export only the callbacks needed by
windoze for example. Check what goes to the outside world. Try to include no or little
details of registration procedure. Your code should give as little information in "clear
text" to the cracker as possible.
- 18. For god's sake, don't give away the working code! Try to provide users with:
- The crapware version, that DOESN'T have the "missing features" compiled in. This thing
should also have no nag screens nor "enter reg# here" suff - it's obvious, since it's
the CRAPWARE ONLY.
- The full version, that HAS the full functionality compiled in but that also requires
user to enter the reg# once somewhere. This version should be only given away to those
that have paid. At least there is no way to get the thing to crack from your site, since
you only expose the crapware for lamers. Alas, your "full" version, maybe even cracked,
will be to be found shortly on warez sites, but not everyone knows where to search,
so you at least can be proud that your app has been stolen by "the enlightened" :-)
- 19. As has been said many times: don't overuse the "registered", "unregistered",
"registration code", etc. words - both in clear text and in encrypted form. If possible,
invent some clever algorith to generate these on the fly basing on some variables,
definitely don't use the "bool Is_Registered" flag anywhere!
- 19a) Notes: It would be quite nice if the program will be having some sample (bad?)
reg# stored inside in some place(s). The user, after entering the registration#, should
have no IMMEDIATE indication of any kind that the software is registered now. THis
indication shouldn't also be deferred using timer, like in unix login. It's childish
easy to find the "sleep x" code. Each function, like save, print, etc. that depends
on registration, should check the reg# by itself, and it should do it in such way
that user nor cracker doesn't see that something is going wrong or good. As said before:
no disabled stuff. If saving is disabled, either don't provide it at all, or make
the unregistered save work just like real one, just that data isn't written to disk
or is written badly. Consider such scheme: user isn't presented with any "this funct
disabled" box, the save works ok (save dialog opens, save completed "%" bar scrolls
as usually), but you may for example lseek back to the beginning of file after writing
each record if the user isn't reg#. This approach is hard to crack since such attempt
requires to delve DEEP into the program's internals and to find what is going bad
and where. People also think sometimes that their version is simply damaged and throw
their program away, whereas that dreaded save might work ok when registered.
- 19b) Notes: CLEARLY INDICATE facts mentioned in 19a), like that:
<<"Reminder: This program, when not registered, won't save nor print any data.
These two functions will operate ONLY when the CORRECT registration number is
entered in the registration dialog box.
Warning: we restate that save and print options WON'T WORK until the CORRECT
registration number is specified. All other features are unaffected in the
Note: You cannot verify that correct registration number has been entered.
Program doesn't display any warinings that you've entered invalid number.
You know that it is ok when save and printing works, else please retype the
registration number and try again.">>
This will indicate:
a) to the user that he definitely WILL enable the program when the CORRECT number
has been entered,
b) to the hacker that it is going to be a tough job since the registration #
checker sits somewhere else.
- 19c) Implementation notes: Move your reg# to the checker routine in an unsuspicious way.
If you can, don't use the "existing" textbox and gettexta, implement the reg#
entering routine by hand, it would be quite wise even to refrain from using the
existing text rendering procedures and render the number on-screen as it is
entered using your own renderer. You can edit your own font resource or use
some preexisting font, then just encode it in some way so that it will be
unreadable by resource workshops, even better include it in the source as
static constants. Although it is still easy to find where the "drawbitmap"
equivalent was used to paint the reg#, it is definitely harder to understand
the whole underlying routine, especially noticing where does that
reg# go. Using a multi-layered approach here is the most feasible one:
- dynamically create accelerator keys in your 'register me' dialog box, these
accells should be for keys used in reg# entry (0-9,a-z for example)
- each accell should call different routine, if feasible (makes breakpointing tougher)
- each routine should store the flag that given char was entered somewhere else,
it would be nice if each keypress would modify some global variable, in some way
that is decodeable for you, but not to the cracker (at first glance)
- then there should be some kind of 'monitoring' routine that acts accordingly,
paining the characters on the dialog box and taking actions upon backspace and enter,
- yet another routine should collect all entered characters and create a reg# and store
it in some untrivial place. using a .vxd here to manipulate the virtual memory to make
some 'backup' copies in a not-easily-debuggeable way is a nice idea. a .vxd can work
like a tiny embedded debugger, bpxing on the place where that reg# goes. it can then copy
it to some quite other place, all that happening in the background and not to be noticed
easily. the cracker will of course (at the first try, at least) try to check where this
location is accessed. since it is accessed nowhere, he will scream: how the hell does this
app know that the reg# has been entered, if it even does not access it? oh well, just
a tiny .vxd or even a background thread has copied it somewhere else. they'll get at
it later, but at first it can stir crackers minds, though.
- 20. Notes on registration numbers:
- balance between security, feasiblity, programmability and end-user headaches
- too long, non-alphanumeric reg#'s tend to be continuously entered badly. at least
provide a "non-persistent" reg# entry field so that user will rewrite the reg# each
time, possibly correctly at last. many people will just "glance-compare" the entered
reg# and the one (possibly) emailed to them, arriving at the final thought that they
did enter it correctly, whereas the font is too small or they are too tired to notice
that this '1' and 'l' have been interchanged (in a reg# like 'l83jjd_0)pH1lTe' )
- refrain from any user feedback. the reg# entry box should accept strings of any
length, without any validation. don't give crackers the knowledge about reg#.
sometimes knowing that it's 10 chars long or that is contains only uppercase alphabet
helps, so don't help them
- the reg# verification scheme (I'm pretty sorry about it, but it just is like that)
needs to take into account the number of prospective users, and thus you oughta do some
- if your reg# is 10 numbers long, there are 10^10 possible reg#'s. but since your app
might find let's say only 10^4 (10'000) users, you should invent an algorithm that
assigns each one of 10^4 users one of 10^10 reg#'s, and does it somewhat uniformly. This
prevents people and programs (some .vxd based "macro" players, like the one presented
some time ago in DDJ, for example) to be used for brute force approach. If there are
only 10^4 users and you allow 10^9 "valid" reg#s out of 10^10, on average each 10th
reg# tried brute-force will be valid, whereas on the case of 10^4 prospective users, that
many valid reg#'s and space of 10^10 reg#s, on average only each 10^6th reg# tried brute
force will be valid. Ever calculated how much time it would take to brute-force search
10^6 numbers, even using a fast machine and extremenly fast macro player (keystroke
generator simulating reg# entry and checking for results)?
- the assignment operator that assigns user# to reg# shouldn't be trivial, and it's
implementation should be done in asm by someone experienced both in maths (topology
known by +ORC helps here, also :-) and asm. check your operator. create graphs of
how it works. understand your own work, especially its drawbacks and vulnerabilities
- be inventive. don't use anything that seems simple, quick and effective. unless you've
come with something like Einstein's relativity theory, your approach is yes, simple,
yes, quick, but no, not effective, and yes, easy to crack. I'm sorry but we aren't
geniuses and developing a good protection scheme takes time, it took time myself, and
still takes although i'm creating protections for fun since 5 years or so. It will
definitely take some time you, dear reader, and don't believe your self-confidence.
Protections written by self-confident and unexperienced prgrmrs end up in the
stupid protection" page of +fravia. a nice and exposed place, and with what a
neighboorhood, but a protection ending there is still the "most stupid". not a nice
definition of your work, huh? :-]'
- 21. Play same game twice. It helps. If you invent some nice and hard to crack memory
move-around scheme to protect reg# inside your data space, do the same to parts of your
data, even better using the same routine just with other parameters. Crackers are
lived up to the fact that protection algorithms are always used for protections only.
If integrity of your data will rely, at least partly, on proper working of your
protection code, crackers get a tough work to do. it's called functional verification
of the "okayness" of protection code. you don't checksum nor crc it, but simply call
it "from the other place" and let it process the protection-unrelated data. if it
will process this data ok, then cracker mustn't have altered it. this places end on
easy cracks like "change that jump here and that cmp there to nops"..
- 22. Strainer to you: does lotus 1-2-3 for dos "diskette" protection (it let you install
it up to 3 times without "zapping" it from hd first) work still in win95? if yes, why?
if not, why? analyze the code, I've learnt MUCH from it. it's only dos code, it's
probably 100 times cracked already, but there are some niceties to learn from it.
not a very good protection scheme, has many holes in it, but remember what +ORC said:
a cracker well educated in historical code is nearly always a perfect one :-)))
(ok, ok, i know, there's nothing perfect aside from moskovskaya in this bad world, but,
can't we joke at times ? 8-0=)
More tips you might take into consideration
Use a serial which is several KB long of arithmetical transforms, to drive
anyone trying to crack it insane. This makes a keygenerator almost
impossible - Also, brute force attacks are blocked very efficiently.
Caution with the Runtime libary! Use it fully when writing the beta versions, in
the final release rewrite some functions at least to make crackers life harder.
Mangle data. Protection that mangles data is usually a good one.
Example: Imagine a charting program .. e.g., just disabling printing and later
on enabling it basing on some registration# is the most often committed
suicide. Let your thingo print. When creating data structures for printing,
mangle them in some way. Unmangle them just before printing, using reg# or
something other for that purpose. Even more, make this mangling subtle.
Assume that you've got a pie chart to print. Don't alter anything, but add
some not too big random numbers to values of data series - this is mangling
then. The chart will look "not that bad", but will be otherwise unuseable (if the
changes are random and on the order of 20%, for example). Finding such
protection, if its connection with reg# is not self-evident can take much time.
One has to delve inside your data structures and find that dreaded mangling
and unmangling code.
Traps. Do a CRC check on your EXE. If it is modified then don't show the typical error
message, but wait a day and then notify the user using some cryptic error
code. When (and if) they contact you with the error code, you know that it is due to
the crack. Be aware: such traps could also be activated due to virus infection
or incorrect downloads. Don't blame a potential customer for software piracy.
The rcr/rcl trick
If a rcr/rcl is performed on a value, it becomes much more of a pain to crack -
you can't reverse it with by negating it's effects without knowing what the
value of the carry flag was before the original operation. If the carry flag is
created as a result of some other pain in the neck operation, you are probably
onto a winner.
Stick conditional jumps in. Everywhere.
Conditional jumps are not fun to reverse engineer. No loops, but jumps which
conditionally bypass/include portions of your wonderful key manipulation
code. There is no easy inverse operation to be performed here.
Use portions of the code as magic number tables.
(preferably critical sections). You have no idea how annoying this can be, if
you're like most crackers and like to change things around using softice (a
popular cracking tool).
Play with the cracker's mind.
This one is fun :-) Stick series of nops in, as though you were doing
self-modifying code (oh my god! what the heck! nops? Aha! Self-modifying
code! Idiot spends next three years trying to find the code that should be
there.). Pepper the code with junk instructions. Cut the code up into little
pieces and put them all over the executable, with (preferably conditional)
jumps between them. - Anything which you would find a pain in the neck.
Detect SoftIce. Early.
Now crash the computer. You can crash a pentium or a pentium with MMX
even without a vxd by the opcode: F0 0F C7 C8 (illegal form of cmpxchg8b
instruction with lock prefix). Beyond that, we have to resort to the tried and
true methods. Using a vxd, take the CPU out of protected mode. Windows
doesn't like that. Wonder why?
an impossible dream?
On Richard Fellner's page I found this snippet:
The Shrinkers discussion
Don't rely on "EXE-packers". For almost any tool which compresses EXE files
(Shrinker, WWPack32, NeoLite - to list the most popular ones) there's an
uncompressor around, so compressors capable for software-protection
should at least support configurable encryption. Unpackers for the above (and
other) tools are not very wide-spreaded, however, don't rely on them as your
program's one and only "protection"!
Now the question is: is this true?
A shrinker, like the one from Blinkinc should
be seen as a 'first line of defense' for a protector. The application will be uncompressed
when it is loaded and cannot therefore be decompiled from disk. This makes the possibility
to mess with it
a little more complicated for the cracker, especially if, after having compressed the
application with Shrinker or with WWpack32 you checksum (twice) the compressed EXE.
The standard binary post-processors seem at the moment to be:
And for these shrinker there are corresponding unshrinkers in the scene... where are they?
Well, a first good tool (yet not for beginners) is
procdump a very good unpacker by Riz+la, Stone and
in the unpack.txt companion file you'll find VERY USEFUL information about the
most common commercial' packers). I'm presenting here version 1.1, build 4 from 11 October
1998. Visit Stone's page to fetch more recent versions.
an impossible dream?
Vitas Ramanchauskas' site:
Some interesting techniques and original ideas
Richard Fellner's anti-crack tips
(most of them have been shamelessly stolen from my site :-)
Rob Beckers' How to Battle Warez:
A VERY interesting part about site tracking and elementary/intermediate stalking techniques
You may be interested in my Counter Intelligence page.
You may be interested in my Why crackers crack? (Reversing reversers' psychology) 'late November' thread.
[Let's melt softice? Pro and contra]
[Funny tricks against idiots]
1. Let's melt softice? Pro and contra
Here you'll be able to check David Eriksson's original (Mid-97) meltice! A tool
for detecting softice written in C, and here you'll have the same code ported
by PhR to Pascal/Delphi (with Hoffmeister's corrections).
Anyway such an approach is of limited use. You may succeed in annoying some
casual crackers, yet the fact that Numega chose to name their kernel
drivers that way doesn't mean much...
there is nothing that prevents any reveser from renaming them...
So I publish the snippets above because this can give some ideas to good protectors. Go beyond
and prepare some good code for Sice detection (or even some 'retaliating code'... come to
think of it, if I were a protection scheme and if I would detect on a computer -say-
softice, wdasm and
smartcheck I would know that I should ring
all possible defcom red alert bells... but READ (and head) THE FOLLOWING CAVEAT!
First of all you should UNDERSTAND what Softice is... many 'sunday' programmers
don't have the slightest clue...
SoftICE is - same as WDEB386.EXE of Microsoft a completely different story,
much to shareware-authors and driver developers dismay.
First of all,
started before Windows starts (more exactly - you run winice.exe and
winice in turn runs win.com). This applies to win9x .
configured to run as a
boot driver, kernel driver, or a dynamic loadable (kernel) driver under
When the gui starts SI is already present and can be invoked via ALT-D
(preset). So there is no "present-not present" thing with softice, it sits
beneath windows and waits till you need it. as the bulk of softice consists
of ring 0 software, you're not limited in what you can view. driver writers
for that reason quite routinely start their machine with si present.
Therefore you go about detecting it like you'd detect any other vxd - seeking the
VMM's DDB and then just walking the linked list of DDBs in the case of
Win9x, and examining the list of loaded drivers in NT.
The problem rather
is, what are you going to do if you find it? Nuke the machine?
increasingly seems to fill up with authors, who think, just because somebody
(mostly by accident) installed one of their vanity proggies, they got the
right to nuke others peoples machines.
As it's part of my job to work with
softice I just can't work with some programs without reversing them at
the moment - o.k. so what, but what will annoy almost all VXD authors
is if you just go about rebooting the machine if they start
your software... maybe putting a link into the autostart group before.
Therefore do me and people like me a favor please: forget
that debugger detection
crap, do a little bit of math and firgure out how to en- and decrypt your
software at runtime (as it is to valuable to be looked at by other people),
be creative and contructive instead of just destructive.
Btw, there is no law against people debugging and reversing your
software, as strange as this may seem to you, but there surely is a law
against deliberatly risking to damage other peoples' property.
2. Funny tricks against idiots.
Publish the following on alt.2600 (or IRC, or wherever lamers abound)
as standard answer to all zombies' questions regarding
"There is a file in the root directory that controls all copy
protection called command.com (or under NT, I believe it's
'ntldr'). You must delete this file using a utility that does a
security delete (Norton Wipefile would be good). Then restart
your computer. Copy protection schemes will no longer function."
"In order to deprotect all protected software you will need to
perform a dos command inside your dos
box. After having started a dos box
(start ~ programs ~ MS-dos prompt), you will see a funny small
black window, with the following (cryptic) message:
Don't worry about its meaning for the moment, just
cd.. (that is "cd point point")
and you will see a small display changement, the prompt will
Now you are "root", as hackers use to say, just type the following:
And you have started the deprotection routines, note in fact that the
All what you need to do, now, is to excute the command
(Notice that there is a space after deltree, before /Y). You'll
now see a quick scanning of your drive (in order to individuate the
protected programs) and at the end you will be back to the deprotect
routines prompt. Have fun with your unprotected programs"
Of course you should NOT add any smiley to these messages... This is called
"darwinian selection among wannabie crackers" :-)
Visit the experimental message board for
all people hanging around at fravia's
most recent essays
Our own tools
Packers & Unp
Is reverse engineering legal?
(c) Fravia, 1995, 1996, 1997, 1998, 1999. All rights reserved