This artifact parses, views and enables simplified upload of RDP cache files.
By default the artifact will parse .BIN RDPcache files.
Filters include User regex to target a user and Accessor to target vss via ntfs_vss.
Best combined with:
name: Windows.Forensics.RDPCache
author: Matt Green - @mgreen27
description: |
This artifact parses, views and enables simplified upload of RDP
cache files.
By default the artifact will parse .BIN RDPcache files.
Filters include User regex to target a user and Accessor to target
vss via ntfs_vss.
Best combined with:
- Windows.EventLogs.RDPAuth to collect RDP focused event logs.
- Windows.Registry.RDP to collect user RDP mru and server info
reference:
- https://github.com/ANSSI-FR/bmc-tools
- https://github.com/BSI-Bund/RdpCacheStitcher
parameters:
- name: RDPCacheGlob
default: C:\{{Users,Windows.old\Users}\*\AppData\Local,Documents and Settings\*\Local Settings\Application Data}\Microsoft\Terminal Server Client\Cache\*
- name: Accessor
description: Set accessor to use. blank is default, file for api, ntfs for raw, ntfs_vss for vss
- name: UserRegex
default: .
description: Regex filter of user to target. StartOf(^) and EndOf($)) regex may behave unexpectanly.
type: regex
- name: ParseCache
description: If selected will parse .BIN RDPcache files.
type: bool
- name: Workers
default: 100
type: int
description: Number of workers to use for ParseCache
- name: UploadRDPCache
description: If selected will upload raw cache files. Can be used for offline processing/preservation.
type: bool
sources:
- name: TargetFiles
description: RDP BitmapCache files in scope.
query: |
LET results = SELECT OSPath, Size, Mtime, Atime, Ctime, Btime
FROM glob(globs=RDPCacheGlob,accessor=Accessor)
WHERE OSPath =~ UserRegex
LET upload_results = SELECT *, upload(file=OSPath) as CacheUpload
FROM results
SELECT * FROM if(condition= UploadRDPCache,
then= upload_results,
else= results )
- name: Parsed
description: Parsed RDP BitmapCache files.
query: |
LET PROFILE = '''[
["BIN_CONTAINER", 0, [
[Magic, 0, String, {length: 8, term_hex : "FFFFFF" }],
[Version, 8, uint32],
[CachedFiles, 12, Array, {
"type": "rgb32b",
"count": 10000,
"max_count": 2000,
"sentinel": "x=>x.__Size < 15",
}],
]],
["rgb32b","x=>x.__Size",[
[__key1, 0, uint32],
[__key1, 4, uint32],
["Width", 8, "uint16"],
["Height", 10, "uint16"],
[DataLength, 0, Value,{ value: "x=> 4 * x.Width * x.Height"}],
[DataOffset, 0, Value,{ "value": "x=>x.StartOf + 12"}],
["__Size", 0, Value,{ "value": "x=>x.DataLength + 12"}],
["Index", 0, Value,{ "value": "x=>count() - 1 "}],
]]]'''
LET parse_rgb32b(data) = SELECT
_value as Offset,
_value + 3 as EndOffset,
len(list=data) as Length,
data[(_value):(_value + 3)] + unhex(string="FF") as Buffer
FROM range(step=4,end=len(list=data))
LET fix_bmp(data) = SELECT
_value as Offset,
_value + 255 as EndOffset,
join(array=data[ (_value):(_value + 256 ) ],sep='') as Buffer
FROM range(step=256, end= len(list=data) )
ORDER BY Offset DESC
LET parse_container = SELECT * OSPath,Name,Size as FileSize,
read_file(filename=OSPath,length=12) as Header,
parse_binary(filename=OSPath,profile=PROFILE,struct='BIN_CONTAINER') as Parsed
FROM foreach(row={
SELECT * FROM glob(globs=RDPCacheGlob,accessor=Accessor)
WHERE OSPath =~ '\.bin$'
AND OSPath =~ UserRegex
AND NOT IsDir
})
LET find_index_differential = SELECT *, 0 - Parsed.CachedFiles.Index[0] as IndexDif
FROM parse_container
LET parse_cache = SELECT * FROM foreach(row=find_index_differential, query={
SELECT OSPath, IndexDif,
OSPath.Dirname + ( OSPath.Basename + '_' + format(format='%04v',args= Index + IndexDif ) + '.bmp' ) as BmpName,
FileSize,Header,Width,Height,DataLength,DataOffset
FROM foreach(row=Parsed.CachedFiles)
})
LET extract_data = SELECT *
FROM foreach(row=parse_cache,query={
SELECT
OSPath,BmpName,FileSize,Header,Width,Height,DataLength,DataOffset,
join(array=parse_rgb32b(data=read_file(filename=OSPath,offset=DataOffset,length=DataLength)).Buffer,sep='') as Data
FROM scope()
}, workers=Workers)
-- change endianess for unint32
LET pack_lt_l(data) = unhex(string=join(array=[
format(format='%02x',args=unhex(string=format(format='%08x',args=data))[3]),
format(format='%02x',args=unhex(string=format(format='%08x',args=data))[2]),
format(format='%02x',args=unhex(string=format(format='%08x',args=data))[1]),
format(format='%02x',args=unhex(string=format(format='%08x',args=data))[0])
],sep=''))
-- build bmp file, adding appropriate header
LET build_bmp(data,width,height) = join(array=[
"BM",
pack_lt_l(data=len(list=data) + 122),
unhex(string="000000007A0000006C000000"),
pack_lt_l(data=width),
pack_lt_l(data=height),
unhex(string="0100200003000000"),
pack_lt_l(data=len(list=data)),
unhex(string="000000000000000000000000000000000000FF0000FF0000FF000000000000FF"),
" niW",
unhex(string="00" * 36),
unhex(string="000000000000000000000000"),
data
], sep='')
SELECT * FROM if(condition= ParseCache,
then={
SELECT
BmpName, Header, Width, Height, DataLength, DataOffset,
upload(
file=build_bmp(data=join(array=fix_bmp(data=Data).Buffer,sep=''),
width=Width, height=Height),
name=BmpName,
accessor='data' ) as BmpUpload,
OSPath as SourceFile
FROM extract_data
ORDER BY BmpName
},
else= Null )
column_types:
- name: BmpUpload
type: upload_preview
- name: CacheUpload
type: upload_preview