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 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

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