Was there a problem with Windows 95-era programs relying on undocumented information disclosure stuff?

Tihiy noted that back in the Windows 95 days, there was a lot of undocumented stuff lying around in places that are formally undefined. For example, the return value of Is­Window is formally zero or nonzero, but it turns out that on Windows 95, the nonzero value actually was a pointer to an internal data structure. I remember another case where a function returned a value in EAX, but it so happened that the ECX register contained a pointer to some interesting data structure.

These bonus undocumented values were not intentional. In the case of Is­Window, it was an optimization: Since the only meaningful values are zero and nonzero, and a null pointer is zero and a non-null pointer is nonzero, it was a clever trick to just return the pointer cast to an integer. In the case of the function that returned a value in ECX, that was completely unintentional: It was just a value that the compiler left in the ECX register by happenstance.

Did these undocumented but potentially useful values cause trouble?

Surprisingly not.

I'm not sure why this was not generally a problem. My guess is that software developers kept one eye on that other version of Windows, Windows NT. Relying on undocumented values wouldn't work on Windows NT, so the developers had to come up with something that would work on both.

I'm sure there were plenty of software developers who simply never tested on Windows NT or didn't consider Windows NT to be part of their customer base. But the number of those who exploited undocumented return values was small enough that I barely remember them.

Bonus chatter: I do remember one customer some time around Windows 8 who asked why the contents of the EBX register no longer contained a copy of the executable's instance handle when the executable entry point as called. We were kind of baffled by this question, because the contents of the EBX register at the executable entry point are formally undefined. Indeed, the code never explicitly sets EBX to anything. The value in EBX is whatever the compiler happened to be using the EBX register for. There was no intentional effort to put a particular value into the EBX register. It just so happened that the instance handle was something the compiler decided to put into the EBX register for 19 years, and then in year 20, it decided to put something else there.

Bonus bonus chatter: You also shouldn't sniff the return address to determine how your module was loaded. That's not part of the API contract either.

Comments (50)
  1. kantos says:

    These days I would imagine that SDL or security review would stop anything like these sorts of shortcuts from happening (or so I’d hope) if only to prevent potential unintended information disclosure. Obviously you can’t depend on what the compiler is going to do; but at least as far as reasonably defined behavior (from a C or C++ language standpoint) you can prevent quite a bit.

    1. Not sure what you can do to stop the compiler from leaving an interesting value in the ecx or ebx register. If it’s all in-process, then you haven’t crossed a security boundary, so technically you didn’t make anything any less secure than it already was. (Though you did make it easier.) If the value crosses a security boundary, then we already have steps in place to stop registers not part of the calling convention from leaking out. (Though sometimes we mess up and they leak out anyway, and then we have to fix them.)

      1. cheong00 says:

        If the contract just say “zero or non-zero”, why don’t they choose some constant value such as “1”? I suppose it’s efficient to zero the register by default, then increment the register when it should be non-zero.

        1. Erik F says:

          If you’re trying to cut as many corners as you can to reduce code (and hopefully by extension, execution time), treating the results of calculations as a Boolean value is quite attractive, because you can just return the exit value of a loop without needing to sanitize it. As long as the caller doesn’t try to do anything creative with the result, there are no issues with the API contract. Obviously, as Raymond notes, if the calculation discloses sensitive information than it has to be fixed, but as Windows 95 was designed with issues other than security as primary goals, it seems like it was an acceptable tradeoff.

        2. smf says:

          >If the contract just say “zero or non-zero”, why don’t they choose some constant value such as “1”? I suppose it’s efficient to zero the
          > register by default, then increment the register when it should be non-zero.

          He explained already that it was because it was faster to execute the code that returns 0 or the pointer value.
          Your question is therefore: Why didn’t Microsoft write slower code.

          1. cheong00 says:

            Yup. My question is therefore: Why didn’t Microsoft write slower code.

            I think both [“xor”-ing EAX by itself, and incrementing EAX] and [copy a random register value to EAX] have the similar speed (I remember all these instructions can complete within 3 CPU cycles, will easily got neglected on call-return of function calls), and I think the resulting code will only be 1 byte longer (ref: 64-ia-32-architectures-software-developer-manual-325462.pdf from Intel website, cannot find older versions now).

            Not bad for the trade off.

            Not bad for preventing people from using the return value in creative way.

          2. Harry Johnston says:

            But the code wouldn’t just be “returning a zero” or “returning a one”, it would also have to check the existing pointer value to see whether it was zero or non-zero. I think you could avoid doing a conditional jump using SUB and ADC to conditionally increment the return value, but that would still be more than one byte longer.

            (On the other hand, I suspect that the main reason was that at that point it wasn’t yet obvious that people would be likely to abuse this sort of implementation detail – or perhaps it wasn’t obvious that it would be Microsoft’s problem when they did.)

          3. smf says:

            >Yup. My question is therefore: Why didn’t Microsoft write slower code.

            “Stupid Microsoft writing software too slow for my 33mhz 386, forcing me to buy a new computer” etc etc.

            Every byte counted when trying to get it to fit in 4mb ram and ship it on floppy disks.

  2. Antonio Rodríguez says:

    I don’t think there were many serious developers out there using Windows 9x for their IDE. Compilers were big beasts back then, needing fast processors and big amounts of RAM and disk, even for moderately big projects. With those requirements, Windows 9x started to struggle and was less stable than with smaller workloads, while Windows NT was rock-solid (and you already had a powerful enough machine to run it, anyway). Maybe they tried to rely on Windows NT’s undocumented features, but had to rewrite that out when the code didn’t work in Windows 9x.

    Also, back then, with several platforms offering the Win32 API (Windows 9x, Windows NT, Win32s, Windows CE…) and a single official documentation, I guess developers were more concerned with complying with the contract. Nowadays, with a “canonical” implementation of Win32 and access to many undocumented details on the Internet, things are very different.

    1. Joshua says:

      From 1996 to 2000 I did all my development on a Windows 95 computer with 16 megs of RAM and Borland C++. I also discovered that 16 bit was king in Windows 95 and so made all my programs 16 bit. (Also, the 32 bit debugger didn’t work…). The only undocumented stuff I ended up depending on was which system menu IDs weren’t in use, and that only because I read the documentation backwards.

      Of course one stupid little mistake in WinProc and the whole machine would hang. You learn to save before run really fast that way.

      I did eventually figure out how to get a DLL loaded with no process (can’t do that except in 16 bit) but try as I might I could get no code in it to run.

    2. Antonio Rodríguez says:

      With 16 MB of RAM, NT 4 Workstation ran quite decently – Microsoft did a pretty good job optimizing it. Sure, it booted more slowly than Windows 95, but once all services were started, it wasn’t slower than Windows 95 for most tasks, and the stability meant you didn’t risk losing an afternoon worth of work if you made a silly mistake. Specially if developing 16 bit software, as you say: 16 bit tasks ran inside an isolated process (the NTVDM, NT virtual DOS machine) in Windows NT, so there was no way they could harm the 32-bit IDE. For me, and for most developers I know, the decision was clear, even when developing DOS applications in Clipper!

    3. Fabian Giesen says:

      One reason to stick with Win9x as a developer during those years were DirectDraw/Direct3D/DirectSound. If you used any of those, you were out of luck on the NT side before Windows 2000. (You did get OpenGL on NT, and a lot of 3D modelling/CAD apps, which already needed fairly beefy workstations, were NT-centric as a result.)

      1. Antonio Rodríguez says:

        Direct3D was available in Windows NT, too, even if it was restricted to version 3 (a near-final DirectX 5 beta was available as an add-on for the Windows 2000 beta, and it happened to install and run fine in Windows NT 4 SP3, but it was a beta, which removed the stability benefit of NT).

        Anyway, if you were developing a game in the 1998-1999 timeframe and relied on DirectX 5, yes, you had no option but to develop on Windows 98 (or have a complicated setup of two networked development and test machines). But that was a pretty narrow spectrum: for most multimedia/casual game developers, DirectX 3 support was more than enough; and productivity applications didn’t need DirectX at all.

    4. smf says:

      I ran windows 95 until I got a dell with an onboard network card that caused it to bluescreen, I tried windows 98 beta but settled on windows nt instead. Which was great until you wanted device drivers and games.

      I ran the hacked direct x 5, but windows 2000 beta just wouldn’t install and run. So I had to wait for the release, but then XP was right around the corner.

    5. alegr1 says:

      Windows 98 on 64M was running Visual C 98 just fine.

      1. smf says:

        >Windows 98 on 64M was running Visual C 98 just fine.

        I didn’t make it to Windows 98 release before switching to NT. The beta that was out at the time had a file corruption bug when you had 192mb of ram (maybe less but this is what I had). I copied some files onto a network share, then copied them back and each had random corruption.

        It seemed the cache was being corrupted over time, but the entries stayed valid for a very long time due to the amount of ram I had. I replaced the ram and had a new motherboard fitted before deciding it was a software issue & NT4 didn’t suffer from the problem.

  3. DWalker07 says:

    There were several books with names like “Undocumented Windows”, and “Windows Secrets”. I don’t know if these books trumpeted things like this as terrific finds, or not….. I didn’t program in Windows at that level back then.

    The EBX register thing was surely a case of “programming by observation” instead of programming to a contract.

    1. Richard Wells says:

      Undocumented Windows and most of the related works were done for Windows 3. Resulted in lots of unnecessary calls to functions that either were obsoleted or were alternate entry points to standard functions. Windows 95 lucked out as some of the books covering internal data structures were done with the betas in mind and those structures changed by release. No one steals clever tricks that don’t work on production OSes.

      1. Yuhong Bao says:

        They even went so far to obfuscate some pointers using XOR.

      2. Harold H20 says:

        I bought a copy of “Unauthorized Windows 95” by Andrew Schulman, right around the same time as Windows 95 (the book was apparently based on a beta version of Win 95). The author did some dis-assembling of various functions and it was quite interesting to get a glimpse into the inner workings of Windows. There was one section I still remember because it struck me as pretty weird.

        The author was stepping thru some code and came to a place where the code jumped to a weird address — the address was located in his computers BIOS, specifically the copyright string. This particular address contained the value 43h, the ASCII code for the letter “c”. It turns out that 43h is also the opcode for an obscure x86 instruction called ARPL. What ARPL does is irrelevant; what was important was that ARPL is an illegal instruction in protected mode.

        Apparently, whenever Windows 95 needed to switch from protected mode to real mode, which apparently happened a lot, it would jump to that address, which is an illegal instruction, which caused an exception, which invoked an error handler and eventually you ended up in real mode (technically “Virtual x86 mode”, some sort of real mode-protected mode hybrid)

        (I’m doing this from memory so you’ll have to read the book for a better explanation. )

        I also seem to remember that Mr. Schulman was briefly kicked out of Microsoft’s forum on CompuServe when the book first came out.

        1. Yuhong Bao says:

          ARPL is an illegal instruction in virtual 8086 mode, but is legal and useful in protected mode. NTVDM uses illegal operands of LDS/LES instructions instead which was used for VEX prefixes. AVX had to be disabled in real and virtual 8086 mode.

          1. Chris Crowther says:

            It is; someone even mentioned the book in the discussion thread.

        2. yukkuri says:

          Schulman loves to build dramatic good vs evil narratives out of ordinary boring engineering. Like Steve Gibson, though admittedly much smarter.

      3. Neil says:

        Back in the 3.x era you could acquire USER’s DS and create a far pointer from that and an HWND and start poking…

        1. Yuhong Bao says:

          Which also reminds me of the problem of programs assuming that HWNDs are even, including the first version of Publisher using the low bit to do windowless controls in the days just before Windows 3.0 was released. PowerPoint decided to require protected-mode Windows 3.0 from the beginning instead. In retrospect, Windows 3.0 should have been delayed anyway because of DOS 5.0.

          1. Yuhong Bao says:

            Project, not Publisher.

  4. Harry Johnston says:

    Is the executable entry point actually formally documented? All I’ve ever been able to find on MSDN is the entry point the C runtime library calls. (I’ve always vaguely assumed that there’s some sort of documentation somewhere for, e.g., people writing compilers, but I’ve never been able to find it.)

    1. kantos says:

      Technically yes https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol has the entry documentation. It’s a __stdcall method that takes three paramters, (LPVOID var1, DWORD var2, LPVOID var3) what those actually mean is dependent on if it’s DLL or an Executable.

      1. Harry Johnston says:

        I believe that’s only for managed images, aka .NET applications. Pretty sure the entry point for ordinary executables is different.

        1. kantos says:

          Read the link, that’s not for managed code. It’s very explicitly for ld.exe

          1. Harry Johnston says:

            I *did* read the link. It says “When creating a managed image, the function specified to /ENTRY must have a signature of (LPVOID var1, DWORD var2, LPVOID var3).”

        2. Stefan Kanthak says:

          For DLLs, https://msdn.microsoft.com/en-us/library/ms682596.aspx as well as https://msdn.microsoft.com/en-us/library/ms682583.aspx give you all the information you need.
          In their indefinite wisdom, M$FT decided NOT to document the prototype of the entry point function for applications.
          He who but never (well, almost never) writes about undocumented interfaces provides it for you: see https://blogs.msdn.microsoft.com/oldnewthing/20110525-00/?p=10573

          I wonder whether all these ISVs^Wcompetitors who dared to develop their own compilers and runtimes sued this 2 bit company which can’t stand 1 bit of competition about that missing piece of documentation.

          1. Please can you limit yourself to one insult per comment?

          2. alegr1 says:

            You need to get that stick out of your rear.

      2. skSdnW says:

        DllMainCRTStartup has the same signature as a normal DllMain but a .exe is void WINAPI (). You can get away with returning int in simple console applications but it is rather risky since Windows will just call ExitThread and not ExitProcess and you don’t know which APIs create worker threads.

        1. Harry Johnston says:

          In modern versions of Windows, you’ll always have worker threads (at least for a while after startup) because the loader uses them.

          1. Joshua says:

            I got broke by that change. :(

    2. Darran Rowe says:

      I remember it being formally defined, but as more and more things started to rely on compiler features and systems had enough resources to make just linking with the CRT a simple choice, the documentation for that vanished.
      You can still find the information in the MSVCRT/VCRuntime source that comes along with Visual Studio though.
      Basically, these are very unlikely to change because of backwards compatibility.

      1. Harry Johnston says:

        Ah. Yes, that makes sense.

        It still pretty much steps on Raymond’s point IMO, if there’s no written contract and you have to rely on reverse engineering, it isn’t obviously all that much less sensible to reverse engineer the loader code that calls the entry point than to reverse engineer the C runtime’s entry point.

        See, for example, https://stackoverflow.com/a/35108128/886887 – though admittedly the particular section of code the answer refers to doesn’t obviously set EBX to any particular value. :-)

    3. Stuart says:

      For the scenario being discussed in the older post, couldn’t you achieve much the same thing by having exe1.exe’s main() method call MyMain(), and then exe2.exe’s main() would LoadLibrary(“exe1.exe”) and call MyMain() as well? Unless I’m missing something really obvious, that’d achieve exactly the same thing without needing to screw around with complicated and undocumented entry point stuff.

      1. Harry Johnston says:

        The problem is that when exe1.exe gets loaded via LoadLibrary its entry point will be called, and that call has to return promptly or the process gets borked. So the entry point has to call main() if it is being run as an executable, or return promptly if it is being loaded as a DLL.

  5. Brian says:

    In reply to the bonus bonus chatter: As handling of asynchronous code becomes more sophisticated, the ability to use a “call stack” to form of causality chain becomes weaker. Even if has worked for years, it would not surprise me at all for code reliant on return addresses to fail in the future.

  6. Myria says:

    Something else like this: GetTickCount in NT 5.2 (Win2003, XP x64) actually happens to return a valid value in EDX (or high half of RAX) that makes the result 64-bit, so you could effectively use it as GetTickCount64, if you did something like:

    reinterpret_cast<ULONGLONG (WINAPI *)()>(GetTickCount)()

  7. BZ says:

    I think dev guidelines for Windows 95 stated that your program must run on NT 3.51. Of course I admit to never testing any of my code on any version of Windows NT. Until 2000 which is when I switched. Then again, my only references back then were MSDN and “Programming Windows”, so I didn’t do anything undocumented.

  8. Dave says:

    There were many cases in early Windows versions where “handles” were actually just pointers to internal memory structures, or slightly disguised pointers. This was very useful when CryptoAPI came out because the “handle” it gave you could be turned into a pointer to internal data structures, which included a flag controlling whether strong crypto was enabled for non-US users. So you could grab the handle and change the flag that enabled strong crypto, turning it on for global users.
    Of course you could also extract the victim^H^H^Huser’s private keys that way, and all sorts of other things.

  9. Søren Mors says:

    I believe that a lot of use of undocumented/undefined functionality is by accident. You write some code that seems right, run it and it works. Hey, were done. But you don’t end up relying on the value in a register being usefull by accident (unless you are writing assembler). Sure, you can get c to tell you what the contents of the register is, but then you are far of from something that happens by accident.

  10. mikeb says:

    I loved those “Undocumented Whatever” books back in the day. They were a great way to learn about the architecture of systems even if you didn’t use any of the undocumented stuff directly. And you also learned a lot of useful debugging techniques.

    And I believe that there there were occasions when using undocumented APIs/data structures/etc was really the only reasonable way to perform certain (also reasonable) functions.

    1. OldBoyCoder says:

      Agreed, back in the early nineties we were developing for Windows for the first time and there just wasn’t a lot of documentation full stop, no stack overflow and no web to search for someone with the same problems as you were having. While I don’t think we ever used any of the undocumented stuff in our production apps having the books really helped me understand and debug Windows issues back then.

    2. cheong00 says:

      Not only Windows. Back when I was in secondary school, the school library have subscription to some programming magazine that have a column that explores different undocumented aspects of VC++ and BorlandC++ compilers.

      Yeah, we did think people who can write those “undocumented” stuffs is cool.

Comments are closed.

Skip to main content