How can I tell that somebody used the MAKEINTRESOURCE macro to smuggle an integer inside a pointer?


Many functions and interfaces provide the option of passing either a string or an integer. The parameter is formally declared as a string, and if you want to pass an integer, you smuggle the integer inside a pointer by using the MAKE­INT­RESOURCE macro. For example, the Find­Resource function lets you load an resource specified by integer identifier by passing the identifier in the form MAKE­INT­RESOURCE(ID). You can tell that it was the resource-loading functions who created the macro in the first place, since the name of the macro is "make integer resource."

But other functions use the MAKE­INT­RESOURCE convention, too. The Get­Proc­Address function lets you obtain a function exported by ordinal if you smuggle the ordinal inside a pointer: Get­Proc­Address(hModule, MAKE­INT­RESOURCEA(ordinal)). (You have to use MAKE­INT­RESOURCEA because Get­Proc­Address explicitly takes an ANSI string.)

What if you're implementing a function whose interface requires you to accept both strings and integers-smuggled-inside strings? For example, maybe you're implementing IContext­Menu::Invoke­Command, which needs to look at the CM­INVOKE­COMMAND­INFO.lpVerb member and determine whether the invoker passed a string or a menu offset.

You can use the IS_INT­RESOURCE macro. It will return non-FALSE if the pointer you passed is really an integer smuggled inside a pointer.

How does MAKE­INT­RESOURCE work? It just stashes the integer in the bottom 16 bits of a pointer, leaving the upper bits zero. This relies on the convention that the first 64KB of address space is never mapped to valid memory, a convention that is enforced starting in Windows 7.

Comments (22)
  1. Damien says:

    That last sentence just made me laugh – is there some convoluted path that could lead to some (correctly written) code on earlier Windows accidentally tripping over this? Or would it always have been hacked-together bodges that could fall foul of it (and have the string parameter misinterpreted)?

    [It has always been the case that you could never pass a pointer to memory in the bottom 64KB to a function that accepts MAKEINT­RESOURCE, because the function would treat it as a smuggled integer. Any code which did that was already broken. -Raymond]
  2. Joshua says:

    [a convention that is enforced starting in Windows 7.]

    Is that 64 bit specific? I'm told passing 0x1 to VirtualAlloc worked on 32 bit until Windows 8. They're still looking for the call on Windows 8 that is used to initialize NTVDM's low memory. On W7, it was believed this was the call.

    The only good reason to do this is to emulate the environment from ancient BSD (to run libraries from that env) that abused the fact that *NULL = 0 for any simple type because the zero page was mapped read-only at address 0.

    [NTVDM is the only program allowed to commit memory at 0. It has a special exemption. -Raymond]
  3. Kevin says:

    @Damien

    I think it is referring more to the possibility that the previous versions of Windows did not use the first 64KB of address space as an implementation detail, whereas now it is specified factually in the docs.

    The old style seems similar to how Raymond gives us lots of implementation details about stuff and then says "this isn't official, and can change at any time (and in fact it has)."

  4. So MAKEINTRESOURCE is conceptually "LPTSTR STRINGIFY_WORD(WORD w);" and IS_INTRESOURCE is conceptually "bool IS_STRINGIFIED_WORD(LPTSTR s);"?

    Is there a conceptual equivalent of "WORD UNSTRINGIFY_WORD(LPTSTR s);" or does one just cast the pointer to an int?

    [LOWORD is the unstringifier. -Raymond]
  5. alegr1 says:

    The origin of this smuggling operation lies in 16-bit Windows protected mode.

    A far pointer consists of 16 bit selector and 16 bit offset. If a selector is zero, the memory is not accessible. Thus, any valid pointer never has a zero selector, and can be distinguished from a resource ID.

    [Actually, it came from 16-bit real mode. The first 64KB of memory was used for interrupt vectors and stuff, so Windows could not use it. -Raymond]
  6. Yuhong Bao says:

    "They're still looking for the call on Windows 8 that is used to initialize NTVDM's low memory. "

    I think this is part of why NTVDM is disabled by default on Win8.

  7. Matt says:

    @Raymond. The convention was only mandated in Windows8 (except in NTVDM, which is opted out via a bit in the EPROCESS structure). In Windows 7 any process can allocate memory below 64KB by requesting it through VirtualAlloc.

  8. Myria says:

    The enforcement started in Windows 8, not Windows 7.  To allocate the zero page on 7 and below, call VirtualAlloc[Ex] with lpAddress equal to 1.

    The reason the zero page has to be available to NTVDM is that the x86 series does not have a way to use a base address for V86-mode segment translation other than 00000000.  It'd be nice if Windows instead used software emulation for 16-bit real-mode programs rather than try to use V86 mode; this would be less risky than V86 mode, and would even allow it to work on 64-bit Windows.

    Then again, backward compatibility isn't as much a concern lately as it has been in the past.

  9. Myria says:

    On a somewhat related note, I recently had to call GetProcAddress from C# and needed both the string version and ordinal version.  This was the trick I used to work around the inability to represent MAKEINTRESOURCEA in C#:

    [DllImport("kernel32.dll", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)]

    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    [DllImport("kernel32.dll", EntryPoint = "GetProcAddress", SetLastError=true)]

    public static extern IntPtr GetProcAddressOrdinal(IntPtr hModule, IntPtr procName);

  10. Matt says:

    @Myria

    You don't need to use IntPtr – just use int:

    [DllImport("kernel32.dll", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)]

    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    [DllImport("kernel32.dll", EntryPoint = "GetProcAddress", SetLastError=true)]

    public static extern IntPtr GetProcAddressOrdinal(IntPtr hModule, int ordinalNumber);

    This works because even on 64-bit (where an int is still 32-bit) because parameters < 64bits are sign-extended to 64-bits before being sent as part of the x64 API, so even if you send an int to the API it'll arrive at the C API's LPCSTR with the top-bits set to zero.

    [Um, there are other 64-bit ABIs beyond x64. -Raymond]
  11. OK, so the complete set is:

    LPTSTR STRING_FROM_WORD(WORD w) { return MAKEINTRESOURCE(w); }

    BOOL STRING_REALLY_WORD(LPTSTR s) { return IS_INTRESOURCE(s); }

    BOOL WORD_FROM_STRING(LPTSTR s, WORD *pw) { if (STRING_REALLY_WORD(s)) { *pw = LOWORD(s); return TRUE; } else { return FALSE; } }

  12. 640k says:

    [Actually, it came from 16-bit real mode. The first 64KB of memory was used for interrupt vectors and stuff, so Windows could not use it. -Raymond]

    The interrupt table only allocates the first 1k bytes. "Stuff" are reserving 63k of the precious 640k real memory to ease resource loading? Bloat.

    [If you want to use a string in the bottom 64KB, just use a 16:16 alias. The only place it doesn't work is the first 16 bytes, so don't put strings there. -Raymond]
  13. Mike Dimmick says:

    @Myria: Backward compatible with applications that, at best estimate, were probably last updated 15 years ago?

    There comes a time to turn it off. I suspect – unlike Yuhong Bao – that off-by-default in Windows 8 is mostly to reduce possible attack surface area (there was a security bulletin, MS13-063, issued just last month), partly to gather telemetry data on whether the feature is even necessary.

    An emulator would be a huge investment of time and effort for little to no benefit. MS didn't bother for x64, I don't expect them to do so for anything else. There's always DosBox.

  14. Matt says:

    @Raymond:

    >> "Um, there are other 64-bit ABIs beyond x64. -Raymond"

    Sure. But x64 is the only supported 64-bit ABI for Windows now that it no longer supports Itanium, and since ARM is 32-bit only for Windows.

  15. Cesar says:

    In case people do not know: the importance to not allowing user space programs to map the bottom few pages is to protect against null pointer dereference attacks against the kernel.

    On x86, the user space and the kernel share a single address space, with the kernel on the top and the user space on the bottom. If the kernel has a bug and tries to read or write through a null pointer (usually plus a offset due to the null pointer being a pointer to a struct), a user space program who could map the bottom few pages could manipulate the result and confuse the kernel enough to make it execute arbitrary code from user space. As a simple example, imagine what happens if the kernel was reading a function pointer from a struct, and the pointer to the struct was a null pointer.

    A few modern processors have other ways (like SMAP and SMEP) to prevent against this class of exploit, but completely forbidding the user space from mapping the first page of the address space is a cheap way to turn many of these kinds of exploitable bugs into mere crashes.

  16. Neil says:

    MAKEINTRESOURCE is just a wrapper around MAKELONG(i, 0) which computes to i | (0 << 16) which is straightforward enough. However 16-bit compilers weren't very good at handling other uses of MAKELONG with arbitrary arguments, handing the result off to a library method to perform the 32-bit shift. (These days compilers are much smarter and can do stuff like (u << 8) | (u >> 8) using a rotate instruction.)

    Given a specific compiler, it was possible to avoid the library call by using a compiler-specific extension. Let's see how many of them I can remember:

    For TopSpeed C, there was a special type void <> * which was a 16-bit pointer into the given segment, so you could write ((void _far *)(void <b> *)a).

    For Turbo C, there was a special type void _seg * to which you just added the offset the the segment to get a far pointer, i.e. ((void _seg *)b + (void _near *)a).

    For MSVC there was a special type _segment which was similar to void _seg * but you had to use a special :> operator to combine the segment with a _based pointer i.e. ((_segment)b + (void _based(void) *)a).

    (Note that to use these in the MAKELONG macro you would also want to cast a and b to unsigned and the result to unsigned long.)

    With Turbo C and MSVC you could also use the segment type as a fast HIWORD(l) operation i.e. ((unsigned)(void _seg *)(void _far *)l). Then something like return HIWORD(f()) would result in call f; mov ax, dx; leave; ret instead of call f; push ax; push dx; push 16; call _lsr32; leave; ret.

  17. Cesar says:

    @Matt: for how long? Now that ARMv8 processors are starting to appear, if AArch64 (64-bit ARM) servers start becoming popular, Microsoft probably will specify its own ABI for it (gratuitously incompatible with the Linux one, like they did on x86-64).

    Depending on the particularities of a single 64-bit ABI is unwise, especially in code which can be deployed to future processors without needing intervention by the original developer (like code written purely in Java or C#).

  18. voo says:

    @Matt Just what Cesar is saying, ARM is pushing heavily into the server space and has its own 64-bit API now.

    Also if MS decided to follow the official ABI defined by the manufacturer for once, if you pass an integer to a function in W0, there's nothing in there that says that it's sign-extended to X0, so doubly unwise to rely on this fact.

  19. Klimax says:

    @Cesar: I doubt it was "gratuitously". There often lots of things to consider, but I doubt one of them is compatibility/incompatibility with Linux.

    (Quite assumptions)

  20. ender says:

    > Backward compatible with applications that, at best estimate, were probably last updated 15 years ago?

    We have a client that still uses DOS-based accounting software (written in Clipper) – that's still getting regular updates…

  21. Joshua says:

    @voo: Yeah I noticed that the ABIs weren't compatible. You know what. It matters so little. Assembly language in x64 is something only used by necessity, not for performance, so portability should hardly be considered an issue on the few k of glue code.

  22. Cesar says:

    @Joshua: What? x86-64 assembly is often used for performance. Take a look at high performance video encoders or decoders, like for instance x264 or ffmpeg. And they are often portable. (ffmpeg has a bit of trouble being compiled with MSVC, but that is because MSVC still does not have support for the C99 standard; someone wrote a C99-to-C89 converter to work around this issue.)

Comments are closed.

Skip to main content