The other day, I downloaded a trial version of a game from a website.
The game was a fairly simple one where you have a series of balls rolling along a track, and you have to shoot coloured balls at them to cause them to pop.
I was enjoying the game a lot - it was great fun - but then after 60 minutes the game turned itself off and displayed a message stating that my trial was over and that I would have to purchase the game to continue playing.
A lot of software these days is based on trial use systems, whereby the user is entitled to a certain amount of use of the program, and then after a this time has been used the program locks itself so that it can no longer function, or functions with severely reduced functionality.
The problem with this method of software protection though, is that the actual program is still on the user's PC - after the trial period the software "locks" but is actually still fully available.
With this in mind, I decided to have some fun seeing how easy it would be to simply bypass the trial protection and to continue playing the game.
Before I continue, I would like to say that I do believe people should pay a fair price for software, as I myself am aware of how much effort goes into producing applications and games, and that I did actually purchased the software in question.
To start, I installed the software on a new PC, thereby unlocking it for full use.
I then ran the software.
The first thing that happened was that a nag screen appeared which informed me that I had only 60 minutes of free gameplay left. I clicked on the Play button on this screen to start the game. The nag screen then disappeared and a couple of seconds later the game appeared on screen.
I closed the program, and then opened OllyDBG. I opened the program in OllyDBG and selected Run. The nag screen appeared.
At this point I Paused OllyDBG and noticed I was in kernel32. I selected "Execute till user code".
I now noticed I was in a large proc that seemed to control the trial window that had been displayed.
As the trial window had been kind enough to inform me how much time was left in my trial period, I knew that this value had to have been supplied from somewhre, so I decided to have a look through the proc.
I saw something that interested me at +5FAD, which was the following:
PUSH 00533940 UNICODE "gTimeLeft"
Now time left could of course be for a number of different things, but it was a good place to start looking for what I was after. I set a breakpoint on this line and then let the program continue to run.
However, my breakpoint did not hit. So I tried a different method, I restarted the program, and then checked to ensure the breakpoint was still set on that line.
I then seleced Run.
A short while later, the breakpoint was hit - this was obviously used as part of the initialisation of the trial screen rather than during it's general running.
The first thing I did now that the breakpoint had hit was to have a look at the new few lines of code:
+5FAD PUSH 00533940 UNICODE "gTimeLeft"
+5FB2 LEA ECX, DWORD PTR SS:[EBP-2C]
+5FB5 CALL +87D3
+5FBA MOV DWORD PTR SS:[EBP-4], 1D
+5FC1 PUSH DWORD PTR DS:[5BA530]
I made the initial assumption that at +5FB5, there was a call to a proc that retrieved this value "gTimeLeft". I quickly stepped through the proc and resulting sub-proc calls to see whether I could find anything obvious, but couldn't.
I decided to get back to the main proc and step through this to see whether there was anything else helpful.
I stepped through until I got to +5FC1, where the value at 5BA530 was pushed onto the stack.
The value at this location turned out to be 0x3C (60), which was identical to the 60 minutes displayed on the trail screen. However, this could be pure coincidence and I wanted to be sure.
I set a new breakpoint on this line and then let the program run. I played it for a couple of minutes and then closed it.
After OllyDBG had informed me that the process had terminated, I restarted and ran the program, and shortly afterwards my breakpoint was hit again. This time the value was 0x3A (58). I let the program run so that the trial screen would appear, and lo and behold the message displayed the fact that I had 58 minutes remaining in my trial.
Armed with an apparent location for the amount of time left in the game, I restarted the program, and then decided to perform an analysis on any code which accessed this memory location (5BA530).
In OllyDBG, I examined the location in memory and then used "Find References" to enumerate all the places where this memory location was referenced.
It turned out that there were 4 locations:
+37B4 MOV DWORD PTR DS:[5BA530], EAX
+5FC1 PUSH DWORD PTR DS:[5BA530]
+6391 CMP DWORD PTR DS:[5BA530], EBC
+68DC PUSH DWORD PTR DS:[5BA530]
The most sensible way to proceed was to set a breakpoint on each of them, which I then did, and I then restarted and ran the program again.
The first breakpoint that was hit was at +37B4, which seemed to load the number of remaining minutes into the memory location. At this stage, I thought that a simple modification of this line would allow one to play indefinitely, but I wanted a slightly cleaner solution, so I continued execution.
The next breakpoint hit was at +5FC1, which is where the value seemed to be loaded for display on the trial screen. I again continued execution. The trial screen was displayed, so I clicked on the Play button to load the game itself.
The next breakpoint to be hit was at +68DC. This was used to generate a debug message output using OutputDebugStringA in Kernel32. I removed this breakpoint (as it was hit every second) and again continued execution.
However, the final breakpoint was not hit, and so I decided to exit the game and see whether that may trigger anything, but it did not. The process terminated.
So now I had a single point at which I could manipulate the game, +37B4, where the initial remaining time had been set. I restarted the program and ran it until this breakpoint was hit again.
I could easily of changed the remaining time value at this point, or simply modify the code so that instead of loading the remaining time into the address, it always loaded a high fixed value, but this didn't seem like a particularly elegant solution, so I wanted to actually find out where the trial screen was displayed so I could see whether there was a better way of influencing the program.
I stepped through the code until the proc returned (at +37C6). I stepped through the code here, and noticed that at +61FD, there was a call to +61BD, and that if I stepped over this, the trial screen was displayed and the game seemed to load. This is where I put my next breakpoint.
I restarted and ran the program, and then stepped into the proc that was called. I saw that the called proc only had a single call, so I stepped into this too.
Now I ended up in the large proc that contained the "gTimeLeft" line I found earlier, and a reference to the remaining time in 5BA530 (at +5FC1).
I don't want to go through this large proc. A quick glance at it tells me that this is where the trial registration screen is displayed, and I want to intercept the execution before it gets here.
With this in mind, I go back to the original call into the large proc, and back to the original call into the calling proc, which is at +61FD.
There is a small proc here, which includes the line at +61E8:
+61E8 PUSH 00533970 ASCII "TrialScreen"
Now this is starting to look more interesting. This proc seems to be concerned with launching the trial screen. I would like to see where it is called from, so I examine the start of this proc, at +61D7 and see that there is a single call from +7FE3, which I go to.
I put a breakpoint on +7FE3, and then restart and run the program. The breakpoint hits straight away, so I decide to step over the line - I am not interested in how the trial screen is displayed at the moment - and see what happens.
As expected, the trial screen is displayed. I click the Play button and see that the trial screen closes, and immediately I am back into my code, at +7FE8. The line I just stepped over is just responsible for displaying the trial screen.
The line I am now on (at +7FE8) contains the a JMP to +81A6.
If I run the program now the game will load.
So, what will happen if I replace the CALL in +7FE8 with NOP instructions? Will the game load without a trial screen?
I decide to try it, and as I hoped, the trial screen is not displayed - the game just loads. I have managed to find where the trial screen is displayed from.
I decide to have a look around to see what is going on with the code - I am particularly interested in the events leading up to the display of the trial screen.
I notice that there is a CMP at +7FD8 and a JNZ at +8FE1 just above the call to the trial screen. I set a breakpoint on these to see what is happening in them.
After restarting the program, I notice that the CMP is checking to see whether a value is 0 or not. If the value is 0, a JMP is made to the next line at +7FED, essentially this skips the trial screen.
I decide to force this jump by altering the Z flag. I then allow the program to run to see what has happened.
I actually end up being presented with a screen that tells me my trial period is over, and I can no longer play the game. I assume, from this, that this must be what gets called when there are no minutes left to use up and you cannot trial the game any more.
This isn't what I'm looking for though, so I have a look further up the proc to see what I can find. I notice that there is another CMP just above, at +7FC9. I put a breakpoint on this line, and notice that it has jumps from +7E9B and +7EB7.
I want to know whether either of these jumps are what leads to this instruction or whether the execution simply continues from the previous instruction (+7FC7) so I set a breakpoint on +7FC7.
I restart and run the code and see that the instruction +7FC9 is jumped to, because the breakpoint at +7FC7 is never triggered.
There is a JBE just after the CMP. This is always taken. If I force it not to be taken, I am presented with a screen asking for my order number for registration purposes. Again, not really what I am looking for.
So I decide to go back a bit further, to examine the jumps that led us to +7FC9, so I set breakpoints at +7E9B and +8EB7. Both jumps are actually JE instructions.
I restart and run the program, and the breakpoint on +7E9B is triggered, but I notice the JE instruction at this address is not followed. I continue execution.
The breakpoint on +7EB7 is then triggered, and this time the JE is taken, so this is where the program execution branches to the whole registration screen\trial screen\end of trial screen area.
Moving back down to look at the registration, trial and end of trial screen section in the proc, between +7FC7 and +7FF2, I notice that after each screen has been displayed (via a CALL), a JMP is made to +81A6.
Now this seems to be when the program is actually executed, so I go and examine the code located there.
Interestingly, the first things I notice are a ASCII reference to the program file, and a UNICODE reference containing the wors "Unable to extract game executable.".
This must be where the game is actually loaded from!
Now if the game is registered, then the entire section about registration screens and trial screens should be skipped, so I have a look and see if +81A6 is jumped to from anywhere else.
I can see the three jumps from the registration, trial and end of trial screens at +7FD6, +7FE8 and +7FF2, but there are also two additional jumps from +802D and +8049.
Both of these are located in some code that is just below the whole registration screen area, which starts at +7FF7.
I see that there are two jumps to +7FF7, one from +7E4F and one from +7E5B.
I go and have a look at these. They are both conditional JNZ based on CMP statements.
I set breakpoints, restart and run the code.
The breakpoint at +7E4F is triggered (as expected; it is first). I quickly notice that neither this or the second JNZ are taken, and then the execution quickly proceeds to the registration screen.
I thought it may be prudent to force one of the JNZ, so I pick the first one at +7E4F and restart the program.
When the breakpoint is triggered, I simply change the Z register so the jump is taken, and run the program. None of the screens are displayed, and the game loads and plays successfully.
I have found where I can bypass the registration and trial screens.
Simply changing the JNZ to a JMP (and filling the leftover byte with a NOP) will cause the program to execute every time, regardless of minutes remaining (0 or more). The software protection is, effectively, disabled.
Just to see for myself, I wanted to modify the actual EXE file to see whether this change could easily be made permanent.
I open the EXE using UltraEdit and quickly see that the code is not encrypted. I modify the few bytes so that the JNZ is a JMP, and then save the file.
I can now run it every time without any prompting what-so-ever. I have even tried patching it after the trial period ran out (after 60 minutes was up) and it still works.
This kind of technique of software protection is poor. It actually took me longer to write this article than it did for me to break the protection in this game.
The method used for this game relies on the fact that a wrapper has been produced around the game itself, and this wrapper provides all the registration and trial functionality. The actual game has no concept of this what-so-ever.
Also, the wrapper itself is poorly produced. The fact that I can bypass all the protection with a change to a single line of code is staggering. It would be better to have put a check into several places in the code, and also embedded further, regular checks into the game itself.
However, a lot of developers do not understand how vulnerable their code can be, particularly when they write their code in a language such as C++ and then compile it to assembler.
The actual assembler output from their C++ code may be very different from what they originally wrote, and could be susceptable to easy attack using methods like the ones I have used today.
I am not going to produce a patch for this particular bit of software, because I am reluctant to give away the ability to bypass the software protection on what is actually a very good game. Like I said at the start of my article, I have actually purchased this game because I like it so much, and the only reason I have produced this article is because of my academic interest in the protection methods used in the real world.
If you have any queries about this article, please do not hesitate to get in touch.
Until next time...
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment