<-- Articles Index / Analysis of Visual Studio's Trial Expiration and Module Rebasing Tricks 
1
Analysis of Visual Studio's Trial Expiration and Module Rebasing Tricks

Date: May 8, 2020
Last-Modified: May 8, 2020

SUMMARY:
In order for Microsoft to stay relevant in a post-Windows era of cell phones, Linux, and open-source, many would be pleased to find that Visual Studio became a mostly free product in version 12.0 (2013) in a distribution known as Community Edition. Containing most of the features of the Professional Edition and made freely available to academia and small businesses, it has been released alongside other Visual Studio editions since Visual Studio 2013.

Upon first launch however, Community Edition prompts the user to log-in to a Microsoft account, providing an alternative "Not now, maybe later" option. So far so good. But, after 30 days or more, whether or not you've used the product every day or just once, you'll see this upon launch:

"30 day trial (for evaluation purposes only)
 Your evaluation period has ended. Please sign in to unlock the product."
Visual Studio 2017 Post-Installation Dialog Visual Studio 2017 Expiration Dialog

Trial? Evaluation Period? Unlock the Product? There was no mention of this in that initial friendly Welcome Dialog. Visual Studio is then forcibly closed after the dialog is dismissed. You are then immediately prompted with the same dialog if you try to re-open Visual Studio or even uninstall the whole thing followed by a re-install. The same dialog rears its ugly head. Game over.

This hidden trial behavior actually goes back to the first Community Edition for Visual Studio 2013 and has been present in each successive version as of the time of writing this article. Tracking user's every keystroke, mouse-click and any other telemetry data may be all the rage these days with tech companies, but they are shenannigans nonetheless.

The fact that Microsoft managed to sell the general public on this idea of a fee-based online subscription service to to continue using the same word-processor and spreadsheet programs people already own several licenses over the years (in older versions of course) under a so-called Software As A Service (SAAS) scheme; is nothing short of pure marketing genius. Add the supposed value of storing everything in the "cloud", touted as being more secure than your own hard-drive by the same companies that lie about the back-doors and spyware tech in their own products for profit, compliance with shady government agencies, and complete disregard for individual privacy. There is no question that phoning home to Microsoft each time you start one of their products is good for them, but is it good for you?

What if Visual Studio's requirements are contrary to the least-privilege security model for your machine(s) and network(s)? What if you don't need online team collaboration features, don't store your code in the cloud, and I don't want to be further restricted by agreeing to the mountain of license terms associated with creating a Microsoft account much less the license terms of the product itself? What if you don't want to give Microsoft a date of birth, gender or telephone number?

Can one actually purchase the product, and then use that without creating a Microsoft account? After doing some research on the matter, you might also be surprised to find that a Microsoft account and network access is required, even for the paid editions! What's more, users of the software have to worry about their license going stale if "You have not used Visual Studio or have had no internet connection for an extended period of time". Quote:

"If you do not reenter your credentials, the token starts to go stale and the Account Settings dialog tells
 you how many days you have left before your token will fully expire. After your token expires, you will
 need to reenter your credentials for this account or license with another method above before you can
 continue using Visual Studio."
Lets see what the Community Edition license terms actually say. Note that the License Terms are NOT provided in the Expiration Dialog; to find them you must go back in time to before the software had expired. That is, using the link provided in the first launch "Welcome" dialog OR if navigating to the "Help" -> "About Microsoft Visual Studio" menu. Both links take you to a page that contains an IMAGE, not searchable text, but an IMAGE of the license terms.

Visual Studio 2017 Community License Terms

At the bottom of the page, the License Agreement looks complete as all sentences have terminated and there is a lot of whitespace below the last paragraph. Below the image however, there is a tiny .docx link that you might miss, as I initially did, containing the REMAINDER of the pages of the License Agreement. Yes that's right: if you want to see ALL the terms and conditions, you need to initiate a separate download. More shenannigans.

Even after downloading the full text, you'd be surprised to find no mention of anything to do with unlocking the product, trial features, etc. In fact, none of the following words were present in the entirety of the fine print document: "evaluation", "trial", "day", "month", "period", "time", "end", "unlock", "lock".

Linking to an archive.org snapshot of their "Sign in to Visual Studio" article, I found it interesting they avoid any implication that one is forced to create a Microsoft account. I mean, come on, they are offering Visual Studio for free, you'd think they could at least mention that they only want to track you in return! They do say this however (emphasis mine):

"Unlock the Visual Studio Community edition - If your Community edition installation prompts
 you for a license, sign in to the IDE to unblock yourself."
The question at this point is how secure did the Visual Studio guys make their log-in requirement for those who felt it was worth their time to crack a free piece of software? If you've got an hour or two to kill, turns out its not too hard.

DISCLAIMER: The information in this article is for educational purposes only. Attempts to bypass, reverse engineer, or perform actions against Microsoft's license agreement(s) may be illegal where you live; or, dependent on your individual circumstances. I do however grant Microsoft, at its sole discretion, to use the information herein in order to harden or otherwise improve future versions of their products.

SOLUTION #1: MODIFY THE SECRET REGISTRY LOCATION:
Some developers using the Community Edition expressed some frustration at Microsoft's log-in requirement in a StackOverflow article. The method described in this article deserves first mention as it is among the easiest, most-effective and least-invasive of all methods described here, such as hacking on the DLLs themselves. Since many users of Visual Studio are likely familiar with the Win32 API, all you need to write is a short program to modify an encrypted registry blob to change the date Visual Studio uses to determine if the product has "expired". Here's a small sampling of code I wrote to view this special date for VS 2017 or VS 2019. Note the use of the CryptUnprotectData() API, which requires no decryption key (the key is embedded within the data blob itself) allowing anybody to decrypt it; discouraging only the most casual browsing of the registry.

const TCHAR* pszRegPath = NULL; if (2017 == uVsYearVer) { pszRegPath = TEXT("Licenses\\5C505A59-E312-4B89-9508-E162F8150517\\08878") } else if (2019 == uVsYearVer) { pszRegPath = TEXT("Licenses\\5C505A59-E312-4B89-9508-E162F8150517\\09278") } else { _tprintf(TEXT("ERROR: unsupported visual studio version: %u\n"),uVsYearVer); } if (pszRegPath) { _tprintf(TEXT("Opening Visual Studio %u license data registry key: HKCR\\%s\n"),uVsYearVer,pszRegPath); //open root registry path HKEY hKey = NULL; if (ERROR_SUCCESS != RegOpenKeyEx(HKEY_CLASSES_ROOT,pszRegPath,0,KEY_QUERY_VALUE,&hKey)) { _tprintf(TEXT("ERROR: unable to open key: HKCR\\%s\n"),pszRegPath); } else { //query the data byte arData[512]; DWORD uType = 0; DWORD uDataSize = sizeof(arData); ZEROMEM(arData); //just a memset macro if (ERROR_SUCCESS != RegQueryValueEx(hKey,NULL,NULL,&uType,arData,&uDataSize)) { _tprintf(TEXT("ERROR: unable to query key's value: HKCR\\%s\n"),pszRegPath); } else { //dump the raw data _tprintf(TEXT("raw data size = %u byte(s)\n"),uDataSize); dumpBufferHelper(arData,uDataSize,TEXT("raw data (encrypted)")); //decrypt the data DATA_BLOB dataBlobIn; DATA_BLOB dataBlobOut; ZEROMEM(dataBlobIn); ZEROMEM(dataBlobOut); dataBlobIn.cbData = uDataSize; dataBlobIn.pbData = arData; WCHAR* pszDesc = NULL; if (!CryptUnprotectData(&dataBlobIn,&pszDesc,NULL,NULL,NULL,0,&dataBlobOut)) { _tprintf(TEXT("ERROR: unable to descrypt data at: HKCR\\%s (err=%u)\n"),pszRegPath,GetLastError()); } else if (!dataBlobOut.cbData) { _tprintf(TEXT("ERROR: descrypted size is empty at: HKCR\\%s\n"),pszRegPath,GetLastError()); } else { _tprintf(TEXT("\ndecrypted size = %u byte(s)\n"),dataBlobOut.cbData); if (pszDesc && *pszDesc) { _tprintf(TEXT("description: \"") TCHAR_FMT_TYPE_AS_WIDE TEXT("\"\n"),pszDesc); } //dump the decrypted data dumpBufferHelper(dataBlobOut.pbData,dataBlobOut.cbData,TEXT("decrypted license data")); if (dataBlobOut.cbData >= 16) { WORD* pExpYear = (WORD*)(dataBlobOut.pbData + dataBlobOut.cbData - 16); WORD* pExpMonth = (WORD*)(dataBlobOut.pbData + dataBlobOut.cbData - 14); WORD* pExpDay = (WORD*)(dataBlobOut.pbData + dataBlobOut.cbData - 12); const WCHAR* pszString1 = (WCHAR*)(dataBlobOut.pbData + 4); const WCHAR* pszString2 = (WCHAR*)(dataBlobOut.pbData + 0x34); const WCHAR* pszString3 = (WCHAR*)(dataBlobOut.pbData + 0x7E); _tprintf(TEXT("\n")); _tprintf(TEXT("expiration date: %u/%u/%u\n"),*pExpMonth,*pExpDay,*pExpYear); _tprintf(TEXT("prodkey: ") TCHAR_FMT_TYPE_AS_WIDE TEXT("\n"),pszString1); _tprintf(TEXT("string2: ") TCHAR_FMT_TYPE_AS_WIDE TEXT("\n"),pszString2); _tprintf(TEXT("string3: ") TCHAR_FMT_TYPE_AS_WIDE TEXT("\n"),pszString3); } //clean up if (pszDesc) LocalFree(pszDesc); if (dataBlobOut.pbData) LocalFree(dataBlobOut.pbData); } //data decrypted } //data successfully queried //cleanup registry key RegCloseKey(hKey); } //root path opened } //have registry path

Program output might look something like this:

Sample Dump of Registry License Data

It is left as an exercise to the reader to add the code to modify the date in the decrypted buffer, call CryptProtectData() on the result and store it back to the same registry key or create a .REG file which can be run to import the modified data. Just don't forget to back up the original registry data as a failsafe since uninstalling the product doesn't remove this data. Another alternative is to simply use the PowerShell script provided in the StackOverflow article to do everything for you.

SOLUTION #2: CHANGE THE SYSTEM DATE:
A brain-dead solution that has worked since the earliest days of trial Shareware is to change the system date. Even better would be to change the system date only when starting Visual Studio and then changing it back after Visual Studio has finished loading. Doing this manually would be a pain, but NirSoft has a great RunAsDate tool that does exactly that. If you start Visual Studio Community Edition using this tool, passing the date you first installed it (or any date within the first 30 days), for the first 20-60 seconds of launch, you are no longer bothered by the Expiration Dialog during startup. The nice thing about this solution is that the time APIs are only altered on a per-process basis and only for the period of time you specify. All that is needed is to modify the Visual Studio IDE shortcut (devenv.exe) with the RunAsDate tool passing your desired time. E.g.:

\path_to\RunAsDate.exe /movetime /returntime 20 25\03\2019 00:00:00 "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\devenv.exe"

This solution appears to work flawlessly, at least for the first half-hour or so. After approximately 30 minutes, Visual Studio performs another time-check and the Expiration Dialog re-appears. The workaround to this is to simply re-open Visual Studio with the RunAsDate shortcut and you'll have another 30 minutes. This ends up being as annoying as the original problem, not to mention you can't switch back to your editing windows before they are forcibly closed. While Visual Studio does perform a global save before closing, this solution is less than ideal. You may be better off with the registry-fix described above, or continue reading to learn how you might patch the software yourself.

SOLUTION #3: GETTING YOUR HANDS DIRTY - PATCHING BINARIES:
When a small amount of time is a better trade-off than than the loss of your privacy, patching binaries for interoperability is always an option when a cleaner way can't be found. As an educational exercise I was curious if Visual Studio employed any anti-debugging, or anti-analysis tricks here. They don't. The only hurdle is that Microsoft updates Visual Studio so often, you will have to make a patch each time you update. Even then, you'll find it isn't that hard and only takes a minute or two once you do it the first time. The method described here uses VS 2017 Community, but all other Community Editions as of the date of this article operate in similar fashion.

The goal is the same as with any time-crippled piece of software: to find the conditional branch responsible for making the Expiration Dialog appear or the branch responsible for determining if the time-period is within limits, or both.

The best place to begin investigation as to what piece of code is responsible for the appearance of a particular dialog, is to attach a debugger when the said dialog is open and look at the call-stack. Because the Expiration Dialog is modal (the user can't interact of the rest of the program while its open), it is likely the branching code we are interested in would be near the call-stack entries as we need to return to this code when the modal dialog loop completes (after the dialog closes). Which call stack entry is just process of elimination.

Begin by starting Visual Studio Community Edition using the original shortcut to devenv.exe and wait for the Expiration Dialog to appear.

Visual Studio 2017 Expiration Dialog

As of the time of this writing, Visual Studio is still a 32-bit x86 application, despite that it can be used to develop applications that run on many different architectures and platforms, so we'll be using the 32-bit OllyDbg for this article. Just make sure you start it with administrator privileges or run it from an administrative command prompt. Once OllyDbg is open, find the "Attach" option in the file menu to attach to the devenv.exe process.

NOTE: I usually avoid the WinDbg family of debuggers when reverse-engineering because OllyDbg, for 32-bit x86 processes, and the similar debugger, x64dbg, for 64-bit x64 processes, can be used to debug as well as assemble patches very easily on the fly. One drawback to OllyDbg however is that is that it is no longer maintained, and when debugging larger applications in modern operating systems, it tends to freeze during module analysis and even during some exceptions. Most problems can be avoided by setting the "Automatic module analysis" option to "Off" under OllyDbg's Analysis options. Ensure you have done this prior to attaching to devenv.exe.

Once attached, OllyDbg pauses the process and populates information for devenv.exe and all of its loaded DLLs. Wait a bit for the debugger to go idle and then switch to the UI thread by double-clicking the "Main" entry in the "Threads" window. In GUI applications with more than one thread, you'll always want to look at the main thread first, because the first thread is usually the UI thread or at least the primary UI thread for the application. Now we want to focus on the "Call stack" window to see how we got here:

Debugging Visual Studio Community Expiration Dialog Main Thread Callstack1

The call-stack can, in theory, be walked all the way back to the start of a thread in NTDLL.RtlUserThreadStart() and contains the nesting level of the currently executing functions.

Depending on where the debugger happened to break in the devenv.exe process, we want to identify the nearest message processing loop from the top of the call-stack window downwards. In other words, look for API calls to GetMessage(), TranslateMessage(), DispatchMessage(), IsDialogMessage(), TranslateAccelerator(), etc. Since the Expiration Dialog is modal, it is likely to have its own message processing loop separate from the owning parent window as do most popup dialogs. The MessageBox() API is no different in that it has its own modal message processing loop while the parent is waiting for it to RETURN control back to it courtesy of the call-stack. Once one of these calls are identified, we will then narrow our focus on call-stack entries below this spot since the conditional logic that determines whether or not to pop up the dialog is likely somewhere within the parent window and not in the expiration dialog itself.

In my debugging session shown here, you can see the program happened to break on GetMessage(). Since we only care about the code that chose to run whatever block GetMessage() is in, we will look at call-stack entries below GetMessage().

At the same time, we can also eliminate any entries involving calls to system API DLLs which we normally wouldn't want to modify as we can be fairly certain this registration requirement is part of Visual Studio and not part of a system API. In my debugging session, these system API DLLs happen to be WindowsBase.ni.dll, PresentationFramework.ni.dll, clr.dll, etc. These can be identified as system API DLLs by looking at the loaded DLL paths from OllyDbg's "Executable modules" window (ALT+E) and seeing which base paths begin with "C:\Windows". This isn't a hard and fast rule, but it generally holds true.

Looking at each entry downwards in the call-stack while ignoring system modules, we are left with modules that are specific to Visual Studio. After process of elimination, the following modules stand out as targets with which to focus our efforts: msenv.dll, Microsoft.VisualStudio.ProductKeyDialog.ni.dll (OllyDbg abbreviates it to "Microsoft.VisualStudio.ProductK"), and devenv.exe.

Microsoft.VisualStudio.ProductKeyDialog.ni.dll sounds like a good candidate. If we look in OllyDbg's modules window (sorted alphabetically by clicking the "Name" column header), we can see that another similar-sounding DLL, Microsoft.VisualStudio.Licensing.dll is also loaded in memory. Although not currently present in the call-stack, this DLL might contain some logic that caused the Expiration dialog to appear. Microsoft at this point doesn't seem to have spent much effort to hide which DLLs are responsible for what functionality as the names seem to describe their purpose pretty well.

Both of these DLLs are .NET (CLR) modules, so we'll want to use a .NET decompiler to see what's inside, such as dnSpy, ilSpy, or .NET reflector. .NET reflector with the Reflexil plugin allows you to patch the CLR's IL (Intermediate Language) opcodes directly which isn't too hard if you can find a good IL reference and are making a small change. Newer versions of dnSpy appear to have IL patching capabilities too, so you can use whatever tool you are comfortable with.

I initially opened Microsoft.VisualStudio.Licensing.dll in .NET Reflector and drilled-down in the left treeview pane to locate the implementation for LicensingState. The method "Microsoft.VisualStudio.Licensing.LicensingState.Validate()" caught my attention. At the bottom of the function, we can see it returns a result of type LicensingAction. Looking at the LicensingAction type details above, we can see the "Success" enumeration is code 0 (zero). Activating the Reflexil pane, we scroll to the bottom of this function to see which IL opcodes are responsible for the function's return value.

Analysis of Microsoft.VisualStudio.Licensing.LicensingState.Validate()

Changing the opcode immediately above the "ret" instruction from "ldloc.0" (load location zero to stack, i.e. last result variable) to "ldc.i4.0" (load constant zero to stack), should cause the function to unconditionally return zero (Success) regardless of whatever it had previously calculated. Now this may have worked, but .NET reflector wouldn't let me save my changes due to the module being a "mixed-mode assembly". When I hit this, I honestly didn't know what the heck that meant other than the module might have interspersed x86 code. Microsoft.VisualStudio.ProductKeyDialog.ni.dll also ended up being the same type of "mixed" module. Since I've personally seen .NET reflector rebuild an entire DLL for the smallest IL opcode change, a change that feels like it should be a patch, is really an entire rebuild using the .NET Reflection APIs. Hence the name "Reflector". Since I'm not a .NET guru, I went back to the debugger to see if I could alter the behavior of Visual Studio using one of the native modules left on my candidate list.

SOLUTION #3: PART 2 - PATCHING NATIVE BINARIES:
If we additionally exclude the managed DLLs in the call-stack, we are left with only msenv.dll and devenv.exe modules that might contain what we're looking for. devenv.exe only has a few entries and they are all located near the bottom of the call-stack window at the point where the program first launched. This is probably because devenv.exe is mostly a container, where the bulk of the functionality of Visual Studio lies in the thousands of DLLs contained within its "Program Files (X86)\Microsoft Visual Studio" location. Just to eliminate the possibility the logic we are looking for is contained within devenv.exe, I NOP'd-out its most-recent CALL instruction (from the top of the call-stack window down to the first devenv.exe entry), saved a copy the modified EXE, and attempted to start Visual Studio studio with that copy (backing up the original) to see if there was a noticeable difference in behavior. There was a difference; Visual Studio launches, but nothing appears. This is because the most recent devenv.exe CALL I eliminated additionally performs a bunch of necessary initialization beyond the check for product expiration.

At this point, we have one remaining module that is implicated by the entries found in the call-stack: msenv.dll. Between devenv.exe and the breakpoint somewhere within the Expiration Dialog, the call-stack was peppered with many entries calling in to, and out of msenv.dll. Luckily msenv is a native win32 x86 module which will take a patch quite well, no .NET Reflection necessary!

We'll start by locating the first msenv.dll routine ("Procedure" column) from the top of the call-stack and go downwards from there. At each msenv.XXXX location, double-click the call-stack entry to synchronize the CPU window with the corresponding assembly code. When anti-debugging tricks are not present, each call-stack entry usually corresponds to the next-nested function CALL ultimately leading to the debugger break. If we work backwards up the chain towards each outer nesting level, we want to look at the conditional logic surrounding each function CALL (and thus each call-stack entry). If you go backwards far enough you will eventually reach the program's entry point (usually main or WinMain) and if this happens, you've gone too far. I will stress again that the slightest bit of trickery or even during the middle of stack frame creation or teardown will make the debugger unable, or temporarily unable to automatically trace where it thinks the stack return-addresses are located, back to the application entry point (called by NTDLL.DLL). Sometimes this is by design as an application does not need to exit through its entry point or where anti-debugging tricks are employed to ensure there is no trace of where the instruction stream originated from at certain protected points in the code.

Disclaimers aside, the selected assembly instruction (in the CPU window) corresponding to each call-stack entry will usually point to the instruction following a CALL instruction (as the CALL instruction is generally what creates the stack entries). You can additionally right-click the same call-stack entry and select "Follow in CPU stack" to synchronize the stack-portion of the CPU window- scrolling into view stack data that might be relevant. This is helpful as it will show the top of the stack when said CALL instruction first executed allowing you to see to variables may have been passed to the function, variables passed to nearby functions or even local variables providing hints as to what the current function might be doing.

We don't know enough about the code to justify too much time spent deciphering what each entry in the call-stack is doing. Since reverse engineering assembly can be like finding a needle in a haystack, we want to eliminate possibilities efficiently before we think we are close enough to dig in to the code. We'll start by using process of elimination to our advantage in this way: assuming one of the call-stack entries contains the surrounding condition we're interested in, "inverting" this condition might prevent the Expiration Dialog from appearing. Since we only have a handful of call-stack entries to test, we can invert (or forcibly JMP or NOP the JMP) each conditional instruction effectively preventing the current call-stack entry from being executed, save the module, swap the changed module for the original, then test the program.

The general technique to find each call-stack condition is, after selecting the call-stack entry, look above the CALL instruction in the CPU window for the nearest branch instruction (JE, JNZ, JS, JB, etc.). So either the block of code that ultimately led to each CALL instruction was explicitly jumped-to as the result of a condition or a jump was skipped as the result of a condition. If the CALL instruction was obviously executed as a result of a jump that DID NOT OCCUR due to the result of some condition, change the Jxxx conditional instruction to an unconditional jump (JMP) instruction. Otherwise if the current CALL instruction was obviously executed as the result of a jump that DID OCCUR, NOP-out the Jxxx instruction so that the jump will always be skipped. Sometimes if you can't tell, place a couple software breakpoints (F2) before the various Jxxx instructions and restart the debugging session to see which ones get hit so you can ascertain the path taken. In the case of Visual Studio, we can reproduce the results with certainty in each debugging session (i.e. the Expiration Dialog will always appear if you've waited long enough for it to expire), so you can assume the same conditionals will follow he same path.

Changing assembly instructions is easy with OllyDbg built-in assembler: with the instruction you want to change hilighted, press <SPACE> to bring up the "Assemble" pop-up edit window containing the current instruction's OPCODE and applicable OPERANDS (if any). Simply edit the instruction in-place or type in an entirely new instruction. By changing a Jxxx OPCODE to JMP (leaving the target address OPERAND intact) or changing a complete Jxxx instruction to a "NOP" should do the trick for purposes of this article. An unconditional jump should take up the same space as a conditional jump, and an instruction can always be removed by converting it into one or more NOP bytes (code 0x90) to pad it out.

If you ever find you don't have room for one or more replacement instructions, you'll usually need to find some slack-space (unused padding zeroes) somewhere in the module to create what is commonly called a code-cave for the new instructions that you can jump in and return from. Code-caves can get complicated such as if the slack-space's section permissions don't have the execute bit already set if not residing in the section as the referencing code along with other details. Therefore code-caves are beyond the scope of this article. Luckily code-caves are not necessary when inverting conditional branches so we will need to ensure the resulting instruction byte sizes are LESS THAN OR EQUAL TO the size of the original. Checks in the "Keep size" and "Fill rest with NOPs" option checkboxes in the Assemble window are helpful in this regard, preventing your instruction from corrupting the instruction(s) that follow if you need to preserve them.

To test each code change, OllyDbg has a non-intuitive way of saving changes that may take a couple of tries before you memorize it. After making changes to the assembly instructions (which are only in-memory changes up to this point), right-click anywhere in the assembly code section of the CPU window, then select "Edit" -> "Copy all modifications to executable". This will open a new window. Right-click that new window and select "Save" and choose a unique name such as msenv_patch1.dll. Stop and close the debugger, then copy the patched module over the original msenv.dll (making sure you have a backup of the original). Re-run the program and see what happens. Expect altered behavior and crashes since we're blindly altering conditionals without regard as to what they may be protecting the application against (such as NULL pointers). After each undesirable result, copy the original msenv.dll back into place and restart the debugging session by attaching to a fresh running launch of Visual Studio.

After trial and error of inverting conditionals down the call-stack chain JUST for msenv.dll entries, you'll build up a list of call-stack entries of interest that don't crash Visual Studio, but offer up altered behavior. I should mention that you should be keeping a little diary of everything you tried in a separate text file using your favorite text editor. This might consist of original versus changed instructions and their addresses, address of which call-stack entry you were at, etc. This is useful to systematically test all possibilities. If you tried a dozen patches, and don't keep a record, the hole you forgot to test may very well be the magic location you are searching for.

As a reminder, we're still not at the stage where we devote too much time understanding the code: we're just observing. I observed a call-stack entry condition that when inverted, prevented the Expiration Dialog from appearing, but with the side-effect that Visual Studio also closed shortly thereafter. Another call-stack entry also independently prevented the Expiration Dialog from appearing, but Visual Studio additionally failed to handle any user-input messages (e.g. keyboard and mouse events didn't work to close the main window or otherwise get the menu bar to respond), effectively hanging the program.

Once you have found one or more call-stack entries associated with altered behavior, you are now at the stage where you should focus on understanding the code in and around those conditional branches, annotating the code with comments as needed (in OllyDbg, this is done with the semicolon key) or snippets of code can be copied to your text-file-diary where they can be manually formatted/annotated or whatever helps you understand the code. It is important to note that modified conditional branches resulting in a crash don't mean that they can be safely eliminated from your investigate-list; it just means for the reverse engineer, you must devote time trying to understanding that code as well.

One of the entries that caused the altered behavior I described above involved the 3rd msenv location from the top of the call-stack. Because we only got altered behavior, but not the exact desired behavior, an understanding of the code is necessary. Digging into an understanding of this surrounding block of code showed that this msenv function was called by the managed Microsoft.VisualStudio.ProductKeyDialog.ni.dll likely because it had calculated that the product had expired.

Debugging Visual Studio Community Expiration Dialog Main Thread Callstack2

Unfortunately, Microsoft.VisualStudio.ProductKeyDialog.ni.dll contains the expired condition/state and we can't easily modify this DLL for the reasons described above. The function represented by the call-stack entry at msenv.5D720496 (might in real life be called something along the lines of "PopupExpirationDialogAndHandleContinueResult") actually handles the dirty work of what to do as a result of the expiration condition/state held in a variable somewhere within that managed DLL.

All is not lost however as we dig in to the functionality surrounding msenv.5D720496, even if we can't control the expiration condition/state directly. If you double-click that entry in the call-stack (shown by the red-arrow), you'll find code looking similar to this:

5D720496 E8 CDFEFFFF CALL msenv.5D720368 ;pop-up Expiration Dialog 5D72049B 8B7D EC MOV EDI, DWORD PTR SS:[EBP-14] 5D72049E 85C0 TEST EAX, EAX 5D7204A0 0F88 C2271400 JS msenv.5D862C68 ;retval negative? close Visual Studio... 5D7204A6 8B53 2C MOV EDX, DWORD PTR DS:[EBX+2C] 5D7204A9 8D45 F0 LEA EAX, [EBP-10] 5D7204AC 50 PUSH EAX 5D7204AD 8BCF MOV ECX, EDI 5D7204AF E8 CBA6FFFF CALL msenv.5D71AB7F ;some other check (?) 5D7204B4 8B75 F0 MOV ESI, DWORD PTR SS:[EBP-10] 5D7204B7 85C0 TEST EAX, EAX 5D7204B9 0F88 A9271400 JS msenv.5D862C68 ;retval negative? also close Visual Studio

The only thing prior to the code shown is some basic function prologue which can be ignored. The first thing performed by the function is to execute the CALL instruction at 0x5D720496, which is what we see in the call-stack. The target of the CALL instruction (0x5D720368) might in real life be called something like "DisplayExpirationDialog" because that's what it and its helper functions perform. When the Expiration Dialog is closed, the function eventually returns control to 0x5D72049B with a result in the EAX register. This value is then checked at 0x5D72049E to see if it is negative. Lucky for us this return value determines whether or not Visual Studio closes afterwards. In other words, since you can register with Microsoft within the Expiration Dialog, the dialog needs the ability to pass back the condition, "the time period expired, but the user just successfully registered, so don't close Visual Studio". A negative return value from this function is what propagates the exit signal back up the chain and anything else keeps Visual Studio open and functional.

Pseudocode for the assembly snippet above is basically as follows:

int PopupExpirationDialogAndHandleContinueResult() { if ( DisplayExpirationDialog(some_args) < 0 || SomeOtherCheck(some_other_args) < 0) ) { //close Visual Studio branch ... } ... return(0); }

So what would happen if we NOP out the first function CALL (preventing the Expiration Dialog from appearing) and also preventing the "close Visual Studio" branch at 5D862C68 from executing? This change ultimately results in the desired behavior, however there is a subsequent function CALL (we'll call it "SomeOtherCheck") which can also trigger the same "close" branch. To be on the safe side, we'll allow that CALL to execute, but NOP out the following JS instruction too.

To clarify, the fix is to NOP-out the CALL at 5D720496 and NOP-out the 2 "JS" instructions that could possibly take us to the "close" branch.

My version of msenv.dll used for the debugging session above was 15.0.26430.16, however the CALL and JS byte offsets will differ if you have a different build of msenv.dll, so searching for these byte patterns won't help much. You'll have to launch a debugger as illustrated here and look at the call-stack location most recently called by Microsoft.VisualStudio.ProductKeyDialog.ni.dll. That is, looking one line above the first instance of Microsoft.VisualStudio.ProductKeyDialog.ni.dll into msenv.dll from the top of the call-stack window.

Once you've saved these changes, the digital signature as well as the checksum for msenv.dll will be broken, however Visual Studio doesn't check either. If you've correctly made the patch, the dialog won't appear at startup or randomly while you're working. In fact, the only way to get the expiration dialog to appear is to explicitly call it out by selecting "Register Product" from the "Help" menu. In this instance, the dialog still says you're running an expired Visual Studio, but dismissing the window with the "Exit Visual Studio" button closes the dialog, but Visual Studio will remain open! Problem solved.

I'm not going to distribute any binaries here because it is highly likely you will have a different version of msenv.dll than the one I was running due to the pace at which Microsoft releases new builds of Visual Studio. Consider this a fun debugging exercise to gain experience patching your own software. You may even find it is fun to try and modify other misbehaving software running on your computer(s)! :)

Summing up the previous steps, I've found that they appear to work for several recent versions of Visual Studio (and possibly into the future).
  • Launch Visual Studio Community Edition, whatever version you have
  • Wait for the Expiration Dialog to appear
  • Launch OllyDbg or whatever debugger you want and attach to the already running devenv.exe
  • Switch the Thread Context to the Main/First Thread (OllyDbg: double-click in Thread window)
  • Find the current position debugger has paused on in the call-stack window and look downwards for first "Microsoft.VisualStudio.ProductKeyDialog.ni.dll" entry (if you reach devenv.exe, you've gone too far)
  • From here, look up one entry in the call-stack; this should be some address within msenv.dll.
  • Double-click this msenv.dll spot and verify you have instructions similar to those shown above (CALL, JS, CALL, JS)
  • Overwrite the CALL instruction bytes at that address with NOP bytes (0x90) (OllyDbg: <SPACE> -> "NOP" -> <ENTER>)
  • NOP out the next two JS instructions the same way
  • Save the modified binary (OllyDbg: Right-click -> Edit -> Copy all modifications to executable, Right-click new window that appears -> Save File)
  • Abort Debugging Session
  • Replace "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community\Common7\IDE\msenv.dll" with the one you just saved (make sure you keep a backup of the original)
  • Restart Visual Studio

DEBUGGER TRICKS: LOADING MODULES AT PREDICTABLE ADDRESSES:
One of the tricks I used to make debugging faster and easier above, is to begin with a modified version of msenv.dll that loads at a predictable, fixed address for each debugging session. This ensures the pointer addresses referencing the module's base address (such as for code and data) remain the same between debugging sessions.

If you've read my prior article on ASLR, you'll know that modern versions of Visual C++ instruct the linker to build executable modules with the PE characteristics DYNAMIC_BASE flag by default; in effect enabling ASLR support. It is not surprising though that modules built for Visual Studio will too have the DYNAMIC_BASE flag. As such, msenv.dll will load at semi-random memory locations each time Visual Studio starts and that is something we'd like to avoid if possible because it makes each debugging session unnecessarily tedious due to the constantly changing pointer addresses.

While OllyDbg is smart enough to adjust simple breakpoints to their correct offset as the base address changes with each session, any diary notes you may be taking separately involving addresses will need to be adjusted manually. If you are trying to correlate addresses from your debugging session with addresses in an IDA window for example, whose addresses are based on the module's "preferred base" address, you'll be constantly converting to and from different addresses. To convert one address to another, you must locate and subtract off the base address to get the offset, then add to that the new base address being converted to.

The small bit of time in preparing your modules prior to debugging is worth the effort even if you ultimately revert to the original load flags after applying your patch(es).

You can usually remove the DYNAMIC_BASE flag from a given module (updating the checksum too with a separate tool as needed), ensuring the module loads at a fixed address with every launch, and have the program continue to function normally without having to forcibly disable other checks (i.e. checksum checks, or digital signature checks) designed to be a red alert that the module has been modified. Most modules don't have digital signatures and if they do, the underlying software rarely checks that all the digital signatures of its modules remain intact.

To remove the DYNAMIC_BASE flag from the PE header (backing up the original file first), turn to the editbin.exe tool from your Visual Studio tools distribution (2010 or higher support the removal of this flag). But, since editbin.exe is nothing more than a link.exe wrapper (all arguments are passed to link.exe with the "/EDIT" switch), we'll cut out the middle-man and just call link.exe directly. The following linker command is the minimum you need in order to stop Windows from loading a module at a random base-address:

link.exe /EDIT /DYNAMICBASE:NO msenv.dll

In many cases, the command above is all that you need, especially if you are dealing with an EXE. EXEs gets first-crack at any requested address ranges because they are they are first to be mapped into memory. That is unless the base address has been manually modified to some OS-unsupported value (e.g. such as above 0x80000000, kernel range, is an invalid address range for user-mode code under typical 32-bit installations of Windows).

The next step is to find the preferred base address embedded within the module itself. Using you're favorite PE dump or analyzer utility (such as dumpbin that comes with Visual Studio or pelook -h <module_name>), locate the module's base address (a.k.a. preferred base).

For purposes of this article, you'll probably discover that the version of msenv.dll you have was built with a base address of 0x10000000, the default base address given to any DLLs that don't explicitly give the linker a base address. What this means is that out of the hundred or so DLLs loaded by Visual Studio, only the first DLL asking for address 0x10000000 will get it (assuming they also have the DYNAMIC_BASE flag removed too). msenv.dll and the rest are forcibly relocated to another seemingly random unused base address, which still translates to ASLR behavior despite the fact that the DYNAMIC_BASE flag was removed. We actually don't care what the address loads at, just that it remains consistent between debugging runs to make reverse-engineering less tedious. So there is an additional step we must make.

Unfortunately, its not as simple as changing the 32-bit base address value in the PE OptionalHeader structure and calling it good. The base address set in the PE header is implicitly hard-coded at other locations within the module's code and data references. This is also why there exists such a thing as a .reloc section. This is a table of all the spots where a base address reference is hard-coded. So, if a DLL cannot load at its preferred base address AND it contains a .reloc section, the system will relocate the DLL to any unused base address it wants; otherwise the DLL will fail to load. These .reloc section entries are known as "fixups" and its what the Windows loader must process for each and every module containing the DYNAMIC_BASE flag or whose base-address is already taken by another module. We can also use the .reloc section to statically relocate the DLL to a new base so that this doesn't have to be calculated at runtime. This process is known as rebasing.

To rebase a module, you get to come up with your own semi-random address, hopefully different from any other executable module's base address that is simultaneously loaded within the same process. In older versions Windows, the base addresses of all executable modules under direct control of a development team would be carefully chosen unique values to decrease application load times. This meant the Windows loader doesn't have to parse the .reloc section and process the fixups each time your application loads. Nowadays, this performance benefit is negligible in most cases and the security gain for rebasing modules at runtime is a reasonable trade-off. However knowledge of rebasing modules is still important and highly useful, even as a temporary measure, for assembly level debugging.

My best recommendation is to note one of the randomly assigned base-addresses of the module in question during a debugging session. In the case of msenv.dll, it had been randomly assigned a base address of 0x5D6A0000 in one of my first debugging sessions with Visual Studio, so that is the address I rebased it to for debugging purposes.

Specifying a module's base address is easiest if you are the one building it, as this is just another argument you pass to the linker using the /BASE option. Since we are modifying a module after it has been built, there is the potential to run into yet another problem we'll get in to shortly. To rebase a module, we'll again use link.exe, invoking its "rebase" feature as shown below:

link.exe /EDIT /REBASE:BASE=0x5D6A0000 msenv.dll

But, sometimes the linker won't allow you to rebase a module. As in the case of rebasing msenv.dll, you'll see this error:

LINK : fatal error LNK1175: failed to rebase 'msenv.dll'; error 32

Truth be told, this is a crappy error message because code 32 doesn't mean anything outside of the linker (it's not a Win32 error) and the linker doesn't document these types of error codes. I checked. The time I wasted searching the internet took longer than attaching a debugger and analyzing the disassembly first-hand.

I did however learn that link.exe doesn't do any rebasing itself. Like editbin.exe, link.exe is also a wrapper for functionality available to the public in API form, which is a good thing. link /REBASE forwards the work onto the ReBaseImage64() API within IMAGEHLP.DLL. Error 32 happens to be the generic code for when IMAGEHLP.ReBaseImage64() fails for some reason although it would be better if the linker simply returned the Win32 last error code set by the failing API. Under a debugger, IMAGEHLP.ReBaseImage64() was failing on msenv.dll with a GetLastError() code of of 0xD9. Luckily this is the documented Win32 error ERROR_EXE_CANNOT_MODIFY_SIGNED_BINARY. Disassembling IMAGEHLP.ReBaseImage64() confirms that the presence of a digital signature is the crux of the issue. The branch within the API function that returns code 0xD9 is executed IF there is a Data Directory entry for index #4 (i.e. IMAGE_DIRECTORY_ENTRY_SECURITY which means DIGITAL SIGNATURE). In other words, if the PE Module has any nonzero values where a digital signature's file offset information is stored, Microsoft's implementation of ReBaseImage64() will refuse to process the file, despite the lack of a technical limitation. The idea is that after rebasing a signed module (as with any modification after a digital signature has been applied), the digital signature will be surely be broken. Duh! Can we pass a flag to IMAGEHLP.ReBaseImage64() to say "rebase anyway" and we'll deal with the consequences of a broken digital signature? Unfortunately, there is no such flag.

Let's fix that.

What we need is for IMAGEHLP.DLL to skip this check, rebase anyway and shut up. Visual Studio doesn't check the digital signature of msenv.dll which makes our lives that much easier, otherwise we'd need further modifications to Visual Studio. After many years of doing this kind of work, I've only had to disable a digital signature check once. I gather that almost no programs bother to perform a code-signing check when trying to determine self-integrity as the checks themselves are easy to find (the WinTrust APIs stand out clearly) and internal checksumming (i.e. small loop(s) that hash and match the module's code/data bytes to expected value(s)) are easier to hide from reverse engineering efforts. Offline forensic tools however are an exception to this rule as they typically scan the entire system and flag things like files with broken digital signatures. Because none of this concerns us for the problem at hand, let's continue.

The offending check in ReBaseImage64() is located at near the top of the function:

4187974D 8D 85 BC FD FF FF lea eax, [ebp+Size] 41879753 50 push eax ; Size 41879754 6A 04 push 4 ; IMAGE_DIRECTORY_ENTRY_SECURITY index 41879756 6A 00 push 0 ; MappedAsImage 41879758 FF B5 8C FD FF FF push [ebp+LoadedImage.MappedAddress] ; Base 4187975E E8 B6 AF FF FF call ImageDirectoryEntryToData 41879763 85 C0 test eax, eax 41879765 0F 85 2A 03 00 00 jnz loc_41879A95 ;fail branch with code 0xD9 (digital signature file offset (RVA) is nonzero) 4187976B 39 85 BC FD FF FF cmp [ebp+Size], eax 41879771 0F 85 1E 03 00 00 jnz loc_41879A95 ;fail branch with code 0xD9 (digital signature size is nonzero)

The fix is to NOP out the two JNZ/JNE instructions above, preventing the failure branch from executing. One quick way to do this is to start OllyDbg to debug the failing link.exe /REBASE command-line. Place a breakpoint on IMAGEHLP.ReBaseImage64(). Once the breakpoint is reached, locate the disassembly code shown above and replace the two JNZ/JNE instructions (marked as fail branch above) each with 6 NOP bytes (0x90's) so the fail branch can never be taken. Save the modified IMAGEHLP.DLL to its own directory for later use; DO NOT overwrite the one in the system directory! The remainder of the debugging session can be aborted.

I should note in all honesty this is not the quickest way to bypass this rebasing error thrown up by the linker. Since we don't care about a broken digital signature, removing the soon-to-be-broken digital signature data chunk prior to running link /REBASE would also work. Tools are available to remove the digital signature from executable modules. NULLifying the digital signature entry in the Data Directory with a hex editor, effectively orphaning the digital signature data at the end of the PE image will also work and is basically what these tools are doing.

But, modifying IMAGEHLP.DLL directly allows me to illustrate how to accomplish a goal by using a modified system DLL just for a specific application (EXE).

The ultimate goal is to force link.exe via IMAGEHLP.DLL to rebase any file, even if the file has a digital signature. Start by copying the link.exe you want to use to the same directory you saved the hacked version of IMAGEHLP.DLL created above. Don't forget to copy all of the link.exe dependencies. For the Visual Studio 2010 version of link.exe, you'll want to copy msdis170.dll, mspdb100.dll, and msvcr100.dll files.

Despite that we will obviously want link.exe to use the hacked IMAGEHLP.DLL residing next to it in the same directory rather than the one in the system directory, this will not happen by default as it used to in older versions of Windows. Modern versions of Windows have disabled this behavior for security reasons. Windows will always give the IMAGEHLP.DLL found in %WINDIR%\System32 precedence over the same named DLLs found in the application directory or current directory. The way to force it is to drop a boilerplate manifest file specifying the IMAGEHLP.DLL without a path. This is done by creating a text file named "link.exe.manifest" alongside link.exe (same directory) and fill it with the content below:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assemblyIdentity name="MyRedirect" version="1.0.0.0" type="win32" /> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <security> <requestedPrivileges> <requestedExecutionLevel level="asInvoker" uiAccess="false" /> </requestedPrivileges> </security> </trustInfo> <file name="imagehlp.dll" /> </assembly>

Now the manifest file created in this step would normally get picked up by link.exe when it loads next, but there is yet another detail to contend with. Since link.exe happens to have its own manifest file embedded within (its resource section), this embedded manifest takes precedence over any external manifest file.

To fix this, you can use a resource editor, such as the reverse-engineering staple: Resource Hacker to delete the embedded manifest from the module. The manifest shown above is actually the embedded link.exe manifest I found within the link.exe I was using, but with the added XML element: <file name="imagehlp.dll" /> (formatting mine though)

You should probably base your link.exe.manifest file's contents based on embedded manifest found within the version of link.exe you are using, so as to avoid any subtle bugs. Once you've built the file and removed the embedded manifest, link.exe is ready to use the hacked version of IMAGEHLP.DLL. You are now finally ready rebase any type of module, signed or not without getting any errors!

<END OF ARTICLE>


Questions or Comments?


 1:1