AudioGrabber 1.2
Reversing ListViews in a Delphi application and then some
by
Lord Soth
All rights
reserved to the Immortal Descendants
Visit us on the
web at : http://www.ImmortalDescendants.org/
Published by +Tsehp, May 2000.
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