I've always felt that the difference between a "splash-screen" and a "nag-screen" lied in how easily it could be ignored. If a screen came up and went away, I'd call it a "splash-screen" and I wouldn't care about it; but if a screen popped up and said "You must click OK before this program will start", I'd hate it and I'd call it names and I'd do everything I could to kill it and make it stay dead. Now I'd call that a nag.
The usual way to eliminate these little nagging beasties required delving into unfriendly code and figuring out the what, where, and how these screens work, before NOPping the hell out of them. Without a doubt, this preferred method has not changed. "But it would be interesting", I thought, "to do this another way."
Let's make Windows do all that work that we don't want to do. This essay will show you how to make Windows simulate mouse clicks. It will show you how to do this from within your target program, and it will show you how to build a front-end loader to call your target program and determine which window handle should get the message. We'll use three separate targets as examples here, all of which have annoying nags that deserve to die: DSTune, Multi-Edit, and WinZip.
American Cybernetic's Multi-Edit
DSTune, written by Dubbeldam Software, lets guitarists tune their instruments from their PC. It has an annoying nag screen. It was chosen as the target for the demonstration of this method primarily because it has the timer code ready-baked, which makes our job alot easier. The code was last revised in 1995, so the code base seems pretty darn stable at this point. The revision we're working with is the 32-bit version 2.2.
Multi-Edit, sold by American Cybernetics, is a multi-function programmer's text/hex editor with an extensize Macro language of it's own. It, too, has an annoying nag screen. It was chosen as a target because it could provide a simple introduction to a front-end loader. We'll be dealing with the newly-released version 8. The "real" crack to this program was actually pretty enjoyable and might make an interesting essay in it's own right. I don't want to spoil anyone's fun just yet, so download it and get rid of that annoying nag, then grab the 8.0 to 8.0b upgrade and get rid of the annoying serial# the same way. It's a little different than your average Windows crack.
WinZip is, well, WinZip. You know it, you love it, you can't live without it. It has an annoying nag screen. It was chosen because it presents a few challenges to the front-end loader model developed for Multi-Edit.
DSTune, v2.2
Running DSTune initially gives us a nag screen that tells us we have a 30-day evaluation period and that we must hit "Continue" to continue. But wait a moment, the continue button is grayed out for one, two, about three seconds! We've got to sit there for three seconds before we can actually run the program, and we actually have to click a damn button to get there? No thanks. Time for a change. Let's see what's happening here.
Load up BRW and let's take a look that resource. It's called DIALOG_4. Edit that resource and then bring up the properties for the "Continue" control. It starts it's life "disabled". Well, we can fix that by un-checking the disabled box and saving. Now it starts up with "Continue" all ready to go, but we still have to click it.
Let's go to the code and find out where it enables the control. The Dialog Box gets created at 410419, and gets passed 4149c5 as it's DlgProc.
149C5 loc_4149C5: ; DATA XREF: sub_41038A+75?o 149C5 push ebp 149C6 mov ebp, esp 149C8 add esp, 0FFFFFF68h 149CE push ebx 149CF push esi 149D0 push edi 149D1 mov eax, [ebp+0Ch] ; eax=(UINT)uMsg 149D4 mov esi, offset unk_420100 ; ??? 149D9 lea edi, [ebp-48h] ; ??? 149DC mov ecx, 5 149E1 rep movsd ; strncpy( (ebp-48), 420100 , 5) 149E3 mov edx, eax ; edx=uMsg 149E5 cmp edx, 110h ; msg = 110 (WM_INITDIALOG) 149EB jg short loc_414A07 ; msgs > 110 go to 414a07 149ED jz short loc_414A35 ; WM_INITDIALOG goes to 414a35 149EF dec edx 149F0 jz short loc_414A21 ; WM_CREATE(1) goes to 414A21 149F2 sub edx, 0Eh 149F5 jz loc_414C8C ; WM_PRINT(0f) goes to 414c8c 149FB dec edx 149FC jz loc_4150D8 ; WM_CLOSE(10) goes to 4150d8 14A02 jmp loc_4150ED ; return 14A07 loc_414A07: ; CODE XREF: CODE:149EB?j 14A07 sub edx, 111h ; 14A0D jz loc_414F20 ; WM_COMMAND goes to 414F20 14A13 sub edx, 2 14A16 jz loc_414EF9 ; WM_TIMER goes to 414EF9 14A1C jmp loc_4150ED ; return
There are a number of ways to create timed events (sleep, select, GetTickCount, etc), but here we see a reference to WM_TIMER. This usually indicates a previous call to SetTimer, which tips us off as to the method being used here. Take a look at 414EF9, and you'll see a call to KillTimer, and as luck would have it, very close to this KillTimer is the call to SetTimer at 414EEA.
14EDE push 0 14EE0 push 0BB8h 14EE5 push 64h 14EE7 push dword ptr [ebp+8] 14EEA call j_SetTimer 14EEF mov dword_421674, eax 14EF4 jmp loc_4150ED 14EF9 loc_414EF9: ; CODE XREF: CODE:14A16?j 14EF9 push dword_421674 14EFF push dword ptr [ebp+8] 14F02 call j_KillTimer 14F07 push 1 ; pushed for EnableWindow call 14F09 push 66h ; Control ID 14F0B push dword ptr [ebp+8] ; handle of DialogBox 14F0E call j_GetDlgItem 14F13 push eax ; Window Handle of control 14F14 call j_EnableWindow 14F19 xor eax, eax 14F1B jmp loc_4150EF
Now we can change the timeout value on the timer, currently BB8 or 3000 milliseconds, to whatever we want by changing 414EE0. The GetDlgItem call gives us the window handle of the Control 66, which is the "Continue" button (you can verify this with BRW), and then it gets passed on to EnableWindow. Now we know completely how this button gets enabled.
Since we enabled the button previously through BRW, we don't need the timer or this code to enable the window. But here's the part where you begin to think, "you know, since they've got the timer code in here already, wouldn't it be just as easy to turn the nag screen into a splash screen by having it PUSH the button, instead of just enabling it?"
So how would we do that, anyway? Well, what happens when you click on a button? You generate messages, specifically, the messages WM_LBUTTONDOWN and WM_LBUTTONUP. Those messages gets processed by your DialogProc or WndProc or the default system procedure. If none of the custom procedures intercepts these messages, the parent window will eventually get a WM_COMMAND message from the control stating that BN_CLICKED, and the parent will process the button click. That's what's happening at 414A0D above.
Apparently then, what we need to do is to send a message to the control saying WM_LBUTTONDOWN and then WM_LBUTTONUP. It's actually a little easier than that. All we really need to do is send the message BM_CLICK to the control. From the API help file: An application sends a BM_CLICK message to simulate the user clicking a button. This message causes the button to receive a WM_LBUTTONDOWN and a WM_LBUTTONUP message, and the button's parent window to receive a BN_CLICKED notification message.
Right on. Now we just need to use SendMessage or PostMessage to get the message to the control. Looking at what we have immediately available, we see that PostMessageA can be called indirectly through 41AB4F. Fantastic, we'll use that.
Now we just need to place somewhere in our program a piece of code that looks like:
mov eax, window_handle_of_control push 0 push 0 push F5 (BM_CLICK) push eax call 41AB4F
Since the program has already called GetDlgItem above, we have the handle of the control in eax. We'll want to keep the timer code for our new "splash" screen, and we don't really want to disturb the surrounding code too much. It seems we've only got about 6 bytes or so to play with here, in that the only thing we can really destroy is the push eax and the call to EnableWindow. So, let's see how much space is at the end of the code segment? Plenty! Let's put our new code at 414c3f, NOP the push eax, and change the call at 414f14 to go to our new code. Don't forget to NOP out the push at 414F07! This looks like it's supposed to be for the call to GetDlgItem, but it's not. NOP it or suffer stack corruption.
So we change our call and make 41ac3f look like:
.0001AC3F: 6800000000 push 000000000 .0001AC44: 6800000000 push 000000000 .0001AC49: 68F5000000 push 0000000F5 .0001AC4E: 50 push eax .0001AC4F: E8FBFEFFFF call .00001AB4F .0001AC54: C3 retn
Test it. Wait 3 seconds (or however long you've made the SetTimer) and watch it click itself away. The nag nags no more; it merely splashes. Cool, if you like that sort of thing.
Multi-Edit, demo version 8.0
We'll be having a front-end program press the 'OK' button on the nag/splash screen.
So, we've got to run the program, let the splash screen come up, get the handle for the "OK" button and then press it. Easy enough.
First, we use CreateProcess to run the proggie. We'll grab the startupinfo from our process and send it along to avoid having to fill in the STARTUPINFO data manually.
GetStartupInfo(&startinfo); if ( CreateProcess(NULL,proggie,NULL,NULL,FALSE,NORMAL_PRIORITY_CLASS,NULL, NULL,&startinfo,&procinfo) == 0 ) { fprintf(stderr,"Cannot create process\n"); exit(-1); }
Now the program will run and we'll grab the main window handle. We use EnumThreadWindows for that, which requires a callback function to receive the window handles. This callback (you'll see it called "EnumThreadWndProc" in your API reference) processes the handles it receives one by one, returning TRUE each time it needs another handle. When the callback returns FALSE or when there are no more handles to process, EnumThreadWindows returns. Since the first handle sent to us will be the handle to the main window, we can simply return FALSE. We can make the HWND variable global so that both our main and our callback can access it. The callback function should look like:
BOOL CALLBACK enumMain(HWND myhwnd, LPARAM lparam) { printf("MAIN HWND: %04x\n",myhwnd); mewhwnd=myhwnd; return(FALSE); }
Get the first handle, then stop enumeration. Boom, done. Now before our call to EnumThreadWindows, we should make sure that the nag screen is displayed. We can simply WaitForInputIdle on the process, and that way we can be sure that the program is waiting for something, and not doing something, before we try anything.
WaitForInputIdle(procinfo.hProcess,4000); EnumThreadWindows(procinfo.dwThreadId,&enumMain,0);
Enumerate the Windows for the Thread ID returned from our CreateProcess into the callback enumMain. Boom. Now we need to get the window handle of the button, and then we need to make sure it's the right button. We'll use EnumChildWindows and search through all the handles we get until we find the right one. To determine which is the handle we need, let's look at what we know: we know that it's a Button class, and we know that the text says 'OK'. If we're unsure about either of these premises, it's simply a matter of running Spy++ or IvySpy and to validate our beliefs.
We can get the classname of a window handle from GetClassName, and then we can find the text from GetWindowText. We'll make a new global HWND for the child handle, and we'll keep enumerating until we have a match. The new callback for EnumChildWindows should look something like:
BOOL CALLBACK enumChild(HWND myhwnd, LPARAM lparam) { char class[200],text[200]; GetClassName(myhwnd,class,sizeof(class)); GetWindowText(myhwnd,text,sizeof(text)); if (!strcmp(class,"Button") && !strcmp(text,"OK")) { printf("found OK box\n"); clickithwnd=myhwnd; return(FALSE); } return(TRUE); }
and we add to our main:
WaitForInputIdle(procinfo.hProcess,1000); EnumChildWindows(mewhwnd,&enumChild,0);
Now we have the appropriate window handle, and we can use the same procedure that we used in DsTune, PostMessage BM_CLICK. Multi-Edit, however, seems to require about a 1-second delay before we the message can be processed correctly, otherwise we get a system error MessageBeep. So, we do:
Sleep(1000); printf("\nClick\n"); PostMessage(clickithwnd,BM_CLICK,0,0);
and we should be done. Put it all together and it should turn that "nag" screen into something more like a "splash" screen.
----------proggie----------- #include <stdio.h> #include <windows.h> #include <winuser.h> char *proggie="c:\\mew8\\mew32.exe"; HWND mewhwnd,clickithwnd; BOOL CALLBACK enumMain(HWND myhwnd, LPARAM lparam) { printf("MAIN HWND: %04x\n",myhwnd); mewhwnd=myhwnd; return(FALSE); } BOOL CALLBACK enumChild(HWND myhwnd, LPARAM lparam) { char class[200],text[200]; GetClassName(myhwnd,class,sizeof(class)); GetWindowText(myhwnd,text,sizeof(text)); printf("HWND: %04x %s ",myhwnd,class); printf("%s %s %s %s\n", IsWindowEnabled(myhwnd)?"ENA":"DIS", IsWindowVisible(myhwnd)?"VIS":"INV", IsWindowUnicode(myhwnd)?"UNI":"ASC", text); if (!strcmp(class,"Button") && !strcmp(text,"OK")) { printf("found OK box\n"); clickithwnd=myhwnd; return(FALSE); } return(TRUE); } main(int argc,char **argv) { STARTUPINFO startinfo; PROCESS_INFORMATION procinfo; GetStartupInfo(&startinfo); if ( CreateProcess(NULL,proggie,NULL,NULL,FALSE,NORMAL_PRIORITY_CLASS,NULL, NULL,&startinfo,&procinfo) == 0 ) { fprintf(stderr,"Cannot create process\n"); exit(-1); } WaitForInputIdle(procinfo.hProcess,4000); EnumThreadWindows(procinfo.dwThreadId,&enumMain,0); WaitForInputIdle(procinfo.hProcess,1000); EnumChildWindows(mewhwnd,&enumChild,0); Sleep(1000); printf("\nClick\n"); PostMessage(clickithwnd,BM_CLICK,0,0); } ---------end of proggie---------
WinZip, the indefatigable
Having gone through Multi-Edit, we'll start on WinZip in the same manner. Use CreateProcess, then EnumThreadWindows to get the main handle, then EnumChildWindows to get the handles of the various controls. Look for a BUTTON class with text that says either "I &Agree" or "I Agr&ee". Find it?
No, you didn't, because it's not in there. Check through the handles of the child windows in the callback; printf the classnames and the text. You'll find quite a few Static text boxes, and four or five Buttons, one labeled "&Ordering Information" and the others blank. Load up Spy++ (IvySpy doesn't work quite as well in this case) and take a look for yourself to make sure your C code isn't lying. So how the heck do these buttons get labeled?
The function that begins at 0040392B does all the dirty work. The function creates a device context, selects fonts, computes the size of the text, sets various colors, builds a bitmap, and draws the text into it. You can watch them as their labels get applied. The code at 00403A5C calls DrawTextA, so run winzip32 under the SoftIce loader and do a bpx 403a5c do "d @(ebp+08)". You'll get to see the text drawn into the buttons one at a time in the data window of SoftIce. The code for "I &Agree" and "I Agr&ee" calls this routine from 00403667 and 0040368C, respectively.
Spy++ can give us much more information about these buttons, so let's check it and see what's different. Well, both buttons have the same size (75x23 non-resizable), the same styles and the same window structures and class structures; in fact, most everything looks the same for both buttons. Their handles obviously differ, but we can't distinguish between the two by their handles. Their control ID's differ, but they can't help us differentiate between the "I agree" and the "quit" buttons.
(The code from 403602-403632 shows us how the control ID's get assigned independent of the text labels. It uses the last bit of the return value from GetTickCount to determine which control ID gets placed into which pointer.)
The question remains, then, how can we differentiate between these two similar, interchangable buttons? We resort to screen-scraping. The buttons, after all, have different bitmaps drawn into their text; if no other difference appears between the two, then we must do what we must do.
On the plus side, we only need to look for one pixel. The position of the "I" never really changes with respect to the bounding rectangle of the button, so if we pick one pixel in the "I" that appears in "I Agree" and never appears in "Quit", we can effectively differentiate between the two buttons. Examining the bitmaps for the buttons closely, which can easily be done by modifying our C code to display the bitmaps as text, we choose appropriately. I selected (21,8). If this pixel equals 0x00000000(black), then we have the "I agree" button; otherwise, we have "Quit". Our callback routine, then, should look for a 75x23 pixel button, and if it finds one, it must check for the color of the pixel at our selected location and compare it with 0.
We'll need to use GetPixel to grab the data from the screen. This has a major drawback: if the WinZip window lies underneath another window, we'll receive the pixel data from the window on top of it; likewise, if the WinZip window has yet to be painted, we'll receive the pixel data of the window or desktop that currently occupies that space. This means that we'll require a small Sleep before doing any work lest we can find ourselves getting incorrect data. Also, we'll need to call SetForegroundWindow before we read any pixel data just in case we've switched windows during that short period of sleep time. GetPixel requires the device context passed as an argument, and we can easily pull that from GetDC since we already have the window handle of the button. I've left in the code to display the image as text, because a 75x23 button displays so well on a 80x24 screen. We'll replace the global HWND variables with pointers passed in the LPARAM parameter of the Enum calls, just to show one possible way for that parameter to be used. Also, we'll make the button blink prior to clicking it, just because we can. Our child callback should now read:
BOOL CALLBACK enumChild(HWND hwnd, LPARAM lParam) { char classname[256]; HDC hdc; RECT rect; int i,j,k; long wh,wl; GetClassName(hwnd,classname,sizeof(classname)); GetWindowRect(hwnd,&rect); wh=rect.bottom-rect.top; wl=rect.right-rect.left; if (!strcmp(classname,"Button") && wh==23 && wl==75) { hdc=GetDC(hwnd); for(i=0;i<23;i++) { for(j=0;j<75;j++) printf("%c",GetPixel(hdc,j,i)?' ':'*'); printf("\n"); } if (GetPixel(hdc,21,8)==(COLORREF)0) { memcpy(lParam,&hwnd,sizeof(HWND)); for(k=0;k<8;k++) { for(i=0;i<23;i++) { for(j=0;j<75;j++) { SetPixel(hdc,j,i,GetPixel(hdc,j,i)^0x00c0c0c0); } } Sleep(300); } return(FALSE); } } return(TRUE); }
and our Enum calls will be changed slightly:
WaitForInputIdle(procinfo.hProcess,4000); EnumThreadWindows(procinfo.dwThreadId, &enumMain, (LPARAM)&wzhand); Sleep(100); SetForegroundWindow(wzhand); EnumChildWindows(wzhand,&enumChild, (LPARAM)&iagreehand); if (iagreehand) { PostMessage(iagreehand,BM_CLICK,0,0); } else { MessageBox(0,"Could not get handle to agree button\n",title,MB_OK|MB_ICONSTOP); }
Build the program and test it. The "I Agree" button now blinks a few times, and then gets clicked. You now have an obnoxious front-end to WinZip.
----------wz.c----------- #include <stdio.h> #include <windows.h> #include <winuser.h> /*#include <wingdi.h>*/ /* LCC doesn't need this, most others will */ char *cwd="C:\\Progra~1\\winzip"; char *prog="c:\\progra~1\\winzip\\winzip32.exe"; BOOL CALLBACK enumMain(HWND hwnd, LPARAM lParam) { memcpy(lParam,&hwnd,sizeof(HWND)); return(FALSE); } BOOL CALLBACK enumChild(HWND hwnd, LPARAM lParam) { char classname[256]; HDC hdc; RECT rect; int i,j,k; long wh,wl; GetClassName(hwnd,classname,sizeof(classname)); GetWindowRect(hwnd,&rect); wh=rect.bottom-rect.top; wl=rect.right-rect.left; if (!strcmp(classname,"Button") && wh==23 && wl==75) { hdc=GetDC(hwnd); for(i=0;i<23;i++) { for(j=0;j<75;j++) printf("%c",GetPixel(hdc,j,i)?' ':'*'); printf("\n"); } if (GetPixel(hdc,21,8)==(COLORREF)0) { memcpy(lParam,&hwnd,sizeof(HWND)); for(k=0;k<8;k++) { for(i=0;i<23;i++) { for(j=0;j<75;j++) { SetPixel(hdc,j,i,GetPixel(hdc,j,i)^0x00c0c0c0); } } Sleep(300); } return(FALSE); } } return(TRUE); } main(argc,argv) int argc; char **argv; { PROCESS_INFORMATION procinfo; STARTUPINFO startinfo; HWND wzhand; HWND iagreehand=(HWND)0; char *title="WinZip Startup Button pusher thingamajig"; GetStartupInfo(&startinfo); if ( ! CreateProcess( NULL, /*lpAppName*/ prog, /*lpCommandLine*/ NULL, /*lpProcAttr*/ NULL, /*lpThreadAttr*/ FALSE, /*BoolInheritHandles*/ NORMAL_PRIORITY_CLASS, /*dwCreationFlags*/ NULL, /*lpEnv*/ cwd, /*lpcwd*/ &startinfo, /*lpStartupInfo*/ &procinfo /*lpProcInfo*/ ) ) { MessageBox(0,"Cannot CreateProcess",title,MB_OK|MB_ICONSTOP); exit(-1); } WaitForInputIdle(procinfo.hProcess,4000); EnumThreadWindows(procinfo.dwThreadId, &enumMain, (LPARAM)&wzhand); Sleep(100); SetForegroundWindow(wzhand); EnumChildWindows(wzhand,&enumChild, (LPARAM)&iagreehand); if (iagreehand) { PostMessage(iagreehand,BM_CLICK,0,0); } else { MessageBox(0,"Could not get handle to agree button\n",title,MB_OK|MB_ICONSTOP); } } ------------stop cutting here-----------
Well, that's it, really. Now you know a little bit about how those Windows Macro utilities work. I don't know if it's really useful information, but now it's in your brain and you're stuck with it.