hfiref0x / UACME

Defeating Windows User Account Control
BSD 2-Clause "Simplified" License
6.09k stars 1.3k forks source link

UAC bypass with NIC (Native Image Cache) injection V2 and 63, 65, 69 methods update #115

Closed AzAgarampur closed 2 years ago

AzAgarampur commented 2 years ago

As mentioned in your W11 status update, method 63 is very slow, depending on config. I have made an update to the project where there is now no need for the scheduled tasks, image generation, etc. You can manually generate the required files, making the attack much more faster and lighter.

Idk if you want to change the code here or leave it as is. If you want to, you can check out the update and how it works on the original repo: https://github.com/AzAgarampur/byeintegrity-uac

For method 65, I think that simply setting the NoOpenWith key for the association that pops up in the OpenWith dialog should fix the problem. (I have not tested this yet).

hfiref0x commented 2 years ago

Sure, I'm interested in any kind of updates. I will look on your new version asap.

Okay I see you switched from Accessibility to MMCEx and self generated ni file for it. Implementing it in uacme will require some additional work as it need porting your image generation code in it along with basically rewritting method from scratch to get rid of old code. I will post any updates here, thanks.

hfiref0x commented 2 years ago

What is the DEADBEEFDEADBEEFDEADBEEFDEADBEEF const btw? You use it as MMCEx subdirectory name. AFAIR, isn't subdirectory name in image cache should be image mvid? The tool also seems just copy mvid of target file to ni file.

AzAgarampur commented 2 years ago

That folder is just a dummy file name to hold the .ni dll and the .aux file. If you remember the schema of the NIC is the Assembly name -> Debug GUID -> files. The debug guid does not matter, as it is just used for logging and information purposes by the CLR, so that's why I just used DEADBEEFDEADBEEFDEADBEEFDEADBEEF, which is the same length as one of the debug guid folder names.

One of the things the tool does is copy the mvid of the assembly to the aux file, but it also does things like set up magics and the full assembly identification string and such.

hfiref0x commented 2 years ago

Well I just thought directory name was mandatory to be exact mvid value. Well this simplify things more. Since this method looks like big update to original I think if everything works good I will add it as separate num71.

AzAgarampur commented 2 years ago

I'm assuming you will leave 63 as unfixed because it "still works"? (hella slow) or will this supersede it?

hfiref0x commented 2 years ago

Well I don't see any attempts to fix it, or any side changes in Windows that can affect it and ruin it yet. So yes, it is unfixed, if this uacme implementation doesn't work with 100% success rate it doesn't mean someone will not reimplement this making their own with 100% rate. It could probably can be signatured with WD later as cheap solution.

One single change to ISecurityEditor or COMAutoApproval list and both these methods will no longer work :)

hfiref0x commented 2 years ago

Are you sure the MVID directory name is not important? In my tests currently this method only works if directory name is correct MVID of assembly file, or magic DEADBEEFDEADBEEFDEADBEEFDEADBEEF. Maybe I'm missing something?

Edit: Nevermind this question. It seems it was my bug.

hfiref0x commented 2 years ago

Update, https://github.com/hfiref0x/UACME/tree/dev358 contains method num71. So far it was tested on 1809 and W11 21H2. It is working fast and well here. As of method 65 I didn't tried NoOpenWith yet.

AzAgarampur commented 2 years ago

In sup.c it seems that you retrieve MVID by reading the first entry in the #GUID heap of the assembly. While this may seem to work I think it is best to parse the #~ stream and lookup it's Module : 0x00 table that contains the correct index of the MVID in the #GUID heap, just in case Microsoft decides to update this assembly in the future.

For method 65, what options are showing up in the 'open with' dialog? I can't get this atm.

hfiref0x commented 2 years ago

I can't tell you this 100% right now because currently I have only win 11 with no MS browser installed (edge was forcefully removed). To reproduce it I need another clean install. But as far as I remember "open with" dialog was showing MS edge as default application when method 65 was executed. Once edge was removed and chrome set as default browser this method started working without any noise.

AzAgarampur commented 2 years ago

Looks like crappy internal changes that force edge upon users is screwing up this whole thing again. I need to look into this more, have no ideas as of now. NoOpenWith requires a key in HKCU, usually all pre-installed edge (on w11 and 10) keys are in HKCR. I tried to create same keynames in HKCU but got unreliable results. Otherwise, I cannot repro. this problem.

On the other hand, I think I may know what's going on for 69. Can you confirm that 69 does NOT work when you run akagi 63, wait for that to finish, then run akagi 69? If it does NOT work, try to attempt it again after restarting PcaSvc. It should still NOT work; can you confirm these behaviors?

hfiref0x commented 2 years ago

So I installed w11 (22000.348 full patch). Windows defender has been wiped after to improve overall performance and get rid of annoynance, snapshot created.

Test results with methods 63 and 69, next will be 65 and 69.

No edge first run.

1 test run. akagi64 63 - no bypass, mmc snapin of wf.msc shown without any sign of bypass. akagi64 69 - no bypass, the following PCA dialog shown, selected "Run the program with ..." option.

123

akagi64 69 (second attempt) - no bypass, same dialog, selected "This program ran correctly" option. akagi64 69 (third attempt) - bypass successful, no PCA dialogs.

VM state reset 2 test run.

Edge first run (hf you should see what they did with this shit~). akagi64 63 - no bypass, mmc snapin of wf.msc shown without any sign of bypass. akagi64 63 - no change, no bypass

VM state reset

3 test run.

akagi64 69 - bypass successful, no PCA dialogs

VM state reset

4 test run.

akagi64 69 - bypass successful, no PCA dialogs akagi64 63 - no bypass, mmc snapin of wf.msc shown without any sign of bypass.

VM state reset

5 test run.

Downloaded and installed google chrome. Manually set it to be default browser and associated with HTTP/HTTPS via Apps->Default Apps system applet.

akagi64 63 - nothing as result. akagi64 63 - after ~10 sec method completed with successful uac bypass.

Note that method 71 works always. I think we can forget about 63 as 71 is much better and stable.

hfiref0x commented 2 years ago

VM state reset akagi64 65 - no bypass, the following "Open With" dialog shown 124

Edge selected in this dialog. akagi64 65 - successful bypass, no "open with" dialogs shown

VM state reset Downloaded and installed google chrome. Manually set it to be default browser and associated with HTTP/HTTPS via Apps->Default Apps system applet.

akagi64 65 - successful bypass, no "open with" dialogs shown

VM state reset akagi64 69 - successful bypass akagi64 65 - no bypass, the following "Open With" dialog shown 124 Edge selected in this dialog. akagi64 65 - successful bypass, no "open with" dialogs shown

So, UserAssocSet is still valid thing and we have proper signatures for it. However I see that Microsoft in attempt to harden and impose Edge presence as "the only bestevar browser for Windows" yet again changed something in the shell to complicate browser switch. I've to mention that even when you manually change associations in their Apps->Default Apps applet this crap throws you baloon telling how this is not good etc. It is just a fking. ridiculous.

I've no idea why method 69 is working again flawlessly. Maybe it is result of WD removal as this crapware bundle affects literally every aspect of shell work.

AzAgarampur commented 2 years ago

For 65, it seems like vmware "default host application" is blocking OpenWith.exe, because that is the only entry in the list that says 'new." If there is a way to find new handlers and their registry keys I'll do something with that.

For 69, I think I have a theory about why it may not be working. Basically, the elevated taskhostw.exe that loads pcadm.dll is also shared across other elevated tasks. If this process is already running, for some reason, the environment variable change won't affect it, causing the PCA dialog to show. I think to get around this we need to enumerate tasks that an elevated taskhostw.exe is running, stop all of them -- this end taskhostw.exe, then do attack to launch fresh taskhostw.exe.

note

In the picture above, method 63 runs the Memory Diagnostic task (from scheduled system maintenance). This persists for a while, so if we try and run method 69 it will use the same taskhostw.exe to run the WDI task, which won't be affected by the environment variable change.

hfiref0x commented 2 years ago

I uninstalled vmware tools and re-tested 65. Now it always works without "Open With" dialog. It would be really good to figure out why vmware element is causing this block and do a workaround.

hfiref0x commented 2 years ago

Update on 65.

So far I fugured out that "new" element in this OpenWith dialog is taken from HKEY_CLASSES_ROOT\VMwareHostOpen.AssocURL

If you kill this entry in registry no OpenWith dialogs will be shown.

VMWare app supported protocols registered in key HKLM\Software\VMWare, Inc\VMWareHostOpen\Capabilities\UrlAssociations, values "http", "https" If you kill "http", "https" values - OpenWith dialog will be shown without "new" element saying you to just keep using MsEdge or lookup in store.

Here is the test

Creating the following registry entries will add "new" element to OpenWith dialog

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\TestHostOpen.AssocURL]
@="Open URL in Test Host"

[HKEY_CLASSES_ROOT\TestHostOpen.AssocURL\shell]

[HKEY_CLASSES_ROOT\TestHostOpen.AssocURL\shell\open]

[HKEY_CLASSES_ROOT\TestHostOpen.AssocURL\shell\open\command]
@="\"C:\\windows\\system32\\notepad.exe\" --url \"%1\""
Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Test]

[HKEY_CURRENT_USER\Software\Test\TestHostOpen]

[HKEY_CURRENT_USER\Software\Test\TestHostOpen\Capabilities]

[HKEY_CURRENT_USER\Software\Test\TestHostOpen\Capabilities\UrlAssociations]
"http"="TestHostOpen.AssocURL"
"https"="TestHostOpen.AssocURL"

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\RegisteredApplications]

"TestHostOpen"="SOFTWARE\\Test\\TestHostOpen\\Capabilities"

123

~It seems Windows looks for *.AssocURL keys in Classes subkey.~ It is lookup from HKCU\Software\RegisteredApplications -> AppName -> Capabilities\UrlAssociations->protocol->HKCR\protocol

https://docs.microsoft.com/en-us/windows/win32/shell/default-programs Example for fictional Contoso browser.

AzAgarampur commented 2 years ago

If application is declared to handle http but has keys under HKLM, we can't change them or add NoOpenWith. I was thinking of creating a duplicate ProgID key under HKCU since that is scanned first, then using NoOpenWith or Disabled=0x00000001 but I tried this and it does not seem to work. I think figuring out how to make "new" app seem "old" in the list will get rid of this dialog.

Also note that if you decide to put this in, you will need to scan for every available handler in the registry and somehow prevent them from coming up in the OpenWith box, if they do. There is some internal code that does this in windows.storage.dll.

hfiref0x commented 2 years ago

What exactly spawns this OpenWith dialog? Shell process? If it on the same integrity level we can try to simple emulate click on first element when this box is shown which would require running separate watchdog thread.

edit: I see it runs by svchost under medium IL.

AzAgarampur commented 2 years ago

Okay so upon reverse engineering of shell32.dll I found undocumented, internal extension of IAssocHandler (https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-iassochandler).

There is an undocumented interface (IAssocHandlerInfo) which is used by the system to check if a "new" program association has been created.

After analyzing this it seems that settings that tell Windows where something is new or not is stored under Computer\HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\ApplicationAssociationToasts If the ProgID in the entry listed here is 0x1 then it is "New," otherwise it is not.

So we can use this -- simply enumerate every entry under this key and set it equal to 0x0. Renaming the key to random value, doing attack, then restoring original name will not work as the system will create this key on demand with required ProgIDs (for current item being queried, example, http: protocol.) if it does not exist. We have to enumerate each value and set it equal to 0. Then there will be no more "New" application prompts.

hfiref0x commented 2 years ago

Well, they all zero here. Even HKLM copy of this key contains only zeroes. However vmware item is listed as new.

AzAgarampur commented 2 years ago

Yeah so that explains why it was causing openwith to open and wait for user input. Most items here will be 0 by default.

hfiref0x commented 2 years ago

Well I mean entire list contain only zeroes however there is still OpenWith dialog with new item inside.

AzAgarampur commented 2 years ago

That's odd. I wonder if this api is being used the same on your machine. Can you send content of this key as .REG export?

hfiref0x commented 2 years ago

reg.zip Here is it, both hkcu/hklm.

hfiref0x commented 2 years ago

I think I found sort of solution.

HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\Explorer
NoNewAppAlert DWORD 1

Restarting Explorer seems required. After applying this changes there will be no OpenWith dialog at all when method 65 executed.

hfiref0x commented 2 years ago

The bad news is that key is not accessible for write without elevation 😄

AzAgarampur commented 2 years ago

Ok I think I have an idea but I still need to test it. I think that using this internal COM interface, we can query all registered ProgID's for this http handler. Then for each one, we create (if needed) the key under ApplicationAssociationToasts and explicitly set it equal to 0. I'll try this later.

Edit: There seems to be another undocumented internal COM interface OpenWith is using to get ProgID in a simple way, I'll use that and make a sample app and paste source here.

Edit 2: Just wasted time reverse engineering to learn that this is documented LMAO

hfiref0x commented 2 years ago

Added these two entries to the ApplicationAssociationToasts

"VMwareHostOpen_http"=dword:00000000
"VMwareHostOpen_https"=dword:00000000

OpenWith dialog still popups.

AzAgarampur commented 2 years ago

So I made this program (C++)

#include <Windows.h>
#include <ShObjIdl.h>
#include <wrl.h>
#include <string>
#include <iostream>

using Microsoft::WRL::ComPtr;

constexpr DWORD ZERO{};
void CreateKeyAt(PCWSTR str)
{
    std::wcout << L"Setting " << str << L" to 0x0\n";
    RegSetKeyValueW(
        HKEY_CURRENT_USER,
        L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\ApplicationAssociationToasts",
        str,
        REG_DWORD,
        &ZERO,
        sizeof DWORD
    );
}

int main()
{
    ComPtr<IEnumAssocHandlers> enumHandlers;

    if (FAILED(SHAssocEnumHandlersForProtocolByApplication(
        L"http",
        IID_IEnumAssocHandlers,
        &enumHandlers
    )))
        return 1;

    IAssocHandler* assocHandlers[10]; // assume we got 10 handlers
    ULONG fetched;
    if (FAILED(enumHandlers->Next(10, assocHandlers, &fetched)))
        return 1;

    for (auto i = 0; i < fetched; ++i)
    {
        ComPtr<IObjectWithProgID> progId;
        assocHandlers[i]->QueryInterface(progId.GetAddressOf());

        PWSTR progIdString;
        progId->GetProgID(&progIdString);
        std::wstring progIdStr{ progIdString };
        CoTaskMemFree(progIdString);

        progIdStr += L"_http";
        CreateKeyAt(progIdStr.c_str());
    }

    for (auto i = 0; i < fetched; ++i)
        assocHandlers[i]->Release();

    return 0;
}

Can you follow these steps:

Hopefully it'll work then. I tested this on W10 and W11 and the attack seems to work if it is run after running this program.

hfiref0x commented 2 years ago
Setting MSEdgeHTM_http to 0x0
Setting VMwareHostOpen.AssocUrl_http to 0x0

Success, no OpenWith dialogs.

AzAgarampur commented 2 years ago

Awesome, so we can use this to suppress all "New" associations in akagi. Also if you want you can update method 68 to use this to make it more reliable. This will also allow us to ditch NoOpenWith for that method as well.

hfiref0x commented 2 years ago

I've updated code for win7. However I don't have currently any VM with win7 so I can't really test if it works.

hfiref0x commented 2 years ago

Well, I tested 63 and 71 on Win7 SP1 full patch.

63 works well. 71 doesn't work, aux file generated, directories created, files moved etc, mmc.exe starts WF snapin and nothing happens.

Generated aux file looks like this Untitled

AzAgarampur commented 2 years ago

Okay, so it seems from testing that AUX file is not existent on .NET framework 2.X, which is what Win7 is using. What is dependent is the name of the NIC entry. So unlike Windows 10, we cannot use a random constant DEADBEEF....., the name means something. I will figure out what this name represents.

hfiref0x commented 2 years ago

Regarding to this win7 issue. If you will use exact MVID as already defined in c:\Windows\assembly\NativeImages_v2.0.50727_64\MMCEx\<MVID> then this method will succeed. Any idea how to query this exact MVID and not MVID from c:\Windows\assembly\GAC_MSIL\MMCEx\?

AzAgarampur commented 2 years ago

To answer your question: look at source for AUXGen on byeintegrity repo, and change ASM_CACHE_GAC to ASM_CACHE_ZAP. Then when assembly is found, you can use IAssemblyName::GetProperty() with ASM_NAME_MVID as the property to retrieve to get the MVID from NIC. (Note: this only works on IAssemblyName from NIC, not GAC).

However, the MVID from NIC images and GAC images are the same, so this isn't that useful.

However, I figured out how to generate the proper path/folder name. The name is actaully the MVID, but encoded in a specific format. In order to generate the correct path name, you need to get the MVID of the assembly (like from the GAC), then encode it, then create a folder with the encoded name.

So I created this example program (C++) to show how to generate the encoded path/folder names:

#include <Windows.h>
#include <iostream>

void EncodeMvid(
    PBYTE mvid,
    PWSTR buffer
)
{
    UINT temp;
    for (auto i = 0; i < sizeof GUID; ++i, ++mvid, buffer += 2)
    {
        if ((temp = *mvid / 0x10) >= 0xA)
            *buffer = static_cast<USHORT>(temp + 0x57);
        else
            *buffer = static_cast<USHORT>(temp + 0x30);
        if ((temp = *mvid & 0xF) >= 0xA)
            *(buffer + 1) = static_cast<USHORT>(temp + 0x57);
        else
            *(buffer + 1) = static_cast<USHORT>(temp + 0x30);
    }
}

int main()
{
    GUID guid;
    WCHAR buffer[33], str[50];
    buffer[32] = L'\0';
    while (true)
    {
        CoCreateGuid(&guid);
        StringFromGUID2(guid, str, 50);

        std::wcout << str;

        EncodeMvid(reinterpret_cast<PBYTE>(&guid), buffer);
        std::wcout << L" -> " << buffer << std::endl;

        Sleep(300);
    }
}
hfiref0x commented 2 years ago

MVID from image -> {AECB8136-3F13-4F84-8DF9-70111807106D} MCIEx directory name -> 025bfc621a1b021a0028ce048fb0db96 Generated by your program for MVID from image -> 3681cbae133f844f8df970111807106d

edit: mis-pasted

AzAgarampur commented 2 years ago

Ok so this is a substantial finding:

So reason my code above didn't work is not because of incorrect algorithm, but because of incorrect MVID being supplied (I'll explain below)

.NET 2.X seems weird when it comes to MVID's. As I said before, the MVID in both NIC and GAC are the same. However, that seems to be only true for the IMAGE. Meaning, if you map the PE and parse the .NET metadata the MVIDs are the same. This is why CFF explorer and ProcessHacker's PE viewer show the same MVID for both NIC and GAC images.

However, when using IAssemblyName::GetProperty the results are different.

Also, if you remove an entry from the NIC on .NET 2.X, but then try to enumerate the NIC using COM interface, the entry still comes up (which is good for us, we can use to create correct MVID path if the NIC entry does not exist for some reason).

So in order to create the correct path for .NET 2.X, we need to do these steps in this order:

Here is picture showing results of IAssemblyName::GetProperty(ASM_NAME_MVID, ...) on NIC enumerated entry (I tested on mscorlib.dll), both from sample program and from CLR Runtime itself:

evidence
hfiref0x commented 2 years ago

Have a look what I found https://github.com/lewischeng-ms/sscli :smiley: Your routine looks like CParseUtils::BinToUnicodeHex

edit, looks like taken from https://github.com/SSCLI/sscli20_20060311

AzAgarampur commented 2 years ago

Yep! That's what I was trying to recreate. Did the steps in the explanation above produce the correct output?

hfiref0x commented 2 years ago

I will try and let you know results as usual. I was looking in that source above and it seems interesting as it looks very similar to what win7 have.

hfiref0x commented 2 years ago

I can confirm that AssemblyName from ASM_CACHE_ZAP with GetProperty(ASM_NAME_MVID) and later converted using the above function produces valid directory name, for both Win7 dotnet 2.0 and for Win10+ dotnet 4.0 cases.

Which mean we could probably optimize method 63 and speedup it a little by getting rid of directory search routine.

AzAgarampur commented 2 years ago

Awesome, but remember I don't think that this is required for .NET 4.X, as CLR just uses recursive directory scan and directory name does not really matter (hence 0xdeadbeef.... constant).

Also remember that the folder name is just the debug GUID of the containing .ni file. (Win10, original method 63).

hfiref0x commented 2 years ago

Okay this works fine on win7/win11 now.

AzAgarampur commented 2 years ago

I'm assuming Win8/8.1 uses .NET 4.X so it already works fine? (Don't have W8 VM right now)

hfiref0x commented 2 years ago

Yeah, I tested this yesterday too on 8.1, it works as expected.

hfiref0x commented 2 years ago

I've moved this to master as 3.5.8.