Click here to Skip to main content
12,694,842 members (34,157 online)
Click here to Skip to main content
Add your own
alternative version


55 bookmarked

High Scores in Solitaire: A More Advanced Approach

, 28 Sep 2008 Ms-PL
Rate this:
Please Sign up or sign in to vote.
This article explains how to make a plug-in for Solitaire that displays a high score table. By way of Codecave, Solitaire will automatically execute a DLL for this plug-in -- thus no user intervention required!



1.0.0 Introduction

A while ago, I was searching for something that would add high score functionality to Windows Solitaire. I even happened to come across an article on this very site. However, none had the functionality that I desired. What I wanted was something that would integrate with the actual Solitaire game. There would be no need to run the Solitaire game through another process, or have anything that would require the user to do anything. After a few minutes of searching, I realized that it was hopeless – so I decided to make my own. In this article, I will detail the process of making such a handy plug-in. A decent knowledge of C++ Win32 API programming is needed. There will be a tiny amount of x86 ASM in one of the sections, but it is simple enough that it doesn’t require any real knowledge of ASM to understand.

1.0.1 Tools needed

This article will be using TSearch and OllyDbg v1.10.

2.0.0 Getting started

Probably, the most important thing in this whole project is getting the score. After all, what use is there in making a high score table without being able to get high scores? There are a few ways to get the score, mainly the long way and the short way. The long (and painful) way is what the author of the other Solitiare high score program did. You can choose to go through hundreds (thousands?) of lines, segment by segment, setting breakpoints on interesting parts, in hopes of getting that elusive address (that poor guy who did this). Or, you can take the easy way, and find the static pointer and trace it to the score in memory (this takes a minute or two). So, the easy way sounds better?

2.0.1 Getting the current score

This approach is where TSearch comes in. To get started, we need to open TSearch, hit the “Open Process” button (top left), and then find our Solitaire process (SOL.EXE, most likely). What we’re going to do is search for our score value in memory. Now that TSearch is focused on Solitaire, we can begin. We notice that the starting score of Solitaire is zero. For memory searching, zero is not such a good number. To cut down on the time, we need to raise our score to something else. Going back to the Solitaire window, I just clicked on an Ace or two, and revealed a few cards to get a score of 30. I went back to TSearch, and searched for this value (as a four byte type), getting 74 results. 74 is still quite a lot, so where to go from here? We can just wait a few seconds since Solitaire decreases the total score by two every ten seconds. Once the score decreases again, you can search for a new value, clicking the Search Next button (to the right of Search, the magnifying glass with … below it). This will search through the 74 addresses for our new value. My score happened to decrease to 24, so I searched for 24. I ended up getting one address.


Note: If you’re following this and happen to still get more than one address, then see which one changes with the Solitaire score.

We see that our score is stored at the address 0xAA268 (yours may be different). If we were to do something similar to:

ReadProcessMemory(hProcess, (LPVOID)0xAA268, &score, sizeof(int), 0);

the score would hold 24. But, let’s restart the game, and see if this holds for all instances. Restarting Solitaire and executing that line again – the score is .. 0? If we were to repeat the TSearch process, we would find that our score is now at a different memory address.

2.0.2 Solitaire has DMA? Seriously..? Solitaire?

Well, it turns out that Solitaire uses dynamic memory allocation (DMA). With this, the score value will be at a different memory address each time. So, where do we go from here? We need to find something called the static pointer, which, we will see, stores the score in a register. Fortunately, TSearch makes this easy for us. We begin by clicking on the “AutoHack” menu item and then selecting “Enable Debugger.” Once this is done, right-clicking on the blank area before the address in our addresses window pops up a context menu – select AutoHack from it.


We’ve now attached a debugger to Solitaire, and we can view what is happening by clicking the “AutoHack” menu item, then selecting “AutoHack window”. Once our score value changes, we should see something similar to this:


Once again, we are lucky that what is going on is really simple. The only thing happening is, EAX is being moved into [ESI+0x30]. We can assume that EAX holds our score, and that it is being moved into [ESI+0x30], with ESI+0x30 being our address (0xAA268, in my case). Knowing this, we can start to track down the static pointer. If we find the value of ESI, then we can see what address it’s pointing to, add 0x30 to it, and then read the value at that new address. But, how to find the address ESI? We need to see what is pointing to it. So, let’s subtract 0x30 from our 0xAA268 (ESI+30) address to get ESI. 0xAA268 - 0x30 = 0xAA238. Converted to decimal, this is 696888. Now, we know ESI, so let’s search for it.


I got four results. With memory, searching the odd-man-out is usually the best pick. You could experiment with all four, or you can take my advice and believe that 0x1007170 is where ESI is located. Well, there we have it – our static pointer. Now, to get the value of the score at any time, we do what I mentioned earlier. We read the value at 0x1007170, add 0x30 to it, and then read the value at that address. Basically, something like this:

ReadProcessMemory(hProcess, (LPCVOID)(0x01007170), &val, sizeof(int), 0);
//val would hold 696888 (0xAA238)
val += 0x30;
//Now it holds (0xAA268)
ReadProcessMemory(hProcess, (LPVOID)val, &score, sizeof(int), 0);
//Read value at 0xAA268 (our score)

And, that is all. By reading the static pointer at 0x01007170 and working with that, we can get the Solitaire score any time. No need to step through debuggers, no need to scan the memory for it – we’ve got direct access to our score.

3.0.0 Making the plug-in

Now that we’ve got the score, we can move into making the plug-in for high scores. What would the approach for this be? What I did was make a DLL (which will be loaded automatically by Solitaire) that subclasses the Solitaire window to add a high score menu. I also made a dialog pop up before the game begins, asking the player for their name (MS Hearts style, more on this later).

3.0.1 Subclassing Solitaire

The approach that I took to this project was to subclass the Solitaire window.

HWND hWnd = FindWindow("Solitaire", "Solitaire");
HMENU hMenu = GetMenu(hWnd);
HMENU hNewMenu = CreateMenu();
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR)hNewMenu, "High &scores");
AppendMenu(hNewMenu, MF_STRING, 1234, "&Show high scores");
AppendMenu(hNewMenu, MF_STRING, 1235, "&Add current score");
AppendMenu(hNewMenu, MF_STRING, 1236, "&Clear high scores");
SolitaireOrigProc = SetWindowLongPtr(hWnd, GWL_WNDPROC,

I found the Solitaire window, got the menu handle, and then added my own items in. The SolitaireNewProc is then responsible for handling all of the important messages that I wish to handle. In this case, it 1234/1235/1236, which correspond to the IDs of the menu options.

From SolitaireNewProc:

case WM_CLOSE:
case WM_QUIT:
    if(saveScore == TRUE)
    case 1234:
                       NULL, HighScoreProc, NULL);
    case 1235:
    case 1236:

For WM_QUIT/WM_CLOSE, I made it so the high score is saved on exit, in the event that someone accidentally closes their game (their boss is nearby). Event 1234, which corresponds to showing high scores, pops up the dialog responsible for displaying the scores. Event 1235, which lets the user dynamically add their score to the high score chart, calls a function that does just that. Event 1236 deletes the high score file, effectively clearing the high score chart.

3.0.2 Handling high scores: A class

Working with storing/retrieving/displaying the high scores is the bulk of this program. Therefore, to handle all of these related operations, I decided to write a class.

Functional view:


Relationship view:


This class works by writing to/reading from a high score file (SOLScores.txt). A brief overview of the functions, since some will be invoked in further code snippets:

void LoadHighScores(void);
//Loads the fileContents vector from the high score file

void AddHighScore(char* name, int score);
//Appends a high score to the file

void ParseParts(void);
//Parses the individual name/score/date components of fileContents

void SortScores(void);
//Selection sorts the score to display from highest to lowest

void MakeProperSpacing(void);
//Further performs operations on the name string
//so it is displayed correctly in the high score table

int getLowestHighScore(void);
 //Returns the lowest score of the top 10

string getFullTable(void); 
//Returns the top 10 in a string so they can be displayed
//by the edit control

The high score file has the following format:


The |x|x| act as delimiters, so parsing the file and finding the important bits becomes a bit easier. I wanted to give an overview of the functions for the upcoming code segments dealing with the important dialogs. If you’re interested in the code inside the functions, then the source code for everything is attached – with comments.

3.0.3 - The Welcome dialog


We need some way to get the user's name. I find that a Welcome dialog that asks for it (like the MS Hearts one) is the best approach to this. In order to play the game, the user has to enter their name, or go with the “Default” name. Later on, when we get the DLL to load automatically, this dialog will pop up before the game loads. The message handling is pretty simple, the most important part is getting the user's name. This is accomplished with GetDlgItemText(hDlg, IDC_NAME, playerName, 32);. The maximum limit of 32 characters was earlier set on the WM_INITIDIALOG message with SendDlgItemMessage(hDlg, IDC_NAME, EM_SETLIMITTEXT, 32, NULL);.

3.0.4 The High Score dialog


Finally, the main dialog we’ve been waiting for:

    FileHandler* DisplayScores = new FileHandler();
    string Table = DisplayScores->getFullTable();
    SetDlgItemText(hDlg, IDC_SCORETABLE, Table.c_str());
    delete DisplayScores;

Upon loading, this gets all of the important information from the high-score file, and displays it in the edit control. It also (by default) checks the “Save score on quit” option. The FileHandler class does all of the grunt work in terms of file reading and parsing, so all that’s left is to just display it.

4.0.0 Making our DLL load automatically

This plug-in is almost done. But, it’s not really a plug-in if it has to be loaded every time a game is started, isn't it? So, what needs to be done is to get this thing to load automatically on Solitaire startup. But, how? The direction that I choose to take is to hardcode a Codecave.

4.0.1 The Codecave approach

This is where OllyDbg comes in. Opening up OllyDbg, select “File” from the menu, and then select “Open” (or hit F3). Locate Solitaire (C:\WINDOWS\system32\SOL.EXE on WinXP) and open it. When the module is loaded, hit Ctrl+A to let OllyDbg clear up a few things in the analysis. What we’re going to do is modify this Assembly code to load our DLL.

4.0.2 Finding a location

Before we do anything, we need to hardcode our DLL name in the program. To do this, we need a place with a lot of zeroes, because we’re also going to be adding our Codecave instructions nearby. If you scroll to the bottom of the module, at around 0x01006D2D, all the way to 0x01006FFF, there is an empty space. This is the perfect spot for the Codecave. Select an instruction in the empty space, and scroll down 15 or more instructions (the highlighted area will be grayed). I chose the values starting at 01006D32 to 0x01006D3E. Once that is highlighted, we can begin hard-coding our DLL name. Hitting Ctrl+E, we are presented with a dialog asking for ASCII/UNICODE/Hex. In the ASCII field is where we will write the DLL name, DialogDLL.dll in my case.


After that is done, we can just hit “OK.” If you’re actually following along, you’ll notice that instead of the text, OllyDbg produces some unusual instructions. To fix this, select the area, and hit Ctrl+A to reanalyze. After doing this, the multiple instructions should be condensed down to one line, with the DLL name in it.

01006D32   . 44 69 61 6C 6F>ASCII "DialogDLL.dll",0

The string is now at 0x01006D32, this location will be used later when we’re calling LoadLibraryA. Now that the string is hardcoded, we need to make the actual Codecave. What a Codecave does, in general, is hijack the flow of the program to execute additional instructions before returning back to normal. We can use the Assembly JMP mnemonic to do just that. But first, we need a location to jump from. We need a segment of code that will be executed at least once when the game loads, preferably before the cards and everything else is displayed. This segment looked pretty interesting to me:

01001468  /$ 837C24 04 00   CMP DWORD PTR SS:[ESP+4],0
0100146D  |. 74 1B          JE SHORT SOL.0100148A
0100146F  |. 6A 00          PUSH 0                                   ; /timer = NULL
01001471  |. FF15 FC110001  CALL DWORD PTR DS:[<&msvcrt.time>]       ; \time
01001477  |. 25 FF7F0000    AND EAX,7FFF
0100147C  |. 50             PUSH EAX                                 ; /seed
0100147D  |. A3 44730001    MOV DWORD PTR DS:[1007344],EAX           ; |
01001482  |. FF15 00120001  CALL DWORD PTR DS:[<&msvcrt.srand>]      ; \srand
01001488  |. 59             POP ECX
01001489  |. 59             POP ECX
0100148A  |> A1 70710001    MOV EAX,DWORD PTR DS:[1007170]
0100148F  |. 6A 00          PUSH 0
01001491  |. FF7424 0C      PUSH DWORD PTR SS:[ESP+C]
01001495  |. 6A 08          PUSH 8

Why? Because, srand will definitely be called at least once (probably just once) at the beginning of the program. We need 5 bytes for our JMP instruction, so it looks like some things are going to be overwritten. Let’s begin by selecting the line:

0100147C  |. 50             PUSH EAX                                 ; /seed

in OllyDbg. This will be the start of the Codecave. I suggest highlighting around 5 instructions before / 5 instructions after 0x01001376, and copying/pasting the results into Notepad. Hitting Ctrl+Space brings up a menu to Assemble an instruction at that address. Let’s just do an empty (for now) space near our text. I chose 01006D42.


4.0.3 Writing the Codecave

You’ll notice that once you do this, there will be a NOP (No operation) instruction after the jump. That’s because we had to overwrite another instruction in order to meet our 5 byte requirement for the Codecave. Now, we go to 0x01006D42. Highlight the new JMP line, and press Enter, or hit Ctrl+G, and type 0x01006D42. OllyDbg should now be pointing to the empty space. Our Codecave will do as follows:

  • Preserve the registers (pushing them on the stack)
  • Load our string into the EAX register
  • Push the EAX register as a parameter for LoadLibraryA
  • Call LoadLibraryA
  • Pop the stack
  • Carry out the instructions that were overwritten
  • Jump back into the original function

To put this into ASM code:

01006D42   > 60             PUSHAD
01006D43   . B8 336D0001    MOV EAX,SOL.01006D32         ;  ASCII "DialogDLL.dll"
01006D48   . 50             PUSH EAX                     ; /FileName => "DialogDLL.dll"
01006D49   . E8 2DB07F7B    CALL kernel32.LoadLibraryA   ; \LoadLibraryA
01006D4E   . 61             POPAD
01006D4F   . 50             PUSH EAX
01006D50   . A3 44730001    MOV DWORD PTR DS:[1007344],EAX
01006D55   .^E9 28A7FFFF    JMP SOL.01001482

The registers are saved, we carry out what we want, and then we restore the registers and execute the overwritten instructions. Like nothing ever happened. To save the changes, select two or more lines of the Codecave and then right click. When the context menu pops up to save everything, select “Copy to executable”, followed by “All modifications”.


Another menu should pop up with four options, “Copy All” is what we want since there is more modified code than just the selection at the bottom that was highlighted. OllyDbg should now give a file dump of the new program. To make the changes and save the new file, close out of this window, and you’ll be presented with yet another message box.


Selecting “Yes” finally ends this task of saving the changes. Now, if this was done correctly and the compiled DLL is in the same directory as the modified Solitaire game, the Welcome dialog should pop up when the game loads. That’s all there is to it. With subclassing and a Codecave, we’ve now got a high score capable Solitaire client. No need to do any extra work, just run Solitaire like normal. I personally recommend changing the Solitaire shortcut in the Start menu to point to a different location, instead of replacing SOL.EXE in system32. Windows uses file protection, and will simply replace it back to the original copy, unless you go through the trouble of disabling it.

5.0.0 Miscellaneous

The source code attached has everything needed to compile the DLL that interfaces with Solitaire. In the executable ZIP file, I have included a patch which patches a normal Solitaire game to the one that uses a high score. This is just for ease of distribution since otherwise the whole SOL.EXE file would have to be sent over. Plus, copyright issues or something of that nature would probably prevent me from uploading my modified Solitaire game.


  • 09.28.2008 - Article submitted.


This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


About the Author

United States United States
No Biography provided

You may also be interested in...


Comments and Discussions

GeneralThanks for this great article! Pin
BubbaARRGG10-Oct-08 7:46
memberBubbaARRGG10-Oct-08 7:46 
Generalpatch failed Pin
mbslk2301-Oct-08 3:23
membermbslk2301-Oct-08 3:23 
GeneralRe: patch failed Pin
AlexAbramov1-Oct-08 7:31
memberAlexAbramov1-Oct-08 7:31 
GeneralRe: patch failed Pin
power__user2-Oct-08 6:25
memberpower__user2-Oct-08 6:25 
GeneralRe: patch failed Pin
AlexAbramov2-Oct-08 13:03
memberAlexAbramov2-Oct-08 13:03 
I didn't think about international versions, but it makes sense now that I do. Due to that 512 byte difference unless all of those 512 come after the address that the patch overrides/redirects the addresses will be shifted/changed around and patching would cause the program to crash.

e.g. in the US version it looks like this
0100147C  |. 50             PUSH EAX                                 ; /seed
0100147D  |. A3 44730001    MOV DWORD PTR DS:[1007344],EAX           ; |
01001482  |. FF15 00120001  CALL DWORD PTR DS:[<&msvcrt.srand>]      ; \srand

Say for example 10 of those 512 bytes were added before these statements, causing them to take on new values in the German version (shown below) -
01001486 |. 50             PUSH EAX                                 ; /seed
01001487 |. A3 44730001    MOV DWORD PTR DS:[1007344],EAX           ; |
0100148C |. FF15 00120001  CALL DWORD PTR DS:[<&msvcrt.srand>]      ; \srand

The problem occurs since the codecave that is written is expecting certain opcodes at certain addresses. Whatever is written at German version 0100147C/0100147D will be overwritten and then doing something like
01006D4F   . 50             PUSH EAX
01006D50   . A3 44730001    MOV DWORD PTR DS:[1007344],EAX
01006D55   .^E9 28A7FFFF    JMP SOL.01001482

is going to cause a lot of problems because at those addresses you were not carrying out those commands that you took out to redirect the flow of the program.
GeneralA detailed description of how to 'hijack' the software Pin
yy123429-Sep-08 15:26
memberyy123429-Sep-08 15:26 
GeneralVery cool stuff! Pin
CodeHead29-Sep-08 7:08
memberCodeHead29-Sep-08 7:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.170118.1 | Last Updated 28 Sep 2008
Article Copyright 2008 by AlexAbramov
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid