0x01 简介
最近整理文件,发现自己之前禁用Windows Defender
的文档。如果安装了其他的安全软件,Defender
是可以完全禁用掉的(TA也会自动设置服务状态为手动),而没安装其他安全软件时,在管理员权限下是没有权限设置WinDefend
和WdNisSvc
服务启动状态的。所以前提是先安装一个其他的安全软件,禁用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++
来实现上面手动禁用的过程。主要使用到了RegOpenKeyExA
、RegGetValueA
、RegSetValueEx
等操作注册表的API函数。以下对实现过程进行分析。
0x3-1 读取服务启动状态
第一步通过读取注册表中WinDefender
各项服务的启动状态(服务注册表里的Start
值):
因为都是读取DWORD
类型的数据,所以将相同的操作封装在了getDWORDValueToReg
函数中,读取数据后返回Start
的数据,如果为4那就说明服务已经是禁用状态了:
0x3-2 设置服务启动状态
第二步是如果对应服务的启动状态不为4的禁用状态,那么就通过封装好的RegCreateKeyExA
和RegSetValueEx
去设置启动状态为4,封装的函数setDWORDValueToReg
如下:
函数RegCreateKeyExA
的语法如下,其中在参数中以LP或者P开头的类型表示该参数类型是一个指针,比如lpdwDisposition
参数就是一个指向DWORD
类型的长指针(LP):
在封装的代码中将dwOptions
选项设置为了REG_OPTION_NON_VOLATILE
,这是默认的选项,是指操作完成后保存到注册表中,不会随着重启而改变设置。同时也给可选的输出参数lpdwDisposition
传入了对应的指针,因为上面说到参数类型是LPDWORD
是一个指针,所以定义了lpdwDisposition
是DWORD
,取地址符后就取得了他的地址,也就获得了一个指向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
):
定位到具体的注册表项,三个子项分别是Run
、Run32
、StartupFolder
,其中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
)。并且在测试中知道了这里的FIleTim
e使用的是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
来确定缓冲区大小,然后通过动态内存分配来获取数据:
读取完成后将指针返回给主函数,并在主函数中 free
掉 calloc
动态分配的指针内存。
确认 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