Windows.Forensics.RDPCache

This artifact parses, views and enables simplified upload of RDP cache files.

By default the artifact will parse .BIN RDPcache files.

Filters include UserRegex 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

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 `UserRegex` 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 unexpectedly.
     type: regex
   - name: DateAfter
     type: timestamp
     description: "search for cache files modified after this date. YYYY-MM-DDTmm:hh:ssZ"
   - name: DateBefore
     type: timestamp
     description: "search for cache files modified before this date. YYYY-MM-DDTmm:hh:ssZ"
   - 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: |
      -- firstly set timebounds for performance
      LET DateAfterTime <= if(condition=DateAfter,
        then=DateAfter, else=timestamp(epoch="1600-01-01"))
      LET DateBeforeTime <= if(condition=DateBefore,
        then=DateBefore, else=timestamp(epoch="2200-01-01"))
            
      LET results = SELECT OSPath, Size, Mtime, Atime, Ctime, Btime
        FROM glob(globs=RDPCacheGlob,accessor=Accessor)
        WHERE OSPath =~ UserRegex
            AND Mtime > DateAfterTime
            AND Mtime < DateBeforeTime

      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