AudioGrabber 1.2Reversing ListViews in a Delphi application and then somebyLord SothAll rights reserved to the Immortal DescendantsVisit us on the web at : http://www.ImmortalDescendants.org/
Greetings everyone. In this essay I'll describe how to remove limitations in a Delphi application.
The target in question is AudioGrabber 1.2, an app used to rip digital audio tracks from CDs.
I must admit though that at the beginning when I set out to crack it, I didn't know it was a Delphi
application untill I saw some familiar markings. Nonetheless this was one of the toughest cracks
I've ever made, and it took a combined time of roughly 10 or 12 hours, spread on several days.
If you wish to follow the essay to its full extent I advise you to get version 1.2 of AG, although
that might be a little hard to come by, since the only available version right now is 1.62.
If you are really into it, I can send you a copy of AG 1.2.
Tools Needed :
SoftIce 3.xx (your fav. version will do :)
A Disassembler (I used W32dasm, but if you prefer IDA, I salute you)
A Hex Editor (Hexworkshop for me)
WinShow (The little app that shows window classes and handles...)
PE Browse Pro (For easy analysis of the PE file)
Diving into AG
Ok, lets first give a first look into AG, and see what it's all about. I assumed you installed
the program and running it already. The splash screen tells us that the program is Freeware.
This is good, but not good enough. The author of this program wants his app to be freely
distributed indeed, but added some limitations to it.
The first most obvious limitation is that only half of the CD tracks can be selected for ripping,
at one time. This is annoying of course. You can rerun the program and find that it randomly
chooses different tracks for you to rip. If you wanna rip many tracks, its gonna take you a few
times, and you'll probably be annoyed too :)
Another limitation (at least in version 1.2) is that you can't start and stop the rip process from
places other than the start/end of the track. Usually though, this is not a problem since you
want the whole song obviously, so we'll not even bother dealing with it.
One word of caution for those who rip, each CD-quality 1 Minute play time is equivalent to
around 10 MB of HD space, so a 4 minute song will take approx. 40 MB of space, give or take.
Now that we know whats the problem, lets figure out a way around it. First of all, lets try selecting
a track that doesn't have a check box next to it. Before we do that, just a note on the display in
this app. Those checkboxes for example, are not normal windows checkboxes of course, as anyone
can see it easily. They are something else, and therefore when we'll wanna tackle them we'll need
to find how they are implemented. We'll get there soon enough.
Try selecting a track that doesn't have a checkbox next to it, and you'll see a MessageBox telling
you that in the unregistered version, one can handle only half the tracks.
We figure, here's a good place to start, and work in reverse, so lets put a BPX on MessageBox :
Load the program into the symbol loader, and at the entry point, enter :
bpx messageboxa
This will activate the BP we wanted. Try to click again on a track to generate the message, and
we break into SI. One F12 (P RET) is needed of course to display the message box and when it
closes we return to the place where it was called from.
:004101A5 8B4810 mov ecx, dword ptr [eax+10]
:004101A8 894DF0 mov dword ptr [ebp-10], ecx
:004101AB 837DF0FF cmp dword ptr [ebp-10], FFFFFFFF
:004101AF 7407 je 004101B8
:004101B1 8B45F0 mov eax, dword ptr [ebp-10]
:004101B4 40 inc eax
:004101B5 48 dec eax
:004101B6 7D32 jge 004101EA
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:004101AF(C)
|
:004101B8 C6057AA24A0001 mov byte ptr [004AA27A], 01
:004101BF 6830400000 push 00004030
* Possible StringData Ref from Data Obj ->"Unregistered version limitation"
|
:004101C4 6822684900 push 00496822
* Possible StringData Ref from Data Obj ->"This freeware version can only "
->"handle half of the tracks."
|
:004101C9 68C4674900 push 004967C4
:004101CE A1B0894A00 mov eax, dword ptr [004A89B0]
:004101D3 E88CD00300 call 0044D264
:004101D8 50 push eax
* Reference To: USER32.MessageBoxA, Ord:0000h
|
:004101D9 E810FD0700 Call 0048FEEE
:004101DE C6057AA24A0000 mov byte ptr [004AA27A], 00 ; <<< We are HERE
:004101E5 E991000000 jmp 0041027B
This code snippet is of course copied from W32Dasm.
Lets take a close look at the code. At 004101D9 our MessageBox is called from, and we landed
right
behind it. When we look at the code above it we can easily see that the only way to reach the
CALL to the MessageBox is if the jump condition at 004101AF is met, that is if [EBP-10] has the
value
FFFFFFFFh in it, or as we know it as -1.
Notice however, that the value is originally taken from [EAX+10], then its transferred to [EBP-10]
for
testing. We'll therefore need to display the memory area pointed to by EAX+10. In order to do
this,
disable your message box BP and put a BPX at 004101A5. Either double-click the line in SI, or do
it
manually : bpx 4101a5 .
Exit SI and try to choose another track that is disabled. Remember the track number you chose.
When you click, SI will pop up at address 4101A5. This time, display the contents at EAX+10 :
d eax+10
You can easily see what's going on there. Of course at eax+10 itself you see FF FF FF FF. This is
the -1
that the program reads initially. Notice the other data in the data view, especially on the ASCII
side.
You can see that there is a list of the tracks, their sizes, and some other data, and for each track,
there's
a certain memory area where either FFFFFFFF is stored, or 00000000. This is a very nice pattern
as this
will tell us which tracks are enabled and which are disabled. You can find out to which track each
"flag"
belongs beause it appears just before it. I understood that because I remembered which track I
clicked on,
and a few lines down the data view that track's name and number appeared. What we'll now try to
do is
change one of those so called flags and see if that alone enables the track. Lets see, edit the
memory
contents and instead of FF FF FF FF, put 00 00 00 00.
Wow, look, suddenly we have a checked checkbox next to the disabled track!! Let's try and see if
it works,
click the Grab button, and as soon as you do, the program will display next to the track info, a
message
saying : "Could not open temp.wav as temporary record file".
Doh we think, what is this ??
You didn't really think it would be THAT simple, did you ?? :)
Now, out of curiousity, lets see if the program remembers we enabled the track. Click the Refresh
button
in AG, and you'll see the checkbox near the newly "enabled" track has disappeared.
This could only mean one thing of course : The place that decides wheather to enable or disable a
track
is someplace ELSE.
How can we find it ? One thought that came to my mind is obviously to put a memory BP at that
address
and see which code writes the FFFFFFFF to it. However, as we're about to discover, this might
not be so
trivial.
I wanted to know more about how everything is arranged in the app main window. That is, how
the checkboxes
and names and info are implemented. To do that, I used WinShow, the Hex version of course :)
Run WinShow and use it to look at all the main window attributes, wheather its buttons or menus,
or whatever.
The first thing to notice is that the main window is called TForm1. This is consistent with either
VB or
Delphi applications. Since this is obviously NOT a VB app (there are no VB function imports at all
!!),
I assume Delphi, and I was correct. For us that only means that this program is extremely
overbloated
and we have lots of unneeded code in it, such as fault checks, initialization of windows and
controls
and many other stuff we don't care about.
Back to our issue at hand, when the cursor goes over the track names, you can see its window
class.
Its a ListView control, and is one of MicroSoft's Common Controls. This means that the common
controls
library is managing the ListView and therefore and data associated with it is taken care of
internally by
the library. This of course means that those nifty FFFFFFFF flags we saw earlier are probably
managed by
the library and the code that put them there is NOT a code of AG.
Thats hardly good for us because we need to know where the real place that holds the information
is at.
For this we'll do some research into ListView controls. In any Windows compiler comes a platform
SDK,
and using it we can read all we want about the controls and messages used by them.
To make long things short, I'll explain what I read from the SDK.
In order to create a ListView control, the app needs to call CreateWindowExA, with the
"LISTVIEW"
window class parameter. We assume this has been taken care of by Delphi.
The LV is initialized with 0 items at creation time, and in order to add items to it, one must send a
message.
The message that adds an item to a LV is LVM_INSERTITEM. As you may or may not know,
each and
every Windows message has 2 parameters, called LPARAM and WPARAM, and are used to
transfer
important information about the message.
Looking in the SDK for LVM_INSERTITEM I saw the following :
LVM_INSERTITEM
wParam = 0;
lParam = (LPARAM) (const LV_ITEM FAR *) pitem;
The wParam is 0, and the lParam is a far pointer (32 bit) to an LV_ITEM structure that contains
info
about the inserted item. Example information is the text string to be displayed at each column, an
image if wanted (remember those checkboxes ??), and some other stuff.
The message is sent to the LV window procedure using the SendMessageA function. Using the
API
reference, you can see that SendMessageA receives 4 parameters :
SendMessage( hWnd, uMsg, wParam, lParam);
The first parameter is the window handle of the LV, and is no concern to us. The second
parameter
is the message ID, which in this case is LVM_INSERTITEM. The wParam would be 0, and the
lParam
would be the pointer to the LV_ITEM structure. Remember that params are passed in reverse so
that
the lParam is the first to be PUSHed.
How are we gonna catch the program sending the LVM_INSERTITEM message when a typical
Windows program sends so many messages all the time ??
What we'll do is conditionalize the BP so that it only breaks on the message we want it to.
For that we need to know what LVM_INSERTITEM is gonna look like when pushed on the
stack.
The only way to do that is to open the commctrl.h file in the compiler's /INCLUDE directory and
in it
look for LVM_INSERTITEM. It is defined as LVM_START+7, and LVM_START equals
1000h, so
LVM_INSERTITEM will be 1007h.
Now we put the BP : bpx sendmessagea if esp->8 == 1007
Why ESP->8 ?? Because ESP+0 is the return address for the CALL. ESP+4 contains the first
parameter,
which is the window handle, and the second (the message) is located at offset 8 from the stack
pointer.
Rerun the program all over again, and you soon enough as expected, SI pops.
When at the beginning of the SendMessageA function, we'll used the famour PARAMS macro to
display
the parameters currently on the stack ( I forgot who invented it, sorry). The PARAMS macro is
defined as:
DD SS:ESP.
This command will display the parameters. As you can see in the data view, for each time we
break, there
is information about a track that is passed. Everytime we'll press F5, and use PARAMS again,
we're gonna
see slightly different information, such as track number and length. Another thing that changes is a
pointer we see a few lines down, the fifth line down to be more exact. Display the contents of the
pointer
a few times, each time you break on the function (but making sure you used PARAMS each time),
and
soon you'll see the same list you saw earlier with track names and length, etc..
Notice again that a line down from where the pointer is pointing to, you're gonna see a line with
some FF's,
and if a track is disabled, the beginning of the line is going to be FF FF FF FF, just as we found out
earlier.
This is good, we're catching the disabling/enabling while the program is inserting items into the
LV.
In order to see who is deciding wheather to write a 0 or a -1 for each track, we'll do the following.
Select a place you know a "flag" such as this is going to be written to. Doesn't matter which, and
put a
range memory BP on it for write access. We want the break to occur only if what is written is a -1,
so we
can trace who wrote it there, and maybe take care of it.
This can be done by the following command : bpr xxxx yyyy w if *(xxxx) == ffffffff
Assuming of course that XXXX is pointing exactly at the flag. Another way to go is without the
BP
condition, but then you'll break even when a 0 is written, so you'll want to run it a few times and
wait for
a -1.
The best way to do this is to wait untill you see some track inforamtion appear, when when you
know where
the NEXT track information is going to appear, put a range BP there.
The NEXT track flag is always 12 lines from where the previous track flag was. In the end I used :
bpmd xxxxxxxx w
When SI popped, I saw this :
:00444720 53 push ebx
:00444721 83C4DC add esp, FFFFFFDC
:00444724 8BD8 mov ebx, eax
:00444726 83EA01 sub edx, 00000001
:00444729 720A jb 00444735
:0044472B 743B je 00444768
:0044472D 4A dec edx
:0044472E 7462 je 00444792
:00444730 E985000000 jmp 004447BA
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00444729(C)
|
:00444735 894B10 mov dword ptr [ebx+10], ecx
:00444738 C7042402000000 mov dword ptr [esp], 00000002 ;<<< We stop HERE
:0044473F C744241CFFFFFFFF mov [esp+1C], FFFFFFFF
We've stopped right after the instruction that changed the memory location. This was the line at
adress 00444735. It stored the value of ECX into our flag location. But where did the program
set ECX ??
We see that at 444720 is a new function. If you don't believe me, rerun AG and do : g 444720.
Look at the code, there is nothing infront of 444720. The program must have set ECX prior to
calling this function. This is not normal, since parameters are usually passed on the stack, and
ECX's
value is just set outside the function and used within.
Press F12 and we'll be back at where we came from, which looks like this :
:00406013 0FB75584 movzx edx, word ptr [ebp-7C] ;Take track #
:00406017 833C9500A84A0000 cmp dword ptr [4*edx+004AA800], 00000000 ;Check
:0040601F 7525 jne 00406046 ;if not 0, disable track
:00406021 8B0DB0894A00 mov ecx, dword ptr [004A89B0]
:00406027 8B81C8010000 mov eax, dword ptr [ecx+000001C8]
:0040602D 8B8030010000 mov eax, dword ptr [eax+00000130]
:00406033 33D2 xor edx, edx
:00406035 E8AAE90300 call 004449E4
:0040603A 83C9FF or ecx, FFFFFFFF ;if 0, disable
:0040603D 33D2 xor edx, edx
:0040603F E8DCE60300 call 00444720 ;disable
function
:00406044 EB21 jmp 00406067 ;continue execution
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040601F(C)
|
:00406046 A1B0894A00 mov eax, dword ptr [004A89B0]
:0040604B 8B88C8010000 mov ecx, dword ptr [eax+000001C8]
:00406051 8B8130010000 mov eax, dword ptr [ecx+00000130]
:00406057 33D2 xor edx, edx
:00406059 E886E90300 call 004449E4
:0040605E 33C9 xor ecx, ecx ;zero ECX
:00406060 33D2 xor edx, edx
:00406062 E8B9E60300 call 00444720 ;enable function
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00406044(U)
|
:00406067 66C745985C00 mov [ebp-68], 005C
Look whats happening here. At 00406013, we load EDX with a track number.
Then we perform a check of some sort (we'll look at it a bit later), and according
to the result, we either jump or not. If the memory location in the check is not
00000000, we jump to address 00406046, which will lead us to zeroing ECX at address
0040605E, and then we'll call the function we met earlier, at 444720, that will store ECX
and make the LV item enabled.
If the memory location is 0, we won't jump at 0040601F, but continue to execute,
reach the code that puts -1 (FFFFFFFF) into ECX (address 40603A), and then call 444720
which will disable the LV item.
It seems that the memory location checked is some sort of enable/disable flag. Further more,
you see that the track number stored in EDX is multiplied by 4 and added to 4AA800, and then
checked. For every track number, we'll get a different address, in increases of 4 bytes. This is
in fact a DWORD flag array, where EDX points to the flag corresponding to the current track.
If you put a BP at address 00406013, you'll see that EDX enumerates all tracks in the CD.
In any case, lets see what is stored from the beginning of the array, at address 4AA800.
dd 4aa800.
This will display an array of DWORDs as we expected. Some of them are going to be 00000000,
and some are going to be 00000001. Those that are 1 signify the tracks to be enabled, and those
that are 0 are going to be disabled.
Notice that at exactly 4AA800, there will always be 00000000, since there is no 0 track, the tracks
begin at track 1 and thus the first element will always be 4AA804.
Before we rush off and jump to conclusions lets see where the program is making this array.
The obvious way to do this is with a memory BP, so write :
bpr 4aa800 4aa830 w
I leave it up to you to manage your BPs and decide which are needed at the moment and which are
not, because as you know, SI can only do 4 memory BPs at a time using debug registers (DR).
Rerun the program, and wait for it to break on our new BP. Once it does, take a look, this is the
heart of
the protection, and the source of all evil ;)
:00405D92 EB1E jmp 00405DB2
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00405DE6(C)
|
:00405D94 E81DA20200 call 0042FFB6
:00405D99 66894584 mov word ptr [ebp-7C], ax
:00405D9D 0FB75584 movzx edx, word ptr [ebp-7C]
:00405DA1 C7049500A84A0001000000 mov dword ptr [4*edx+004AA800],
00000001
:00405DAC FF8574FFFFFF inc dword ptr [ebp+FFFFFF74]
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00405D92(U)
|
:00405DB2 0FB70D249B4A00 movzx ecx, word ptr [004A9B24]
:00405DB9 0FB705269B4A00 movzx eax, word ptr [004A9B26]
:00405DC0 2BC8 sub ecx, eax
:00405DC2 41 inc ecx
:00405DC3 898D54FDFFFF mov dword ptr [ebp+FFFFFD54], ecx
:00405DC9 DB8554FDFFFF fild dword ptr [ebp+FFFFFD54]
:00405DCF D80DC5684000 fmul dword ptr [004068C5]
:00405DD5 DC05C9684000 fadd qword ptr [004068C9]
:00405DDB E810A60700 call 004803F0
:00405DE0 3B8574FFFFFF cmp eax, dword ptr [ebp+FFFFFF74]
:00405DE6 7DAC jge 00405D94
:00405DE8 0FB715249B4A00 movzx edx, word ptr [004A9B24]
:00405DEF 899574FFFFFF mov dword ptr [ebp+FFFFFF74], edx
Since there is lack of space to add comments after most of these instructions, I'lll describe what
goes on here.
We break when a 1 is written to the element of the array, after the instruction at 405DA1, but only
if a 1 was written. As you can see for yourself, zeroes fill the array from the beginning.
Trace a little and you can see that you jump back from 405dE6 to 405D94. Therefore I advise to
disable or clear the memory BP for the array and put a BPX at 405D94.
Now look what happens here. Step OVER the CALL at 405D94, and EAX will change and will be
a number.
This is not just a number, as you're about to see, AX will be put into [EBP-7C], and from there
EDX will take
its value. EDX will be used to put a 1 into the array at a certain location. What this means is of
course that
the function call at 405D94 generates a random track number which is then used to enable the
track.
Continueing, we see that at 405DB2 , a number is loaded into ECX. Check your Audio CD, and
you'll see that
the value in ECX (in Hex of course) is the number of tracks in the CD. Write this down, it could
be important.
What to write down you say ?? That the global variable at 004A9B24 contains the number of
tracks in the
Audio CD of course!! :-)
EAX is then loaded with a number (guess which :) and then we have some floating point
calculations, and a CALL. When stepping over the CALL and reaching the compare right adter it I
can see
that the memory location [EBP+FFFFFF74] contains the number of tracks in the CD divided by 2.
It checks EAX (this is the counter if you haven't figured it yet) against half the number of tracks, to
detemine
when to exit the loop !!
This loop will therefore randomize half the number of tracks in a CD and enable their flags in the
flag array at
4AA800.
When we reach 00405DE8, the whole process is finished. Also notice that the CALL at 405D94
will not just
randomize a number, it will generate a random number that HASN'T been used before! Very subtle
difference.
Ok, now its time for some experiments. After the whole loop ends, look into the array, choose
some places
with 0's and change them to 1's. Don't change all of them because you'll see the program locks. All
of them
means you have 1 for every track, and if you have 15 tracks in a CD, you're gonna have 15
DWORDs with
a 1 stored in them. We'll deal later with the lockup.
For now, change a few tracks to 1 as well, and exit SI to view the results. Isn't it cool ? Now you
see a few
more tracks with checkboxes next to them!
Press the refresh button to see if the program indeed remembers the "change", and YES, it DOES
remember.
These tracks are permanently enabled!!
Pleased with ourselves, we have done a few steps in the right direction but its far from over. Try to
grab a
newly changed track you enabled, and you get the same error : "Could not open temp.wav as.....".
Something still isn't right after all.
What to do ? What to do ?
Well this time we'll do some nice reversing and locating of important things. We already know
where the
program stores its flags for the LV display, now we need to understand what is happening when a
track
is enabled, so we could enable them all.
My first thought was that the rest of the code after the loop, initializes some kind of a virtual file or
something
for each enabled track. This is a pain of course, finding where it does that. Soon I'll show that I
was way off
base in my guess, as no one is that sophisticated :)
I wanted to find two things. First I wanted to know when the program decides the track is indeed
invalid
and disabled and program flow changes so we DON'T grab, or if it's enabled, continue to grab.
This is the REAL check we were after all along. Yes, having checkboxes near the tracks is
important, but
it won't do us any good if we can't grab them!
Ok, so I thought, what happens when the program grabs a track ? Naturally, that nifty looking
DialogBox
appears and shows the rip process. So I naturally looked in PE Browse for any DialogBox related
imports,
only to find there were NONE!!!
This was puzzling, as I'm sure a DialogBox is created , there's no mistake in that!
Another way a program can make a DialogBox is to make it itself. Create each control and button
using the
almighty function CreateWindowExA.
So, when the program was at the main window, I entered SI and put a BP on the funtion :
bpx createwindowexa
Then, clicked a GOOD track and grabbed it. The dialog appeared, but our beloved debugger didn't
even
twinch!! What gives ?????
Tried it again, and again I found that the function is not called at all. This was time to look at the
window
handles this program has. In SI, type the HWND command, adding AUDIOGRA afterwards (to
tell it to show
only AG owned windows) :
hwnd audiogra
And we receive quite a list !
Now go out of SI, and grab a track. Using WinShow, at the same time, position it on the window
caption
of the dialog to see what's the handle and window name. Surprisingly enough, its called TForm2.
Laughing myself till I fell I realized that the window is probably always open !!
In SI, look at all of AG's windows, and you'll see a TForm2. Abort the rip process, and look again.
WOAH, it's still there!!
Delphi Note : Delphi creates ALL the Windows the program is ever gonna need in advance,
and hides
those that are not needed at the moemnt. At least in this case. Always remember this!
Naturally enough, the only way to enable such a hidden window is using the ShowWindow
function.
ShowWindow can minimize, maximize, restore, hide and show a window. Therefore, when we
grab, we
expect it to be called.
Clear the CreateWindowExA BP and put one on ShowWindow : bpx showwindow.
Grab a track, and walla, SI pops!!
Try to grab a "disabled" track, and SI doesn't pop!
Grab a good track again, and SI pops at ShowWindow. Do a F12 and return to caller. Now we
need to
figure out what the hell is going on.
First I tried to return as much as I can. This lead me back to Windows code, and thinking about it
for
a second, its no surprise, the Window procedure is continueing to work.
How are we going to get to the end of the rip process where we can see a CALL that inside it the
window
is unhidden ?
Well, lets try something else, lets try waiting for the rip process to end. Grabbing and then pressing
F12
untill we're back with Windows will do it for us.
The rip process continues and when it finishes, well, program stops!!! This is so cool, now we can
try
to find where exactly the program either shows the window or not. But its not over yet.
We don't have a clue where to return to.
What we'll do is elimination of possibilities. After the return, lets do some stepping. There more
calls and
conditional jumps we know nothing about, just staring at our face. However soon after we start
stepping
over stuff, we see that a jump takes us to the end of the subroutine, and we soon see a RET
instruction.
After returning, we again see a RET instruction close by. After that, we see some interesting code.
Soon after returning (can't recall if it was 2 or 3 times), we are placed after the CALL at address
00409285.
We assume this is where all the interesting stuff started from. From somewheer within that CALL,
the
ShowWindow is being called from. Again we'll need to step over each call inside that CALL
instruction
and see when it is called. After that, run the process again, and this time enter the next call in the
list.
If you don't understand what I'm saying, I'll elaborate. The CALL at 00409285 contains several
other calls
and conditional jumps. In order to figure out which call is the right one, we'll step over them all,
while
the BPX for ShowWindow is atill active. Its what we call a "sticky" BP and will be armed when
we step
over any function call. If it breaks while we are trying to step over a certain call, that means that
inside
that call the call to ShowWindow is at. The next time we run the process we'll trace into it, and
again
step over all calls, untill we figure out exactly where the ShowWindow call is at. This might seem
futile,
because you can find an occurance of ShowWindow with a disassembler, but a disassembler can't
show
you the program flow like it happens in real-time. What if the function in question is being called
from many
places ? where will you look ?
One thing in particular to look for is a conditional that will cause the program somehow NOT to
reach
the point where the ShowWindow is called from. That happens after entering a CALL inside the
main call
at 409285. There lies a conditional jump that will jump over a call that is probably containing the
ShowWindow call, and among it we assume the rip process code.
Here it looks, as I found it :
:0042C973 0FBF4DC0 movsx ecx, word ptr [ebp-40]
:0042C977 66833C4D98B3550000 cmp word ptr [2*ecx+0055B398], 0000 ;<<<
look here!!
:0042C980 7557 jne 0042C9D9
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0042C971(C)
|
:0042C982 66A190B35500 mov ax, word ptr [0055B390]
:0042C988 668945C2 mov word ptr [ebp-3E], ax
:0042C98C EB3E jmp 0042C9CC
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0042C9D7(C)
|
:0042C98E 668B55C2 mov dx, word ptr [ebp-3E]
:0042C992 52 push edx
:0042C993 E80794FFFF call 00425D9F
:0042C998 59 pop ecx
:0042C999 8945C4 mov dword ptr [ebp-3C], eax
Take a look. I went through all the conditionals and calls before this part, only to find that they are
executed in exactly the same way both when trying to rip a 'good' track, and when ripping a track
we 'forced' to be enabled.
Everything is the same untill the check at 0042C977. There a value is checked, probably a flag of
some sort.
It looks somewhat like an array just like we saw before, however, this time an array element is only
2 bytes as we can see from the instruction itself. ECX is the track we want to rip (duh, what a
surprise..), and it is multiplied by 2. Lets view the data area starting from 55B398.
We'll do that without ruining the fact that we're homed in on the array at 4AA800, or any other
data for that matter.
Change a data window with the command : data 1.
This will change to a different data view. As you may or may not know, SI has 4 data view, 0 to 3.
Now type : dw 55b398.
This will show us what's in 55B398 in WORD alignment, and unsurprisingly, we see yet
ANOTHER array of
flags. Whats incriminating this array of course is that in every place there is a 1, there is also an
enabled track,
that we CAN rip!
Do you recall which tracks you enabled in the 4AA800 array ? Go ahead and enable them now in
this new array, and go back to AG and try to rip them. This time it works perfectly! :)
Halleluya !!!
Now that we know what needs to be fixed, lets see where exactly we need to fix it. We still have
to figure out
where the program stores these values of 0 and 1 to this new array. Naturally, we'll use a range
memory BP.
bpr 55b398 55b3b8 w
Will give us a good enough range. Lets rerun the program, make sure the BP is active, and disable
all others.
Soon enough as expected, SI will pop up and we'll end up here :
:00425D66 668945FE mov word ptr [ebp-02], ax
* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:
|:00425D2C(C), :00425D4B(U)
|
:00425D6A 0FBF55FE movsx edx, word ptr [ebp-02]
:00425D6E 66833C5598B3550001 cmp word ptr [2*edx+0055B398], 0001
;already set ?
:00425D77 74D4 je 00425D4D ;is so, go back
:00425D79 0FBF4DFE movsx ecx, word ptr [ebp-02] ;get track #
:00425D7D 66C7044D98B355000100 mov word ptr [2*ecx+0055B398], 0001 ;enable flag
:00425D87 668B45FE mov ax, word ptr [ebp-02] ;ready AX
:00425D8B EB02 jmp 00425D8F
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00425CF6(C)
|
:00425D8D 33C0 xor eax, eax
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00425D8B(U)
|
:00425D8F 59 pop ecx
:00425D90 59 pop ecx
:00425D91 5D pop ebp
:00425D92 C3 ret
Now what goes down here ??
We see at address 00425D6E that the program checks to see if the selected track has already been
enabled. If it has, it goes back. Back in this case is to randomly select another track.
If not, it'll retake the track's number, and use it to store a 1 in the new array, starting with 55B398.
Once that is done, the track's value will be put into AX, and we'll exit the CALL we are at. Go
back twice, and
suddenly you find yourself at a familiar place!
We're right after the CALL that was at address 00405D94. That was the CALL that generated a
random track number to enable!! As it turned out, it didn't just randomize a number, it also stored
it in the secret array!!
Lets think about this for a moment. This might seem like a trivial matter. Anyone tracing the
function call could have found this out too, however, its not as simple as it appears to be.
Crackers, when going into code
to reverse, prefer to trace through as little code as possible, and use the process of elimination in
order to find
checks (or tests as I prefer to call them).
Now, this secret array got stored in the function call, but nested two levels down, assuring that
crackers will
not enter the call unless absolutely necessary. This is in fact an elaborate trap that programmers
can use to
trick us crackers. It causes us to lose valuable time when cracking.
However, now that we know where all the goods are at, we can work around the problem.
How will we fix it so that the program can have all the tracks enabled ?
First of all, the function call at address 405D94 is redundant. It randomizes a number and uses it to
put a 1 in the flag array at 55B398. If we're gonna make all the tracks enabled, all the flags should
have a 1 and therefore
this whole function is not needed.
What about the code following the function call ? Just as a remider, I've pasted it here as well :
:00405D92 EB1E jmp 00405DB2
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00405DE6(C)
|
:00405D94 E81DA20200 call 0042FFB6
:00405D99 66894584 mov word ptr [ebp-7C], ax
:00405D9D 0FB75584 movzx edx, word ptr [ebp-7C]
:00405DA1 C7049500A84A0001000000 mov dword ptr [4*edx+004AA800],
00000001
:00405DAC FF8574FFFFFF inc dword ptr [ebp+FFFFFF74]
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00405D92(U)
|
:00405DB2 0FB70D249B4A00 movzx ecx, word ptr [004A9B24]
:00405DB9 0FB705269B4A00 movzx eax, word ptr [004A9B26]
:00405DC0 2BC8 sub ecx, eax
:00405DC2 41 inc ecx
:00405DC3 898D54FDFFFF mov dword ptr [ebp+FFFFFD54], ecx
:00405DC9 DB8554FDFFFF fild dword ptr [ebp+FFFFFD54]
:00405DCF D80DC5684000 fmul dword ptr [004068C5]
:00405DD5 DC05C9684000 fadd qword ptr [004068C9]
:00405DDB E810A60700 call 004803F0
:00405DE0 3B8574FFFFFF cmp eax, dword ptr [ebp+FFFFFF74]
:00405DE6 7DAC jge 00405D94
:00405DE8 0FB715249B4A00 movzx edx, word ptr [004A9B24]
:00405DEF 899574FFFFFF mov dword ptr [ebp+FFFFFF74], edx
Now, we know the loop ends at 405DE8, that is the first instruction being executed after the JGE
before it fails to jump. That area of code is ONE BIG portion of memory for us to use !!!
Lets do this then : we'll erase the whole damn thing and program our patch there.
First of all disable all BPs, and rerun the app.
At the entry point type : g 405d94
This will get us to exactly where we wanna patch. I don't know if this program has NOP detection
or not, so lets use XOR EDX,EDX to fill in the whole memory area. The opcodes for XOR
EDX,EDX are 33D2.
So in SI , when we're at address 405D94, enter the following commands :
? 405de8 - 405d94
This will give us the length of the block we wanna fill. The result is 54h.
Now enter :
f 405d94 l 54 33,d2
This will fill the entire block with XOR EDX,EDX.
Now we can start working on the new patch. What we want to do is to run on all tracks and put
1's in all array elements, both the DWORD array at 4AA800, and the WORD array at 55B398.
First though, we need the track count. We wrote where it is located before, at memory address
4A9B24.
So here's what we need to assemble at 405D94 :
a 405d94 :
mov ecx, dword ptr [4A9B24]
mov dword ptr [ecx*4+4AA800],1
mov word ptr [ecx*2+55B398],1
dec ecx
jnz 405d9B
nop
jmp 405de8
OK, why this code in particular ? note also that there is still plenty of room left the author could
have spared if he had made the program without limitations :)
First of all, the first line of code takes the CD's number of tracks and puts it in ECX. The next
instruction writes a 1 at the DWORD array starting from 4AA800. The next does the same for the
55B398 array.
Then ECX is decreased, and checked for 0. When it reaches zero, we'll continue execution.
Notice the NOP instruction. This was done to fix the following instructions so that they appear as
they should, otherwise the CPU interprets the wrong opcodes. See how it looks when assembled in
the SI code window.
The final jump is to jump to code location 405DE8, which is the end of the original loop. We're
doing that just for the sake of being nice. We jump over all the XOR EDX,EDX's that fill the
remaining block. Just a matter of preference by myself.
Now what's left to do is to try and run the app and see if it works the way we engineered it to!
Backup the original exe, change the bytes (I found the beginning of the loop at offset 5394h in the
original EXE file).
You can assemble the instructions in SI and copy or dump the opcodes so you can enter them in a
hex editor.
Or you can look at this dump from SI :
0177:00405D94 0F B7 0D 24 9B 4A 00 C7-04 8D 00 A8 4A 00 01 00 ...$.J......J...
0177:00405DA4 00 00 66 C7 04 4D 98 B3-55 00 01 00 49 75 E8 90 ..f..M..U...Iu..
0177:00405DB4 EB 32 33 D2 33 D2 33 D2-33 D2 33 D2 33 D2 33 D2
.23.3.3.3.3.3.3.
0177:00405DC4 33 D2 33 D2 33 D2 33 D2-33 D2 33 D2 33 D2 33 D2 3.3.3.3.3.3.3.3.
0177:00405DD4 33 D2 33 D2 33 D2 33 D2-33 D2 33 D2 33 D2 33 D2 3.3.3.3.3.3.3.3.
0177:00405DE4 33 D2 33 D2 0F B7 15 24-9B 4A 00 89 95 74 FF FF 3.3....$.J...t.
After I assembled the instructions I did : d cs:405d94 l 60
That was the result. I then saved the history and entered the opcodes into HexWorkShop.
Okie, run the program after its patched, and what do you get ???
Access violation at address 34354947, read of address is 34354947.
Or something similar to this...
Naturally, my immediate thought was a CRC is in place and kicking our butt around the block.
I set out to search for it.
As you may or may not know, CRCs check the current exe file's opcode values at some places, and
use those to calculate a checksum value. This is the general way of doing it. The calculation can be
anything the programmer want. But in all CRCs, the app needs to open the exe file and read some
data from it.
So I set my BPs on CreateFileA and OpenFile (just in case), and _lopen.
Running the program, CreateFileA did break, but using the PARAMS macro, I checked the first
parameter (which should be the file path pointer), and didn't see the program opening the exe file
at all !!
Also, none of the other APIs have been used !
This was puzzling to me. However, there is one more to try. You can also read data from files by
mapping the file's content into memory. This is done with the MapViewOfFile function.
Setting a BP on it gave me absolutely no results at all, so this was striken out as well.
Now what can we do ??
The program refuses to run, and we have no clue why !!
Rerun the program, and lets at least try to see if we even reach the patch location 405D94.
At entry point write : g 405d94.
You won't get there, I promise. Something along the way causes an access violation. An access
violation is when the program tries to access a memory page that is not allocated to the process'
virtual memory map.
Identify those by the question marks in the data window, we all know them.
How can we find the CRC ?
The program lets us know about the problem using a message box, so put a BPX on
MessageBoxA, once again.
After returning, I did some more tracing and ended back at windows code. I couldn't figure out the
program's flow before the message box was called from. I even tried W32Dasm to figure this out,
only to look at lots of
code I didn't understand and didn't help me when I tried breaking on in SI.
Soon though, everything will be clear.
In such cases where you need to know the program flow before a certain code location, you might
be tempted to use trace simulation mode. I admit I never used it in my life. Its a nice feature, but
with possibly many headaches.
What I did was something else. We can always count on overbloated code to have many CALLs in
it. So it's safe to assume that the message box was called from within several nested calls. If we
want to backtrace to the point of origin, we'll need to know where it came from, and therefore
we'll look at the stack !!
The Stack contains the return address for every CALL that was executed. It also contains variables
and stuff, but those are easily distinguishable.
so when the MessageBox returns, lets type : d ss:esp.
And now lets look at the stack. Positive offsets from ESP means stuff that has already been stored
before.
Look for any addresses that are in the legal range of process memory, which is between 00400000
and 7FFFFFFh.
This is the normal process space, and is almost 2 GB in size (virtual of course).
Find the first occurance of such a thing and display its contents in the data view, to make sure its
not pointing to a string or some other known data type. You can also go about it a different way, if
you look at how many bytes have been pushed on the stack prior to the CALL, you can find the
next return address.
When you find one, rerun the program and put a BPX on the code. We need it to break prior to
the access violaion. Soon enough I found such a code location. I stepped over some CALLs to
figure out where the offensive code is at, and when I found it, I traced into it. Nothing special so
far.
After some more tracing, I ended up in once again the most familiar place, our patch location !!!
What happened was that just before we got to it, there was a JMP instruction (at 405D92), that
jumped INSIDE one of our patch instructions!
That was of course not wanted, because it would change the whole thing the CPU executes. I
traced through this JMP to see what happens next, and as it jumped INTO one of the MOV
instructions in our patch, the code reshaped itself and now I saw this :
CALL 34354947
Familiar ?? Indeed!
That's our error message! Display whats in location 34354947 and you'll see its paged out!
This is why we get an access violation. There was NO CRC checksum, there was a bug in our
patching!
To fix it, we'll want to kill that JMP instruction that jumped us into our patch code. Since its only 2
bytes long, and ECX is already in use in the next instruction, I'll replace it with XOR ECX,ECX,
which is 33C9.
Open your Hex Editor and make the change, its at offset 5392h.
IMPORTANT NOTE : When patching make sure that the code is going to be executed
EXACTLY as you entered it, without any surprises, such as jumps to the middle of it. This
is an important lesson !!
NOW run the program again. What goes on now ?
No error message, thats good. We wait, and we wait some more, and after a few seconds we
realize something is wrong. The program locks up. This was the lockup I mentioned earlier.
Hmmf, Now what have we got to do ?
We'll need to figure out where and why the lockup occurs. You can always kill the AG task using
CTRL+ALT+DEL.
First of all, we'll rerun AG. We'll wanna break at the patch location, so do : g 405d94.
Now lets see if the lockup occurs in THIS subroutine. do an F12 to return to the caller routine. So
far so good, SI broke again. What you see is this :
:004131B4 6A01 push 00000001
:004131B6 E86E26FFFF call 00405829
:004131BB 59 pop ecx ;<<< We are HERE!!
:004131BC 6A03 push 00000003
:004131BE E8270CFFFF call 00403DEA
:004131C3 59 pop ecx
:004131C4 E8BD10FFFF call 00404286
:004131C9 59 pop ecx
Ok, we returned from the CALL and now we're on the POP instruction. Lets again step over calls
to see which one is the one we want. The first one we step over is at address 004131BE, and is
harmless, however the second one at address 004131C4 is causing us to exit SI and make AG lock
up.
After some tracking inside I found this :
:0040453C 0FB715249B4A00 movzx edx, word ptr [004A9B24] ;number of LV items in
EDX
:00404543 0FB70D269B4A00 movzx ecx, word ptr [004A9B26]
:0040454A 2BD1 sub edx, ecx
:0040454C 42 inc edx
:0040454D 3B55B4 cmp edx, dword ptr [ebp-4C] ;compare EDX to # of tracks
:00404550 750A jne 0040455C ;<< Look at THIS!!
:00404552 837DB402 cmp dword ptr [ebp-4C], 00000002
:00404556 0F8FEDFEFFFF jg 00404449
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00404550(C)
|
:0040455C 8B45D8 mov eax, dword ptr [ebp-28]
:0040455F 64A300000000 mov dword ptr fs:[00000000], eax
:00404565 8BE5 mov esp, ebp
:00404567 5D pop ebp
:00404568 C3 ret
What's that ??
You can see that at 40453C , EDX is loaded with the numbers of items that are enabled in the LV.
I found this out because when I just added one flag to the 4AA800 array, this value changed. Now
look at [EBP-4C].
No matter what, it will contain the number of tracks in the CD. The check is done between those,
and if they are equal (if all tracks are NOT enabled, we will jump to 0040455C, which is good).
This is a clever precaution our programmer put in order to lock the program if its being crack, and
guess what, it IS being cracked!! :)
I immediatly wrote down 40454D and put a BPX on it.
So we'll wanna make that conditional a permanent jump. This is standard procedure. You can use
your disassembler to find out where this line of code is and at what offset at the EXE file, and
change the 75 to EB, which will make the JNE a JMP.
Now the program can continue, and we see ALL the tracks enabled!! How cool we think to
ourselves.
Try to click on one, and the BP I put at 40454D will pop us again. The jump is fixed, and no
problems there.
Lets disable the BP on 40454D and try to grab some tracks.
Sure enough we grab alright, but when we try either the Abort of Skip buttons, the program yet
again locks up on us !!
DOH!!! Another trap someplace else in the program.
Now we need to find another such trap.
I don't really recall the exact way I did it. It might have been another CALL to ShowWindow (but
this time for the HIDING of the TForm2 window), or it was something else. I managed to find
myself into this code, using some zen I believe :
:0040B374 6A02 push 00000002
:0040B376 E84DF60000 call 0041A9C8
:0040B37B 59 pop ecx
:0040B37C 85C0 test eax, eax ;if a file for this track exists...
:0040B37E 751E jne 0040B39E
:0040B380 EB07 jmp 0040B389
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040B390(C)
|
:0040B382 C68573FCFFFF00 mov byte ptr [ebp+FFFFFC73], 00
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040B380(U)
|
:0040B389 80BD73FCFFFF00 cmp byte ptr [ebp+FFFFFC73], 00 ;<< FLAG
:0040B390 74F0 je 0040B382 ;This could potentially make trouble
:0040B392 EB13 jmp 0040B3A7
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040B3A5(C)
|
:0040B394 A1FCFF5500 mov eax, dword ptr [0055FFFC]
:0040B399 E872380500 call 0045EC10
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040B37E(C)
|
:0040B39E 80BD73FCFFFF00 cmp byte ptr [ebp+FFFFFC73], 00
:0040B3A5 74ED je 0040B394 ;again troubles..
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040B392(U)
|
:0040B3A7 833D5C15490001 cmp dword ptr [0049155C], 00000001
:0040B3AE 755F jne 0040B40F
First of all, this will be the last code snippet I quote from AG :)
Second, I traced through this code several times to figure out what everything means.
The first thing I noticed is that the 40B376 will return 1 in EAX if a file already exists with a name
we want to write to. When AG grabs a file, it copies it to temp.wav, and afterwards it renames it
to TrackX.wav, where X is the number of the track. If a file with that name already exists (if we
ripped it before), it will return 1.
Thats of no concern to us. Now the flag at [EBP+FFFFFC73] will be checked. I found this flag
will be 0 if the number of CD tracks is equal to the number of LV items. It is probably calculated
elsewhere.
If they are equal, we're jumped to 40B382, which will zero it out again, and we'll get stuck in an
endless loop.
This is not permissable :)
Another thing to notice is that if we made it to the code after this loop, the CALL at adress
40B399 will also change the memory location [EBP+FFFFFC73] to 0 (in our case because the
number of tracks is equal to the number of LV enabled items, we engineered it that way..).
So afterwards, if again it equals 0, we'll go back to 40B394, and the CALL would be performed
again, resulting in the flag being set to 0 again and again....
What we need to do is to patch the conditional at 40B390 to NOP and NOP (why not hehe), and
the next conditional at 40B3A5 to NOP and NOP again.
After that, check out how AG rips all the CD tracks and all the buttons do what they are supposed
to. Note that the Skip button takes a couple of seconds to respond. This has always been this way.
The patching of those are left as a finishin exercise for the reader :)
Final Words of wisdom
Well, I learned at least 3 things in this reversing endeavor.
I learned to use the Stack to backtrace program flow. I learned to pay careful attention to what
and where I patch, and HOW things interact with my new code.
I also learned how to do some elimination on subroutine CALLs, and finally, I learned how to
reverse a Delphi application. How it works, how its extremely overbloated, and so on...
I made some more reversing when attempting several ideas I had, but I didn't write them here
because this tutorial was long enough and they are irrelevant to the protection.
It took me quite a while to crack this program, and at times I thought I would just go ask for help
on the message boards, but taking a break for a few hours each time gave me some fresh ideas to
try. I didn't give up as I knew I could do it, and its DONE!! :)
Ok ok, enough with that, greetings go to ALL the Immortal Descendants members, all the +HCU
members, the +Sandman (I owe him special thanks , since because of him I am what I am today),
The Snake (my dear friend), The Hobgoblin (just love that guy :), Lazarus (love him too, loved the
Laz_Calc.. hehe), and Jeff of course, all those that visit the Newbies Forum, +Fravia, +Frog's print,
+tsehp, DQ, _mammon, and all those gods of knowledge that roam the cyberspace and
occasionally rear their heads either at +fravia's forum or the newbies forum...
Of course, Ghirrizibo (not sure how to type it) for his wonderful IceDump util, and many other I
probably forgot, if I did, I apologize!!
Whew, that was one HELL of a typing project!!!
For comments, bug reports (ya hehe) and just to bug me, please contact me at :
LordSoth@ImmortalDescendants.org
lordsoth8@hotmail.com
or via ICQ : 5178515
Have fun reversing!
Lord Soth
23.5.2000