This artifact compares executables in memory (RAM) with those on hard disk. This way, RAM injections are detected. This rarely happens legitimately and is mostly used by malware. This check is executed without dumping the memory and works live on the target system(s). See also https://github.com/lautarolecumberry/DetectingFilelessMalware
(Ignored) False Positives
ASLR: If jumps or comparisons are not relative and Address Space Layout Randomization
(ASLR) is enabled, then addresses within these jumps are adjusted in RAM
with a constant offset. This offset can be computed and ignored.BaseOfData: Relative Virtual Adresses (RVA) cause an offset in the code in memory of a
32-bit process. This is the case when the field BaseOfData is set.
Like ASLR it is a constant offset that is added to addresses.Test this artifact using AMSIBypassPatch.ps1
name: Windows.Memory.Mem2Disk
author: Lautaro Lecumberry, Dr. Michael Denzel
description: |
This artifact compares executables in memory (RAM) with those
on hard disk. This way, RAM injections are detected. This rarely
happens legitimately and is mostly used by malware.
This check is executed without dumping the memory and works live
on the target system(s).
See also https://github.com/lautarolecumberry/DetectingFilelessMalware
(Ignored) False Positives
- `ASLR`: If jumps or comparisons are not relative and Address Space Layout Randomization
(ASLR) is enabled, then addresses within these jumps are adjusted in RAM
with a constant offset. This offset can be computed and ignored.
- `BaseOfData`: Relative Virtual Adresses (RVA) cause an offset in the code in memory of a
32-bit process. This is the case when the field BaseOfData is set.
Like ASLR it is a constant offset that is added to addresses.
Test this artifact using `AMSIBypassPatch.ps1`
references:
- https://github.com/okankurtuluss/AMSIBypassPatch/blob/090b54a518fecf1ccf8f54f8691805ef0f9a30f1/AMSIBypassPatch.ps1
parameters:
- name: UploadFindings
description: Upload all executables where code in memory does not match code on disk. This
can potentially generate a lot of traffic. Dry-run before enabling this option.
default: False
type: bool
- name: ProcessNameFilter
type: regex
default: .
- name: PidFilter
default: .
type: regex
- name: ModuleRegEx
type: regex
description: Filter for modules to check. If you want to scan all modules (i.e. libraries) within
a binary, replace with `.*`. The default parameter checks the original binary itself (.exe)
as well as kernelbase.dll, ntdll.dll, user32.dll, kernel32.dll, shell32.dll, msvcrt.dll,
advapi32.dll, and comdlg32.dll (i.e. commonly injected libraries).
default: "\\.exe$|(KERNELBASE|ntdll|amsi|user32|kenrel32|shell32|msvcrt|advapi32|comdlg32)\\.dll$"
- name: Workers
type: int
default: "5"
description: "Number of parallel workers to use"
precondition: SELECT OS From info() where OS = 'windows'
export: |
-- These functions help to resolve the Kernel Device Filenames
-- into a regular filename with drive letter.
LET DriveReplaceLookup <= SELECT
split(sep_string="\\", string=Name)[-1] AS Drive,
upcase(string=SymlinkTarget) AS Target,
len(list=SymlinkTarget) AS Len
FROM winobj()
WHERE Name =~ "^\\\\GLOBAL\\?\\?\\\\.:"
LET _DriveReplace(Path) = SELECT Drive + Path[Len:] AS ResolvedPath
FROM DriveReplaceLookup
WHERE upcase(string=Path[:Len]) = Target
LET DriveReplace(Path) = _DriveReplace(Path=Path)[0].ResolvedPath ||
Path
sources:
- query: |
-- get all processes
LET GetPids = SELECT Pid,
Name,
Username,
if(condition=IsWow64, then=4, else=8) AS IntSize
FROM pslist()
WHERE Name =~ ProcessNameFilter
AND format(format="%d", args=Pid) =~ PidFilter
-- get all memory pages for a certain pid
LET InfoFromVad(Pid) = SELECT Address,
Size,
DriveReplace(Path=MappingName) AS Path
FROM vad(pid=Pid)
WHERE MappingName
AND Protection =~ "xr-"
AND MappingName =~ ModuleRegEx
LET GetTextSegment(Path) = filter(condition="x=>x.Name = '.text'",
list=parse_pe(file=Path).Sections)[0]
-- parse the executable (PE) from memory (specifically, the text segment)
LET GetMetadata(Pid) = SELECT
Path,
str(str=Pid) AS PidFilename,
Address,
GetTextSegment(Path=Path) AS TextSegmentData
FROM InfoFromVad(Pid=Pid)
WHERE Address != 0
AND TextSegmentData.FileOffset
-- helper function for formating
LET Hex(X) = format(format="%#x", args=X)
LET Int64(X) = parse_binary(profile="",
struct="int64",
accessor="data",
filename=X)
-- ASLR may be shifted due to alignment.
-- This is OK so the following values are allowed.
LET CalculateAllowedASLR(ASLR) = SELECT *
FROM foreach(row={
SELECT format(format="00" * 7 + "%08x" + "00" * 8, args=ASLR) AS Data
FROM scope()
}, query={
SELECT int(int="0x" + Data[_value:(_value + 16)]) AS Allowed
FROM range(start=0, end=22, step=2)
})
-- read the executable from memory and hard disk
LET GetContent(Pid, Name) = SELECT
*,
Name,
Path,
Address AS MemAddress,
TextSegmentData.RVA AS BaseRVA,
--calculate ASLR offset to later filter it out
Address - TextSegmentData.VMA AS ASLR,
CalculateAllowedASLR(ASLR=Address - TextSegmentData.VMA).Allowed AS AllowedASLR,
read_file(accessor="process",
offset=Address,
filename=PidFilename,
length=TextSegmentData.Size) AS MemoryData,
TextSegmentData.FileOffset AS DiskAddress,
TextSegmentData.Size AS SegmentSize,
read_file(accessor="file",
offset=TextSegmentData.FileOffset,
filename=Path,
length=TextSegmentData.Size) AS DiskData
FROM GetMetadata(Pid=Pid)
WHERE MemoryData
AND log(dedup=-1,
message="Inspecting Pid %v (%v): %#x-%#x vs %#x-%#x in %v",
args=[Pid, Name, Address, Address + SegmentSize,
DiskAddress, DiskAddress + SegmentSize, Path])
LET FilterContent(Pid, Name) = SELECT *
MemoryData = DiskData AS Comparison
FROM GetContent(Pid=Pid, Name=Name)
-- Filter out not needed comparisons early
WHERE NOT Comparison
-- parameter for start PAGESIZE; compare 1 MB pages first
LET PAGESIZE <= 1024 * 1024
-- helper function for comparisons of PAGESIZE
LET _CompareRegions(Base, X, Y, PAGESIZE) = SELECT
_value + Base AS Offset,
X[_value:(_value + PAGESIZE)] AS XInt,
Y[_value:(_value + PAGESIZE)] AS YInt
FROM range(end=len(list=X), step=PAGESIZE)
WHERE XInt != YInt
-- compare full pages (to speed up comparison)
-- for each 1 MB page which does not match
-- compare 4096 B page
-- for each 4096 B page which does not match
-- compare single integers
LET CompareRegions(X, Y, IntSize) = SELECT
Offset,
Hex(X=XInt) AS X,
Hex(X=YInt) AS Y,
Int64(X=XInt) AS ValueX,
Int64(X=YInt) AS ValueY
FROM foreach(row={
-- 1 MB pages
SELECT *
FROM _CompareRegions(Base=0, X=X, Y=Y, PAGESIZE=PAGESIZE)
},
query={
SELECT *
FROM foreach(row={
-- 4096 B pages
SELECT *
FROM _CompareRegions(Base=Offset, X=XInt, Y=YInt, PAGESIZE=4096)
},
query={
-- single integers
SELECT *
FROM _CompareRegions(Base=Offset, X=XInt, Y=YInt, PAGESIZE=IntSize)
})
})
LIMIT 500
-- check if offsets between X and Y (i.e. RAM and disk) are
-- always the same offset. Then it is ASLR or BaseOfData.
LET CompareUniqueRegions(X, Y, IntSize, ASLR) = SELECT *,
ValueX - ValueY AS Difference
FROM CompareRegions(X=X, Y=Y, IntSize=IntSize)
WHERE ValueX AND NOT Difference IN AllowedASLR
GROUP BY Difference
LET DescribeAddress(rva, module) = version(function="describe_address") != NULL &&
describe_address(rva=rva, module=module).func
-- compare the executable from memory and hard disk
-- only print the ones where they do not match
LET Compare(Pid, Name, IntSize) =
SELECT Pid,
Name,
Path,
ASLR,
{
SELECT Offset + BaseRVA AS RVA,
Offset AS TextOffset,
DescribeAddress(rva= Offset + BaseRVA, module=Path) AS Func,
X AS MemoryValue,
Y AS DiskValue,
Hex(X=Difference) AS Difference,
Hex(X=ASLR) AS ASLR
FROM CompareUniqueRegions(
X=MemoryData,
Y=DiskData,
IntSize=IntSize,
ASLR=ASLR)
} AS Differences,
MemAddress,
DiskAddress,
SegmentSize
FROM FilterContent(Pid=Pid, Name=Name)
WHERE Differences AND log(dedup=-1,
message="Comparing process %v - %v", args=[Pid, Name])
-- compare with uploading the suspicious executables
LET CompareAndUpload(Pid, Name, IntSize) = SELECT
Pid,
Name,
Path,
ASLR,
MemAddress,
DiskAddress,
SegmentSize,
upload(
file=pathspec(DelegateAccessor="process",
DelegatePath=PidFilename,
Path=[dict(Offset=MemAddress, Length=SegmentSize), ]),
name=pathspec(parse=format(format="%s.%d.mem", args=[Path, Pid]),
path_type="windows"),
accessor="sparse") AS UploadMem,
upload(
file=pathspec(DelegateAccessor="file",
DelegatePath=Path,
Path=[dict(Offset=DiskAddress, Length=SegmentSize), ]),
name=pathspec(parse=format(format="%s.%d.disk", args=[Path, Pid]),
path_type="windows"),
accessor="sparse") AS UploadDisk,
Differences
FROM Compare(Pid=Pid, Name=Name, IntSize=IntSize)
-- for every process, evaluate the memory-harddisk-comparison
SELECT *,
Hex(X=MemAddress) AS MemAddress,
Hex(X=DiskAddress) AS DiskAddress,
Hex(X=SegmentSize) AS SegmentSize
FROM foreach(row=GetPids,
workers=Workers,
query={
SELECT *
FROM if(condition=UploadFindings,
then={
SELECT *
FROM CompareAndUpload(Pid=Pid, Name=Name, IntSize=IntSize)
},
else={
SELECT *
FROM Compare(Pid=Pid, Name=Name, IntSize=IntSize)
})
})