Friday, August 10, 2007 12:10 PM
Investigating Outlook's Single-Instance Restriction (PART 1)
If you use Outlook and have multiple e-mail account profiles, you know how frustrating it is to have Outlook restrict you to a single running instance of Outlook per interactive login. For those of you not familiar with this "feature", here's the scoop: if you have one instance of Outlook running and then launch another instance, a new Outlook window is created in the context of the original instance, but you don't have the option to load another e-mail account profile. This is a pain because it requires you to close and restart Outlook each time you want to check a different e-mail account (assuming you have a separate profile for each account).
Tim Mullen, a colleague of mine, had the ingenious idea of using RunAs to launch the second Outlook process as another user, to try to circumvent whatever "feature" was restricting Outlook to a single instance. "What a great idea!" I thought, and I kicked myself for not having thought of that myself! But when we tested it out, it had the same results as running a second instance of Outlook without RunAs; an extra window popped up for the first instance and we weren't given the option to load another profile.
This piqued my interest and I wondered how Outlook was determining whether or not another instance was already running in the interactive login session.
Typically when I'm trying to figure out how specific functionality works, I have an API function or string to use as my guide. For example, if I'm red-teaming a DRM solution and I get a message box saying, "Invalid license key." then I can search in the binary for that string to see what code references it, or I can set a breakpoint on the Windows API functions that display message boxes. However, for the case of Outlook here, I didn't have any strings to base my investigation on, and I didn't know which API function(s) were being used to check for the first instance.
My first idea was to use an API logging tool like AutoDebug and run it once on the first Outlook session and once on the second Outlook session. I could then compare the API call logs and see where they differed, and then begin to investigate what caused them to differ at that point. However, I quickly found that API loggers such as AutoDebug are not suited for such a heavyweight program as Outlook (which imports a few thousand DLLs and a few million API functions (yes, I'm exaggerating, but it's still a lot)).
My second idea was to use a conditional-branch logger, such as http://www.woodmann.com/ollystuph/Conditional_Branch_Logger_v1.0.zip and run the same comparison as described above. However, I didn't have that plugin downloaded at the time and I didn't have Internet access, so I had to make-do with what was already on my laptop.
I used Process Explorer to watch what happens when the second instance of Outlook is launched. Sure enough, the process starts and then terminates. So I used OllyDbg to set a breakpoint on ExitProcess(...) to see if I could get a decent call-stack to see what code in Outlook led to the ExitProcess(...) call. The good news is that this allowed me to find the code that led to the process termination. The bad news is that it was called via _cexit(...) from ___tmainCRTStartup(...), so whatever code was detecting the first instance of Outlook was bailing out via ret's, not via a direct call to _cexit(...) or ExitProcess(...).
This led me to the old trustworthy Trial-and-Error-with-F8 method. The idea is simple -- starting from the process's Entry Point, step over (F8 in OllyDbg) every function call until you see the desired results, at which point you know the code in question lies within that function call. For this case, I was watching for a new window to pop up in the context of the first Outlook instance; by that time the check would already have been made to see if another instance of Outlook was running. The great thing about this approach is that it's incredibly straight-forward. The downside is that if you're looking for functionality that doesn't happen near the beginning of the process execution, it can be very time consuming. Luckily though, this method worked like a charm for Outlook!
I started the second Outlook process in OllyDbg, stepped over the first call and into a jump. No windows popped up yet, so I hadn't yet stepped over the call-in-question. I kept pressing F8 until I found that when I tried stepping over the call from address 0x2FD251C8 (this of course is specific to my computer; your addresses will differ), an Outlook window popped up in the context of the first Outlook process. So I set a breakpoint on 0x2FD251C8 and restarted my second Outlook process, this time stepping in (F7) to that call and pressing F8 again until I found the next call that opened the first Outlook window. I found that stepping over the call at address 0x2FD25228 caused the window to pop up, so I set a breakpoint on that address, restarted, stepped in, and continued this process for about two minutes until I found the following code:
.text:30006BB7 push offset WindowName ; "Microsoft Outlook"
.text:30006BBC push offset aMspim_wnd32 ; "mspim_wnd32"
.text:30006BC1 mov [ebp+var_42C], edi
.text:30006BC7 call ds:FindWindowA
This looks like the culprit! During Outlook's initialization, it checks to see if a window named "Microsoft Outlook" with class name "mspim_wnd32" exists, and if so, it assumes that another instance is already running. To test this, I set the return value of FindWindowA(...) from the call above to NULL, and Outlook opened a full second instance of itself in a separate process, and allowed me to use a different account profile.
This is a great example of where a very straight-forward reverse-engineering approach (Trial-and-Error-with-F8) can yield excellent results in just a few minutes given the right conditions.
As a disclaimer, I don't know the reason that the Outlook development team decided to restrict Outlook to a single instance. Perhaps multiple instances will cause massive data corruption. In other words, if you're going to patch your Outlook executable so that it does allow for multiple instances, do so at your own risk!
This post continued in Part 2.