通过 C++ 操作注册表禁用 Windows Defender


0x01 简介

最近整理文件,发现自己之前禁用Windows Defender的文档。如果安装了其他的安全软件,Defender是可以完全禁用掉的(TA也会自动设置服务状态为手动),而没安装其他安全软件时,在管理员权限下是没有权限设置WinDefendWdNisSvc服务启动状态的。所以前提是先安装一个其他的安全软件,禁用Defender后重启再卸载安全软件,达到裸机的目的~。

这篇文章主要是为了研究程序开发,通过C++操作注册表禁用WinDefender的服务(注:该种方法并没有绕过Defender安全限制)。文章将对程序的编写和操作过程进行记录,也会涉及到一些C/C++数据类型方面的内容,方便以后复习。

0x02 手动禁用

在禁用之前可以先查看服务项目中关于Defender的一些服务:

直接看到的名称是服务的显示名称,操作注册表时需要这些服务的服务名称,从上到下这些服务的名称分别是Sense(Windows Defender Advanced Threat Protection Service)WdNisSvc(Windows Defender Antivirus Network Inspection Service)WinDefend(Windows Defender Antivirus Service)mpssvc(Windows Defender Firewall,该服务是防火墙,不要禁用)SecurityHealthService(Windows 安全中心服务)
这些服务一般来说是不能在服务控制里面直接禁用的:

以管理员权限启动注册表后定位到所有服务的注册表项HKLM\SYSTEM\CurrentControlSet\Services下:

然后根据上面的服务名称找到对应的服务操作,将Start改为4 (即禁用):

服务操作完成后在任务管理器中选择禁用托盘图标:

重启系统后,Defender将会从系统中禁用,使用Powershell命令查看对应的服务都是停止状态:

Get-Service -Name windefend,WdNisSvc,Sense,SecurityHealthService


也没有了托盘图标显示:

0x03 通过编程实现

这一部分通过C++来实现上面手动禁用的过程。主要使用到了RegOpenKeyExARegGetValueARegSetValueEx等操作注册表的API函数。以下对实现过程进行分析。

0x3-1 读取服务启动状态

第一步通过读取注册表中WinDefender各项服务的启动状态(服务注册表里的Start值):

因为都是读取DWORD类型的数据,所以将相同的操作封装在了getDWORDValueToReg函数中,读取数据后返回Start的数据,如果为4那就说明服务已经是禁用状态了:

0x3-2 设置服务启动状态

第二步是如果对应服务的启动状态不为4的禁用状态,那么就通过封装好的RegCreateKeyExARegSetValueEx去设置启动状态为4,封装的函数setDWORDValueToReg如下:

函数RegCreateKeyExA的语法如下,其中在参数中以LP或者P开头的类型表示该参数类型是一个指针,比如lpdwDisposition参数就是一个指向DWORD类型的长指针(LP):

在封装的代码中将dwOptions选项设置为了REG_OPTION_NON_VOLATILE,这是默认的选项,是指操作完成后保存到注册表中,不会随着重启而改变设置。同时也给可选的输出参数lpdwDisposition传入了对应的指针,因为上面说到参数类型是LPDWORD是一个指针,所以定义了lpdwDispositionDWORD,取地址符后就取得了他的地址,也就获得了一个指向lpdwDisposition参数内容的一个指针,也就是下面这样的使用方法:

这个参数是来显示当前打开的这个键值是新创建的还是直接打开的,用于一些可能要创建键值的情况下的标注:

因为这几个服务都是存在的,所以直接打开了对应的句柄进行操作。再来看API函数RegSetValueExA,是对值的数据进行写入,函数语法如下:

我们在第四个参数中指定了数据的类型为REG_DWORD,第五个参数需要传入
const BYTE *类型,所以在传这个参数的时候是先取传入DWORD的指针后,强制转换到的BYTE指针 (BYTE*)&szValue。如果不使用强制转换,通过下面的方式可以实现吗?

DWORD szValue123 = 255;
BYTE M = szValue123;
BYTE* MM = &M;
lResult = RegSetValueExA(hKey, szValueName, 0, REG_DWORD, MM, sizeof(DWORD));

运行程序后,显然失败了,Start的数据变成了这样一个随机的地址形式:

因为BYTE指针 MM指向的是BYTE的数据,BYTE sizeof = 1,而RegSetValueExA最后一个参数是指向4字节大小DWORD类,所以这里就无法正确获取到数据,而将最后一个参数改为 sizeof(BYTE)的话,就会报错为不正确的DWORD32值:

正确的操作应该是直接进行内存复制,S是一个指针,指向的具体内容由指向的地址决定:

szValue = 7777;
BYTE* S = (BYTE*)calloc(2, sizeof(DWORD));
if (S == NULL) return FALSE;
memcpy(S, &szValue, sizeof szValue);
printf("S Data = %ld \n", *S);
lResult = RegSetValueExA(hKey, szValueName, 0, REG_DWORD, S, sizeof(DWORD));
free(S);

代码执行后 7777 就成功写入了注册表:

但是这里的 S Data = 97 是为什么,不该等于7777吗?这里是因为将该数据打印出来的时候,用的是BTYE类型(虽然你内存中实际存的可能不是BYTE,但是系统按照指针指向的类型BYTE一个字节一个字节的获取数据),同时在系统定义中,BYTE就是无符号的unsigned char :

char类型在百度百科中的解释如下:

所以char类型最多能表示总共128+127=255个字符,表示int就只能到255,到256就溢出到0,这也循环。7777 溢出 30 次个256还余97,所以这里 S Data = 97。分析了这些也可以知道系统的强制转换,是会按规则计算并进行转换的。

在这一节里,实现了对服务注册表对应值的数据写入,可以将服务的状态设置为禁用了,接下来会继续设置托盘图标的启动项。

0x3-3 托盘图标的禁用

托盘图标程序的启动项就是注册表启动项中常见的Run键:

64位:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

32位:
HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run

可以在64位的Run键下找到Defender的托盘启动项:

这里可以将任务管理中启动项目的一些其他属性点开,可以看到详细的启用状态、启动类型、禁用时间和命令行等,这里的编程就是要实现这里的禁用功能,最开始看到火绒、AutoRun等软件的一些禁用启动会建立一个*DisabledAutoruns的项:

以为也可以通过这样的方式来禁用启动项目,尝试操作了下并不能成功(这步操作还被某数字安全卫士阻止了很久,表现就是新建了项无法重命名,也无法删除。还以为是代码出了问题,最后在虚拟机中反复尝试,卸载了才成功,不得不说保护确实到位!)。思考这里应该也是操作注册表来实现的,于是就找到了一款监控程序操作注册表的软件(RegFromApp),发现了任务管理写入注册表的位置(HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run):

定位到具体的注册表项,三个子项分别是RunRun32StartupFolder,其中StartupFolder是管理启动目录的:

StartupApproved\Run值对应的是启动项中的CurrentVersion\Run的值,都有值才会在任意管理器的启动项中显示出来,数据是REG_BINANY二进制类型的:

通过分析发现,总共有3个区块共12个二进制位数据,前面4个二进制位0x02是控制是否启用的,偶数是启用,奇数是禁用:

后面的8个二进制用来设置禁用的时间:

其实到这里就已经可以设置启用和禁用了,但是还想能够操作禁用的时间,让整个的功能实现更完善。于是又进行了研究。

0x3-3-1 禁用时间的秘密

这里的禁用时间最开始一直以为就是时间戳转换为16进制,尝试了半天发现数据怎么都对不上,通过直接修改注册表的数据发现禁用时间变成了1601年1月1日:

通过搜索该时间发现:

原来在WINDWOS上使用的是 FILETIME :

0x3-3-2 FILETIME 的测试

在发现了FILETIME结构后,参考微软的文档对该结构进行了一些转换测试,编写的Demo源码: 

#include <windows.h>
int main()
{
    // 源码示例: https://docs.microsoft.com/en-us/windows/win32/sysinfo/changing-a-file-time-to-the-current-time
    FILETIME ft, ft1;
    SYSTEMTIME st, f2l, st1, f2l1;
    // 时区
    TIME_ZONE_INFORMATION tzi;
    // 64位的无符号整型值 ,利用 ULONGLONG QuadPart; 算术运算得到 FileTime
    ULARGE_INTEGER uli;

    //1.  GetLocalTime <=> LocalFileTime 本地时间转为本地文件时间
    GetLocalTime(&st);              // Gets the current Local system time
    printf("Local system time:%d-%d-%d %d:%d:%d\n", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
    // 获取时区
    GetTimeZoneInformation(&tzi);
    // 本地时间转本地文件时间
    LocalSystemTimeToLocalFileTime(&tzi, &st, &ft);

    // FileTime 分为高低两段存储,以前的方法不能表示64位,所以有两段。
    printf("dwLowDateTime: %d\n", ft.dwLowDateTime);
    printf("dwHighDateTime: %d\n", ft.dwHighDateTime);

    // 利用 ULARGE_INTEGER 来运算高低位
    // https://wenku.baidu.com/view/2acab930376baf1ffc4fad0a.html
    // http://t.zoukankan.com/findumars-p-5401616.html 分别拷贝到 ULARGE_INTEGER
    uli.LowPart = ft.dwLowDateTime;
    uli.HighPart = ft.dwHighDateTime;

    // 用 ULARGE_INTEGER 的 QuadPart 成员进行算术运算,得到了LocalFileTime
    printf("LocalFileTime: %llu \n", uli.QuadPart);

    // 再将LocalFileTime转回LocalSystemTime
    LocalFileTimeToLocalSystemTime(&tzi, &ft, &f2l);
    printf("LocalFileTimeToLocalSystemTime: %d-%d-%d %d:%d:%d\n", f2l.wYear, f2l.wMonth, f2l.wDay, f2l.wHour, f2l.wMinute, f2l.wSecond);

    printf("\n");

    //2. UTC 时间转文件时间 GetSystemTime <=> FileTime
    GetSystemTime(&st1);   // Gets the current system time
    printf("GetSystemTime Time:%d-%d-%d %d:%d:%d\n", st1.wYear, st1.wMonth, st1.wDay, st1.wHour, st1.wMinute, st1.wSecond);
    // 系统时间转换到文件时间
    SystemTimeToFileTime(&st1, &ft1);   // Converts the current system time to file time format 
    // 结构体运算 . 运算符
    printf("dwLowDateTime: %d\n", ft1.dwLowDateTime);
    printf("dwHighDateTime: %d\n", ft1.dwHighDateTime);
    // ULARGE_INTEGER
    uli.LowPart = ft1.dwLowDateTime;
    uli.HighPart = ft1.dwHighDateTime;
    printf("FileTime: %llu \n", uli.QuadPart);
    // 再将得到的FileTime转回SystemTime
    FileTimeToSystemTime(&ft1, &f2l1);
    printf("FileTimeToSystemTime: %d-%d-%d %d:%d:%d\n", f2l1.wYear, f2l1.wMonth, f2l1.wDay, f2l1.wHour, f2l1.wMinute, f2l1.wSecond);
    return 0;
}

运行结果如下:


而且将高位的10进制转为对应的16进制后发现了在注册表中相同的1d8部分:

所以可以得出结论了,注册表中保存的数据按照低位优先的顺序,第5-8二进制位保存的是FileTime的低位(dwLowDateTime),9-12二进制位保存FileTime的高位数据(dwHighDateTime)。并且在测试中知道了这里的FIleTime使用的是UTC时间,在任务管理器显示的时候又会将这个时间转换到本地时区的时间。

关于禁用时间的表示就分析完成了,接下来会通过注册表设置这个时间。

0x3-3-3 二进制写入与注册表重定向

首先在代码中还是会先读取HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run中的值,检查是否存在SecurityHealth,如果存在该值就尝试设置禁用:

在查询值的时候,需要设置打开键类的API标志位KEY_WOW64_64KEY,设置在64位的注册表项中操作,避免在使用32位程序时被重定向到32位注册表项去:

尝试读取SecurityHealth值的数据,使用了RegGetValueA这个API来读取数据,读取数据时在第四个参数上设置了RRF_RT_REG_EXPAND_SZ | RRF_NOEXPAND的标识位,这不会将数据中的环境变量解析出来(即不会将 %windir%解析为C:\WINDOWS):

读取值的数据时,由于不能事先确定读取的数据大小,所以在代码中,通过两次调用 RegGetValueA 来确定缓冲区大小,然后通过动态内存分配来获取数据:

读取完成后将指针返回给主函数,并在主函数中 freecalloc 动态分配的指针内存。

确认 SecurityHealth 值存在后,调用实现的DisableTray()函数设置该禁用项:

利用RegSetValueExA设置写入二进制数据,其中第四个参数指定了该数据类型为REG_BINARY二进制类型,第五个参数是一个指向了DWORD数组的BYTE类型指针(dwDate):

在上文分析的FILETIME结构体时,已经知道该数据是低位优先的3块数据,所以在dwData数组中:

DWORD dwData[] = { 0x00000003,0x751ee8b0,0x1d82ff5 };

0x03是表示禁用,0x751ee8b0是16进制表示的FILETIME低位数据,0x1d82ff5是16进制表示的FILETIME高位数据。

可以将当前的时间设置为禁用时间,直接改动数组中的数据即可:

显示的禁用时间是转换后的本地时间(虚拟机里的时间没联网同步,所以还是6号):

0x04 效果演示

嗯,先安装好一个其他安全软件,然后运行程序,服务的注册表设置和启动项的设置都可以正常运行:

设置完成后直接重启,重启完成后确认WinDefender禁用完成后,再卸载安全软件后重启一次,就发现WinDefender已经被禁用成功了:

0x05 总结

通过该实验,学习了操作注册表API的一些用法,包括设置注册表重定向、写入二进制数据、解决读取数据时缓冲区不足的报错、FILETIME时间的转换与设置、动态内存分配和释放;还掌握了一些注册表启动、禁用位置、注册表操作监控程序的使用。源码获取:SetRegDisableDefender

0x06 参考链接

禁用启动项的注册表
filetime
win_startup_dirs_complete_list.txt
C++ changing-a-file-time-to-the-current-time
系统时间
KEY_WOW64_64KEY
Windows中的时间(SYSTEMTIME和FILETIME)
https://stackoverflow.com/questions/66903672/how-to-enable-disable-windows-startup-items-programmatically
https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-filetimetosystemtime
https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime
https://halove23.blogspot.com/2021/08/executing-code-in-context-of-trusted.html
https://docs.microsoft.com/en-us/windows/security/identity-protection/access-control/security-identifiers
https://docs.microsoft.com/zh-cn/windows/win32/api/winreg/nf-winreg-reggetvaluea
https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regenumkeyexa
https://docs.microsoft.com/en-us/windows/win32/sysinfo/enumerating-registry-subkeys


文章作者: YangHao
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YangHao !
评论
 上一篇
360隔离沙箱逃逸 360隔离沙箱逃逸
研究在360安全沙箱环境下的几种沙箱逃逸,分析这些方式的权限和区别,最后使用最优的利用方式进行POC和EXP的编写,并完成实验。
下一篇 
PHP任意文件上传绕过多重限制 PHP任意文件上传绕过多重限制
一次授权渗透测试中遇到一个任意文件上传的漏洞点,但是存在云WAF和php-GD库图像二次渲染。通过多次尝试,最终成功获取目标权限。
2021-11-24
  目录