<-- Articles Index / Analysis of Visual Studio's Trial Expiration and Module Rebasing Tricks
Analysis of Visual Studio's Trial Expiration and Module Rebasing Tricks
Date: May 8, 2020
Last-Modified: May 8, 2020
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."
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.
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
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")
_tprintf(TEXT("ERROR: unsupported visual studio version: %u\n"),uVsYearVer);
_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);
//query the data
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);
//dump the raw data
_tprintf(TEXT("raw data size = %u byte(s)\n"),uDataSize);
dumpBufferHelper(arData,uDataSize,TEXT("raw data (encrypted)"));
//decrypt the data
dataBlobIn.cbData = uDataSize;
dataBlobIn.pbData = arData;
WCHAR* pszDesc = NULL;
_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());
_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("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);
if (pszDesc) LocalFree(pszDesc);
if (dataBlobOut.pbData) LocalFree(dataBlobOut.pbData);
} //data decrypted
} //data successfully queried
//cleanup registry key
} //root path opened
} //have registry path
Program output might look something like this:
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.:
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.
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:
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"),
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.
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.
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:
5D720496E8 CDFEFFFFCALLmsenv.5D720368;pop-up Expiration Dialog5D72049B8B7D ECMOVEDI, DWORD PTR SS:[EBP-14]5D72049E85C0TESTEAX, EAX5D7204A00F88 C2271400JSmsenv.5D862C68;retval negative? close Visual Studio...5D7204A68B53 2CMOVEDX, DWORD PTR DS:[EBX+2C]5D7204A98D45 F0LEAEAX,[EBP-10]5D7204AC50PUSHEAX5D7204AD8BCFMOVECX, EDI5D7204AFE8 CBA6FFFFCALLmsenv.5D71AB7F;some other check (?)5D7204B48B75 F0MOVESI, DWORD PTR SS:[EBP-10]5D7204B785C0TESTEAX, EAX5D7204B90F88 A9271400JSmsenv.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:
if ( DisplayExpirationDialog(some_args) < 0 || SomeOtherCheck(some_other_args) < 0) )
//close Visual Studio branch
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
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!
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:
4187974D8D 85 BC FD FF FFleaeax,[ebp+Size]4187975350pusheax; Size418797546A 04push4; IMAGE_DIRECTORY_ENTRY_SECURITY index418797566A 00push0; MappedAsImage41879758FF B5 8C FD FF FFpush[ebp+LoadedImage.MappedAddress]; Base4187975EE8 B6 AF FF FFcallImageDirectoryEntryToData4187976385 C0testeax, eax418797650F 85 2A 03 00 00jnzloc_41879A95;fail branch with code 0xD9 (digital signature file offset (RVA) is nonzero)4187976B39 85 BC FD FF FFcmp[ebp+Size], eax418797710F 85 1E 03 00 00jnzloc_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:
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:
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!