查看原文
其他

计划任务的攻防战 | Window 应急响应

NOP Team NOP Team 2024-01-18

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.exeSysinternalsSuite 套件中一款工具

https://learn.microsoft.com/zh-cn/sysinternals/downloads/sysinternals-suite

PsExec64.exe -i -s regedit

这里可以看到 Id、Index、SD ,按照文章描述,修改 IndexSD 都可以完成隐藏

尝试将 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 的时候才有隐藏效果

所以可以通过排查计划任务的注册表,找出所有 Index0 的就可以了

这样检查难度就小很多了,但是如果将计划任务放在了很深的目录,手动检查还是比较困难,得整个脚本来做

这里提供一个 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

这里我惊讶的发现,原来默认存在这么多 Index0 的计划任务

当然这里也打印了我们的隐藏计划任务 test1

这里将 Windows Server 2016 默认的计划任务中 Index0 的详细内容记录下来,方便大家对比,当然,也可以写一个程序将其保存

先写个程序根据 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

经过查询发现, Index0 的计划任务基本上都没有 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.exeSysinternalsSuite 套件中一款工具

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 将上面两种结合起来什么效果

直接给出结论吧

  • 计划任务程序看不到计划任务
  • 日志管理器可以看到
  • 两种方法的排查脚本可以看到
  • 删除时补 SDpowershell 也删除不了,还是需要修改 Index
  • 计划任务服务重启不影响计划任务执行

0x04 仅修改SD能实现隐藏效果吗?

1. 创建计划任务

2. 修改注册表隐藏计划任务

主要涉及以下注册表

计划任务的 Id、Index、SD 在此位置
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree

修改注册表需要 SYSTEM 权限,这里通过 PsExec64.exe 来获取 SYSTEM 权限,编辑注册表

PsExec64.exeSysinternalsSuite 套件中一款工具

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.txtstdout.txt 用来中转,如果当前目录本来就有这两个文件,建议新建文件夹,并在其中执行,免得出现覆盖正常文件

2. 删除计划任务

直接通过 powershell 删除就好,如果这种方法还同时使用了 Index0 ,可以考虑从注册表修改 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 总结

一场有意思的对抗

将这类隐藏大体按照以下分类

  • 保留计划任务注册表项

    • 修改 Index0 隐藏

      这种隐藏的弱点也就是 Index0 。重启计划任务服务计划任务继续执行,不耽误

    • 删除 SD

      这种隐藏的弱点也就是计划任务项没有 SD 项,重启计划任务服务计划任务继续执行,不耽误

    • 修改 SD

      这种隐藏弱点主要在能被 powershell 发现,  schtasks 对于这种和不存在的注册表显示有差异,可以枚举,重启计划任务服务计划任务继续执行,不耽误

    • 删除文件夹 SD

      这种隐藏的弱点也就是计划任务文件夹没有 SD 项,重启计划任务服务计划任务继续执行,不耽误

  • 不保留计划任务注册表项

    • 删除注册表中的计划任务

    • 删除注册表中的计划任务文件夹

    • 直接将计划任务注册表搞坏

      其实这几种都是一样的,因为计划任务服务会“缓存”计划任务配置,所以修改注册表后,只有在计划任务服务重启后,才会生效,因此这类隐藏隐蔽性很强,但是计划任务服务重启后就会呈现修改后的注册表的效果,可能是消失、执行失败、执行攻击者定义的计划任务

文章稍长,为保证观感,为大家准备了 PDF  版本

https://pan.baidu.com/s/14nbBw-nXFnjF8xmllMu7bg?pwd=yjxy 提取码: yjxy

插个 flag ,如果恶趣味的攻击者向计划任务执行日志中定期写一堆不存在的计划任务日志那会怎么样 ?

真的遇到了,但愿你是个不认真的人~


往期文章


有态度,不苟同


继续滑动看下一个

计划任务的攻防战 | Window 应急响应

NOP Team NOP Team
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存