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.
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
})