NTFS Files Attributes

Every New Technology File System (NTFS) formatted partition contains a Master File Table (MFT) that maintains a record for every file/directory on the partition. Within MFT entries are file attributes, such as Extended Attributes and Data known as Alternate Data Streams (ADSs) when more than one Data attribute is present], that can be used to store arbitrary data (and even complete files.

Adversaries may store malicious data or binaries in file attribute metadata instead of directly in files. This may be done to evade some defenses, such as static indicator scanning tools and anti-virus.

U0501

Code Snippets

Jean-Pierre LESUEUR

Description

This code let you handle Alternate Data Streams using two different techniques.

  • FindFirstStreamW / FindNextStreamW : Available since Windows Vista and easier to use.
  • BackupRead : Available since Windows XP and more tricky to use.

You can:

  • Enumerate ADS Files attached to a target file.
  • Backup ADS File(s) attached to a target file.
  • Copy any file to target file ADS.
  • Delete ADS File(s) attached to a target file.

If you want to learn more about how to use this tiny library you can check this example project on Github.

unit UntDataStreamObject;

interface

uses WinAPI.Windows, System.Classes, System.SysUtils, Generics.Collections,
      RegularExpressions;

type
  TEnumDataStream = class;
  TADSBackupStatus = (absTotal, absPartial, absError);

  TDataStream = class
  private
    FOwner      : TEnumDataStream;
    FStreamName : String;
    FStreamSize : Int64;

    {@M}
    function GetStreamPath() : String;
  public
    {@C}
    constructor Create(AOwner : TEnumDataStream; AStreamName : String; AStreamSize : Int64);

    {@M}
    function CopyFileToADS(AFileName : String) : Boolean;
    function BackupFromADS(ADestPath : String) : Boolean;
    function DeleteFromADS() : Boolean;

    {@G/S}
    property StreamName : String read FStreamName;
    property StreamSize : Int64  read FStreamSize;
    property StreamPath : String read GetStreamPath;
  end;

  TEnumDataStream = class
  private
    FTargetFile            : String;
    FItems                 : TObjectList<TDataStream>;
    FForceBackUpReadMethod : Boolean;

    {@M}
    function Enumerate_FindFirstStream() : Int64;
    function Enumerate_BackupRead() : Int64;
    function ExtractADSName(ARawName : String) : String;
    function CopyFromTo(AFrom, ATo : String) : Boolean;
    function GetDataStreamFromName(AStreamName : String) : TDataStream;
  public
    {@C}
    constructor Create(ATargetFile : String; AEnumerateNow : Boolean = True; AForceBackUpReadMethod : Boolean = False);
    destructor Destroy(); override;

    {@M}
    function Refresh() : Int64;

    function CopyFileToADS(AFilePath : String) : Boolean;
    function BackupFromADS(ADataStream : TDataStream; ADestPath : String) : Boolean; overload;
    function DeleteFromADS(ADataStream : TDataStream) : Boolean; overload;
    function BackupAllFromADS(ADestPath : String) : TADSBackupStatus;
    function BackupFromADS(AStreamName, ADestPath : String) : Boolean; overload;
    function DeleteFromADS(AStreamName : String) : Boolean; overload;

    {@G}
    property TargetFile : String                   read FTargetFile;
    property Items      : TObjectList<TDataStream> read FItems;
  end;

implementation

{+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


   TEnumDataStream


+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++}

{
  FindFirstStream / FindNextStream API Definition
}
type
  _STREAM_INFO_LEVELS = (FindStreamInfoStandard, FindStreamInfoMaxInfoLevel);
  TStreamInfoLevels = _STREAM_INFO_LEVELS;

  _WIN32_FIND_STREAM_DATA = record
    StreamSize : LARGE_INTEGER;
    cStreamName : array[0..(MAX_PATH + 36)] of WideChar;
  end;
  TWin32FindStreamData = _WIN32_FIND_STREAM_DATA;

var hKernel32         : THandle;
    _FindFirstStreamW : function(lpFileName : LPCWSTR; InfoLevel : TStreamInfoLevels; lpFindStreamData : LPVOID; dwFlags : DWORD) : THandle; stdcall;
    _FindNextStreamW  : function(hFindStream : THandle; lpFindStreamData : LPVOID) : BOOL; stdcall;


{-------------------------------------------------------------------------------
  Return the ADS name from it raw name (:<name>:$DATA)
-------------------------------------------------------------------------------}
function TEnumDataStream.ExtractADSName(ARawName : String) : String;
var AMatch : TMatch;
    AName  : String;
begin
  result := ARawName;
  ///

  AName := '';
  AMatch := TRegEx.Match(ARawName, ':(.*):');
  if (AMatch.Groups.Count < 2) then
    Exit();

  result := AMatch.Groups.Item[1].Value;
end;

{-------------------------------------------------------------------------------
  Scan for ADS using method N�1 (FindFirstStream / FindNextStream). Work since
  Microsoft Windows Vista.
-------------------------------------------------------------------------------}
function TEnumDataStream.Enumerate_FindFirstStream() : Int64;
var hStream     : THandle;
    AData       : TWin32FindStreamData;

    procedure ProcessDataStream();
    var ADataStream : TDataStream;
    begin
      if (String(AData.cStreamName).CompareTo('::$DATA') = 0) then
        Exit();
      ///

      ADataStream := TDataStream.Create(self, ExtractADSName(String(AData.cStreamName)), Int64(AData.StreamSize));

      FItems.Add(ADataStream);
    end;

begin
  result := 0;
  ///

  self.FItems.Clear();

  if NOT FileExists(FTargetFile) then
    Exit(-1);

  if (NOT Assigned(@_FindFirstStreamW)) or (NOT Assigned(@_FindNextStreamW)) then
    Exit(-2);

  FillChar(AData, SizeOf(TWin32FindStreamData), #0);

  // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirststreamw
  hStream := _FindFirstStreamW(PWideChar(FTargetFile), FindStreamInfoStandard, @AData, 0);
  if (hStream = INVALID_HANDLE_VALUE) then begin
    case GetLastError() of
      ERROR_HANDLE_EOF : begin
        Exit(-3); // No ADS Found
      end;

      ERROR_INVALID_PARAMETER : begin
        Exit(-4); // Not compatible
      end;

      else begin
        Exit(-5);
      end;
    end;
  end;

  ProcessDataStream();

  // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextstreamw
  while True do begin
    FillChar(AData, SizeOf(TWin32FindStreamData), #0);

    if NOT _FindNextStreamW(hStream, @AData) then
      break;

    ProcessDataStream();
  end;

  ///
  result := self.FItems.Count;
end;

{-------------------------------------------------------------------------------
  Scan for ADS using method N�2 (BackupRead()). Works since
  Microsoft Windows XP.
-------------------------------------------------------------------------------}
function TEnumDataStream.Enumerate_BackupRead() : Int64;
var hFile           : THandle;
    AStreamId       : TWIN32StreamID;
    ABytesRead      : Cardinal;
    pContext        : Pointer;
    ALowByteSeeked  : Cardinal;
    AHighByteSeeked : Cardinal;
    AName           : String;
    ABytesToRead    : Cardinal;
    ASeekTo         : LARGE_INTEGER;
    AClose          : Boolean;
begin
  result := 0;
  AClose := False;
  ///
  hFile := CreateFile(
                        PWideChar(self.TargetFile),
                        GENERIC_READ,
                        FILE_SHARE_READ,
                        nil,
                        OPEN_EXISTING,
                        FILE_FLAG_BACKUP_SEMANTICS,
                        0
  );
  if (hFile = INVALID_HANDLE_VALUE) then
    Exit(-1);
  try
    pContext := nil;
    try
      while True do begin
        FillChar(AStreamId, SizeOf(TWIN32StreamID), #0);
        ///

        {
          Read Stream
        }
        ABytesToRead := SizeOf(TWIN32StreamID) - 4; // We don't count "cStreamName"

        if NOT BackupRead(hFile, @AStreamId, ABytesToRead, ABytesRead, False, False, pContext) then
          break;

        AClose := True;

        if (ABytesRead = 0) then
          break;

        ASeekTo.QuadPart := (AStreamId.Size + AStreamId.dwStreamNameSize);

        case AStreamId.dwStreamId of
          {
            Deadling with ADS Only
          }
          BACKUP_ALTERNATE_DATA : begin
            if (AStreamId.dwStreamNameSize > 0) then begin
              {
                Read ADS Name
              }
              ABytesToRead := AStreamId.dwStreamNameSize;
              SetLength(AName, (ABytesToRead div SizeOf(WideChar)));
              if BackupRead(hFile, PByte(AName), ABytesToRead, ABytesRead, False, False, pContext) then begin
                Dec(ASeekTo.QuadPart, ABytesRead); // Already done

                FItems.Add(TDataStream.Create(self, ExtractADSName(AName), AStreamId.Size));
              end;
            end;
          end;
        end;

        {
          Goto Next Stream.
        }
        if NOT BackupSeek(hFile, ASeekTo.LowPart, ASeekTo.HighPart, ALowByteSeeked, AHighByteSeeked, pContext) then
          break;

        (*
          //////////////////////////////////////////////////////////////////////
          // BackupSeek() Alternative (Manual method)
          //////////////////////////////////////////////////////////////////////

          var ABuffer : array[0..2096-1] of byte;
          // ...
          while True do begin
            if (ASeekTo.QuadPart < SizeOf(ABuffer)) then
              ABytesToRead := ASeekTo.QuadPart
            else
              ABytesToRead := SizeOf(ABuffer);

            if ABytesToRead = 0 then
              break;

            if NOT BackupRead(hFile, PByte(@ABuffer), ABytesToRead, ABytesRead, False, False, pContext) then
              break;
            ///

            Dec(ASeekTo.QuadPart, ABytesRead);

            if (ASeekTo.QuadPart <= 0) then
              break;
          end;
          // ...

          //////////////////////////////////////////////////////////////////////
        *)
      end;
    finally
      if AClose then
        BackupRead(hFile, nil, 0, ABytesRead, True, False, pContext);
    end;
  finally
    CloseHandle(hFile);
  end;
end;

{-------------------------------------------------------------------------------
  Refresh embedded data stream objects using Windows API. Returns number of
  data stream objects or an error identifier.
-------------------------------------------------------------------------------}
function TEnumDataStream.Refresh() : Int64;
var AVersion : TOSVersion;
begin
  result := 0;
  ///

  if (AVersion.Major >= 6) then begin
    {
      Vista and above
    }
    if self.FForceBackUpReadMethod then
      result := self.Enumerate_BackupRead()
    else
      result := self.Enumerate_FindFirstStream();
  end else if (AVersion.Major = 5) and (AVersion.Minor >= 1) then begin
    {
      Windows XP / Server 2003 & R2
    }
    result := self.Enumerate_BackupRead();
  end else begin
    // Unsupported (???)
  end;
end;

{-------------------------------------------------------------------------------
  Refresh ADS Files and retrieve one ADS file by it name.
-------------------------------------------------------------------------------}
function TEnumDataStream.GetDataStreamFromName(AStreamName : String) : TDataStream;
var I       : Integer;
    AStream : TDataStream;
begin
  result := nil;
  ///

  if (self.Refresh() > 0) then begin
    for I := 0 to self.Items.count -1 do begin
      AStream := self.Items.Items[i];
      if NOT Assigned(AStream) then
        continue;
      ///

      if (String.Compare(AStream.StreamName, AStreamName, True) = 0) then
        result := AStream;
    end;
  end;
end;

{-------------------------------------------------------------------------------
  ADS Classic Actions
    - Copy file to current ADS Location.
    - Copy ADS item to destination path.
    - Delete ADS Item.
-------------------------------------------------------------------------------}

function TEnumDataStream.CopyFromTo(AFrom, ATo : String) : Boolean;
var hFromFile     : THandle;
    hToFile       : THandle;

    ABuffer       : array[0..4096-1] of byte;
    ABytesRead    : Cardinal;
    ABytesWritten : Cardinal;
begin
  result := False;
  ///

  hFromFile := INVALID_HANDLE_VALUE;
  hToFile   := INVALID_HANDLE_VALUE;

  try
    hFromFile := CreateFile(PWideChar(AFrom), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0);
    if (hFromFile = INVALID_HANDLE_VALUE) then
      Exit();

    hToFile := CreateFile(
                            PWideChar(ATo),
                            GENERIC_WRITE,
                            FILE_SHARE_WRITE,
                            nil,
                            CREATE_ALWAYS,
                            FILE_ATTRIBUTE_NORMAL,
                            0
    );

    if (hToFile = INVALID_HANDLE_VALUE) then
      Exit();
    ///

    while True do begin
      {
        Read
      }
      if NOT ReadFile(hFromFile, ABuffer, SizeOf(ABuffer), ABytesRead, nil) then
        Exit();

      if ABytesRead = 0 then
        break; // Success

      {
        Write
      }
      if NOT WriteFile(hToFile, ABuffer, ABytesRead, ABytesWritten, nil) then
        Exit();

      if (ABytesWritten <> ABytesRead) then
        Exit();
    end;

    ///
    result := True;
  finally
    if hFromFile <> INVALID_HANDLE_VALUE then
      CloseHandle(hFromFile);

    if hToFile <> INVALID_HANDLE_VALUE then
      CloseHandle(hToFile);

    ///
    self.Refresh();
  end;
end;

function TEnumDataStream.CopyFileToADS(AFilePath : String) : Boolean;
begin
  result := CopyFromTo(AFilePath, Format('%s:%s', [self.FTargetFile, ExtractFileName(AFilePath)]));
end;

function TEnumDataStream.BackupFromADS(ADataStream : TDataStream; ADestPath : String) : Boolean;
begin
  result := False;

  if NOT Assigned(ADataStream) then
    Exit();

  result := CopyFromTo(ADataStream.StreamPath, Format('%s%s', [IncludeTrailingPathDelimiter(ADestPath), ADataStream.StreamName]));
end;

function TEnumDataStream.DeleteFromADS(ADataStream : TDataStream) : Boolean;
begin
  result := DeleteFile(ADataStream.StreamPath);
end;

function TEnumDataStream.BackupAllFromADS(ADestPath : String) : TADSBackupStatus;
var I       : integer;
    AStream : TDataStream;
begin
  result := absError;
  ///

  if (self.Refresh() > 0) then begin
    for I := 0 to self.Items.count -1 do begin
      AStream := self.Items.Items[i];
      if NOT Assigned(AStream) then
        continue;
      ///

      if AStream.BackupFromADS(ADestPath) and (result <> absPartial) then
        result := absTotal
      else
        result := absPartial;
    end;
  end;
end;

function TEnumDataStream.BackupFromADS(AStreamName, ADestPath : String) : Boolean;
var AStream : TDataStream;
begin
  result := False;
  ///

  AStream := self.GetDataStreamFromName(AStreamName);
  if Assigned(AStream) then
    result := self.BackupFromADS(AStream, ADestPath);
end;

function TEnumDataStream.DeleteFromADS(AStreamName : String) : Boolean;
var AStream : TDataStream;
begin
  result := False;
  ///

  AStream := self.GetDataStreamFromName(AStreamName);
  if Assigned(AStream) then
    result := self.DeleteFromADS(AStream);
end;

{-------------------------------------------------------------------------------
  ___constructor
-------------------------------------------------------------------------------}
constructor TEnumDataStream.Create(ATargetFile : String; AEnumerateNow : Boolean = True; AForceBackUpReadMethod : Boolean = False);
begin
  self.FTargetFile := ATargetFile;
  self.FForceBackUpReadMethod := AForceBackupReadMethod;

  FItems := TObjectList<TDataStream>.Create();
  FItems.OwnsObjects := True;

  if AEnumerateNow then
    self.Refresh();
end;

{-------------------------------------------------------------------------------
  ___destructor
-------------------------------------------------------------------------------}
destructor TEnumDataStream.Destroy();
begin
  if Assigned(FItems) then
    FreeAndNil(FItems);

  ///
  inherited Destroy();
end;

{+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


   TDataStream


+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++}

constructor TDataStream.Create(AOwner : TEnumDataStream; AStreamName : String; AStreamSize : Int64);
begin
  self.FOwner      := AOwner;
  self.FStreamName := AStreamName;
  self.FStreamSize := AStreamSize;
end;

{-------------------------------------------------------------------------------
  Generate Stream Path Accordingly
-------------------------------------------------------------------------------}
function TDataStream.GetStreamPath() : String;
begin
  result := '';

  if NOT Assigned(FOwner) then
    Exit();

  result := Format('%s:%s', [FOwner.TargetFile, self.FStreamName]);
end;

{-------------------------------------------------------------------------------
  ADS Classic Actions (Redirected to Owner Object)
-------------------------------------------------------------------------------}

function TDataStream.CopyFileToADS(AFileName : String) : Boolean;
begin
  if Assigned(FOwner) then
    result := FOwner.CopyFileToADS(AFileName);
end;

function TDataStream.BackupFromADS(ADestPath : String) : Boolean;
begin
  if Assigned(FOwner) then
    result := FOwner.BackupFromADS(self, ADestPath);
end;

function TDataStream.DeleteFromADS() : Boolean;
begin
  if Assigned(FOwner) then
    result := FOwner.DeleteFromADS(self);
end;

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

initialization
  _FindFirstStreamW := nil;
  _FindNextStreamW  := nil;

  hKernel32 := LoadLibrary('KERNEL32.DLL');
  if (hKernel32 > 0) then begin
    @_FindFirstStreamW := GetProcAddress(hKernel32, 'FindFirstStreamW');
    @_FindNextStreamW := GetProcAddress(hKernel32, 'FindNextStreamW');
  end;

finalization
  _FindFirstStreamW := nil;
  _FindNextStreamW  := nil;

end.

Additional Resources

Subscribe to our Newsletter and don't miss important updates