You can read a contract from the other side

Date:December 26, 2003 / year-entry #178
Tags:code
Orig Link:https://blogs.msdn.microsoft.com/oldnewthing/20031226-00/?p=41343
Comments:    8
Summary:An interface is a contract, but remember that a contract applies to both parties. Most of the time, when you read an interface, you look at it from the point of view of the client side of the contract, but often it helps to read it from the server side. For example, let's look at...

An interface is a contract, but remember that a contract applies to both parties. Most of the time, when you read an interface, you look at it from the point of view of the client side of the contract, but often it helps to read it from the server side.

For example, let's look at the interface for control panel applications.

Most of the time, when you're reading this documentation, you are wearing your "I am writing a Control Panel application" hat. So, for example, the documentation says

When the controlling application first loads the Control Panel application, it retrieves the address of the CPlApplet function and subsequently uses the address to call the function and pass it messages.

With your "I am writing a Control Panel application" hat, this means "Gosh, I had better have a function called CPlApplet and export it so I can receive messages."

But if you are instead wearing your "I am hosting a Control Panel application" hat, this means, "Gosh, I had better call GetProcAddress() to get the address of the application's CPlApplet function so I can send it messages."

Similarly, under the "Message Processing" section it lists the messages that are sent from the controlling application to the Control Panel application. If you are wearing your "I am writing a Control Panel application" hat, this means "Gosh, I had better be ready to receive these messages in this order." But if you are wearing your "I am hosting a Control Panel application" hat, this means "Gosh, I had better send these messages in the order listed."

And finally, when it says "the controlling application release the Control Panel application by calling the FreeLibrary function," your "I am writing a Control Panel application" hat says "I had better be prepared to be unloaded," whereas your "I am hosting a Control Panel application" hat says, "This is where I unload the DLL."

So let's try it. As always, start with our scratch program and change the WinMain:

#include <cpl.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                   LPSTR lpCmdLine, int nShowCmd)
{
  HWND hwnd;

  g_hinst = hinst;

  if (!InitApp()) return 0;

  if (SUCCEEDED(CoInitialize(NULL))) {/* In case we use COM */

      hwnd = CreateWindow(
          "Scratch",                      /* Class Name */
          "Scratch",                      /* Title */
          WS_OVERLAPPEDWINDOW,            /* Style */
          CW_USEDEFAULT, CW_USEDEFAULT,   /* Position */
          CW_USEDEFAULT, CW_USEDEFAULT,   /* Size */
          NULL,                           /* Parent */
          NULL,                           /* No menu */
          hinst,                          /* Instance */
          0);                             /* No special parameters */

      if (hwnd) {
        TCHAR szPath[MAX_PATH];
        LPTSTR pszLast;
        DWORD cch = SearchPath(NULL, TEXT("access.cpl"),
                     NULL, MAX_PATH, szPath, &pszLast);
        if (cch > 0 && cch < MAX_PATH) {
          RunControlPanel(hwnd, szPath);
      }
    }
    CoUninitialize();
  }

  return 0;
}

Instead of showing the window and entering the message loop, we start acting like a Control Panel host. Our victim today is access.cpl, the accessibility control panel. After locating the program on the path, we ask RunControlPanel to do the heavy lifting:

void RunControlPanel(HWND hwnd, LPCTSTR pszPath)
{
  // Maybe this control panel application has a custom manifest
  ACTCTX act = { 0 };
  act.cbSize = sizeof(act);
  act.dwFlags = 0;
  act.lpSource = pszPath;
  act.lpResourceName = MAKEINTRESOURCE(123);
  HANDLE hctx = CreateActCtx(&act);
  ULONG_PTR ulCookie;
  if (hctx == INVALID_HANDLE_VALUE ||
      ActivateActCtx(hctx, &ulCookie)) {

    HINSTANCE hinstCPL = LoadLibrary(pszPath);
    if (hinstCPL) {
      APPLET_PROC pfnCPlApplet = (APPLET_PROC)
        GetProcAddress(hinstCPL, "CPlApplet");
      if (pfnCPlApplet) {
        if (pfnCPlApplet(hwnd, CPL_INIT, 0, 0)) {
          int cApplets = pfnCPlApplet(hwnd, CPL_GETCOUNT, 0, 0);
          //  We're going to run application zero
          //  (In real life we might show the user a list of them
          //  and let them pick one)
          if (cApplets > 0) {
            CPLINFO cpli;
            pfnCPlApplet(hwnd, CPL_INQUIRE, 0, (LPARAM)&cpli);
            pfnCPlApplet(hwnd, CPL_DBLCLK, 0, cpli.lData);
            pfnCPlApplet(hwnd, CPL_STOP, 0, cpli.lData);
          }
        }
        pfnCPlApplet(hwnd, CPL_EXIT, 0, 0);
      }

      FreeLibrary(hinstCPL);
    }

    if (hctx != INVALID_HANDLE_VALUE) {
      DeactivateActCtx(0, ulCookie);
      ReleaseActCtx(hctx);
    }
  }
}

Ignore the red lines for now; we'll discuss them later.

All we're doing is following the specification but reading it from the host side. So we load the library, locate its entry point, and call it with CPL_INIT, then CPL_GETCOUNT. If there are any control panel applications inside this CPL file, we inquire after the first one, double-click it (this is where all the interesting stuff happens), then stop it. After all that excitement, we clean up according to the rules set out for the host (namely, by sending a CPL_EXIT message.)

So that's all. Well, except for the red parts. What's that about?

The red parts are to support Control Panel applications that have a custom manifest. This is something new with Windows XP and is documented in MSDN here.

If you go down to the "Using ComCtl32 Version 6 in Control Panel or a DLL That Is Run by RunDll32.exe" section, you'll see that the application provides its manifest to the Control Panel host by attaching it as resource number 123. So that's what the red code does: It loads and activates the manifest, then invites the Control Panel application to do its thing (with its manifest active), then cleans up. If there is no manifest, CreateActCtx will return INVALID_HANDLE_VALUE. We do not treat that as an error, since many programs don't yet provide a manifest.

Exercise: What are the security implications of passing NULL as the first parameter to SearchPath?


Comments (8)
  1. Jack Mathews says:

    Well, someone could either:

    Jam a new .cpl in the same dir as the EXE.. Or if they’re on a LUA, they could put a malicious .cpl in their home directory and set it as the current directory before running your program. That would let them run their own code, bypassing policies.

  2. Jordan Russell says:

    "What are the security implications of passing NULL as the first parameter to SearchPath?"

    A rogue access.cpl file could be placed in the current directory, and SearchPath would find it before the one in the system directory. (?)

    BTW: Why do some functions (such as this CreateFile and this CreateActCtx) return INVALID_HANDLE_VALUE when they fail while others return zero?

  3. Jordan Russell says:

    Somehow I missed Jack’s comment. Whoops.

  4.    if (cch &gt; 0 &amp;&amp; cch &lt; MAX_PATH) {
    

    > RunControlPanel(hwnd, szPath);

    > }

    Missing a } :-)

  5. Raymond Chen says:

    Sorry bout that. Lost during editing.

  6. Why does INVALID_HANDLE_VALUE even exist? When CreateFile() fails to open a file, its return value can be either 0 or -1. The only reliable way to check is to ignore the MSDN Library and just test the result for <= 0.

  7. Raymond Chen says:

    Believe it or not, that’s a topic I already had planned out for a future entry.

  8. JBohm says:

    INVALID_HANDLE_VALUE is -1 cast to the data type HANDLE. HANDLE is either ULONG_PTR or PVOID depending if STRICT is defined at compile time (always is for C++, optional for C). In either case, the test <= 0 is wrong because ((HANDLE)-200) is not an error (it is a real handle in some functions) and because ((ULONG_PTR)-1) > 0 .

    Anyway, INVALID_HANDLE_VALUE exists just for the benefit of people porting code from the POSIX/UNIX/C functions that return -1 on error rather then NULL, such as creat(), open() lseek() etc.

    I have never heard of CreateFile returning NULL, but then my code routinly does:

    if (h == INVALID_HANDLE_VALUE) h = NULL;

    if (!h)

    ItFailed!

    Oh and INVALID_HANDLE_VALUE is a valid handle to the current process (at least on ntoskrnl.exe based operating systems). It is the success return value from GetCurrentProcess().

Comments are closed.


*DISCLAIMER: I DO NOT OWN THIS CONTENT. If you are the owner and would like it removed, please contact me. The content herein is an archived reproduction of entries from Raymond Chen's "Old New Thing" Blog (most recent link is here). It may have slight formatting modifications for consistency and to improve readability.

WHY DID I DUPLICATE THIS CONTENT HERE? Let me first say this site has never had anything to sell and has never shown ads of any kind. I have nothing monetarily to gain by duplicating content here. Because I had made my own local copy of this content throughout the years, for ease of using tools like grep, I decided to put it online after I discovered some of the original content previously and publicly available, had disappeared approximately early to mid 2019. At the same time, I present the content in an easily accessible theme-agnostic way.

The information provided by Raymond's blog is, for all practical purposes, more authoritative on Windows Development than Microsoft's own MSDN documentation and should be considered supplemental reading to that documentation. The wealth of missing details provided by this blog that Microsoft could not or did not document about Windows over the years is vital enough, many would agree an online "backup" of these details is a necessary endeavor. Specifics include:

<-- Back to Old New Thing Archive Index