Windows.Carving.SquirrelWaffle

This artifact yara-scans memory or process dumps for unpacked SquirrelWaffle Dlls, decodes the configuration and returns the C2s and the payload.

Depending on the initial infection vector (the macro within .doc or .xls maldoc), SquirrelWaffle packed droper will be loaded by either rundll32 or regsvr32 and unpack itself in memory.

The decoded configurations found so far contain (1) a list of C2 URLS, (2*) may contain a list of C2 IPs, and lastly, (3) contains the command “regsvr32.exe -s”. The command is used to launch its second-stage payload downloaded from its C2 addresses, as a “.txt” file that is in fact a disguised PE, to be loaded in memory.

NOTE

This content simply carves the configuration and does not unpack files on disk. That means pointing this artifact as a packed or obfuscated file will not obtain the expected results.


name: Windows.Carving.SquirrelWaffle
author: "Eduardo Mattos - @eduardfir & Kostya Iliouk - @kostyailiouk"
description: |
    This artifact yara-scans memory or process dumps for unpacked
    SquirrelWaffle Dlls, decodes the configuration and returns the C2s
    and the payload.

    Depending on the initial infection vector (the macro within .doc or
    .xls maldoc), SquirrelWaffle packed droper will be loaded by either rundll32
    or regsvr32 and unpack itself in memory.

    The decoded configurations found so far contain (1) a list of C2
    URLS, (2*) *may* contain a list of C2 IPs, and lastly, (3)
    contains the command "regsvr32.exe -s". The command is used to
    launch its second-stage payload downloaded from its C2 addresses,
    as a ".txt" file that is in fact a disguised PE, to be loaded in
    memory.

    ### NOTE
    This content simply carves the configuration and does not unpack
    files on disk. That means pointing this artifact as a packed or
    obfuscated file will not obtain the expected results.

type: CLIENT

reference:
  - https://github.com/OALabs/Lab-Notes/blob/main/SquirrelWaffle/SquirrelWaffle.ipynb
  - https://www.zscaler.com/blogs/security-research/squirrelwaffle-new-loader-delivering-cobalt-strike

parameters:
  - name: TargetFileGlob
    default:
  - name: PidRegex
    default: .
  - name: ProcessRegex
    default: .
  - name: DetectionYara
    default: |
        rule SquirrelWaffle {
           meta:
              description = "Detects Unpacked SquirrelWaffle DLLs in Memory"
              author = "Eduardo Mattos - @eduardfir"
              reference = "https://www.malware-traffic-analysis.net/2021/09/17/index.html"
              date = "2021-09-29"
              hash = "ea4e9be41fa3f6895423e791596011f88ba45cde"
           strings:
              $s1 = { 20 48 54 54 50 2F 31 2E 31 0D 0A 48 6F 73 74 3A 20 } // HTTP/1.1 Host:
              $s2 = { 41 50 50 44 41 54 41 00 54 45 4D 50 } // APPDATA TEMP
              $s3 = { 34 30 34 00 32 30 30 00 2E 74 78 74 } // 404 200 .txt
              $s4 = { 20 03 2C 35 3E 18 58 59 48 0F 37 26 } // xored regsvr32.exe
              $s5 = "C:\\Users\\Administrator\\source\\repos\\Dll1\\Release\\Dll1.pdb"
           condition:
              4 of ($s*)
        }
sources:
  - query: |
        LET CountBlock <= starl(code='''
        def Main(arr):
            res=[]
            for i in range(0,len(arr),2):

                res.append({"Length":arr[i],"DataBlock":arr[i+1],"Count":i/2})

            return Pair(sorted(res, key=GetLength, reverse=True))

        def GetLength(dic):
           return dic["Length"]

        def Pair(arr):
            res=[]
            for dic in arr:
                found = False
                for tdic in arr:
                    if (tdic["Count"] == dic["Count"] + 1):
                        res.append({"DataBlock":dic,"Key":tdic})
                        found = True
                        break
                if (found == False):

                    res.append(({"DataBlock":dic,"Key":0}))
            return res
        ''')
        -- find target files
        LET TargetFiles = SELECT FullPath FROM glob(globs=TargetFileGlob)

        -- find velociraptor process
        LET me <= SELECT Pid
                  FROM pslist(pid=getpid())

        -- find all processes and add filters
        LET processes <= SELECT Name AS ProcessName, CommandLine, Exe, Pid
                        FROM pslist()
                        WHERE Name =~ ProcessRegex
                            AND format(format="%d", args=Pid) =~ PidRegex
                            AND NOT Pid in me.Pid

        -- scan processes in scope with our Detection
        LET processDetections <= SELECT * FROM foreach(row=processes,
                                query={
                                    SELECT * FROM if(condition=TargetFileGlob="",
                                        then={
                                            SELECT ProcessName, CommandLine, Exe, Pid, Rule AS YaraRule, Strings[0].Base AS BaseOffset
                                            FROM proc_yara(pid=Pid, rules=DetectionYara)
                                            GROUP BY Pid
                                        })
                                })

        -- return the VAD region size from yara detections for later use
        LET regionDetections = SELECT *
                                FROM foreach(row=processDetections,
                                    query={
                                        SELECT YaraRule, Pid, ProcessName, CommandLine, Exe, BaseOffset, Size AS VADSize
                                        FROM vad(pid=Pid)
                                        WHERE Address = BaseOffset
                                })

        -- scan files in scope with our rule
        LET fileDetections = SELECT * FROM foreach(row=TargetFiles,
                                query={
                                    SELECT * FROM if(condition=TargetFileGlob,
                                        then={
                                            SELECT * FROM switch(
                                                a={ -- yara detection
                                                    SELECT FullPath, Rule AS YaraRule, (String.Offset - 1000) AS IdealOffset
                                                    FROM yara(files=FullPath, rules=DetectionYara)
                                                },
                                                b={ -- yara miss
                                                    SELECT FullPath, Null AS YaraRule
                                                    FROM TargetFiles
                                                })
                                        },
                                        else={ -- no yara detection run
                                            SELECT FullPath, 'N/A' AS YaraRule
                                            FROM TargetFiles
                                        })
                             })

        -- scan files in scope with our rule
        LET fileConfiguration = SELECT * FROM foreach(row=fileDetections,
                                    query={
                                        SELECT FullPath, YaraRule,
                                            read_file(filename=FullPath, offset=IdealOffset, length=10000) AS PEData
                                        FROM scope()
                                    })

        -- get data from the rdata section, or whole PE
        LET processConfiguration <= SELECT YaraRule, Pid, ProcessName, CommandLine, Exe, BaseOffset,
                                        read_file(filename=str(str=Pid), accessor='process', offset=BaseOffset, length=VADSize) AS PEData
                                    FROM regionDetections

        -- store the SquirrelWaffle configuration in blocks split by null bytes.
        LET parsedRdata = SELECT *,
                            split(string=format(format="% X", args=parse_binary(filename=PEData, accessor="data", profile='''[
                                ["SquirrelRdata", 0, [
                                        ["__prefix", 0, "String", {"length": x=> 100000, "term_hex":"004142434445464748494A4B4C4D4E4F505152535455565758595A6162636465666768696A6B6C6D6E6F707172737475767778797A303132333435363738392B2F00", "max_length": x=> 100000}],
                                        ["ConfigSection", "x=>len(list=x.__prefix) + 66", "String", {"length": x=> 10000, "term_hex":"7374617274202F69"}]
                                    ]
                                ]
                            ]''', struct="SquirrelRdata").ConfigSection ), sep="00") AS SplitBlocks
                          FROM if(condition=TargetFileGlob,
                                then= fileConfiguration,
                                else= processConfiguration)

        -- generate a list of sorted blocks and then pair encoded blocks with their keys using Starlark
        LET blocks <= SELECT *, CountBlock.Main(arr=array(a=enumerate(items=NewDict))) AS EnumDict
                      FROM foreach(row=parsedRdata,
                        query= {
                            SELECT *, FullPath, YaraRule, Pid, ProcessName, CommandLine, Exe
                            FROM foreach(row=SplitBlocks,
                                query= {
                                        SELECT dict(Length=len(list=_value), DataBlock=_value) AS NewDict
                                        FROM scope()
                                        WHERE NewDict.Length > 45
                                })
                        })
                      GROUP BY if(condition=TargetFileGlob,
                                then= FullPath,
                                else= CommandLine)

        -- store encoded blocks and their keys in separate columns, filtering out FPs based on key size
        LET finalPairs <= SELECT *, unhex(string=regex_replace(source=DataBlock.DataBlock, re=" ", replace="")) AS DataBlock,
                            unhex(string=regex_replace(source=Key.DataBlock, re=" ", replace="")) AS Key
                          FROM foreach(row=blocks,
                                query= {
                                    SELECT *, FullPath, YaraRule, Pid, ProcessName, CommandLine, Exe FROM foreach(row=EnumDict,
                                        query={
                                            SELECT DataBlock, Key FROM scope()
                                    })
                            })
                          WHERE len(list=Key) > 32 AND len(list=Key) < 256

        -- return our results
        SELECT * FROM if(condition=TargetFileGlob,
            then= {
                SELECT YaraRule, FullPath, regex_replace(source=xor(key=Key,string=DataBlock), re="(\r)|(\\|)", replace=",\n") AS DecodedConfigs
                FROM finalPairs
            },
            else= {
                SELECT YaraRule, Pid, ProcessName, CommandLine, Exe, regex_replace(source=xor(key=Key,string=DataBlock), re="(\r)|(\\|)", replace=",\n") AS DecodedConfigs
                FROM finalPairs
        })