ez-cdda sleep

Mike Hearn m.hearn at signal.QinetiQ.com
Wed Sep 29 10:51:23 CDT 2004


I think Marcus is right, this looks like copy protection.

Let's engage on a little log analysis shall we? This is something that 
just comes with practice ...

Just after the program starts, it does a CreateProcess:

> 0023:Call kernel32.CreateProcessA(406cd2f0 "C:\\Program Files\\Easy CD-DA Extractor 7\\ezcddax.exe",403810d8 "ezcddax.exe",00000000,00000000,00000001,00000004,00000000,00000000,406cd2ac,406cd61c) ret=00750ec9

ezcddax is the real program and what you launched is just a stub.

Note the 6th parameter: it's 4, which is CREATE_SUSPENDED. So, the new 
process isn't supposed to start.

[ snip lots of traces from CreateProcess ]

> 0023:Ret  kernel32.CreateProcessA() retval=00000001 ret=00750ec9

Here we are at the end.

> 0023:Call kernel32.GetModuleHandleA(00000000) ret=00750fdb
> 0023:Ret  kernel32.GetModuleHandleA() retval=00400000 ret=00750fdb
> 0023:Call kernel32.GetModuleHandleA(00000000) ret=00750ffe
> 0023:Ret  kernel32.GetModuleHandleA() retval=00400000 ret=00750ffe
> 0023:Call kernel32.GetModuleHandleA(00000000) ret=00751012
> 0023:Ret  kernel32.GetModuleHandleA() retval=00400000 ret=00751012

Interesting. GetModuleHandle(NULL) returns the HMODULE of the current 
process. An HMODULE is simply a pointer to the base of the file which is 
mapped in: in the case of an EXE it'll be the headers.

So, it looks like this program is walking its own headers in memory - 
probably inspecting them for signs of tampering. More and more likely 
that this is copy protection.

> 0023:Call kernel32.ReadProcessMemory(00000058,00761060,0078c96c,00000002,406cbc0c) ret=007556bd

It then reads the memory of the newly created process, 2 bytes from 
0x761060. I wonder what is at that address?

Grep the log for it and bingo!

> 0025:Starting process L"C:\\Program Files\\Easy CD-DA Extractor 7\\ezcddax.exe" (entryproc=0x761060)

So it's reading the first two bytes of the entry point. Checking for a 
breakpoint perhaps?

> 0023:Call ntdll.NtReadVirtualMemory(00000058,00761060,0078c96c,00000002,406cbc0c) ret=404fab7a
> 0023:Ret  ntdll.NtReadVirtualMemory() retval=00000000 ret=404fab7a
> 0023:Ret  kernel32.ReadProcessMemory() retval=00000001 ret=007556bd
> 0023:Call kernel32.WriteProcessMemory(00000058,00761060,406cbc08,00000002,406cbc0c) ret=00755723

Then it writes back 2 bytes to the same address. Maybe this is the bit 
that lets ezcddax know it was started by the launcher program and not 
directly by the user. I suspect if you suppress this WPM call, the 
program will pop up an error asking you to run the launcher app 
directly. It's probably writing a jump opcode.

> 0023:Call ntdll.NtWriteVirtualMemory(00000058,00761060,406cbc08,00000002,406cbc0c) ret=404fabea
> 0023:Ret  ntdll.NtWriteVirtualMemory() retval=00000000 ret=404fabea
> 0023:Ret  kernel32.WriteProcessMemory() retval=00000001 ret=00755723
> 0023:Call kernel32.GetExitCodeProcess(00000058,0078ca58) ret=0074f235

Now it goes into a loop, attempting to get the exit code of the process.

> 0023:Call ntdll.NtQueryInformationProcess(00000058,00000000,406cb86c,00000018,00000000) ret=404f5dfa
> 0023:Ret  ntdll.NtQueryInformationProcess() retval=00000000 ret=404f5dfa
> 0023:Ret  kernel32.GetExitCodeProcess() retval=00000001 ret=0074f235

It's trying to find out what the exit code of the process was. 
Unfortunately, we don't know what the answer is because the return code 
is a success/failure bool, not the actual exit code. You'd have to whack 
an ERR in here or something to find out.

> 0023:Call kernel32.ResumeThread(0000005c) ret=007557c5
> 0023:Call ntdll.NtResumeThread(0000005c,406cb8a0) ret=4050e85e
> 0023:Ret  ntdll.NtResumeThread() retval=00000000 ret=4050e85e
> 0023:Ret  kernel32.ResumeThread() retval=00000001 ret=007557c5
> 0023:Call kernel32.Sleep(00000064) ret=007557cd

Then it tries to wake it up (remember, the remote process was started 
suspended) and sleeps for a moment.

> 0023:Call ntdll.NtDelayExecution(00000000,406cb888) ret=40507cff
> trace:relay:RELAY_InitDebugLists RelayExclude = L"RtlEnterCriticalSection;RtlLeaveCriticalSection;_EnterSysLevel;_LeaveSysLevel;LOCAL_Lock;LOCAL_Unlock;TlsGetValue;kernel32.GetLastError;kernel32.SetLastError"
> 0025:Call PE DLL (proc=0x401d3bb4,module=0x401c0000 L"ntdll.dll",reason=PROCESS_ATTACH,res=0x1)

At this point the kernel does a context switch into the new process, and 
it begins initializing. Note that CREATE_SUSPENDED doesn't mean nothing 
runs in the new process. It still gets the ATTACH notifications (at 
least, it does in Wine ... maybe not in real windows). So there's a lot 
of stuff we can ignore here generated by the startup sequence.

Let's find out what the first process is doing:

> 0025:Ret  ntdll.RtlAllocateHeap() retval=40393550 ret=4083acae
> 0023:Ret  ntdll.NtDelayExecution() retval=00000000 ret=40507cff
> 0023:Ret  kernel32.Sleep() retval=00000000 ret=007557cd
> 0023:Call kernel32.SuspendThread(0000005c) ret=007557da
> 0023:Call ntdll.NtSuspendThread(0000005c,406cb8a0) ret=4050e80e
> 0023:Ret  ntdll.NtSuspendThread() retval=00000000 ret=4050e80e
> 0023:Ret  kernel32.SuspendThread() retval=00000000 ret=007557da
> 0023:Call kernel32.GetThreadContext(0000005c,406cb948) ret=0075580e
> 0023:Call ntdll.NtGetContextThread(0000005c,406cb948) ret=4050e7ae
> 0023:Ret  ntdll.NtGetContextThread() retval=00000000 ret=4050e7ae
> 0023:Ret  kernel32.GetThreadContext() retval=00000001 ret=0075580e

Context switch after the first line, and it awakens from its sleep.

It then suspends the thread, and grabs its context. Why does it suspend? 
Reading MSDN reveals that the target thread has to be suspended for 
GetThreadContext to work. The CONTEXT structure holds the register state 
of the thread. I wonder what it's looking for in this structure?

> 0023:Call kernel32.GetExitCodeProcess(00000058,0078ca58) ret=0074f235
> 0023:Call ntdll.NtQueryInformationProcess(00000058,00000000,406cb86c,00000018,00000000) ret=404f5dfa
> 0023:Ret  ntdll.NtQueryInformationProcess() retval=00000000 ret=404f5dfa
> 0023:Ret  kernel32.GetExitCodeProcess() retval=00000001 ret=0074f235
> 0023:Call kernel32.ResumeThread(0000005c) ret=007557c5
> 0023:Call ntdll.NtResumeThread(0000005c,406cb8a0) ret=4050e85e
> 0023:Ret  ntdll.NtResumeThread() retval=00000000 ret=4050e85e
> 0023:Ret  kernel32.ResumeThread() retval=00000001 ret=007557c5
> 0023:Call kernel32.Sleep(00000064) ret=007557cd

OK, and we go back into a loop. In fact, this is an infinite loop.

Probably it looks like this:

while (1)
{
     int code;
     CONTEXT86 context;

     GetExitCodeProcess(process, &code);

     if (code == ???) do something;

     ResumeThread(thread);

     Sleep(64);

     SuspendThread(thread);
     GetThreadContext(thread, &context);

     // do something with context here
     if (context.???) break ???
}

So the question is, what condition will make it break out of the loop, 
and why isn't it getting it in Wine?

It looks like it's waiting for some condition to become true in the 
remote process. This will never happen because ResumeThread here doesn't 
seem to be waking it up! We just loop over and over, resuming it, 
sleeping for a while, grabbing its context to check something which 
never changes, and starting over.

So, I guess the problem is that ResumeThread isn't actually waking up 
the suspended process. Question is, why not?

Here's an idea. Hack the Sleep() call like this:

if (delay == 64) delay = 3000;

Ie, rule out the possibility that the delay between resume and suspend 
is so short Wine can't react in time. Then continue your investigation 
from there.

Good luck!

thanks -mike



More information about the wine-devel mailing list