0x01 简介
AzureAttestService
是安装SQL Server 2019
后存在的服务,用于远程验证平台的可信度和其中运行的二进制文件的完整性,详细说明见 Microsoft Azure Attestation。AzureAttestService
存在DLL类型服务文件AzureAttestService.dll
和服务安装程序AzureAttestServiceInstaller.exe
,可以通过服务安装程序对服务进行安装、启动停止和卸载。服务安装程序对DLL文件的验证存在问题,其未对要安装的服务DLL进行合法性检测,可将任意服务DLL安装为系统服务,通过这种方式可以绕过安全软件防御进行权限维持。本文测试系统环境为WIN10 x64 1809
。
0x02 服务程序测试
AzureAttestServiceInstaller.exe
和 AzureAttestService.dll
均是由微软签名的文件,其中AzureAttestServiceInstaller.exe
有4个参数用于服务的操作(-h):
其中 -Install <path to service dll>
参数可以将DLL注册成为svchost.exe
托管的system
权限系统服务:
注册完成后可以在注册表HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\AzureAttestService
项目中查询到对应注册信息:Parameters
项中可查询服务DLL路径和导出函数信息:
因为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
保持一致 :
在dwServiceType
参数中设置SERVICE_WIN32_SHARE_PROCESS
表示服务与其他服务可共享进程以便资源、环境变量等共享,一般宿主进程是svchost.exe
。
接下来通过ReportSvcStatus
向SCM
返回服务状态,然后就进入用户处理函数ExecuteServiceCode
。IRed
的示例中是在ExecuteServiceCode
函数的合适位置执行持久化代码:
在这里执行执行代码时需要考虑到线程阻塞问题,如果直接使用指针方式(*(void(*)())buffer)()
执行shellcode
,会导致SCM
无法正常对该服务进行控制。以下代码通过创建线程执行shellcode
注入函数:
注入shellcode
时使用远程线程注入的方式进行,这样做可以让要执行的shellcode
与当前DLL
模块分离开,代码会在当前svchost.exe
进程中分配空间执行:
编写完服务DLL代码后,以64位Release
模式编译,得到一个服务DLL(ServiceDLL.dll
):
该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);
}
为了让随机数种子随机一点,通过获取当前时间的毫秒数来设置种子,获取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以下:
发现可以通过随机两次来获得比较随机的值:
#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
即为我们需要等待的秒数:
当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()
方法赋值:
还需要注意的是,加密脚本中将iv
值放在加密文件末尾,API
函数GetFileSize
在读取文件时如果在文件末尾存在\x00
时会忽略该数据,导致读取到的数据大小与实际不符:
在AES
加密的python
脚本中有对应注释:
实际使用的是随机提取的iv
值 iv = Random.new().read(AES.block_size)
,在没完全理解该随机方法之前错误估计该值最后一位有几率取到\x00
,改了下代码把最后一个元素设置为了固定值,导致了后续解密时一个bug
产生:
这样改动后解密出来的shellcode
就有部分字节与源数据不一致:
意识到解密存在错误后,将加密代码恢复原样后,成功在测试程序中解密并执行了shellcode
,同时windows defender
保持静默:
0x4-3 主机指纹识别
在获得目标主机权限后需要获取其产品uuid
,然后硬编码到服务DLL中用于和WQL
查询得到的主机uuid
信息对比,检测该主机是否为预设主机,以此规避沙箱环境。命令模式下可使用wmic csproduct list full
查询产品信息中的主机uuid
:GUI
下可使用wbemtest
命令打开实用工具枚举实例 Win32_ComputerSystemProduct
实例:
DLL中通过C++
使用COM
编程执行WQL
查询获取uuid
的代码直接修改自MSDN
上的示例wmi-data-from-the-local-computer,需要查询Win32_ComputerSystemProduct
类中uuid
的值:
查询到结果后判断uuid
是否与预设相等:
程序执行后成功确认了主机uuid
:
在测试程序中实现uuid
检测后,需要将测试好的代码移植到服务DLL中,完成后运行测试发现一个之前遇到的CoInitializeSecurity
已被调用问题,因为是在线程中进行COM
调用,可能在之前已经有过COM
调用,所以加入返回值判断即可:
启动服务后查看写入的日志,发现已经成功执行shellcode
:
最后将官方AzureAttestService.dll
(147kb)的资源信息也拷贝到自编写的服务DLL中(72kb):
这一步的规避措施就编写完毕了,接下来对服务DLL进行测试。
0x05 安装服务DLL
在上文中已经编写好了服务类型DLL并使用了一些规避手段,直接通过安装程序命令安装服务:
在PID为1664的svchost.exe
进程中成功执行了shellcode
返回beacon
:
在实际利用过程中,如果直接将自编写的服务DLL进行注册,会被安全软件行为拦截:
所以应该先使用都具有签名的文件注册服务,然后停止服务并替换服务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
服务成功在PID为2920的svchost.exe
中启动:
之后成功返回了beacon
:
最终我们可以在windows defender
和数字卫士全家桶保护的主机中成功启动PID为428的svchost
服务进程:
在等待一段时间之后,成功返回了beacon
:
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