Geoff Chappell - Software Analyst
Windows 98 includes a Personal Web Server (PWS) version 4.0 which the Windows 98 Resource Kit describes as “the answer to your personal information sharing and Web development needs.” A notable use is that “you can create your site in the office or at home and test it by using Personal Web Server before hosting it on the corporate server or an Internet service provider.” PWS is supplied in an ADD-ONS directory on the Windows 98 CD. Instructions for installation are given in the Windows 98 Help.
The Personal Web Server includes a program for administration. This program is named the Personal Web Manager and is implemented in the PWS.EXE file. The PWS setup program prepares two methods for easy access to this PWS administrator program. First, there is ordinarily a shortcut named Personal Web Manager for use from the Start menu (via Programs, Internet Explorer and Personal Web Server). Second, registry entries are ordinarily set so that a program named PWSTRAY.EXE will run automatically at startup. This leaves an icon in the system tray. The Personal Web Manager can then be run easily by double-clicking that icon (or by right-clicking on the icon and selecting Properties).
The PWS.EXE file is ordinarily in the INETSRV subdirectory of the Windows SYSTEM directory, but it can be elsewhere. PWSTRAY finds PWS.EXE via the registry:
Key | HKEY_LOCAL_MACHINE\Software\Microsoft\INetStp |
Value | InstallPath |
Closing the Personal Web Manager—even just opening and closing, without doing anything in the program—may produce the following error message:
This program has performed an illegal operation
and will be shut down.If the problem persists, contact the program
vendor.
The title of this message box is simply Pws. Clicking the Details button reveals something like:
PWS caused an invalid page fault in
module MSVCRT.DLL at 015F:78001799.
along with Registers, Bytes at CS:EIP and a Stack dump.
Inspection reveals a coding error in the PWS.EXE program (described in detail below). In its essence, the error is that when cleaning up on exit, PWS calls the C Run-Time Library (CRT) function named free twice for the same address.
In the design of PWS.EXE, the free function is obtained through dynamic linking with MSVCRT.DLL. Some MSVCRT versions are highly susceptible to this particular programming error of attempting to free a heap block twice. See the Microsoft Knowledge Base article FIX: Freeing memory multiple times may cause an application error in Visual C++.
The coding error has been identified in two PWS.EXE versions. Details such as file sizes, time stamps and known distribution packages are given in the following table.
Version | Size | Date and Time | Package |
---|---|---|---|
4.02.0622 | 396,576 | 15:25, 16th November 1997 | Windows NT Option Pack 4.0 |
4.02.0634 | 397,600 | 17:28, 25th February 1998 | Windows 98 |
The coding error may go unnoticed, depending on the MSVCRT version. Detailed examination of different MSVCRT versions for their susceptibility to PWS’s coding error is beyond the scope of this article, but the following summary seems reasonable. The MSVCRT versions supplied in the same package as PWS are not highly susceptible. Symptoms such as invalid page faults are likely to occur only if a more recent MSVCRT has been acquired by installing other software. The Knowledge Base article FIX: You receive an “invalid page fault in module MSVCRT.DLL” error message after you install the run-time libraries from Visual C++ 6.0 is relevant, despite being directed at users of Visual Studio rather than users of the affected programs.
Among much other work, PWS defines a simple class, named CImpIMSAdminBaseSink, that implements an IMSAdminBaseSinkW interface and a reference count. The interface is defined in IADMW.H, as supplied with the Platform SDK. Microsoft’s name for the class is knowable from Run-Time Type Information (RTTI) in the executable, but anyway follows a naming convention in Microsoft’s COM and OLE literature.
As usual for objects in the Component Object Model (COM), a virtual function named AddRef increments the given object’s reference count. A virtual function named Release decrements the reference count. If this brings the reference count to zero, then Release destroys the object.
A new CImpIMSAdminBaseSink comes from the heap, as implemented in MSVCRT. The CImpIMSAdminBaseSink constructor sets zero as the initial reference count.
A CImpIMSAdminBaseSink object is created to help with the implementation of a more complex class that is named CPwsDoc. This name is known not only from RTTI but also because PWS registers it for MFC run-time class information. When a CPwsDoc object is to be destroyed and a CImpIMSAdminBaseSink object remains associated with the CPwsDoc object, then the CPwsDoc destructor also destroys and deletes the associated CImpIMSAdminBaseSink object.
A CPwsDoc object may pass its associated CImpIMSAdminBaseSink object elsewhere, through the Advise method of an IConnectionPoint interface. The first part of the coding error is that when a CImpIMSAdminBaseSink object is passed outside PWS, the object’s reference count is still zero. The recipient may legitimately call the CImpIMSAdminBaseSink object’s AddRef function and match these with calls to the object’s Release function. Because PWS has let the reference count start as zero for these operations, the last call to the object’s Release function will bring the object’s reference count back to zero and cause the object to destroy itself. The space the object occupied on the heap will be freed. This is the first of the two calls to the free function in MSVCRT.
For the second part of the error, consider the effect on the CPwsDoc object with which the CImpIMSAdminBaseSink object had been associated. Neither the recipient of the CImpIMSAdminBaseSink object nor the object itself knows anything of the pointer that the CPwsDoc object keeps to the CImpIMSAdminBaseSink object. The latter object exists no more, but the pointer in the CPwsDoc object remains. When the time eventually comes to destroy the CPwsDoc object, there will seem to be an associated CImpIMSAdminBaseSink object that should be destroyed also. This is the second of the two calls to the free function.
If the association of CImpIMSAdminBaseSink object to CPwsDoc object persists while the CImpIMSAdminBaseSink object is passed to essentially unknowable code outside PWS, then the CImpIMSAdminBaseSink object’s reference count should be at least one by the time the object is passed outside. The easiest way to achieve this, given the impossibility of modifying the source code to fix the problem properly, is to have the object’s constructor initialise the reference count to one, not zero.
The constructor begins:
mov eax,ecx and dword ptr [eax+4],0
Here, ecx is the constructor’s this argument, being the address of a new (but uninitialised) CImpIMSAdminBaseSink object. The address of this object is also to be returned by the function, hence the copy from ecx to eax. The second instruction initialises the object’s reference count to zero. After this, it remains only to set the object’s pointer to its virtual function table, and then to return.
To have the constructor set 1 as the initial reference count, the ideal would be to patch just the second instruction. The difficulty here is that the existing coding saves space by doing the initialisation to zero as an and (rather than the immediately obvious mov), making good use of a short form in which sign extension of the immediate operand from a byte to a dword is implied. Done this way, the instruction for initialising to 0 takes just four bytes. An initialisation of a dword to 1 does not seem possible in just four bytes. The best seems to be five.
Since the constructor is not required to preserve ecx, one extra byte can be taken from the first instruction. This produces the patch. The six bytes of the two instructions above are to be replaced by the six bytes of the three instructions below:
xchg eax,ecx push 1 pop dword ptr [eax+4]
The problem can be corrected by patching six bytes in the PWS.EXE file. The patch site, given as an offset in bytes from the start of the file, varies with the version:
Version | File Offset |
---|---|
4.02.0622 | A8D1h |
4.02.0634 | AB3Ch |
The expected bytes at the site are 8B C1 83 60 04 00. They are to be changed to 91 6A 01 8F 40 04. If you are even slightly uncertain how to patch a file, do not try it.