📜 ⬆️ ⬇️

Changing the contents of Web.config at runtime when debugging in Visual Studio and IISExpress

Technologically, this article is nothing new, just another useful application of winapi-hooks for solving a specific problem.

When working with web projects in Visual Studio, there is one unpleasant trifle: when using several development branches in the development process, each of which must use its own copy of the environment (for example, a database or some external services), there is a problem with configuration files at the time of debugging - IISExpress uses only the main web.config in the project folder, where usually all connection strings contain default values ​​and where there are no branch-specific settings, and no transformations are applied to it at startup. You can of course forcefully, either automatically or manually, apply transformations to the web.config, but firstly the modified file will constantly hang in pending changes, which creates the risk of committing unwanted changes that will then fall into other branches, and secondly it creates a lot inconvenience when editing it, because before committing any changes in the configuration file, such transformations will have to be manually removed.

Consider how to avoid this.

The solution is quite simple - it is necessary to intercept the reading of the configuration file by the IISExpress process, and instead of the source file, slip another, temporary file in which the corresponding corrections have been made, and which is not added to Source Control. The list of fixes that need to be applied, depending on which folder the project runs from, can be specified, for example, in a simple xml file.
')
For this you will need:

Background utility tracking the creation of new processes, 32 and 64-bit dll with hooks, 32 and 64-bit exe launched by the background utility and loading the corresponding dll with the hook into the process of the corresponding capacity.

The background utility monitors processes through WMI using the ManagementEventWatcher class and a request for __InstanceOperationEvent with filtering by the type of Win32_Process object and the required process names. Receiving the ____InstanceCreationEvent event means that a process was created, information about which can be obtained from EventArrivedEventArgs.NewEvent. In this case, only ProcessId is required.

processWatcher.Query.QueryString = @"SELECT * FROM __InstanceOperationEvent WITHIN 1" + "WHERE TargetInstance ISA 'Win32_Process' AND (" + string.Join(" OR ", processNames.Select(x => "TargetInstance.Name = '" + x + ".exe'")) + ")"; processWatcher.EventArrived += (sender, e) => { if (e.NewEvent.ClassPath.ClassName == "__InstanceCreationEvent") { var processId = (uint)((ManagementBaseObject)e.NewEvent["TargetInstance"]) .Properties["ProcessId"].Value; // ... Do smth useful } }; 

The dll loading algorithm in the foreign process is standard - in the foreign process, VirtualAllocEx allocates memory for the path to the dll and creates a stream by passing the LoadLibrary address to CreateRemoteThread. If the initialization code of the hooks is in DllMain, then no additional action will be needed. But the background utility will not be able to self-embed dll in both 32 and 64-bit processes at the same time. Theoretically, of course, calling CreateRemoteThread from a 64-bit process can create a stream in a 32-bit process, but in this case, LoadLibrary is used as a function for the stream. And as simple as possible, using GetProcAddress, you can get its address only for the same bitness as the current process. The kernel32.dll has a fixed base address, so the address of the function is the same for different processes of the same bitness. In theory, of course, it would be possible to manually parse the PE headers and not use additional processes for implementation, but this is more difficult.

Of course, you need to intercept the function CreateFileW. At first, I wrote all the code entirely in C #, but in practice, with interception of some too fundamental functions like this, errors with loader lock and the like occur when managed code is called, for example, from DllMain of any third-party libraries loaded by the process. Therefore, it was necessary to install hooks and filtering calls that require processing into a native dll in C, which in turn loads the managed dll and calls the managed code from there only when CreateFileW is called for .config files. To install hooks, I used the time-tested third-party library MinHook , there is a lot of information about it on the Internet and I will not dwell on its description. Maybe someone will have a question - “wouldn’t it be easier to do everything completely in C and not create a bunch of .net assemblies”, maybe yes, but this is boring.

The filtering logic should verify that the file exists, is a file and not a directory, contains web.config in the name, is not located in the windows \ microsoft.net \ ... folder (we are not interested in system configs). If all these conditions are met, then we pass the HANDLE received from the call to the original system function CreateFileW by the interceptor, as well as all the parameters CreateFileW in the managed handler, which will read the contents of this HANDLE. For simplicity, it is better to use the original HANDLE in order not to do protection against recursion, which will be caused by reading the same file by some File.ReadAllText, since the same hook will work. Further, in the received content all necessary lines are replaced and the changed content is written to a temporary file whose name does not match the above filtering criteria (again so as not to get into recursion). We call CreateFileW for this temporary file with the same parameters with which web.config was opened and the resulting HANDLE is returned from CreateFileW interceptor. The original HANDLE is no longer needed and should be closed.

Hook
 HANDLE WINAPI _CreateFileW( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) { DWORD attributes = GetFileAttributesW(lpFileName); HANDLE result = CreateFileWOriginal(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); HANDLE newFile = NULL; if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0 && (StrStrI(lpFileName, L"web.config") != NULL || StrStrI(lpFileName, L"app.config") != NULL) && StrStrI(lpFileName, L"Windows") == NULL) { fileHandler(result, &newFile, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); } if (newFile != NULL) { CloseHandle(result); result = newFile; } return result; } 


Managed handler
 public static void GetUpdatedConfigF(IntPtr handle, IntPtr newHandleAddress, uint access, uint share, IntPtr securityAttributes, uint creationDisposition, uint flagsAndAttributes, IntPtr templateFile) { try { if (config == null) return; var path = new StringBuilder(260); if (GetFinalPathNameByHandle(handle, path, (uint)path.Capacity, 0) == 0) return; var matchedSection = config.FirstOrDefault(x => path.ToString().IndexOf(x.Branch, StringComparison.OrdinalIgnoreCase) >= 0); if (matchedSection == null) return; var size = GetFileSize(handle, IntPtr.Zero); if (size == 0) return; var buffer = new byte[size]; uint bytesRead; if (!ReadFile(handle, buffer, (uint)buffer.Length, out bytesRead, IntPtr.Zero)) return; var content = Encoding.UTF8.GetString(buffer); foreach (var replacement in matchedSection.Replacements) content = content.Replace(replacement.Find, replacement.ReplaceWith); var tempFile = Path.GetTempFileName(); MoveFileEx(tempFile, null, 4); File.WriteAllText(tempFile, content); var newHandle = CreateFileW(tempFile, access, share, securityAttributes, creationDisposition, flagsAndAttributes, templateFile); Marshal.WriteIntPtr(newHandleAddress, newHandle); } catch { } } 


A native dll with a hook calls the handler from a managed dll via LoadLibrary and GetProcAddress. To do this, you need to export the static method as a normal dll function. This is done in a slightly shamanic way through disassembling ildasm, adding special options to the method in the il-code and assembling back to the dll. There are also many articles on the Internet about this, I will not repeat, they are easy to find by looking for example ".vtentry". The source code contains the simplest utility that processes assemblies in this way.

In addition to IISExpress, a similar problem is also relevant for wcf-services launched via wcfsvchost. However, in this case, the use of transformations works fine, but so that everything is consistent and in order not to clone extra files with transformations and switch nothing in the Configuration Manager, consider this case as well. There are some differences - wcfsvchost reads the configuration immediately when the process starts, and the WMI event comes too late, and the hook is installed later than necessary. But the path to the configuration file is transmitted via the command line, so you should embed it in the parent process and intercept CreateProcessW.

The parent process in this case is devenv.exe, i.e. Visual Studio. In this case, before calling the original system function CreateProcessW, in the managed handler we pass a string of parameters with which the process is created and the address of the array where the corrected string is written. In the handler, the line is divided into parameters by calling CommandLineToArgvW, then the path to the configuration file is defined among them, and then a temporary file with corrected contents is created in the same way and the path is replaced with the parameters in the parameters.

Hook
 BOOL WINAPI _CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation) { BOOL result; LPWSTR buffer = NULL; if (lpCommandLine != NULL && StrStrI(lpCommandLine, L".config") != NULL) { buffer = (LPWSTR)malloc(BUFFER_SIZE); memset(buffer, 0, BUFFER_SIZE); processHandler(lpCommandLine, buffer); lpCommandLine = buffer; } result = CreateProcessWOriginal(lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation); if (buffer != NULL) free(buffer); return result; } 


Managed handler
 public static void GetUpdatedConfigP(IntPtr commandLine, IntPtr newCommandLine) { var commandLineText = Marshal.PtrToStringUni(commandLine); try { int numArgs; var argArray = CommandLineToArgvW(commandLineText, out numArgs); if (argArray != IntPtr.Zero) { var pointerArray = new IntPtr[numArgs]; Marshal.Copy(argArray, pointerArray, 0, numArgs); var arguments = pointerArray.Select(x => Marshal.PtrToStringUni(x)).ToArray(); var configFile = arguments.FirstOrDefault(x => x.EndsWith(".config", StringComparison.OrdinalIgnoreCase)); var matchedSection = config.FirstOrDefault(x => configFile.ToString() .IndexOf(x.Branch, StringComparison.OrdinalIgnoreCase) >= 0); if (matchedSection != null && configFile != null && configFile.StartsWith("/config:", StringComparison.OrdinalIgnoreCase) && commandLineText.IndexOf("wcfsvchost", StringComparison.OrdinalIgnoreCase) >= 0) { configFile = configFile.Substring("/config:".Length); var content = File.ReadAllText(configFile); foreach (var replacement in matchedSection.Replacements) content = content.Replace(replacement.Find, replacement.ReplaceWith); var tempFile = Path.GetTempFileName(); MoveFileEx(tempFile, null, 4); File.WriteAllText(tempFile, content); commandLineText = commandLineText.Replace(configFile, tempFile); } } } catch { } Marshal.Copy(commandLineText.ToCharArray(), 0, newCommandLine, commandLineText.Length); } 


→ Code to the article (perfectionists, please do not resent - this is the minimum working version made hastily, without error handling and with many assumptions)

Source: https://habr.com/ru/post/321768/


All Articles