July 2003 "Paintshop Pro 8.10 Try&Buy"

If at first you don't succeed...

Win32 Program
by Santa Clawz
Code Reversing For Beginners
Program Details:

Program Name: Paint Shop Pro.exe
Program Type: Popular Graphics Package
Program Location: JASC
Program Size: 5,980,160 bytes

Tools Used:

W32Dasm V8.93 - A Disassembler/Debugger
Hex Workshop - A Hex Editor
eXeScope - A Resource Editor

Rating Easy ( X ) Medium ( ) Hard ( ) Pro ( ) There is a crack, a crack in everything. That's how the light gets in.

Paintshop Pro 8.10 Try&Buy

If at first you don't succeed...

Written by Santa Clawz


First, a sad note

After returning to 'the scene' hopefully a little wiser and experienced in the ways of software [reverse] engineering, it seems many people have also departed, at least my peers of the time. Some still remain but mostly dedicate their time and efforts towards Linux, which is no bad thing. Where do you think I’ve been for the past 5 years!? Anyway, I felt it was time to return to the windows RCE front for a little fun. Seems reverse engineering is becoming a necessary skill these days for analysing virii and other malware. So as a re-entry project what better target to choose than the current version of Paint Shop Pro (I lacked inspiration at the time – I have nothing against this fantastic piece of photo/imaging software)! I’ll move onto other targets later. Before we start with the tutorial I’d like give a quick shout to those I remember and those I learnt from. If you haven’t yet heard of these guys (especially those of the +HCU) then I seriously recommend learning all you can from them (an incredible amount to digest). So in no particular order; +ORC, +Fravia, Mammon_, grugq, The Sandman, Jeff, Icezillion, Torn@do, +Greythorne, Dose, TKC, Eternal Bliss, CrackZ and Volatility. The style of this webpage is in the format of +Fravia’s old pages almost as a tribute to those of the +HCU and in particular +Fravia’s incredible and significant dent in the WWW! Most of the text here was also written alongside the cracking so I explore some routes which end up being red herrings, fear not however, we end up with a successful patch...


I may use the following words interchangeably during the text; code, assembler, ASM and instructions, nag and splash screen, opcode and mnemonic (although strictly they are different), disassembly and deadlisting, breakpoint and BP. Some others I may have forgotten!

About This Protection System

Initial Reconnaissance work

After starting the target for the first time a nag screen (splash screen) pops up with the following information:

8.10 Try&Buy
This is a 60-day evaluation of Paint Shop Pro. If after the 60 day period you would like to continue to use it you must purchase the licensed version. You are on day 1 of your 60-day evaluation period.
Installation ID: 42099247

Your installation ID will be different obviously. During start-up the splash/nag screen displays sequentially all the DLLs it's loading. The nag screen has two buttons, Start and Order. The nag screen sits on top of all other windows (even those not belonging to Paintshop Pro) and is centred. Only after clicking Start does the program resume normal operation and the nag screen disappear. 'Help->About Paint Shop Pro...' gives similar information with the substitution of the Order button for 'System Info' and 'Start' for 'OK'.


As you can tell, it relies on a simple time-trial. A simple check to see if the number of days the program has sat on your HDD has expired past it's pre-determined curfew. In this case 60 days.

The Essay

Reverse Engineering

After a disassembly of the target executable and viewing the strings found in the .rsrc section we can see the nag screen text noted above as a string id of 04023. This is commented just above offset 0x004A1181. At offset 0x004A117C a conditional jump branches execution to 0x004A1189 where it seems a different message would be displayed:

"You are approaching the end of the trial period for this software..."

This is probably due to only n days out of 60 left for the evaluation. n is probably a number like 5, 10, 30 or even 7 (for one week). We discover the exact value later. In fact, by setting a break point at 0x004A117C and running W32Dasm in debug mode following the next few instructions we can prove the jump is responsible for printing the different messages. Run the process. At the breakpoint (execution of PSP will pause automatically) patch the code. Change the conditional jump (JGE - Jump if greater than) to an unconditional jump (JMP - Jump). The opcode will automatically change from 7D to EB. Step over the instruction currently in EIP (you've just modified it), clear the break point and allow normal execution of the target to continue. You'll now see that the text in the nag screen has changed slightly. The text with a string ID of 04023 has been replaced by the next string in the sequence (04024). String resource ID 04040 begins with "Registered To:" and an unprintable character after the colon. This line/area looks promising. This string reference is just above offset 0x004A149D.

Further down the assembly we can see a call to a local function/routine (local meaning not an imported function from a DLL). This is at offset 0x004A14B0 and is a call to the function at 0x004A0C20. Another few instructions down from 0x004A14B0 and another local routine is called at 0x004A0CB0. These two routines (0x004A0C20 and 0x004A0CB0) are, at a glance, very similar. Setting a break point at 0x004A14B0 and debugging the process leads us to believe these function calls are only made once the program has been registered with Jasc because these breakpoints never interrupt the execution of the program. Therefore we need to take a look at the code before 0x004A149D.

The call to 0x005C052E seems responsible for rendering the variable text on the splash screen. As well as being called for printing our friendly reminder messages it is also called to print the installation ID to the bottom of the nag. These offsets are of interest:

0x004A151C - Prints the reminder
0x004A15EA - Prints the DLLs loading
0x004A16BF - Prints the copyright message
0x004A1192 - Prints ‘Start’ and ‘Order’ in their buttons

All of the above are called according to the following code:

mov eax, dword ptr [ebp+FFFFFF4C]
lea ecx, dword ptr [ebp+FFFFFF4C]
call [eax+70]

The immense number of pushes that happen between 0x004A154B and 0x004A1563 inclusive are parameters [pushed onto the stack] for the Win32 API call CreateFontA (at 0x004A1565) which is called before printing the text at the above offsets. As you can see most of the parameters are NULL (indicated by the 0).

What happens when you NOP the instructions at offset 0x004A127C? RESULT!! It bypasses the unconditional jump and instead of printing the reminder it prints:

'Registered To:'

But we still need it to print a name! And more importantly check that the NOP bypasses the evaluation timer (e.g. how many days are left). Nopping a program is isn’t elegant cracking so we’ll also try and find an alternative way to print this message…

Anyway, back to the nopping. The instruction after the unconditional jump (which would not be executed in normal operation) should, or at least could, be referenced by a conditional jump earlier on in the code which bypasses the whole check routine. If we search backwards in the deadlisting (from 0x004A1281) we find this:

0x004A1044    84C0    test al, al
0x004A1046    0F8535020000    jne 004A1281

There are a few comparison/tests made in this area. Other possibilities follow:

The first conditional jump in the series leading up to the possible bypass seems to be at 0x004A0FC0. Starting here, normal (read: no debugging) operation obeys the series of conditional jumps (2 x JNE) up until:

0x004A1044    84C0    test al, al
0x004A1046    0F8535020000    jne 004A1281

Patching the first JNE results in program crash. At the second location JNE jumps because the zero flag is set (to zero). However, changing the mnemonic to JE the instructions execute in sequence. Which leads to a similar set of instructions as listed above except at offset 0x004A1025. The jump at this location isn't executed and we end up at 0x004A1046 as usual. Patching the JNE at 0x004A1025 to a JE bypasses the sequence at 0x004A1046 and again results in the nag text/reminder. Looks like the address we want to patch is at 0x004A1046. However, we still need to know where it loads the name to sit along side the 'Registered To:' text.

To our list of offsets that print text onto the splash we can now add 0x004A151F:

0x004A151C - prints the 'Registered To:' message.

Note this holds the same address as the call responsible for printing the reminder.


Break at 0x004A9FBE and the Message box doesn't pop up (0x0045F864 is the actual location where the call to the Win32 message box function happens). From here we have a chance of tracing the time-trial code. Starting here, follow the assembler to a local call at 0x0045F864. This (when stepped over) brings up the dialog. Set a breakpoint (BP) at 0x0045F847 and run the program through the debugger. Note after the test mnemonic the zero flag is zero therefore the JLE instruction will jump. So modify the flag by changing it to a 1. Continue execution of the target and the message box disappears - splash screen as normal. Well, almost! It says we're 386 days over the trial period and the thread crashes.

Well, so far we have stopped the time check and the printing of our 'trial' reminder and getting it to print 'Registered To:' instead. The only thing left to do is find how it loads a name to append to the previous string. To summarise (code wise):

When debugging (live) modify the zero flag prior to stepping into the following offsets:


In a deadlisting change the opcodes to reflect the opposite of what should be there:

0x0045F847    0F8EE8010000    jle 0045FA35 ; change to jg/jnle (0F8F)
0x004A1046    0F8535020000    jne 004A1281 ; change to je (0F84)

These can be changed in a hex editor by searching for the byte sequence 85C00F8EE80100006A00 and 84C00F8535020000680C respectively. You may want to note down the virtual offset in the file for later. These sequences contain the sequence of instructions we wish to patch with the 4 bytes before and after them - to ensure we patch the correct location.

However, after patching these locations with the newly chosen opcodes it crashes after the splash screen prints ‘Registered To:’. Back the drawing board!

OK, so, scouring around 0x0045F847 we see a few more interesting comparisons:

0x0045F834    83FA01    cmp edx, 00000001
0x0045F837    7C09    jl 0045F842
0x0045F839    83FA3C    cmp edx, 0000003C
0x0045F83C    0F8EF3010000    jle 0045FA35

Now, funny thing is that 3C in decimal is 60. How long is the trial for? 60 days!! Obviously, just above it a comparison is made to see if the 32-bit data register (EDX) is 1. If so CMP sets the flag ready for the JL jump which is a slightly different location to the second jump, JLE. When debugging the app live we can see that stored in EDX (in hex) is the number of days we've been running the app. At 0x004A11C2 the number of days we’ve been using PSP once again appears, this time loaded into the EAX register from EBP-30.

As you may have noticed from the splash by now is that after 30 days the warning gets a paint-job. RED! The interesting thing is that here:

0x004A11C8    83F81E    cmp eax, 0x0000001E

A comparison is made between 1E and the value (our # of days) in the EAX register. 1E in decimal is 30 – half of the 60 days we’re allowed. There is (obviously) a conditional jump right after the CMP so maybe (this is just a guess here!) after 30 days the warning gets a different coloured background.

Tracing back through the assembly looking to find where our number is first stored we come across a MOV instruction at 0x004A112E. This MOV instruction copies the number from EAX (which has been calculated by the several steps before hand) to EBP. You may have noticed that EBP has been used in the code before – at the other locations we attempted an attack. So we may have found where it is first stored. Sure enough changing EAX to 1 prints 1 day instead of the real number! Patching works also – no crash! Notice when patching we need to add a couple of NOPs too to ensure the overall byte size isn’t changed. Here is the byte sequence we need to change:

0x004A112E    8945D0    mov dword ptr [ebp-30], eax

So from 8945D0:


However, this only prints 1 and changing our clock shows once again we’ve failed. Anyhow, this is also rather a poor crack – too many NOPs and we override a couple of the subsequent mnemonics.

OK, reading over what we’ve done so far lets go back and try something we may have overlooked.

0x0045F834    83FA01    cmp edx, 00000001
0x0045F837    7C09    jl 0045F842
0x0045F839    83FA3C    cmp edx, 0000003C
0x0045F83C    0F8EF3010000    jle 0045FA35

This has shown that a comparison is made between 1, 60 and our number. Our number, I am certain, is loaded from a registry key. A call to a function (address held in a register) is at offset 0x0045F819 – a few bytes up from the above snippet. Setting a BP on this address we can see that EDX+20 resolves to (in debug mode) 0x00429B60 (click on EDX in the EIP monitor window and locate +20) or step into the function (F7). Eventually, after a lot of scouring W32Dasm kindly pops up a few API calls and their associated parameters – some of which end up being RegQueryValueExA.

The call we’re interested in is at offset 0x00429C64. It seems if we nop this call (5 bytes therefore 5 NOPs as one NOP, 0x90, is one byte) it assumes we’re still on day one. Is this finally it!! A reinstall will help confirm it.

We can also invert the conditional jump here:

00429C6E    89442414    MOV, DWORD PTR [ESP+14], EAX
00429C72    0F8433010000    JE 00429DAB

Which to me, seems a better, or at least more elegant crack. Change the JE to JNE (which is 0F85) at virtual offset 0x00029C72 in a hex editor. Remember to invert the conditional jump found at 0x004A1046 in the deadlisting to print the ‘Registered To:’ message as well. All in all, so far we just need to change two bytes. But, let’s see if we can get our name next to the registration message too.

At 0x004A13D7 the address of the string ‘8.10 Try&Buy’ is loaded into the EAX register which is later (by only a few bytes) pushed onto the stack [as a parameter] prior to the Win32 call that prints it to the splash screen. So, simply nop the call (all it does is print this string) and Try&Buy won’t appear on the splash screen.

The Crack

Finding Resources

Opening up our target in a hex editor we can search for PE executable section headers i.e. .text, .data and the one we’re interested in .rsrc. The section name .rsrc denotes resources - the executable contains, or makes use of, for example; Menus, Bitmaps, Dialogs, Strings. Ah, strings. Yes we’re going to try and find our ‘Registered To:’ string in here and extend it.

At virtual offset 0x000002A0 we find the resource name of ‘.rsrc’. Above you can also see the .data (read/write initialised data), .rdata (read-only initialised data) and .text (program code) sections.

Use a good resource or PE header editor like eXeScope or ProcDump32 to discover further details about the structure of our target. Hopefully you should find that the virtual size of the resource section is 0x0033E860 or 3401848 bytes. You can discover this also by reading backwards (remember IA32 processors are little endian) from virtual offset 0x000002A8 for 4 bytes in a hex editor. Also, and more importantly you’ll need to take note that the virtual offset of the actual resource data begins at 0x0028A000. Starting from this position, search through the entire binary for the word ‘Registered’ in hexadecimal which is 52656769737465726564. Didn’t find anything huh? That’s because the text is in Unicode, where two bytes are reserved for every character rather than just one. Every other byte in this case is 00 (if your language is English). So again, search for 52006500670069007300740065007200650064 and you’ll find it starts at virtual offset 0x005AD86E. However we can’t extend the string ‘Registered To:’ as it will change the virtual size of the resource section and of course the overall binary. It’s possible of course but too much work for something we can accomplish in a simpler way…

Remember our trial message at the beginning? “This is a 60-day…”? That paragraph is 424 bytes in size (remember when counting the characters double the resulting number as we’re dealing with Unicode again). We can patch the application to replace the first n bytes at virtual offset 0x005AACAA with as many as we need until our whole string (Registered To: Your Name) is complete. We can pad the rest of the bytes up till 0x005AAE52 with zeros.

As we now need this (our new) message printed we don’t need to change the jump at 0x004A1046. One last thing to do is to get rid of the Try&Buy message that pops up on the left since we’ve changed the path of our crack again (above).

Another 3 simple NOPs at 0x000A10F6 will bypass the printing of this message. Not exactly elegant another NOP but unfortunately for this target it seems best.


So, that’s it. An inversion of a conditional jump (JE to JNE); 1 byte. One bypassed call (3 NOPs); 3 bytes. And however many bytes for your name and the ‘Registered To:’ string. Now the simple bit – writing the patch! I suggest you write your own in whatever language you know (preferably C, C++ or ASM). Below is the C++ code for Santa’s patch. It’s not brilliant, nor lean either (I have a soft spot for this OO paradigm). Maybe C is better for patching…

Source Code

// Description: C++ source to patch Paint Shop Pro 8
// Author: Santa Clawz

#include <string>
#include <fstream>
#include <iostream>
#include <algorithm>
#include <exception>

using std::ios;
using std::cin;
using std::fill;
using std::cout;
using std::cerr;
using std::endl;
using std::string;
using std::fstream;
using std::exception;
using std::streampos;

static void rewind (fstream &stream) {
   stream.seekp (0, ios::beg);
   stream.seekg (0, ios::beg);

static const long copy_file (fstream &from, fstream &to) {
   char byte = 0;
   long size = 0;

   while (from.get (byte)) {
      to.put (byte);

   if (!from.eof () || !to)
      return -1;

   return size;

static const bool registration (fstream &target) {
   const short prefix[] = {0x52, 0x00, 0x65, 0x00,
                           0x67, 0x00, 0x69, 0x00,
                           0x73, 0x00, 0x74, 0x00,
                           0x65, 0x00, 0x72, 0x00,
                           0x65, 0x00, 0x64, 0x00,
                           0x20, 0x00, 0x54, 0x00,
                           0x6F, 0x00, 0x3A, 0x00,
                           0x20, 0x00};
   int j = 0;
   const int max = 210;
   int offset = 0x005AACAA;
   const int size = sizeof (prefix) / sizeof (prefix[0]);

   for (target.seekp (offset) ; j < size ; target.seekp (offset)) {
      target.put (prefix[j++]);

   char name[max];
   fill (name, name + max, 0x00);
   cout << "\nEnter your forename and surname separated by a space: ";
   cin.getline (name, max – 1);

   for (target.seekp (offset), j = 0 ; j < max ; target.seekp (offset++))
      target.put (static_cast<short> (name[j++]));

   return true;

static const bool patch (fstream &target) {
   int offset2 = 0x000A10F6;
   const int offset1 = 0x00029C73;
   const short JE = 0x84, NOP = 0x90, JNE = 0x85, CALL[3] = {0xFF, 0x50, 0x70};

   target.clear ();
   rewind (target);

   cout << "\nPatching virtual offsets:\n";
   cout << " " << offset1 << ": ";
   target.seekp (offset1);

   if (target.peek () != JE) {
      cout << "Failed. Expecting " << JE << ", found " << target.peek () << endl;
      return false;
   } else {
      cout << "Opcode " << JNE << endl;
      target.put (JNE);

   for (int i = 0 ; i < (sizeof (CALL) / sizeof (CALL[0])) ; i++) {
      cout << " " << offset2 << ": ";
      target.seekp (offset2++);

      if (target.peek () != CALL[i]) {
         cout << "Failed. Expecting " << CALL[i] << ", found " << target.peek () << endl;
         return false;
      } else {
         cout << "Opcode " << NOP << endl;
         target.put (NOP);

   rewind (target);
   target.flush ();

   return registration (target);

static void terminate (const int &rc) {
   char key = 0;
   cout << "\nPress any key then Return to exit...";
   cin >> key;
   exit (rc);

int main (int argc, char *argv[]) {
   int rc = 1;
   fstream input, backup;
   string program = (argv[1]) ? argv[1] : "Paint Shop Pro.exe";

   cout << "Simple CLI patch for Paint Shop Pro 8.\n";
   cout << "Written by Santa Clawz.\n\n";
   cout.flags (ios::hex | ios::showbase);

   try {
      input.open (program.c_str (), ios::in | ios::out | ios::binary);

      if (!input) {
         cerr << ": Failed to open " << program << " for reading.\n";
         terminate (rc);

      cout << "Target: " << program << endl;
      program.replace (program.find (".exe"), 4, ".bak");
      backup.open (program.c_str (), ios::out | ios::binary);

      if (!backup) {
         cerr << ": Failed to open " << program << " for writing.\n";
         terminate (rc);

      long size = copy_file (input, backup);

      if (size < 0) {
         cerr << "Backup failed.\n";
         terminate (rc);

      cout << "Backup: " << program << endl;
      cout << "Size: " << size << " bytes.\n";
      backup.close ();

      if (!patch (input))
         cerr << "\nPatching failed.";
      else {
         cout << "\nPatching complete.";
         rc = 0;
   } catch (const exception &e) {
      cerr << "C++ exception: " << e.what () << endl;
      terminate (1);

Disclaimer ;-)

I use Linux. Not Windows. Therefore I have no need to use Paint Shop Pro. Any image editing I need to do (which is practically none) I do in The Gimp (which is available for Windows too). Paint Shop Pro is excellent and has stood the test of time. I have no idea how much it costs but if you intend on using it (especially for non-home use!) please buy it. Believe me, it takes a great deal of effort and skill to design and write a package like this.

Page by: Santa Clawz
Page Created: 9th August 2003