Debugging - SSRS Report Viewer and IE11
03 Jun 2015 TweetRecently we upgraded IE in order to keep up with the EOL of Internet Explorer—for most of our enterprise that means we’re going to IE11. This started a while ago and it took some time for it to trickle down to a majority of the machines. In fact, some standard user machines were upgraded before the developer’s machines were. This lead to a number of broken pages and functionality. Aside from broken HTML pages was the SSRS Report Viewer control—after the upgrade it started crashing IE. You may have run into something like this after your upgrades and you don’t have a lot of options. Be prepared to read the words unsupported and not recommended … A LOT.
TL;DR
So, here is a disclaimer that you should read and take seriously. The patching I’m providing is not meant to be a long term fix. Hell, it might not even be something you’d even consider. So, in short I’d stay away from them unless you don’t have much of a choice. Also, to keep errant versions of the DLL from getting out in the wild, I will NOT provide the code. You will need to patch it yourself.
Not Recommended
- Patch the RSClientPrint.dll
- There is a 7 byte patch that will jump over the offending code. But, this would not be supported by Microsoft and could cause undefined behavior. Jump to the patch section.
Recommended
- Upgrade to the latest and greatest version of the Report Viewer control
- Write your own (if you can)
The Problem - IE11 crashes when printing a report
The crash would happen when attempting to print from inside IE11 using the print button on the report viewer control. The version information is below. When the crash would occur it there was no message, no warning, just a hard crash of the IE process. Depending on your organizations policies or even some old lurking fixes and workarounds, you may only be executing your browser sessions in one process (TabProcGrowth) if this was the case everything died and you could lose a lot of work progress.
In our case, reporting is part of a critical work flow and it definitely has an impact to the end users.
Name: RSClientPrint 2005 Class
Publisher: Microsoft Corporation
Type: ActiveX Control
Architecture: 32-bit
Version: 9.00.4053.00
File date: Wednesday, May 27, 2009, 3:26 AM
Date last accessed: Today, June 02, 2015, 1 hour ago
Class ID: {0D221D00-A6ED-477C-8A91-41F3B660A832}
Use count: 26
Block count: 0
File: RSClientPrint.dll
Folder: C:\Windows\Downloaded Program Files
Before we start down the path of who’s at fault, it should be noted that this version is unsupported by MS in our configuration. The lowest supported version is 2008 CU1 for IE11.
The Analysis
In the wild, we were getting reports of IE crashing randomly. It was crashing more times than it was not, but there were instances where the user was able to complete the workflow, which seems odd. If it crashes once, it should crash all of the time. Turns out it was during one specific action. Trying to print reports.
On the workstations we set up an ADPlus script to capture the unhandled exceptions. There was one exception in particular that was causing the crash. Let’s look at the stack trace below.
0:020> k
# ChildEBP RetAddr
00 0ae096e8 76eaf96a RSClientPrint!CPrintDlg::PrintHookProc+0xb7
01 0ae09710 76eaf64f comdlg32!PrintInitPrintDlg+0x422
02 0ae0980c 7708c4e7 comdlg32!PrintDlgProc+0xc57
03 0ae09838 770a5855 user32!InternalCallWinProc+0x23
04 0ae098b4 770a59f3 user32!UserCallDlgProcCheckWow+0xd6
05 0ae098fc 770a5be3 user32!DefDlgProcWorker+0xa8
06 0ae09918 7708c4e7 user32!DefDlgProcW+0x22
07 0ae09944 7708c5e7 user32!InternalCallWinProc+0x23
08 0ae099bc 77085294 user32!UserCallWinProcCheckWow+0x14b
09 0ae099fc 770a4f6c user32!SendMessageWorker+0x4d0
0a 0ae09ab8 770a3b16 user32!InternalCreateDialog+0xb0d
0b 0ae09ae8 770a3b76 user32!InternalDialogBox+0xa7
0c 0ae09b08 76eaec0f user32!DialogBoxIndirectParamAorW+0x37
0d 0ae09b3c 76eaf13e comdlg32!PrintDisplayPrintDlg+0xec
0e 0ae09f90 76eae95d comdlg32!PrintDlgX+0x575
0f 0ae0a448 63963a8b comdlg32!PrintDlgA+0x5e
10 0ae0a464 0cd87d04 IEShims!NS_UISuppression::APIHook_PrintDlgA+0x7d
11 0ae0a4f0 0cd87dab RSClientPrint!CPrintDlg::Print+0x102
12 0ae0a598 0cd7ee73 RSClientPrint!CPrintDlg::PrintDlgA+0x4d
13 0ae0b27c 774e3e75 RSClientPrint!CRSClientPrint::Print+0x6b
Pretty much it says that the RSClientPrint application is faulting at this instruction. And as it would be the context in this stack indicates it’s when we open the print dialog. We had two dumps that looked the exact same so I’m certain it’s going to occur here. As it would be, nothing has changed from the application perspective or the machine, so it’s definitely IE.
Let’s look at the instruction that caused the fault.
0:020> r
eax=0000006c ebx=0bf66770 ecx=00005511 edx=773770f4 esi=0bf67300 edi=0ae09fa4
eip=0cd873ee esp=0ae096e0 ebp=0ae096e8 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
RSClientPrint!CPrintDlg::PrintHookProc+0xb7:
0cd873ee 8b4804 mov ecx,dword ptr [eax+4] ds:0023:00000070=????????
So, we’re definitely trying to access some invalid memory, address 0x70 is certainly not something we can get to directly in user mode. And something we shouldn’t be fiddling with in kernel mode either.
If we look back up the instruction stack we can see that the EAX register is populated with ESI+0x44. That instruction is at 0x0cd873e7. Looks simple enough, but what is so important that it needs to get this data?
0:020> ub @eip
RSClientPrint!CPrintDlg::PrintHookProc+0xa1:
0cd873d8 83664400 and dword ptr [esi+44h],0
0cd873dc 59 pop ecx
0cd873dd eb48 jmp RSClientPrint!CPrintDlg::PrintHookProc+0xf0 (0cd87427)
0cd873df ff7508 push dword ptr [ebp+8]
0cd873e2 e83bf5ffff call RSClientPrint!CPrintDlg::SetDialogResources (0cd86922)
0cd873e7 8b4644 mov eax,dword ptr [esi+44h]
0cd873ea 85c0 test eax,eax
0cd873ec 741e je RSClientPrint!CPrintDlg::PrintHookProc+0xd5 (0cd8740c)
0:020> u @eip L 10
RSClientPrint!CPrintDlg::PrintHookProc+0xb7:
0cd873ee 8b4804 mov ecx,dword ptr [eax+4]
0cd873f1 8b780c mov edi,dword ptr [eax+0Ch]
0cd873f4 8b10 mov edx,dword ptr [eax]
0cd873f6 8b4008 mov eax,dword ptr [eax+8]
0cd873f9 6a01 push 1
0cd873fb 2bf9 sub edi,ecx
0cd873fd 57 push edi
0cd873fe 2bc2 sub eax,edx
0cd87400 50 push eax
0cd87401 51 push ecx
0cd87402 52 push edx
0cd87403 ff7508 push dword ptr [ebp+8]
0cd87406 ff157c13d70c call dword ptr [RSClientPrint!_imp__MoveWindow (0cd7137c)]
<<< REMOVED >>>
Ah, so it’s calling MoveWindow()
and setting the window position of the dialog and forcing a redraw. Look at the instruction at 0x0cd873ea in the first disassembly—test eax,eax
. This is checking for 0 (or nullptr) at that memory address. If it’s not 0 it will continue down the execution path. So, if we were to put a little thought into the entire disassembly of the RSClientPrint!CPrintDlg::PrintHookProc
code, it might look like this:
INT_PTR CALLBACK DialogProc(
HWND hwndDlg,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
if (uMsg == WM_INITDIALOG)
{
SetDialogResources(hwndDlg);
if (CPrintDlg::m_WindRect != NULL)
{
MoveWindow(hwndDlg, CPrintDlg::m_WindRect->top,
CPrintDlg::m_WindRect->left,
(CPrintDlg::m_WindRect->bottom - CPrintDlg::m_WindRect->top),
(CPrintDlg::m_WindRect->right - CPrintDlg::m_WindRect->left),
TRUE);
}
return 0;
}
else if (uMsg == WM_COMMAND)
{
if (wParam == CMD_PREVIEW) //0x2B - Open Preview
{
// Do some checks
CPrintDlg::g_WindRectFlag = true;
if (CPrintDlg::m_WindRect != NULL)
{
delete CPrintDlg::m_WindRect;
}
g_WindRect = new RECT();
if (g_WindRect != NULL)
{
ZeroMemory(CPrintDlg::m_WindRect, sizeof(RECT));
GetWindowRect(hwndDlg, CPrintDlg::m_WindRect);
MoveWindow(hwndDlg, g_WindRect->top,
CPrintDlg::m_WindRect->left,
(CPrintDlg::m_WindRect->bottom - CPrintDlg::m_WindRect->top),
(CPrintDlg::m_WindRect->right - CPrintDlg::m_WindRect->left),
TRUE);
// Start the preview Window
SendMessageA(hwndDlg, WM_COMMAND, MAKEWPARAM(1, 0),0);
return 0;
}
}
if (wParam < CMD_UNK1) //0x02 - Close Preview
{
if (CPrintDlg::m_WindRect != NULL && CPrintDlg::g_WindRectFlag != false)
{
delete CPrintDlg::m_WindRect;
CPrintDlg::m_WindRect = NULL;
}
return 0;
}
if (wParam < CMD_UNK2) //0x22B - Exit Dialog
{
CPrintDlg::m_WindRectFlag = false;
return 1;
}
}
return 0;
}
The code is what I reversed from the assembly for the DialogProc. I didn’t go full out on it and recreate the class. But you will see a couple of names like
CPrintDlg::m_WindRect
which are just made up.
The code is pretty solid (save for any errors I may have added). But there is one problem. In the WM_INITDIALOG
if statement at the top there is a potential for us to use uninitialized or once-initialized memory. In the case of the exception we were using address space 0x70
which, as you (should) know, is invalid most of the time.
The Patch
In order to perform this I used HxD. Any good hex editor will do.
Here are the quick steps for this:
- Open %WINDIR%\Downloaded Program Files\RSClientPrint.dll
- Search for
e83bf5ffff
(call to SetDialogResources) - Directly after that enter
C6 46 44 00 EB 3C 90
starting at offset 0x167E7 - Save file.
You can also check out my post on Fixing a deadlock if you’d like to know how to do it all in WinDbg.
NOTE This has only been tested on version 9.00.4053.00, if you use any other version you will have to do the analysis to find the code and offest.
Here is what we added. Pay special attention to the jmp instruction eb 3c
. This is a short jump to an offset 0x3c (60) bytes from this location. If you have a different file version there is no guarantee this will work for you.
c6464400 mov [esi+44h],0
eb3c jmp RSClientPrint!CPrintDlg::PrintHookProc+0xf0
90 nop
The key bits of code are right near the top of the reversed C++ code in the previous section. If we can somehow avoid the else statement we shouldn’t fault when we initialize the application.
// Snippet from previous section.
if (uMsg == WM_INITDIALOG)
{
SetDialogResources(hwndDlg);
if (CPrintDlg::m_WindRect != NULL)
{
MoveWindow(hwndDlg, CPrintDlg::m_WindRect->top,
CPrintDlg::m_WindRect->left,
(CPrintDlg::m_WindRect->bottom - CPrintDlg::m_WindRect->top),
(CPrintDlg::m_WindRect->right - CPrintDlg::m_WindRect->left),
TRUE);
}
return 0;
}
And the assembly.
RSClientPrint!CPrintDlg::PrintHookProc+0xa1:
0cd873d8 83664400 and dword ptr [esi+44h],0
0cd873dc 59 pop ecx
0cd873dd eb48 jmp RSClientPrint!CPrintDlg::PrintHookProc+0xf0 (0cd87427)
0cd873df ff7508 push dword ptr [ebp+8]
0cd873e2 e83bf5ffff call RSClientPrint!CPrintDlg::SetDialogResources (0cd86922)
0cd873e7 8b4644 mov eax,dword ptr [esi+44h]
0cd873ea 85c0 test eax,eax
0cd873ec 741e je RSClientPrint!CPrintDlg::PrintHookProc+0xd5 (0cd8740c)
If you look at line 0cd873e7
you see where we are moving the pointer into the EAX register and testing it for NULL. If we look at the code we have 7 bytes to play with and there is a set of code that happens to fit in this spot.
// Snippet from previous section.
if (uMsg == WM_INITDIALOG)
{
SetDialogResources(hwndDlg);
CPrintDlg::m_WindRect = NULL;
return 0;
}
And the assembly.
RSClientPrint!CPrintDlg::PrintHookProc+0xa1:
0cd873d8 83664400 and dword ptr [esi+44h],0
0cd873dc 59 pop ecx
0cd873dd eb48 jmp RSClientPrint!CPrintDlg::PrintHookProc+0xf0 (0cd87427)
0cd873df ff7508 push dword ptr [ebp+8]
0cd873e2 e83bf5ffff call RSClientPrint!CPrintDlg::SetDialogResources (0cd86922)
0cd873e7 c6464400 mov [esi+44h],0
0cd873ec eb3c jmp RSClientPrint!CPrintDlg::PrintHookProc+0xf0 (45637427) Branch
0cd873ef 90 nop
With this “simple” patch it effectively sets g_WindRect to 0 and jumps over the code that tries to set the Window position. I could have also just set a bunch of nop
instructions and jumped to RSClientPrint!CPrintDlg::PrintHookProc+0xf0
which is the start of the return 0
portion of the disassembly. However, that would leave a dangling pointer.
Of course it goes without saying that this is completely unsupported and it brings about some other issues. Namely we haven’t taken care of [esi+0x48] (CPrintDlg::m_WindRectFlag)
who knows what state this is in and if it’s getting overwritten.
Final Notes
All of the ribs about IE11 breaking compatibility and the lack of preparation aside; IE11 is way better for security and “future proofing” your web applications. Plus as you might already know you only have until Feb 2016 to upgrade. This doesn’t leave a lot of time. Depending on your application, your vendor, or your budget you may not be in a position to upgrade.
The real fix here is to upgrade. If you can’t upgrade you can try these solutions for yourself, but they come with a cost of possible instability or security errors. So, of course, proceed with caution.