计划任务的攻防战 | Window 应急响应
0x00 简介
一些安全研究员发现,通过修改创建的计划任务的注册表,同时删除计划任务文件,可以完全隐藏计划任务,并且执行不受影响
大家想了解相关技术,可以查看以下文章
https://mp.weixin.qq.com/s/ktGug1VbSpmzh9CEGKbbdw
https://mp.weixin.qq.com/s/aS5MRwnYR5pqE1PmKiH24w
https://payloads.cn/2021/0805/advanced-windows-scheduled-tasks.html
本文测试环境:Windows Server 2016
我觉得任务计划程序有些拗口,所以文中均用计划任务程序代替
0x01 修改 Index 实现隐藏
1. 创建计划任务
创建计划任务 test1
设置每分钟打开一次计算器,无限期执行下去
2. 修改注册表隐藏计划任务
主要涉及以下注册表
计划任务的 Id、Index、SD 在此位置
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree
修改注册表需要 SYSTEM
权限,这里通过 PsExec64.exe
来获取 SYSTEM
权限,编辑注册表
PsExec64.exe
是SysinternalsSuite
套件中一款工具https://learn.microsoft.com/zh-cn/sysinternals/downloads/sysinternals-suite
PsExec64.exe -i -s regedit
这里可以看到 Id、Index、SD
,按照文章描述,修改 Index
和 SD
都可以完成隐藏
尝试将 Index
的值设置为 0
计划任务程序中刷新
发现计划任务 test1
消失了
3. 删除计划任务文件
计划任务文件默认位置
C:\Windows\System32\Tasks
删除 test1
4. 计划任务执行效果
计划任务正常执行
5. 排查计划任务
刚才已经查看过了,计划任务程序是看不到了
1) schtasks 命令
schtasks /query /fo LIST /v | findstr "test1"
查询不到
如果我们知道该计划任务的路径和名称,尝试查询
schtasks /query /tn "\test1" /V /FO LIST
这样就可以查询到了
但是这样逻辑不通,我们默认都看不到计划任务,没理由知道计划任务的名字和路径
2) powershell
Get-ScheduledTask | findstr "test1"
3) Autoruns
Autoruns 是 SysinternalsSuite
套件中一款工具,可以很方便查看包括计划任务等启动项排查
https://learn.microsoft.com/zh-cn/sysinternals/downloads/sysinternals-suite
4) 计划任务文件
刚刚我们将计划任务文件删除了,我们可以看一下,计划任务执行了几次后,看看是否产生了新文件
没有,文件角度也看不到
5) 计划任务日志
开启计划任务日志
等待下一次执行后,去查看计划任务日志
打开日志管理器
eventvwr
应用程序和服务日志 -> Microsoft
-> Windows
-> TaskScheduler
筛选 任务已完成
即事件ID 102
通过计划任务日志可以查询到隐藏计划任务的执行
这样配合 schtasks
就可以获取到计划任务的详细信息了
这里就给做持久控制的兄弟们建议了,加一条检测如果开启了日志,可以考虑删除相关的日志
6) 通过注册表检查
通过我在 Windows Server 2016
上一顿尝试,发现 Index
的值只有是 0
的时候才有隐藏效果
所以可以通过排查计划任务的注册表,找出所有 Index
为 0
的就可以了
这样检查难度就小很多了,但是如果将计划任务放在了很深的目录,手动检查还是比较困难,得整个脚本来做
这里提供一个 powershell
脚本
# 检索注册表中 Index 值为 0 的计划任务名称及其注册表位置
$taskRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree"
$tasks = Get-ChildItem -Path $taskRegistryPath -Recurse | ForEach-Object {
$taskName = $_.PSChildName
$indexValue = (Get-ItemProperty -Path $_.PSPath).Index
if ($indexValue -eq 0) {
[PSCustomObject]@{
TaskName = $taskName
RegistryPath = $_.PSPath -replace "Microsoft.PowerShell.Core\\Registry::"
}
}
}
# 打印计划任务名称及其注册表位置
$tasks | Select-Object TaskName, RegistryPath
这里我惊讶的发现,原来默认存在这么多 Index
为 0
的计划任务
当然这里也打印了我们的隐藏计划任务 test1
这里将 Windows Server 2016
默认的计划任务中 Index
为 0
的详细内容记录下来,方便大家对比,当然,也可以写一个程序将其保存
先写个程序根据 Id
值获取 Actions
吧
$taskCacheTreePath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree"
$taskCacheTasksPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks"
# 检索注册表中 Index 值为 0 的计划任务名称及其注册表位置
$tasks = Get-ChildItem -Path $taskRegistryPath -Recurse | ForEach-Object {
$taskName = $_.PSChildName
$indexValue = (Get-ItemProperty -Path $_.PSPath).Index
if ($indexValue -eq 0) {
$taskId = (Get-ItemProperty -Path $_.PSPath).Id
$actionPath = $taskCacheTasksPath + "\" + $taskId
if(Test-Path -Path $actionPath) {
$taskActions = (Get-ItemProperty -Path $actionPath).Actions
} else {
$taskActions = "无记录"
}
[PSCustomObject]@{
TaskName = $taskName
RegistryPath = $_.PSPath -replace "Microsoft.PowerShell.Core\\Registry::"
Actions = $taskActions
}
}
}
# 打印计划任务名称、注册表位置以及 Actions
$tasks | Select-Object TaskName, RegistryPath, Actions
经过查询发现, Index
为 0
的计划任务基本上都没有 Actions
,在 Tasks
注册表下都没有其对应 Id
的项
如果服务器安装了微软 Edge 浏览器,浏览器升级程序的 Index 会动态变化,偶尔会是 0
这样也就不用保存什么了
7) 删除计划任务
通过注册表直接修改 Index
值,之后通过计划任务程序直接删除就可以
这里尝试使用 schtasks
进行删除
schtasks /Delete /TN "TaskName" /F
成功删除
0x02 删除 SD 实现隐藏
这次尝试删除 SD 实现隐藏
1. 创建计划任务
删除 test1
,创建新的计划任务 test2
2. 修改注册表隐藏计划任务
主要涉及以下注册表
计划任务的 Id、Index、SD 在此位置
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree
修改注册表需要 SYSTEM
权限,这里通过 PsExec64.exe
来获取 SYSTEM
权限,编辑注册表
PsExec64.exe
是SysinternalsSuite
套件中一款工具https://learn.microsoft.com/zh-cn/sysinternals/downloads/sysinternals-suite
PsExec64.exe -i -s regedit
直接删除 SD
项
刷新计划任务程序
果然又消失了
3. 删除计划任务文件
计划任务文件默认位置
C:\Windows\System32\Tasks
4. 计划任务执行效果
计划任务正常执行
5. 排查计划任务
刚才已经查看过了,计划任务程序是看不到了
1. schtasks 命令
schtasks /query /fo LIST /v | findstr "test2"
查询不到
如果我们知道该计划任务的路径和名称,尝试查询
schtasks /query /tn "\test2" /V /FO LIST
这回就厉害了,schtasks
也查询不到了
2. powershell
Get-ScheduledTask | findstr "test2"
3. Autoruns
Autoruns 是 SysinternalsSuite
套件中一款工具,可以很方便查看包括计划任务等启动项排查
https://learn.microsoft.com/zh-cn/sysinternals/downloads/sysinternals-suite
4. 计划任务文件
刚刚我们将计划任务文件删除了,我们可以看一下,计划任务执行了几次后,看看是否产生了新文件
没有,文件角度也看不到
5. 计划任务日志
开启计划任务日志
等待下一次执行后,去查看计划任务日志
打开日志管理器
eventvwr
应用程序和服务日志 -> Microsoft
-> Windows
-> TaskScheduler
筛选 任务已完成
即事件ID 102
通过计划任务日志可以查询到隐藏计划任务的执行
6. 通过注册表检查
通过我在 Windows Server 2016
上一顿尝试,发现基本上计划任务都有 SD
,这样只要查询没有 SD
的就可以发现了
这里提供一个 powershell
脚本
$registryPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree"
# 定义函数来递归获取子项并打印没有 "SD" 项的子项的注册表地址
function Get-SubKeysWithoutSD($path) {
$subKeys = Get-ChildItem -Path $path -ErrorAction SilentlyContinue
foreach ($subKey in $subKeys) {
$subKeyPath = Join-Path -Path $path -ChildPath $subKey.PSChildName
$sdValue = Get-ItemProperty -Path $subKeyPath -Name "SD" -ErrorAction SilentlyContinue
if ($null -eq $sdValue) {
Write-Output $subKeyPath
}
Get-SubKeysWithoutSD -Path $subKeyPath
}
}
# 调用函数开始递归获取子项
Get-SubKeysWithoutSD -Path $registryPath
可以直接找到隐藏的计划任务
7. 删除计划任务
尝试通过 schtasks
进行删除,估计不太行,毕竟刚才查询都找不到
schtasks /Delete /TN "TaskName" /F
找不到计划任务
尝试通过注册表给隐藏的计划任务补一个 SD
值,之后看看是否可以在计划任务程序中删除
随便写点,例如两个 00
刷新计划任务程序
还是没显示,不过我发现 powershell
似乎已经可以发现它了
Get-ScheduledTask -TaskName "test2"
尝试使用 powershell
删除计划任务
Unregister-ScheduledTask -TaskName "test2"
此时再查看注册表
成功删除计划任务
当然也可以尝试将其他计划任务的 SD
复制给它,这样就能在计划任务程序中显示了,可以直接删除
0x03 将上面两种结合起来什么效果
直接给出结论吧
计划任务程序看不到计划任务 日志管理器可以看到 两种方法的排查脚本可以看到 删除时补 SD
后powershell
也删除不了,还是需要修改Index
值计划任务服务重启不影响计划任务执行
0x04 仅修改SD能实现隐藏效果吗?
1. 创建计划任务
2. 修改注册表隐藏计划任务
主要涉及以下注册表
计划任务的 Id、Index、SD 在此位置
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree
修改注册表需要 SYSTEM
权限,这里通过 PsExec64.exe
来获取 SYSTEM
权限,编辑注册表
PsExec64.exe
是SysinternalsSuite
套件中一款工具https://learn.microsoft.com/zh-cn/sysinternals/downloads/sysinternals-suite
PsExec64.exe -i -s regedit
尝试删除 SD
中的一部分,例如删除结尾的一个 00
刷新计划任务程序
没有什么影响,看来删除的不够多,尝试删除一半
刷新计划任务程序
果然又消失了
3. 删除计划任务文件
计划任务文件默认位置
C:\Windows\System32\Tasks
4. 计划任务执行效果
计划任务正常执行
这个细节之前的文章里没有写,这才是让应急响应人员头疼的部分
5. 尝试常规检查
刚才已经查看过了,计划任务程序是看不到了
1. schtasks 命令
schtasks /query /fo LIST /v | findstr "test4"
schtasks /query /tn "\test4" /V /FO LIST
查询不到,这里有一个细节大家注意,指名道姓地查询 test4
的时候显示的是拒绝访问,查询不存在的 test5
的时候是找不到指定的文件
如果大家在测试的过程中没有注意细节,很可能与一些发现失之交臂
2. powershell
Get-ScheduledTask | findstr "test4"
这种情况下 powershell
是可以直接看到的
Get-ScheduledTask -TaskName "test4" | Format-List *
所以修改 SD
可以对 schtasks
和计划任务程序隐藏,对 powershell
没有隐藏效果
这样显示的 Actions
不是很清晰,通过下面的命令查看
(Get-ScheduledTask -TaskName "test4").Actions
3. Autoruns
Autoruns 是 SysinternalsSuite
套件中一款工具,可以很方便查看包括计划任务等启动项排查
https://learn.microsoft.com/zh-cn/sysinternals/downloads/sysinternals-suite
Autoruns
依旧没有
4. 计划任务文件
刚刚我们将计划任务文件删除了,我们可以看一下,计划任务执行了几次后,看看是否产生了新文件
没有,文件角度看不到
5. 计划任务日志
开启计划任务日志
等待下一次执行后,去查看计划任务日志
打开日志管理器
eventvwr
应用程序和服务日志 -> Microsoft
-> Windows
-> TaskScheduler
筛选 任务已完成
即事件ID 102
通过计划任务日志可以查询到隐藏计划任务的执行
6. 通过注册表检查
这回通过注册表就没什么好办法了,但是可以作为辅助之一
如果此计划任务的名称以及
Actions
等都看起来和正常的计划任务差不多,那么即使通过powershell
查到了一堆信息,也不容易从中发现,尤其是它还处于一个比较深的目录,powershell
的结果和计划任务程序的结果一一对比工作量会比较大,不是很好处理
0x05 计划任务服务重启会怎样
上述的两种隐藏,在计划任务服务重启后,还会有效吗
这里不演示了,直接给结论
1. 修改 Index 实现隐藏
计划任务服务重启,不会影响通过 Index 实现隐藏的计划任务的执行,计划任务一切照常
不会生成计划任务文件
2. 删除 SD 实现隐藏
计划任务服务重启,不会影响通过删除 SD 实现隐藏的计划任务的执行,计划任务一切照常
不会生成计划任务文件
3. 修改 SD 实现隐藏
计划任务服务重启,不会影响通过删除 SD 实现隐藏的计划任务的执行,计划任务一切照常
不会生成计划任务文件
4. 同时进行以上两种隐藏
隐藏效果依旧,计划任务服务重启,不会影响通过删除计划任务的执行,计划任务一切照常
不会生成计划任务文件
为什么要在这里反复提是否生成计划任务文件呢?其实这里有一个小细节
如果一个计划任务禁用,再启用,其实是会按照注册表重新生成计划任务文件
如果一个计划任务属性进行了修改,例如调整了
Actions
,也会按照新的情况重新生成计划任务文件但是,重启计划任务服务并不会重新生成计划任务文件
0x06 对抗仅修改 SD 隐藏
对于仅修改 SD
而不是删除这种情况,可以有几个方向考虑(当然,遇到这种情况肯定是前两种脚本已经执行过了,没有发现隐藏的计划任务)
想办法让所有的计划任务禁用再启用或者统一修改属性,在这之前监控计划任务文件所在的文件夹,通过文件变化 powershell
结果与其他程序结果进行对比schtasks
将注册表中所有的计划任务都执行一次,查找报错
这里就以第三种方法做个演示吧
1. 找到计划任务
直接通过 powershell
脚本来完成
$start = Get-Date
$basePath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree"
# 获取所有计划任务
$tasks = Get-ChildItem -Path $basePath -Recurse
# 创建一个空的数组来存储找到的计划任务信息
$taskInfo = @()
# 遍历计划任务并显示路径和名称
foreach ($task in $tasks) {
$taskPath = $task.PSPath.Replace("Microsoft.PowerShell.Core\Registry::", "")
$taskName = $task.Name.Replace("HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree", "")
$exePath = "schtasks.exe"
$arguments = "/query /tn ""$taskName"""
# 执行可执行文件
$process = Start-Process -FilePath $exePath -ArgumentList $arguments -NoNewWindow -PassThru -RedirectStandardOutput "stdout.txt" -RedirectStandardError "stderr.txt" -Wait
# 读取执行结果
$exitCode = $process.ExitCode
# $stdout = Get-Content "stdout.txt"
$stderr = Get-Content "stderr.txt"
if ($stderr -and $stderr.Contains("错误: 拒绝访问。")) {
$taskInfo += [PSCustomObject]@{
"TaskName" = $taskName
"RegistryPath" = $basePath + $taskName
}
}
}
# 将计划任务信息以表格形式显示
$table = $taskInfo | Format-Table -AutoSize | Out-String -Width 500
Write-Host $table
$end = Get-Date
Write-Host -ForegroundColor Red ('Total Runtime: ' + ($end - $start).TotalSeconds)
脚本执行比较慢,会在当前目录生成 stderr.txt
和 stdout.txt
用来中转,如果当前目录本来就有这两个文件,建议新建文件夹,并在其中执行,免得出现覆盖正常文件
2. 删除计划任务
直接通过 powershell
删除就好,如果这种方法还同时使用了 Index
置 0
,可以考虑从注册表修改 Index
为非 0
值, 之后通过 powershell
删除
Unregister-ScheduledTask -TaskName "test4"
成功删除计划任务
0x07 文件夹 删除或修改 SD 会怎样
1. 创建文件夹及计划任务
文件夹 testdir
计划任务 test5
2. 查看注册表信息
文件夹也有一个 SD
值,或者说只有一个 SD
值
3. 修改 SD 值
先把值保存下来,这样一次就能把实验做完
保存后删除一半
刷新计划任务程序
并没有隐藏效果
4. 删除 SD 值
刷新计划任务程序
报错了,显示无法找到文件夹,再次刷新
文件夹以及文件都没了
5. 计划任务效果
计划任务效果正常
6. 删除计划任务文件
计划任务正常执行
7. 删除计划任务文件夹
计划任务正常执行
8. 常规检查情况
计划任务日志处依旧可以看到
其他方式看不到
9. 通过注册表进行查询
思路就是获取所有注册表子项,并将其中无 SD
项的找出来,直接使用计划任务删除 SD
时使用的脚本
$registryPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree"
# 定义函数来递归获取子项并打印没有 "SD" 项的子项的注册表地址
function Get-SubKeysWithoutSD($path) {
$subKeys = Get-ChildItem -Path $path -ErrorAction SilentlyContinue
foreach ($subKey in $subKeys) {
$subKeyPath = Join-Path -Path $path -ChildPath $subKey.PSChildName
$sdValue = Get-ItemProperty -Path $subKeyPath -Name "SD" -ErrorAction SilentlyContinue
if ($null -eq $sdValue) {
Write-Output $subKeyPath
}
Get-SubKeysWithoutSD -Path $subKeyPath
}
}
# 调用函数开始递归获取子项
Get-SubKeysWithoutSD -Path $registryPath
这样就可以找出这个隐藏的注册表了
10. 重启计划任务服务
重启计划任务后,计划任务依旧隐藏,没有产生新的计划任务文件,计划任务仍旧有效
11. 把注册表项都删除了会怎样呢?
上面能够被排查出来,是因为存在无 SD
项的文件夹,如果攻击者再变态一点,直接把注册表中文件夹都删除了,会怎么样呢?
不着急直接变成变态,我们先尝试将文件夹中的计划任务test5
删除掉
不耽误计划任务执行
开始变态,删除掉 testdir
不耽误计划任务执行
12. 再次尝试通过注册表进行排查
这次就排查不出来了,当然了计划任务日志里还是有的
13. 再次重启计划任务服务
重启后计划任务就没了
如果看过上一篇文章的朋友肯定能知道这是为什么 《计划任务执行由谁决定 | Windows 应急响应》
0x08 总结
一场有意思的对抗
将这类隐藏大体按照以下分类
保留计划任务注册表项
修改
Index
为0
隐藏这种隐藏的弱点也就是
Index
为0
。重启计划任务服务计划任务继续执行,不耽误删除
SD
项这种隐藏的弱点也就是计划任务项没有
SD
项,重启计划任务服务计划任务继续执行,不耽误修改
SD
项这种隐藏弱点主要在能被
powershell
发现,schtasks
对于这种和不存在的注册表显示有差异,可以枚举,重启计划任务服务计划任务继续执行,不耽误删除文件夹
SD
项这种隐藏的弱点也就是计划任务文件夹没有
SD
项,重启计划任务服务计划任务继续执行,不耽误不保留计划任务注册表项
删除注册表中的计划任务
删除注册表中的计划任务文件夹
直接将计划任务注册表搞坏
其实这几种都是一样的,因为计划任务服务会“缓存”计划任务配置,所以修改注册表后,只有在计划任务服务重启后,才会生效,因此这类隐藏隐蔽性很强,但是计划任务服务重启后就会呈现修改后的注册表的效果,可能是消失、执行失败、执行攻击者定义的计划任务
文章稍长,为保证观感,为大家准备了 PDF
版本
https://pan.baidu.com/s/14nbBw-nXFnjF8xmllMu7bg?pwd=yjxy 提取码: yjxy
插个
flag
,如果恶趣味的攻击者向计划任务执行日志中定期写一堆不存在的计划任务日志那会怎么样 ?真的遇到了,但愿你是个不认真的人~