07-文件系统取证 (Filesystem Forensics)
Windows 文件系统(NTFS)包含大量 Linux ext4 没有的取证特性:Alternate Data Streams(ADS)可以隐藏数据、$MFT 记录完整的文件元数据、$STANDARD_INFORMATION 和 $FILE_NAME 双时间戳可以检测 timestomping。从 Linux 背景转到 Windows IR,文件系统取证是差异最大的领域之一。
前置知识 :01-系统基础与注册表 | 04-取证制品分析
关联章节 :06-进程与网络分析 | 09-注册表持久化审计
Linux 对照 :06-文件系统取证
1. Alternate Data Streams (ADS) 1.1 ADS 基本概念 ADS 是 NTFS 文件系统的特性,允许一个文件拥有多个数据流
默认的数据流是 $DATA(即文件的正常内容),额外的流不改变文件大小
Linux 类比 :Linux ext4 没有 ADS,最接近的概念是 Extended Attributes(xattr),但 ADS 能存储任意大小的数据
攻击者利用场景 :
在正常文件中隐藏恶意 payload
存储 C2 配置数据
绕过安全产品的文件扫描(部分产品不扫描 ADS)
1.2 ADS 创建方法(了解攻击手法) 1 2 3 4 5 6 7 8 9 10 11 12 :: 创建 ADS(攻击者视角,了解即可) :: 将 payload.exe 隐藏在 normal.txt 的 ADS 中 type payload.exe > normal.txt:hidden.exe:: 写入文本到 ADS echo "C2 config data" > normal.txt:config.ini:: PowerShell 创建 ADS Set -Content -Path "normal.txt" -Stream "hidden.exe" -Value (Get-Content payload.exe -Raw):: 甚至可以在目录上创建 ADS echo "hidden data" > C:\Users\Public:secret.txt
1.3 ADS 检测方法 方法 1:dir /r
1 2 3 4 5 6 7 8 9 10 :: 显示文件的所有数据流 dir /r C:\Users\Public\:: 输出示例: :: 2024 -01 -15 03 :22 1 ,024 normal.txt :: 512 normal.txt:hidden.exe:$DATA :: 28 normal.txt:config.ini:$DATA :: 递归扫描 dir /r /s C:\Users\ 2 >nul | findstr ":$DATA"
方法 2:PowerShell Get-Item -Stream
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Get-Item "C:\Users\Public\normal.txt" -Stream * | Where-Object { $_ .Stream -ne ':$DATA' -and $_ .Stream -ne 'Zone.Identifier' } Get-ChildItem "C:\Users" -Recurse -ErrorAction SilentlyContinue | ForEach-Object { $streams = Get-Item $_ .FullName -Stream * -ErrorAction SilentlyContinue | Where-Object { $_ .Stream -ne ':$DATA' -and $_ .Stream -ne 'Zone.Identifier' } if ($streams ) { foreach ($stream in $streams ) { [PSCustomObject ]@ { FilePath = $_ .FullName StreamName = $stream .Stream StreamSize = $stream .Length } } } } | Format-Table -AutoSize Get-Content "C:\Users\Public\normal.txt" -Stream hidden.exe -Encoding Byte | Set-Content "C:\IR\extracted_hidden.exe" -Encoding Byte
方法 3:streams.exe(Sysinternals)
1 2 3 4 5 :: 扫描目录中的 ADS streams.exe -s C:\Users\ :: 递归扫描并显示详细信息 streams.exe -s -d C:\inetpub\wwwroot\
1.4 Zone.Identifier 流 Windows 从网络下载的文件会被标记 Zone.Identifier ADS
这是 Mark of the Web (MOTW) 机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Get-Content "C:\Users\Admin\Downloads\tool.exe" -Stream Zone.IdentifierGet-ChildItem "C:\Users\*\Downloads\*" -ErrorAction SilentlyContinue | ForEach-Object { $zone = Get-Content $_ .FullName -Stream Zone.Identifier -ErrorAction SilentlyContinue if ($zone ) { [PSCustomObject ]@ { File = $_ .Name Size = $_ .Length Modified = $_ .LastWriteTime ZoneInfo = ($zone -join ' | ' ) } } } | Format-Table -AutoSize
2. MACE 时间戳与 Timestomping 检测 2.1 NTFS 时间戳体系 NTFS 为每个文件维护两组时间戳:
**$STANDARD_INFORMATION (SI)**:
Modified, Accessed, Changed (MFT), Created — 即 MACE
用户和程序可以通过 API 修改(SetFileTime)
这是 dir 和 Explorer 显示的时间戳
**$FILE_NAME (FN)**:
同样有 MACE 四个时间戳
只有操作系统内核在文件被移动/重命名时才会更新
用户无法直接修改 (除非直接修改磁盘原始数据)
Linux 对比 :
Linux ext4 有 mtime, atime, ctime, crtime(birth time)
touch -t 可以修改 mtime 和 atime,但 ctime 无法直接修改
Windows 的 $FILE_NAME 时间戳类似于 Linux 的 ctime,更难被篡改
2.2 Timestomping 检测方法 原理 :攻击者修改了 $STANDARD_INFORMATION 时间戳,但 $FILE_NAME 时间戳未被修改
如果 SI 时间戳明显早于 FN 时间戳,说明被篡改
PowerShell 基础检测 (只能看到 SI 时间戳):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Get-ChildItem "C:\Windows\System32" -ErrorAction SilentlyContinue | Where-Object { $_ .CreationTime -gt $_ .LastWriteTime } | Select-Object Name, CreationTime, LastWriteTime, @ {N='Diff_Hours' ; E={ [math ]::Round(($_ .CreationTime - $_ .LastWriteTime).TotalHours, 1 ) }} | Sort-Object Diff_Hours -Descending | Select-Object -First 20 $osInstall = (Get-CimInstance Win32_OperatingSystem).InstallDateGet-ChildItem "C:\Windows\System32" | Where-Object { $_ .CreationTime -lt $osInstall .AddYears(-1 ) } | Select-Object Name, CreationTime | Format-Table
2.3 $MFT 和 $UsnJrnl 分析 **$MFT (Master File Table)**:记录所有文件的元数据,包括已删除文件
**$UsnJrnl (Update Sequence Number Journal)**:记录文件变更日志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
3. Recent Files 与 Jump Lists 3.1 Recent Files 用户打开的文件会在 Recent 目录中创建 LNK 快捷方式
位置 :C:\Users\<user>\AppData\Roaming\Microsoft\Windows\Recent\
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Get-ChildItem "$env:APPDATA \Microsoft\Windows\Recent" -Filter "*.lnk" | Sort-Object LastWriteTime -Descending | Select-Object Name, LastWriteTime | Format-Table -AutoSize $shell = New-Object -ComObject WScript.ShellGet-ChildItem "$env:APPDATA \Microsoft\Windows\Recent" -Filter "*.lnk" | Sort-Object LastWriteTime -Descending | ForEach-Object { $lnk = $shell .CreateShortcut($_ .FullName) [PSCustomObject ]@ { LnkName = $_ .Name TargetPath = $lnk .TargetPath Arguments = $lnk .Arguments WorkingDir = $lnk .WorkingDirectory LnkModified = $_ .LastWriteTime } } | Format-Table -AutoSize Get-ChildItem "C:\Users\*\AppData\Roaming\Microsoft\Windows\Recent" -Filter "*.lnk" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object @ {N='User' ;E={$_ .FullName.Split('\' )[2 ]}}, Name, LastWriteTime | Select-Object -First 50 | Format-Table -AutoSize
3.2 Jump Lists Jump Lists 记录了应用程序最近打开的文件列表
位置 :
自动(AutomaticDestinations):C:\Users\<user>\AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations\
自定义(CustomDestinations):C:\Users\<user>\AppData\Roaming\Microsoft\Windows\Recent\CustomDestinations\
文件名格式:<AppID>.automaticDestinations-ms
常见 AppID :
AppID (十六进制)
应用程序
5f7b5f1e01b83767
cmd.exe
b91050d8b077a4e8
记事本
9b9cdc69c1c24e2b
画图
12dc1ea8e34b5a6
画图 (Win10)
7e4dca80246863e3
照片
1b4dd67f29cb1962
Explorer (文件夹)
1 2 3 4 5 6 7 8 Get-ChildItem "C:\Users\*\AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations\*.automaticDestinations-ms" -ErrorAction SilentlyContinue | Select-Object Name, Length, LastWriteTime, @ {N='User' ;E={$_ .FullName.Split('\' )[2 ]}} | Sort-Object LastWriteTime -Descending | Format-Table -AutoSize
4. 回收站取证 4.1 回收站结构 回收站位于 C:\$Recycle.Bin\<SID>\
每个用户有独立的回收站目录,以 SID 命名
删除文件后生成两个文件:
$I<随机ID>.<原扩展名>:元数据文件(原路径、删除时间、文件大小)
$R<随机ID>.<原扩展名>:实际文件内容
Linux 对比 :Linux 的回收站(如 GNOME Trash)在 ~/.local/share/Trash/,结构类似但格式不同
4.2 回收站分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 Get-ChildItem "C:\`$Recycle.Bin" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_ .Name -like '$I*' } | ForEach-Object { [PSCustomObject ]@ { User_SID = $_ .DirectoryName.Split('\' )[-1 ] MetaFile = $_ .Name Size = $_ .Length DeleteTime = $_ .LastWriteTime FullPath = $_ .FullName } } | Format-Table -AutoSize function Parse-RecycleBinInfo { param ([string ]$IFilePath ) $bytes = [System.IO.File ]::ReadAllBytes($IFilePath ) $fileSize = [BitConverter ]::ToInt64($bytes , 8 ) $deleteTime = [DateTime ]::FromFileTime([BitConverter ]::ToInt64($bytes , 16 )) $nameLen = [BitConverter ]::ToInt32($bytes , 24 ) $originalPath = [System.Text.Encoding ]::Unicode.GetString($bytes , 28 , $nameLen * 2 ) [PSCustomObject ]@ { OriginalPath = $originalPath FileSize = $fileSize DeleteTime = $deleteTime } } Get-ChildItem "C:\`$Recycle.Bin" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_ .Name -like '$I*' -and $_ .Length -gt 28 } | ForEach-Object { try { $info = Parse-RecycleBinInfo $_ .FullName $info | Add-Member -NotePropertyName "SID" -NotePropertyValue $_ .DirectoryName.Split('\' )[-1 ] $info } catch {} } | Sort-Object DeleteTime -Descending | Format-Table -AutoSize
使用 RBCmd(Eric Zimmerman) :
1 2 3 4 5 :: 解析单个回收站目录 RBCmd.exe -d "C:\$Recycle.Bin\S-1 -5 -21 -xxxx-500 " --csv "C:\IR\" --csvf recycle_bin.csv :: 解析所有用户的回收站 RBCmd.exe -d "C:\$Recycle.Bin" --csv "C:\IR\" --csvf recycle_bin_all.csv
5. 浏览器取证 5.1 浏览器数据路径
浏览器
数据目录
关键文件
Chrome
%LOCALAPPDATA%\Google\Chrome\User Data\Default\
History, Downloads, Cookies, Login Data, Bookmarks
Edge
%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\
同 Chrome(Chromium 内核)
Firefox
%APPDATA%\Mozilla\Firefox\Profiles\<profile>\
places.sqlite, cookies.sqlite, logins.json
IE/Legacy Edge
%LOCALAPPDATA%\Microsoft\Windows\WebCache\
WebCacheV01.dat (ESE 数据库)
5.2 Chrome/Edge 历史记录查询 Chrome 和 Edge 使用 SQLite 数据库存储历史记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $chromeHistory = "$env:LOCALAPPDATA \Google\Chrome\User Data\Default\History" $copyPath = "C:\IR\Chrome_History_copy" Copy-Item $chromeHistory $copyPath -Force -ErrorAction SilentlyContinueGet-ChildItem "$env:LOCALAPPDATA \Google\Chrome\User Data\Default\" -ErrorAction SilentlyContinue | Where-Object { $_ .Name -in @ ('History' ,'Downloads' ,'Cookies' ,'Login Data' ,'Web Data' ,'Bookmarks' ) } | Select-Object Name, Length, LastWriteTime | Format-Table
5.3 Firefox 历史记录查询 1 2 3 4 5 6 7 8 9 10 11 12 $ffProfiles = Get-ChildItem "$env:APPDATA \Mozilla\Firefox\Profiles\*" -Directory -ErrorAction SilentlyContinueforeach ($profile in $ffProfiles ) { Write-Host "Profile: $ ($profile .Name)" -ForegroundColor Cyan $placesDb = Join-Path $profile .FullName "places.sqlite" if (Test-Path $placesDb ) { $copyPath = "C:\IR\ff_places_$ ($profile .Name).sqlite" Copy-Item $placesDb $copyPath -Force Write-Host " 已复制到:$copyPath " } }
5.4 浏览器缓存与 Cookie 1 2 3 4 5 6 7 8 9 10 11 12 $chromeCache = "$env:LOCALAPPDATA \Google\Chrome\User Data\Default\Cache\Cache_Data" if (Test-Path $chromeCache ) { $cacheFiles = Get-ChildItem $chromeCache -ErrorAction SilentlyContinue Write-Host "Chrome 缓存文件数量:$ ($cacheFiles .Count)" Write-Host "最近缓存文件:" $cacheFiles | Sort-Object LastWriteTime -Descending | Select-Object Name, Length, LastWriteTime -First 10 | Format-Table }
6. Temp 目录与可疑文件扫描 6.1 关键 Temp 目录
路径
说明
重点关注
%TEMP% / %TMP%
当前用户临时目录
恶意软件 drop 位置
C:\Windows\Temp
系统临时目录
服务/计划任务的 drop 位置
C:\Users\Public
公共用户目录
攻击者常用的写入目录
C:\ProgramData
程序数据
恶意软件配置/数据
C:\Users\<user>\Downloads
下载目录
钓鱼附件
C:\Users\<user>\Desktop
桌面
用户手动保存的工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 $tempDirs = @ ( $env:TEMP , "C:\Windows\Temp" , "C:\Users\Public" , "C:\ProgramData" ) $suspiciousExts = @ ('.exe' ,'.dll' ,'.ps1' ,'.bat' ,'.cmd' ,'.vbs' ,'.js' , '.wsf' ,'.hta' ,'.scr' ,'.pif' ,'.com' ,'.msi' ,'.jar' ) foreach ($dir in $tempDirs ) { $files = Get-ChildItem $dir -Recurse -ErrorAction SilentlyContinue | Where-Object { $suspiciousExts -contains $_ .Extension.ToLower() } if ($files ) { Write-Host "[!] $dir 中发现可疑文件:" -ForegroundColor Yellow $files | Select-Object Name, Length, CreationTime, LastWriteTime, FullName | Format-Table -AutoSize } } $cutoff = (Get-Date ).AddDays(-7 )Get-ChildItem "C:\" -Recurse -ErrorAction SilentlyContinue | Where-Object { $suspiciousExts -contains $_ .Extension.ToLower() -and $_ .CreationTime -gt $cutoff } | Select-Object FullName, Length, CreationTime | Sort-Object CreationTime -Descending | Format-Table -AutoSize
6.2 文件哈希计算与 VirusTotal 查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Get-FileHash "C:\suspect\malware.exe" -Algorithm SHA256Get-FileHash "C:\suspect\malware.exe" -Algorithm MD5Get-ChildItem "C:\Windows\Temp" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_ .Extension -in @ ('.exe' ,'.dll' ,'.ps1' ,'.bat' ) } | ForEach-Object { [PSCustomObject ]@ { File = $_ .FullName SHA256 = (Get-FileHash $_ .FullName -Algorithm SHA256).Hash Size = $_ .Length Created = $_ .CreationTime } } | Format-Table -AutoSize function Check-VTHash { param ([string ]$Hash , [string ]$ApiKey ) $headers = @ { "x-apikey" = $ApiKey } $response = Invoke-RestMethod -Uri "https://www.virustotal.com/api/v3/files/$Hash " -Headers $headers -ErrorAction SilentlyContinue if ($response ) { $stats = $response .data.attributes.last_analysis_stats [PSCustomObject ]@ { Hash = $Hash Detections = "$ ($stats .malicious)/$ ($stats .malicious + $stats .undetected)" Name = $response .data.attributes.meaningful_name } } }
7. 文件签名验证 7.1 Authenticode 签名检查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Get-AuthenticodeSignature "C:\Windows\System32\svchost.exe" Get-ChildItem "C:\Windows\System32\*.exe" | ForEach-Object { $sig = Get-AuthenticodeSignature $_ .FullName if ($sig .Status -ne 'Valid' ) { [PSCustomObject ]@ { File = $_ .Name Status = $sig .Status Path = $_ .FullName } } } | Format-Table -AutoSize
7.2 sigcheck.exe(Sysinternals) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 :: 检查目录中未签名的文件 sigcheck.exe -u -e -s C:\Windows\System32\ :: 检查并提交到 VirusTotal sigcheck.exe -u -e -s -vt C:\Windows\System32\ :: 参数说明: :: -u 仅显示未签名文件 :: -e 扫描可执行文件(EXE/DLL 等) :: -s 递归子目录 :: -vt 提交到 VirusTotal 检查 :: -h 显示文件哈希 :: -c CSV 输出 :: 检查特定文件并显示 PE 信息 sigcheck.exe -a C:\suspect\file.exe :: 显示:签名者、编译时间、熵值、版本信息等
8. Webshell 检测 8.1 IIS 网站目录排查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 Import-Module WebAdministration -ErrorAction SilentlyContinueGet-Website | Select-Object Name, PhysicalPath, State$webRoots = @ ( "C:\inetpub\wwwroot" , "C:\inetpub" ) try { $iisRoots = (Get-Website ).PhysicalPath | ForEach-Object { [Environment ]::ExpandEnvironmentVariables($_ ) } $webRoots += $iisRoots } catch {} $webshellExts = @ ('.aspx' ,'.asp' ,'.ashx' ,'.asmx' ,'.cshtml' ,'.php' ,'.jsp' ,'.jspx' )foreach ($root in ($webRoots | Select-Object -Unique )) { if (Test-Path $root ) { Write-Host "[*] 扫描 Web 目录:$root " -ForegroundColor Yellow Get-ChildItem $root -Recurse -ErrorAction SilentlyContinue | Where-Object { $webshellExts -contains $_ .Extension.ToLower() -and $_ .CreationTime -gt (Get-Date ).AddDays(-30 ) } | Select-Object FullName, CreationTime, LastWriteTime, Length | Format-Table -AutoSize Get-ChildItem $root -Recurse -Include *.aspx,*.asp,*.ashx -ErrorAction SilentlyContinue | Select-String -Pattern 'eval\(|Execute\(|cmd\.exe|powershell|System\.Diagnostics\.Process|Runtime\.getRuntime|ProcessStartInfo|unsafe|Request\[|Request\.Item' -List | Select-Object Path, LineNumber, Line | Format-Table -AutoSize } }
8.2 Webshell 特征关键字
语言
常见 Webshell 关键字
ASPX
eval(, Execute(, ProcessStartInfo, cmd.exe, Request["cmd"]
ASP
Execute(, Eval(, CreateObject("WScript.Shell"), cmd /c
PHP
eval(, system(, exec(, passthru(, shell_exec(, $_POST[
JSP
Runtime.getRuntime().exec(, ProcessBuilder, <%@ page import="java.io.*"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Get-ChildItem $webRoots [0 ] -Recurse -Include *.aspx,*.asp,*.ashx -ErrorAction SilentlyContinue | Where-Object { $_ .Length -lt 10 KB } | Select-Object FullName, Length, CreationTime | Sort-Object CreationTime -Descending | Format-Table -AutoSize Get-ChildItem $webRoots [0 ] -Recurse -Include *.aspx -ErrorAction SilentlyContinue | ForEach-Object { $acl = Get-Acl $_ .FullName $owner = $acl .Owner if ($owner -like '*IIS*' -or $owner -like '*IUSR*' -or $owner -like '*NETWORK SERVICE*' ) { [PSCustomObject ]@ { File = $_ .FullName Owner = $owner Created = $_ .CreationTime Size = $_ .Length } } } | Format-Table -AutoSize
9. 实战练习:发现隐藏 ADS 与回收站取证 9.1 练习 1:ADS 检测 目标 :在指定目录中发现隐藏的 ADS,提取其内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Get-ChildItem "C:\Investigation\Sample\" -Recurse | ForEach-Object { Get-Item $_ .FullName -Stream * -ErrorAction SilentlyContinue | Where-Object { $_ .Stream -ne ':$DATA' -and $_ .Stream -ne 'Zone.Identifier' } }
9.2 练习 2:回收站元数据恢复 目标 :从回收站中恢复被删除文件的原始路径和删除时间
1 2 3 4 5 6 7 8 9 10 11 Get-ChildItem "C:\`$Recycle.Bin" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_ .Name -like '$I*' }
9.3 Linux 工程师对照
Linux 操作
Windows 对应
find / -newer ref_file
Get-ChildItem -Recurse | Where { $_.LastWriteTime -gt $date }
find / -name "*.php" -exec grep eval {} \;
Get-ChildItem -Recurse -Include *.aspx | Select-String "eval"
stat file.txt (查看时间戳)
Get-Item file.txt | Select *Time*
file malware.bin (文件类型)
sigcheck.exe malware.bin
sha256sum file
Get-FileHash file -Algorithm SHA256
ls -la ~/.local/share/Trash/
Get-ChildItem "C:\$Recycle.Bin" -Force -Recurse
Extended Attributes (getfattr)
Alternate Data Streams (Get-Item -Stream *)
10. 快速参考 10.1 文件取证命令速查
目标
命令/工具
ADS 检测
dir /r, Get-Item -Stream *, streams.exe
时间戳分析
MFTECmd.exe, Get-Item | Select *Time*
Recent Files
Get-ChildItem %APPDATA%\...\Recent\*.lnk
Jump Lists
JLECmd.exe
回收站
RBCmd.exe, 解析 $I 文件
浏览器历史
sqlite3 + Chrome History DB
文件签名
Get-AuthenticodeSignature, sigcheck.exe
Webshell 扫描
Select-String + 关键字匹配
文件哈希
Get-FileHash -Algorithm SHA256