> ## Documentation Index
> Fetch the complete documentation index at: https://ahmed-walid.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Using Edge Legacy's WebView in an unpackaged Win32 app

> And debugging a mysterious crash on app exit

## **Intro**

Edge Legacy's XAML [WebView](https://learn.microsoft.com/uwp/api/windows.ui.xaml.controls.webview) control has always been incompatible with both unpackaged apps and legacy Win32 apps.

Any attempt to using this control under these conditions will always result in an exception, whether on XAML Islands or any other non-UWP XAML hosting techniques such as XamlHost or XamlPresenter.

I wanted to use WebView as fallback SVG renderer for [MrmTool](https://github.com/ahmed605/MrmTool) since the default [NanoSVG](https://github.com/memononen/nanosvg)-based renderer is limited and doesn't cover all of SVG features, but the problem is that MrmTool is an unpackaged Win32 app so it cannot normally use the WebView control, and I wanted to avoid [WebView2](https://developer.microsoft.com/microsoft-edge/webview2) since it would increase the app size and it's not available (or at least not installed by default) on all platform environments that MrmTool supports.

So I decided to investigate why it throws exceptions and see if I can fix it or at least patch it to work...

## **Investigating**

The first exception we face seems to be `E_XAMLPARSEFAILED`

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B2AB7BA9E-5661-459E-B2B6-CCD58384B088%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=5ed96048f7ca6dddf253d4e2bf5be7a1" alt="img" width="1441" height="740" data-path="images/{2AB7BA9E-5661-459E-B2B6-CCD58384B088}.png" />

But that error is from the XAML parser so it's already too late and the actual error was absorbed by the parser, so let's initialize WebView early manually and see what we get...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B58FF6CAF-9A09-44CF-B9A8-327E20E4C2F2%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=1d1d74235d59b48a34e2ee40ce2a0dad" alt="img" width="1898" height="505" data-path="images/{58FF6CAF-9A09-44CF-B9A8-327E20E4C2F2}.png" />

Ah `E_FAIL` that makes more sense, but sadly it seems like the actual location of the exception has been lost, the callstack is from the generic XAML **HRESULT** handler, so lets see if we can track where that HRESULT was originally returned...

The easiest way to do that would be following the WebView initialization code under IDA Pro and seeing where that **HRESULT** first occurs.

it seems like the init logic happens in `CWebView::CreateComponent` which then calls `DirectUI::WebView::CreateComponent` which then calls `DirectUI::CoreWebViewHost::CreateComponent`, and here we found our first occurrence of `E_FAIL` (**0x80004005**):

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BF4482F43-883F-4399-8ECF-BF724760EF00%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=aa1e94e75187eab216c36d4524da51e0" alt="img" width="1086" height="451" data-path="images/{F4482F43-883F-4399-8ECF-BF724760EF00}.png" />

It seems to be returned if the global field `s_bWebPlatformSecurityManagerFactoryCallbackRegistered` is `false`, so lets see Xrefs of this field to see what sets it

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BAB6E7701-839D-4C4E-B669-0E7C225A02BE%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=8dc44f5344b2673961c54f52da94bd64" alt="img" width="1410" height="466" data-path="images/{AB6E7701-839D-4C4E-B669-0E7C225A02BE}.png" />

As you can see, it seems to be set by a function called `DirectUI::CoreWebViewHost::RegisterWebViewPermanentSecurityManager`

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B03DA7E2F-2101-4FA5-9C0B-3D0EE264F7B9%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=fd49cdb0938b1761a3522a8ba8ec5f57" alt="img" width="1058" height="352" data-path="images/{03DA7E2F-2101-4FA5-9C0B-3D0EE264F7B9}.png" />

I put a breakpoint on this function to see what goes wrong in it but the breakpoint was never hit??

So there must be something else preventing this function from being called, lets see what calls this function and how it is called...

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BD9745386-7689-4818-9858-89769BC436FB%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=ce5470c007041524beddde17a205b618" alt="img" width="965" height="321" data-path="images/{D9745386-7689-4818-9858-89769BC436FB}.png" />

It seems to be called by `EnsureDelayedInit` so lets check it out....

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B3051D914-A757-4592-B6A7-548B362301B4%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=977bf99dd34897244b5a63ac7c870b98" alt="img" width="816" height="428" data-path="images/{3051D914-A757-4592-B6A7-548B362301B4}.png" />

Ah so it's indeed called conditionally based on `DesignerInterop::GetDesignerMode`, `DirectUI::XamlRuntime::IsWebViewEnabled`, and  `ShouldProcessRegisterWebViewSecurityManager`.

We are not in the designer so we can ignore the first check, and by debugging I found out that the second check returns `true` so we can ignore that one too, so it must be the third one, so lets check it out...

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BF5C453BE-12DF-4907-91A3-E087FB8C5704%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=5ab1a013fba70a370f04eac60194f4c5" alt="img" width="1008" height="732" data-path="images/{F5C453BE-12DF-4907-91A3-E087FB8C5704}.png" />

By looking at this function it seems like for it to return `true` one of these conditions must be true:

* The application is running under **AppContainer**
* The application is ***not*** the **Settings** app ***and*** the application is ***not*** using `ClassicDesktop` AppModel windowing policy

As expected both are false in our case, the second condition seems to be the easier one to patch since we only need to patch the return value of the `AppPolicyGetWindowingModel` function.

## **Patching & More Investigation**

The most logical thing to do would be hooking the function using **Detours** (or similar hooking libraries), but since **MrmTool** is written in **C#** it would be harder to statically link **Detours** without sarcrificing **CoreCLR** and being exclusively on **NativeAOT**, and using **Detours** as a dynamic library would increase the app size and the number of files it carries, so I decided to use [IAT patching](https://blog.neteril.org/blog/2016/12/23/diverting-functions-windows-iat-patching/) instead.

Luckily a project I'm part of called [XWine1](https://twitter.com/XWineOne) already had helpers so I ported them to **C#** with few adaptations to unpatch the functions on process exit to accommodate for **GC Shutdown**:

```csharp expandable theme={null}
internal static readonly Dictionary<nuint, nuint> PatchedFunctions = [];

// Originally written by @DaZombieKiller for the XWine1 project
private static HRESULT XWineFindImport(HMODULE Module,
                                       byte* Import,
                                       IMAGE_THUNK_DATA* pImportAddressTable,
                                       IMAGE_THUNK_DATA* pImportNameTable,
                                       IMAGE_THUNK_DATA** pThunk)
{
    for (nuint j = 0; pImportNameTable[j].u1.AddressOfData > 0; j++)
    {
        if ((pImportNameTable[j].u1.AddressOfData & IMAGE.IMAGE_ORDINAL_FLAG) != 0)
        {
            if (!IS_INTRESOURCE((nuint)Import))
                continue;

            if (((pImportNameTable[j].u1.Ordinal & ~IMAGE.IMAGE_ORDINAL_FLAG) == (nuint)Import))
            {
                *pThunk = &pImportAddressTable[j];
                return S.S_OK;
            }

            continue;
        }

        var name = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)Unsafe.AsPointer(in ((IMAGE_IMPORT_BY_NAME*)((byte*)Module + pImportNameTable[j].u1.AddressOfData))->Name.e0));
        var importName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(Import);

        if (!name.SequenceEqual(importName))
            continue;

        *pThunk = &pImportAddressTable[j];
        return S.S_OK;
    }

    *pThunk = null;
    return E.E_FAIL;
}

// Originally written by @DaZombieKiller for the XWine1 project
internal static HRESULT XWineGetImport(HMODULE Module,
                                       HMODULE ImportModule,
                                       byte* Import,
                                       IMAGE_THUNK_DATA** pThunk)
{
    if (ImportModule.Value is null)
        return E.E_INVALIDARG;

    if (pThunk == null)
        return E.E_POINTER;

    if (Module.Value is null)
        Module = GetModuleHandleW(null);

    var dosHeader = (IMAGE_DOS_HEADER*)Module;
    var ntHeaders = (IMAGE_NT_HEADERS*)((byte*)Module + dosHeader->e_lfanew);
    var directory = &ntHeaders->OptionalHeader.DataDirectory[IMAGE.IMAGE_DIRECTORY_ENTRY_IMPORT];

    if (directory->VirtualAddress <= 0 || directory->Size <= 0)
        return E.E_FAIL;

    var peImports = (IMAGE_IMPORT_DESCRIPTOR*)((byte*)Module + directory->VirtualAddress);

    for (nuint i = 0; peImports[i].Name > 0; i++)
    {
        if (GetModuleHandleA((sbyte*)((byte*)Module + peImports[i].Name)) != ImportModule)
            continue;

        var iatThunks = (IMAGE_THUNK_DATA*)((byte*)Module + peImports[i].FirstThunk);
        var intThunks = (IMAGE_THUNK_DATA*)((byte*)Module + peImports[i].OriginalFirstThunk);

        if (SUCCEEDED(XWineFindImport(Module, Import, iatThunks, intThunks, pThunk)))
            return S.S_OK;
    }

    var delayDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE.IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT];
    if (delayDir->VirtualAddress > 0 && directory->Size > 0)
    {
        var delayImports = (IMAGE_DELAYLOAD_DESCRIPTOR*)((byte*)Module + delayDir->VirtualAddress);

        for (nuint i = 0; delayImports[i].DllNameRVA > 0; i++)
        {
            if (GetModuleHandleA((sbyte*)((byte*)Module + delayImports[i].DllNameRVA)) != ImportModule)
                continue;

            var iatThunks = (IMAGE_THUNK_DATA*)((byte*)Module + delayImports[i].ImportAddressTableRVA);
            var intThunks = (IMAGE_THUNK_DATA*)((byte*)Module + delayImports[i].ImportNameTableRVA);

            if (SUCCEEDED(XWineFindImport(Module, Import, iatThunks, intThunks, pThunk)))
                return S.S_OK;
        }
    }

    *pThunk = null;
    return E.E_FAIL;
}

// Originally written by @DaZombieKiller for the XWine1 project
internal static HRESULT XWinePatchImport(HMODULE Module,
                                         HMODULE ImportModule,
                                         byte* Import,
                                         void* Function)
{
    HRESULT hr;

    uint protect;
    IMAGE_THUNK_DATA* pThunk;
    if (!SUCCEEDED_LOG(hr = XWineGetImport(Module, ImportModule, Import, &pThunk)))
    {
        return hr;
    }

    if (!VirtualProtect(&pThunk->u1.Function, (nuint)sizeof(nuint), PAGE.PAGE_READWRITE, &protect))
    {
        return HRESULT_FROM_WIN32(GetLastError());
    }

    nuint originalFunction = (nuint)pThunk->u1.Function;
    pThunk->u1.Function = (nuint)Function;

    PatchedFunctions.TryAdd((nuint)(void*)&pThunk->u1.Function, originalFunction);


    if (!VirtualProtect(&pThunk->u1.Function, (nuint)sizeof(nuint), protect, &protect))
    {
        return HRESULT_FROM_WIN32(GetLastError());
    }

    return S.S_OK;
}
```

I didn't need few of the features this helper includes so I removed them for performance reasons:

```csharp expandable theme={null}
internal static readonly (nuint Thunk, nuint OriginalFunction)[] PatchedFunctions = new (nuint, nuint)[Constants.PatchesCount];

// Originally written by @DaZombieKiller for the XWine1 project
private static HRESULT XWineFindImport(HMODULE Module,
                                       ReadOnlySpan<byte> Import,
                                       IMAGE_THUNK_DATA* pImportAddressTable,
                                       IMAGE_THUNK_DATA* pImportNameTable,
                                       IMAGE_THUNK_DATA** pThunk)
{
    for (nuint j = 0; pImportNameTable[j].u1.AddressOfData > 0; j++)
    {
        if ((pImportNameTable[j].u1.AddressOfData & IMAGE.IMAGE_ORDINAL_FLAG) != 0)
            continue;

        var name = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)Unsafe.AsPointer(in ((IMAGE_IMPORT_BY_NAME*)((byte*)Module + pImportNameTable[j].u1.AddressOfData))->Name.e0));

        if (!name.SequenceEqual(Import))
            continue;

        *pThunk = &pImportAddressTable[j];
        return S.S_OK;
    }

    *pThunk = null;
    return E.E_FAIL;
}

// Originally written by @DaZombieKiller for the XWine1 project
internal static HRESULT XWineGetImport(HMODULE Module,
                                       HMODULE ImportModule,
                                       ReadOnlySpan<byte> Import,
                                       IMAGE_THUNK_DATA** pThunk)
{
    if (ImportModule.Value is null)
        return E.E_INVALIDARG;

    if (pThunk == null)
        return E.E_POINTER;

    if (Module.Value is null)
        Module = GetModuleHandleW(null);

    var dosHeader = (IMAGE_DOS_HEADER*)Module;
    var ntHeaders = (IMAGE_NT_HEADERS*)((byte*)Module + dosHeader->e_lfanew);
    var directory = &ntHeaders->OptionalHeader.DataDirectory[IMAGE.IMAGE_DIRECTORY_ENTRY_IMPORT];

    if (directory->VirtualAddress <= 0 || directory->Size <= 0)
        return E.E_FAIL;

    var peImports = (IMAGE_IMPORT_DESCRIPTOR*)((byte*)Module + directory->VirtualAddress);

    for (nuint i = 0; peImports[i].Name > 0; i++)
    {
        if (GetModuleHandleA((sbyte*)((byte*)Module + peImports[i].Name)) != ImportModule)
            continue;

        var iatThunks = (IMAGE_THUNK_DATA*)((byte*)Module + peImports[i].FirstThunk);
        var intThunks = (IMAGE_THUNK_DATA*)((byte*)Module + peImports[i].OriginalFirstThunk);

        if (SUCCEEDED(XWineFindImport(Module, Import, iatThunks, intThunks, pThunk)))
            return S.S_OK;
    }

    var delayDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE.IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT];
    if (delayDir->VirtualAddress > 0 && directory->Size > 0)
    {
        var delayImports = (IMAGE_DELAYLOAD_DESCRIPTOR*)((byte*)Module + delayDir->VirtualAddress);

        for (nuint i = 0; delayImports[i].DllNameRVA > 0; i++)
        {
            if (GetModuleHandleA((sbyte*)((byte*)Module + delayImports[i].DllNameRVA)) != ImportModule)
                continue;

            var iatThunks = (IMAGE_THUNK_DATA*)((byte*)Module + delayImports[i].ImportAddressTableRVA);
            var intThunks = (IMAGE_THUNK_DATA*)((byte*)Module + delayImports[i].ImportNameTableRVA);

            if (SUCCEEDED(XWineFindImport(Module, Import, iatThunks, intThunks, pThunk)))
                return S.S_OK;
        }
    }

    *pThunk = null;
    return E.E_FAIL;
}

// Originally written by @DaZombieKiller for the XWine1 project
internal static HRESULT XWinePatchImport(HMODULE Module,
                                         HMODULE ImportModule,
                                         ReadOnlySpan<byte> Import,
                                         void* Function,
                                         int Index)
{
    HRESULT hr;

    uint protect;
    IMAGE_THUNK_DATA* pThunk;
    if (!SUCCEEDED_LOG(hr = XWineGetImport(Module, ImportModule, Import, &pThunk)))
    {
        return hr;
    }

    if (!VirtualProtect(&pThunk->u1.Function, (nuint)sizeof(nuint), PAGE.PAGE_READWRITE, &protect))
    {
        return HRESULT_FROM_WIN32(GetLastError());
    }

    nuint originalFunction = (nuint)pThunk->u1.Function;
    pThunk->u1.Function = (nuint)Function;

    PatchedFunctions[Index] = ((nuint)(void*)&pThunk->u1.Function, originalFunction);

    if (!VirtualProtect(&pThunk->u1.Function, (nuint)sizeof(nuint), protect, &protect))
    {
        return HRESULT_FROM_WIN32(GetLastError());
    }

    return S.S_OK;
}
```

Then it was time to patch the `AppPolicyGetWindowingModel` function:

```csharp expandable theme={null}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static int AppPolicyGetWindowingModelHook(HANDLE processToken, AppPolicyWindowingModel* policy)
{
    *policy = AppPolicyWindowingModel.AppPolicyWindowingModel_None;
    return TerraFX.Interop.Windows.ERROR.ERROR_SUCCESS;
}

internal static bool PatchWebViewAppModelChecks()
{
    var appmodel = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernel.appcore.dll"u8)));
    var xaml = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("Windows.UI.Xaml.dll"u8)));

    if (appmodel.Value is not null && xaml.Value is not null)
    {
        var fptr = (delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, int>)&AppPolicyGetWindowingModelHook;
        if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "AppPolicyGetWindowingModel"u8, fptr, 0)))
        {
            AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
            return true;
        }
    }

    return false;
}

private static void OnProcessExit(object? sender, EventArgs e)
{
    foreach (var (Thunk, OriginalFunction) in PatchingHelper.PatchedFunctions)
    {
        var thunk = (nuint*)Thunk;

        uint protect;
        if (VirtualProtect(thunk, (nuint)sizeof(nuint), PAGE.PAGE_READWRITE, &protect))
        {
            *thunk = OriginalFunction;
            VirtualProtect(thunk, (nuint)sizeof(nuint), protect, &protect);
        }
    }
}
```

So did it fix the exception? ***Yes!***

So the WebView control worked? well, ***No!***

We got another exception!!!

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B808B5B35-C54E-4962-820E-BCD2E50B937D%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=e685d3ebcb7833bf1892f14269372e53" alt="img" width="1918" height="658" data-path="images/{808B5B35-C54E-4962-820E-BCD2E50B937D}.png" />

This time we got `APPMODEL_ERROR_NO_PACKAGE`, so XAML must be calling some packaging related APIs in the WebView init code, lets see if we can find any...

Hm this call in `DirectUI::CoreWebViewHost::CreateComponent` seems interesting, it mentions "Appx" so it must be doing something with packaging APIs:

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B905288E0-687E-40BD-B5B6-4ABFB08BB38D%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=04529550b03e17e5283b6dee953084ad" alt="img" width="1049" height="431" data-path="images/{905288E0-687E-40BD-B5B6-4ABFB08BB38D}.png" />

So lets see what it does...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B8EE9711B-AD36-4785-BD41-169CED53249B%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=667fe29895a50e55823a34e5e9ecc9fb" alt="img" width="935" height="738" data-path="images/{8EE9711B-AD36-4785-BD41-169CED53249B}.png" />

Judging by the function name and its code, it seems to be getting the current package info then uses that to register activation protocols the package registers for WebView to be able to invoke them, so it doesn't seem to be a cruical thing for our use case, so lets see if we can redirect it to use another package that is always installed on the system for that, we can use the Settings app for example, so lets start by checking what functions are used to get the package info...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B97CCE94A-F3E2-4091-9415-868733380DD1%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=2dd357ec8fafd17c5ae6f096d39910f0" alt="img" width="1148" height="723" data-path="images/{97CCE94A-F3E2-4091-9415-868733380DD1}.png" />

it's using `GetCurrentPackageInfo` for that, so lets patch it and redirect it to the Settings app package...

```csharp expandable theme={null}
private static readonly string? settingsPFN =
    new PackageManager().FindPackagesForUser(null, "windows.immersivecontrolpanel_cw5n1h2txyewy").FirstOrDefault()?.Id.FullName;

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static int GetCurrentPackageInfoHook(uint flags, uint* bufferLength, byte* buffer, uint* count)
{
    if (settingsPFN is not null)
    {
        fixed (char* pSettingPFN = settingsPFN)
        {
            PACKAGE_INFO_REFERENCE pir;
            if (OpenPackageInfoByFullName(pSettingPFN, 0, &pir) is TerraFX.Interop.Windows.ERROR.ERROR_SUCCESS)
            {
                var result = GetPackageInfo(pir, flags, bufferLength, buffer, count);
                _ = ClosePackageInfo(pir);
                return result;
            }
        }
    }

    return GetCurrentPackageInfo(flags, bufferLength, buffer, count);
}

internal static bool PatchWebViewAppModelChecks()
{
    var appmodel = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernel.appcore.dll"u8)));
    var xaml = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("Windows.UI.Xaml.dll"u8)));

    if (appmodel.Value is not null && xaml.Value is not null)
    {
        var fptr = (delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, int>)&AppPolicyGetWindowingModelHook;
        if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "AppPolicyGetWindowingModel"u8, fptr, 0)))
        {
            var fptr2 = (delegate* unmanaged[Stdcall]<uint, uint*, byte*, uint*, int>)&GetCurrentPackageInfoHook;
            if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "GetCurrentPackageInfo"u8, fptr2, 1)))
            {
                AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
                return true;
            }
        }
    }

    return false;
}
```

Now, lets see what that changed...

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BAE2F4459-CB54-4BEE-A613-87C166BD52D2%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=96c2dd74225cf35a83a5c404e2505b3d" alt="img" width="1917" height="626" data-path="images/{AE2F4459-CB54-4BEE-A613-87C166BD52D2}.png" />

Hm, an interrupt from a function called `Abandonment::InduceHRESULTAbandonment` inside **edgehtml.dll**, lets check this function to see if we can get the **HRESULT** somehow\...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B3E9D1A4C-6ED1-4B96-9090-0A1F88F0EFBC%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=9c76e67326a3d2ffad6086cc8a32999f" alt="img" width="745" height="299" data-path="images/{3E9D1A4C-6ED1-4B96-9090-0A1F88F0EFBC}.png" />

It seems to be setting the **HRESULT** on a global field called `Abandonment::LastError`, lets check its address in Visual Studio disassembler so we can read it from the debugger...

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BFAAA5F25-94E6-4298-9216-4C16744F51F2%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=ac521f1c02e87094fe6831a5f1713b5c" alt="img" width="1906" height="827" data-path="images/{FAAA5F25-94E6-4298-9216-4C16744F51F2}.png" />

Interesting, it's `E_ACCESSDENIED` (**-2147024891** = **0x80070005**), and it's thrown from `CCoreWebViewTaskHandler::_CreateWebPlatform` so lets see if we can find it in that function...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B3C61EF6C-3E74-4C6F-A5E4-F0A28399AC0E%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=106a05e83d6358ea3c19e539a0181178" alt="img" width="1251" height="772" data-path="images/{3C61EF6C-3E74-4C6F-A5E4-F0A28399AC0E}.png" />

First call seems to be about getting user identity and it doesn't seem to be the one that is failing judging by the disassembly at function offset shown in the exception callstack, so lets see the second usage...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B8203E98E-5D9E-47D2-8DC6-EEDAD71FD063%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=03f309635fa795e96a644c8b480e89a6" alt="img" width="1556" height="359" data-path="images/{8203E98E-5D9E-47D2-8DC6-EEDAD71FD063}.png" />

It seems to be checking the result of the `CWebPlatform::CreateInstance` function call, so lets check this one...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B48A2BC7D-B648-48CC-BCA7-477D4684D27E%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=1a4b8e481188912dea934d77b6dde47b" alt="img" width="1108" height="760" data-path="images/{48A2BC7D-B648-48CC-BCA7-477D4684D27E}.png" />

And here's our `E_ACCESSDENIED`!

It seems like the execution never reaches this block or otherwise the **HRESULT** would have changed since this block changes it

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BB37638F4-4B19-42D4-8F19-3C805D1D0105%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=078bb08f85eba546fc5020917420c0d8" alt="img" width="1107" height="467" data-path="images/{B37638F4-4B19-42D4-8F19-3C805D1D0105}.png" />

So both checks of that `if` condition must be failing, and I was able to confim via debugger that they indeed are...

The second check seems to be for checking the device family so lets ignore that one since it would also fail for UWP under the same device family, the first check seems to be checking some boolean state **536870925** / **0x2000000D** using the `IEConfiguration_GetBool` function from **iertutil.dll**, so lets check what this state is...

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BC3783FEF-0538-4EAC-8A7B-41C5506022CF%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=f71f587cbcf2798862b62cee5c5d2dac" alt="img" width="765" height="270" data-path="images/{C3783FEF-0538-4EAC-8A7B-41C5506022CF}.png" />

It seems to be reading the state from global field `unk_18029808A` (name generated by IDA Pro), so lets see what sets that...

It seems to be set through `_IEProcessState_GetStateHolder` function but it seems like this function is only called by `IEConfiguration_SetBool` function which isn't called by that state ID in the dll, so `unk_18029808A` must be a part of a global struct field and it's accessed through it instead, there seem to be a function called `IEConfiguration_Initialize` and judging by its name it seems to be the one responsible for initializing these state values, so lets check it out...

<img src="https://mintcdn.com/ahmedwalid/aBR0w-G_qvBTDmIv/images/%7BE38E7648-3B50-4852-AE78-5B47AC6CDF9B%7D.png?fit=max&auto=format&n=aBR0w-G_qvBTDmIv&q=85&s=bbb503ba42a716e4489d2ebf2e76537f" alt="img" width="1032" height="588" data-path="images/{E38E7648-3B50-4852-AE78-5B47AC6CDF9B}.png" />

It seems to be calling another function `_IEConfiguation_InitializeHelper` with a global field `byte_180298070` casted to `struct SIEProcessState*` passed as the second paramter, which is just **26** bytes away from `unk_18029808A` so this must be its parent struct, so lets see if anything sets `[second parameter] + 26` in that function....

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B178E1068-D968-40DF-B6A4-DAFCB68D38E7%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=cffc702498c4ceb98372ab2d28664a27" alt="img" width="821" height="338" data-path="images/{178E1068-D968-40DF-B6A4-DAFCB68D38E7}.png" />

And there indeed is!

It's set to `true` if `IEIsWebPlatformProcess` returned `true`, so lets check that function...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B5C7A8376-D6D1-499E-80E1-36F0E42E62D3%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=dff43ccae95871fa7eb59b6fde9dbf83" alt="img" width="971" height="539" data-path="images/{5C7A8376-D6D1-499E-80E1-36F0E42E62D3}.png" />

the call to `IEIsImmersiveProcess` immediatly caught my attention, since that would definetly be `true` on UWP but `false` on legacy Win32, and comparing the results of all these checks with a UWP app under a debugger confirms that this indeed is the different result out of them, so lets check that function and see if we can patch it...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B0B0E3904-F136-49FE-B72B-8FB9B0CDD521%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=a25b66e0bfb40c13f51dfe87c14ea128" alt="img" width="986" height="759" data-path="images/{0B0E3904-F136-49FE-B72B-8FB9B0CDD521}.png" />

Hm, so it uses `IsImmersiveProcess` for that but it's dynamically loaded so a direct **IAT** patch wouldn't work, we need to **IAT** patch `GetProcAddress` first then from that we can return our custom `IsImmersiveProcess`, so lets do that!

```csharp expandable theme={null}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static int IsImmersiveProcessHook(void* unk)
{
    return 1;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static void* GetProcAddressHook(HMODULE module, sbyte* procName)
{
    if (procName is not null && !IS_INTRESOURCE((nuint)procName))
    {
        var name = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)procName);
        if (name.SequenceEqual("IsImmersiveProcess"u8))
        {
            return (delegate* unmanaged[Stdcall]<void*, int>)&IsImmersiveProcessHook;
        }
    }

    return GetProcAddress(module, procName);
}

internal static bool PatchWebViewAppModelChecks()
{
    var appmodel = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernel.appcore.dll"u8)));
    var xaml = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("Windows.UI.Xaml.dll"u8)));
    var iertutil = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("iertutil.dll"u8)));
    var kb = GetModuleHandleA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernelbase.dll"u8)));

    if (appmodel.Value is not null &&
        xaml.Value is not null &&
        iertutil.Value is not null)
    {
        var fptr = (delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, int>)&AppPolicyGetWindowingModelHook;
        if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "AppPolicyGetWindowingModel"u8, fptr, 0)))
        {
            var fptr2 = (delegate* unmanaged[Stdcall]<uint, uint*, byte*, uint*, int>)&GetCurrentPackageInfoHook;
            if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "GetCurrentPackageInfo"u8, fptr2, 1)))
            {
                var fptr3 = (delegate* unmanaged[Stdcall]<HMODULE, sbyte*, void*>)&GetProcAddressHook;
                if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(iertutil, kb, "GetProcAddress"u8, fptr3, 2)))
                {
                    AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
                    return true;
                }
            }
        }
    }

    return false;
}
```

Now, lets see what that changed...

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BEF52CA24-A8E9-48D2-AE6B-B864861B4135%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=d28dfb28cae38fe370ddeda0fdb4cd78" alt="img" width="1920" height="619" data-path="images/{EF52CA24-A8E9-48D2-AE6B-B864861B4135}.png" />

It failfasts inside `CreateUriPriv` function, lets check the function and see what could cause that...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B4C2D78FF-B832-43ED-8DE9-D701CA908D0B%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=ce3ea501ca17921460f58b3830356e3f" alt="img" width="833" height="267" data-path="images/{4C2D78FF-B832-43ED-8DE9-D701CA908D0B}.png" />

Hm, it failfasts if value for state ID `0x2000000F` isn't what it expects, which I assume is `true`, using the same method we used previously I found the global field (or struct member to be more accurate) for this state ID to be `unk_18029808C` which is **28** bytes away from `byte_180298070`, so lets see what sets this state ID in `_IEConfiguation_InitializeHelper`

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7B9F10CC0F-4EB3-4AFE-A582-696329E34AAC%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=909077f264000d7fce186e1fc3ac5f96" alt="img" width="1074" height="146" data-path="images/{9F10CC0F-4EB3-4AFE-A582-696329E34AAC}.png" />

it seems to be set to value of `byte_180296648` which is set by `InitOnceIsCurrentProcessEdgeContentHost`, so lets check it out...

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BB04A43A6-E791-44D4-880A-DF8E3D7E0898%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=32237afed9f7c63a94874001b5e46540" alt="img" width="1023" height="759" data-path="images/{B04A43A6-E791-44D4-880A-DF8E3D7E0898}.png" />

It seems to return `true` if the value of **DWORD** state ID **0x1000002D** is **2**, so lets see where that state ID is stored by checking `IEConfiguration_GetDWORD`

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BE212024F-5C48-44C5-94D6-031C8121B7EF%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=251bf0fd3b12d010e8a20bcde8aad72d" alt="img" width="1063" height="753" data-path="images/{E212024F-5C48-44C5-94D6-031C8121B7EF}.png" />

<br />

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B5F663398-4ACF-454B-8A69-DFCF11588431%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=9a21f8ecf2e9903e5fa63111cf80f0c6" alt="img" width="1095" height="753" data-path="images/{5F663398-4ACF-454B-8A69-DFCF11588431}.png" />

<br />

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B7C41E460-1611-47AB-ABA2-BB700627FA82%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=849f60bead9571f82001aefbd9c4b849" alt="img" width="783" height="275" data-path="images/{7C41E460-1611-47AB-ABA2-BB700627FA82}.png" />

The function checks if the ID is **0x1000002D** and if so then it sets `v7` to RVA **0x1802987C0** then later sets `v3` to `*((DWORD*)v7 + 292)` and then it returns `v3`, so `(DWORD*)v7 + 292` must be the RVA where state value is stored, and it can be calculated like this: **0x1802987C0** + (`sizeof(DWORD)` \* **292**) = **0x1802987C0** + (**4** \* **292**) = **0x180298c50**, so lets check Xrefs of that RVA...

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BA73FE8D2-5725-475B-A791-F65A325AAFF0%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=bb2896c1b152529c6bfce70ae1998983" alt="img" width="1114" height="379" data-path="images/{A73FE8D2-5725-475B-A791-F65A325AAFF0}.png" />

It seems to be set by `IEConfiguration_SetBrowserAppProfile` function, so lets check it out...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B0D99E4DE-5443-4B22-B65C-38E5737069C2%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=68a451e878be2cd87ed1da7ecc5e0887" alt="img" width="1120" height="671" data-path="images/{0D99E4DE-5443-4B22-B65C-38E5737069C2}.png" />

<br />

<img src="https://mintcdn.com/ahmedwalid/3aS87ruU-Ms584nd/images/%7BAD9739DE-64EC-4EFB-A4BC-44A741152CA4%7D.png?fit=max&auto=format&n=3aS87ruU-Ms584nd&q=85&s=4d194f80623e265ed186eaca6cd7779c" alt="img" width="1014" height="540" data-path="images/{AD9739DE-64EC-4EFB-A4BC-44A741152CA4}.png" />

The function seems to be setting it to the value of the second parameter of the function,and this function seems to be exported at ordinal `#797`!

So now we have to figure out the params of the function and call it from our app.

The first parameter seems to be a string judging by the `lstrcmpW` call and calls to this function from other functions in the dll confirm this and it seems to be the selected profile judging by the function name, and we know that we need to set the second parameter to **2** for `0x2000000F` to be `true` and it seems like this parameter is the type of the profile selected? since there seem to be other valid values, and the last paramter is unknown but judging by the call in `IEConfiguration_SetBrowserAppProfileDefault` it seems like it can be **0**

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B0D258DC9-5C2D-46D8-BA8E-B975E034C13D%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=c698ad21dc065de9023343efd49db764" alt="img" width="950" height="610" data-path="images/{0D258DC9-5C2D-46D8-BA8E-B975E034C13D}.png" />

I checked **Edge Legacy** binaries and it seems to be calling this function with the first parameter being "MicrosoftEdge", so lets go ahead and call this function...

```csharp expandable theme={null}
[PreserveSig]
[DllImport("iertutil.dll", EntryPoint = "#797")]
private static extern HRESULT IEConfiguration_SetBrowserAppProfile(char* profile, uint type, uint unk);

internal static bool PatchWebViewAppModelChecks()
{
    var appmodel = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernel.appcore.dll"u8)));
    var xaml = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("Windows.UI.Xaml.dll"u8)));
    var iertutil = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("iertutil.dll"u8)));
    var kb = GetModuleHandleA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernelbase.dll"u8)));

    if (appmodel.Value is not null &&
        xaml.Value is not null &&
        iertutil.Value is not null)
    {
        var fptr = (delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, int>)&AppPolicyGetWindowingModelHook;
        if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "AppPolicyGetWindowingModel"u8, fptr, 0)))
        {
            var fptr2 = (delegate* unmanaged[Stdcall]<uint, uint*, byte*, uint*, int>)&GetCurrentPackageInfoHook;
            if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "GetCurrentPackageInfo"u8, fptr2, 1)))
            {
                var fptr3 = (delegate* unmanaged[Stdcall]<HMODULE, sbyte*, void*>)&GetProcAddressHook;
                if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(iertutil, kb, "GetProcAddress"u8, fptr3, 2)) &&
                    SUCCEEDED_LOG(IEConfiguration_SetBrowserAppProfile((char*)Unsafe.AsPointer(in MemoryMarshal.GetReference("MicrosoftEdge".AsSpan())), 2, 0)))
                {
                    AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
                    return true;
                }
            }
        }
    }

    return false;
}
```

## **Results**

Now, lets see if it finally works...

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/Untitled4.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=58d16340e557c331839c36ed67942967" alt="Untitled4" width="1425" height="777" data-path="images/Untitled4.png" />

**IT WORKS!!! 🎉**

## **Fixing a Crash on Process Shutdown**

The app seems to throw an exception on shutdown after WebView is loaded

<img src="https://mintcdn.com/ahmedwalid/_2kE0rkSYkQv7-jJ/images/%7B71103DC0-552D-4503-BB36-F53A8CCBF87D%7D.png?fit=max&auto=format&n=_2kE0rkSYkQv7-jJ&q=85&s=beb82c23b37b9502c7f1e4a014e3ac4a" alt="img" width="1173" height="697" data-path="images/{71103DC0-552D-4503-BB36-F53A8CCBF87D}.png" />

It seems like the **edgehtml** dll trying to call `SubmitThreadpoolWork` when detaching/unloading on process shutdown which [isn't allowed](https://github.com/microsoft/libHttpClient/issues/458), but we can workaround this by forcing **edgehtml.dll** to cleanup early by sending it a `DLL_PROCESS_DETACH` signal inside our `OnProcessExit` event handler:

```csharp expandable theme={null}
internal static void* GetModuleEntryPoint(HMODULE Module)
{
    if (Module.Value is null)
        return null;

    var dosHeader = (IMAGE_DOS_HEADER*)Module;
    var ntHeaders = (IMAGE_NT_HEADERS*)((byte*)Module + dosHeader->e_lfanew);

    return (byte*)Module + ntHeaders->OptionalHeader.AddressOfEntryPoint;
}

private static void OnProcessExit(object? sender, EventArgs e)
{
    foreach (var (Thunk, OriginalFunction) in PatchingHelper.PatchedFunctions)
    {
        var thunk = (nuint*)Thunk;

        uint protect;
        if (VirtualProtect(thunk, (nuint)sizeof(nuint), PAGE.PAGE_READWRITE, &protect))
        {
            *thunk = OriginalFunction;
            VirtualProtect(thunk, (nuint)sizeof(nuint), protect, &protect);
        }
    }

    var edge = GetModuleHandleA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("edgehtml.dll"u8)));
    if (edge.Value is not null)
    {
        var dllmain = (delegate* unmanaged[Stdcall]<HINSTANCE, uint, void*, BOOL>)PatchingHelper.GetModuleEntryPoint(edge);
        if (dllmain is not null)
        {
            LOG_LAST_ERROR_IF(!dllmain(edge, DLL_PROCESS_DETACH, null));
        }
    }
}
```

And this fixes the problem!

***That was all, thank you for reading!***
