Nostalgiskt, sprudlande sensommarmys med Alternate Data Streams
Strax innan sommaren stod i full blom och vi alla såg fram emot en hemester med vacker stockholmsskärgård och tankeväckande regnig västkust råkade jag på en skadlig kod som placerat en del av sig själv i en ADS (Alternate Data Stream). ADS har funnits med i decennier men ifall man behöver mer info om fenomenet så finns det på en länk här: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/b134f29a-6278-4f3f-904f-5e58a713d2c5 . ADS:er för att sidsteppa web-filter och liknande kan förstås också vara värt att ägna några tankar åt vid eventuella penetrationstestningsövningar (se https://cwe.mitre.org/data/definitions/69.html).
Jag funderade ett tag över syftet med att som skadlig-kod-författare låta delar av payload placeras i en ADS. Fördelen med ADS är ju att de inte normalt visas i Microsofts gränssnitt (i dos-prompten behöver man exempelvis använda /R-flaggan för att det ska visas i dir-kommandot) men nackdelen är att det torde vara lätt att detektera ett misstänkt beteende för ett antivirusprogram och taktiken känns på det sättet möjligen lite over-engineered.
Nåväl, när jag så började analysera den skadliga koden saknade jag ett verktyg som listar alla ADS i en fil och presenterar de första bytes av innehållet i ADS:en för att enkelt få en uppfattning om vad varje ström är utan att behöva kopiera ut innehållet till disk. Om man exempelvis ser MZ i början av dataströmmen är det möjligen en exekverbar kod osv.. Primärt letade jag efter delar av exekverbar kod som placerats i ADS:erna. Som, mycket annat i Windows, så går det åstadkomma via powershell. Exempel visas nedan:
get-item -Stream * -Path .\jamestest1.txt |
select-object -Property Stream,Length,FileName |
foreach {
if($_.Stream.IndexOf(":") -ne 0) {
write-host $_.Stream ;
$f=$_.FileName + ":" + $_.Stream; $content=gc $f ;
write-host $content.Substring(0,
[System.Math]::Min(10,$content.Length))
}
}
Men om man använder powershell för att lista ADS:er så kommer det kunna bli en rejäl väntetid ifall vi vill undersöka många filer (exempelvis i en hel temp-mapp med tustentals filer). Detta eftersom man ofta tvingas hemfalla till .NET-kod i Powershell-skript för att inte arbetsdagen ska bestå av väntetid. Dessutom kan det vara intressant att veta vilka Windows-API:er som kan nyttjas för att lista och hämta ut ADS:erna ur en fil (eller mapp). Framförallt så känns det alltid bättre att ha egna verktyg som man kan få att fungera precis som man vill. Därför tycktes det, som i så många andra sammanhang, lämpligt vid tillfället att skapa ett eget verktyg. Utöver det kunde jag inte dölja min glädje över att få dyka ned i Windows API:er upprymd av nostalgiska känslor från min tonårsperiod, inte olikt doften från Madeleinekakor doppade i lindblomste från tiden som hade flytt (eller dylikt).
Information om ADS:er ligger förstås i NTFS-strukturen vilket vi behöver en funktion för att komma åt. Efter en snabb googling visar det sig att det finns ett antal metoder som verkar dominerande för ADS-syften; Windows-API:erna BackupRead/BackupSearch, NtQueryInformationFile eller, vad jag förstått, genom FindFirstStream/FindNextStream (vilket endast fungerar i nya Windows-versioner så det tycktes alldeles för modernt vid tillfället).
Hur gör Sysinternals?
Få mjukvaruföretag har skänkt så mycket lycka till världen som Sysinternals. Sysinternals har förstås en streams-applikation (https://docs.microsoft.com/en-us/sysinternals/downloads/streams) och Sysinternals gör antagligen på smidigaste sättet … varvid första tanken var att titta på källkoden till den applikationen. Jag hade bestämt för mig att Sysinternals någon gång har lagt ut källkoder till sina små verktyg på nätet. Men det verkar som att de av någon anledning slutat göra det (eller aldrig gjort det om jag minns fel). Därmed blir man tvungen att använda en disassembler ifall man vill veta hur Sysinternals gör i sitt lilla streams-verktyg. Genom att ladda in streams.exe i Ghidra (https://ghidra-sre.org/) går det snabbt att se att Sysinternals inte importerar BackupRead-funktionen genom standard-biblioteken i Windows.
Det finns inte heller med någon sträng-referens till BackupRead. Därför gissar vi att de använder någon av de andra metoderna.
Vi startar igång streams.exe med x64dbg (https://x64dbg.com/) och söker efter strängreferens till ”NtQueryInformationFile”. Visst finns det en sådan i streams.exe och efter att vi satt en brytpunkt på referensen hamnar vi vid ett anrop till GetProcAddress.
För att använda NtQueryInformationFile behöver man ladda in funktionen med GetProcAddress eftersom det inte finns i länkningsinformationen i Microsofts C++-kompilator (vilket gäller ett antal Windows API calls sedan urminnes tider). Om vi tittar på den funktionen i Ghidras fina decompiler ser den ut enligt nedan.
Detta verkar stämma med vad vi vill åstadkomma. Nu vet vi således till stora delar hur Sysinternals gör för att lista ADS:er. Om vi översätter enumererings-delen till en förenklad C-kod blir det ungefär nedanstående kod (förkortad för att bli enkel att följa).
HMODULE hNtdll = LoadLibrary(_T("ntdll.dll"));
NTQUERYINFORMATIONFILE NtQueryInformationFile = (NTQUERYINFORMATIONFILE)GetProcAddress(hNtdll, "NtQueryInformationFile");
PFILE_STREAM_INFORMATION pStreamInfo = (PFILE_STREAM_INFORMATION)btsInfoBuffer;
IO_STATUS_BLOCK ioStatus;
HANDLE hFile = CreateFileA(pFilename, 0, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
NtQueryInformationFile(hFile, &ioStatus, btsInfoBuffer, sizeof(btsInfoBuffer), 22);
if (pStreamInfo->StreamName != NULL) printf("%ls\r\n", pStreamInfo->StreamName);
ULONG currentPos = 0;
while (pStreamInfo->NextEntryOffset > 0)
{
currentPos += pStreamInfo->NextEntryOffset;
… Buffer-check här kanske kan vara något…
pStreamInfo = (PFILE_STREAM_INFORMATION)(btsInfoBuffer +
currentPos);
if (pStreamInfo->StreamName != NULL) printf("%ls\r\n",
pStreamInfo->StreamName);
}
Ifall vi vill använda BackupRead istället (se exempelvis https://docs.microsoft.com/en-us/archive/msdn-magazine/2006/january/net-matters-iterating-ntfs-streams) kan man tänka sig följande förenklade körschema:
- Hitta första ström för filen i fråga med BackupRead
- Läs ut Stream-info-headern genom BackupRead-funktionen
- Ifall ett stream-namn finns så hämta in även namnet.
- Leta upp nästa Ström BackupSeek/BackupRead.
Därefter, när vi vet hur ADS:erna ser ut så kan vi enkelt dumpa informationen till konsolen vilket var det ville göra från början. Det finns ett gammalt CodeProject-projekt som ser ut att fungera på det sättet på följande länk: https://www.codeproject.com/Articles/13667/Enumerating-Alternate-Data-Streams.
Nåväl, eftersom Sysinternals gör det mesta rätt så antar vi att ”NtQueryInformationFile-metoden” är den metod som är mest lämplig även för mitt egna lilla verktyg och min exempelkod som blir en prototyp på implementationen hittas på mitt git-repository här: https://github.com/jamesrep/altgalt/.