Search code examples
delphicomputer-forensicsusn

USN_RECORD all values ok, except TimeStamp = 0?


Using below record structure and function I get the FileName,Reference Numbers,Versions etc. at high speed. (trying with Delphi, Win 7)

Problem is that I don't get the file DateTime (TimeStamp.QuadPart = 0)

I have added the working code. For testing I am adding the filename (shows correct) and timestamp (shows empty) to memo1

procedure Tform_main.Button1Click(Sender: TObject);
begin
   FillFileListFromUSNJournal('C') ;
end;


procedure Tform_main.FillFileListFromUSNJournal(pDrive:Char);
var
  ARootHandle: Cardinal;
  AMFTEnumBuff: Pointer;
begin
    ARootHandle := GetRootHandle(pDrive);
    if AllocMFTEnumBuffer(ARootHandle,AMFTEnumBuff) then
      EnumMFTEntries(ARootHandle, AMFTEnumBuff, MFTEnumCallback, @pDrive) ;
end;

function Tform_main.MFTEnumCallback(AUSN: PUSNRecord; Extra: Pointer): Boolean;
var
  AName,ProgressMsg: String;
  Drive: Char;
begin
  Drive  := PChar(Extra)^;
  Result := True;
  USNRecFromPointer(AUSN) ;
end;




{
  ** uMFT.pas
  ** zm
  ** created: 13.11.2010
  **
  *  Copyright (c) 2010, Zeljko Marjanovic <[email protected]>
  *  This code is licensed under MPL 1.1
  *  For details, see http://www.mozilla.org/MPL/MPL-1.1.html
  *
}

unit uMFT;

interface

uses
  Windows, SysUtils;

const
  FILE_DEVICE_FILE_SYSTEM = $00000009;
  METHOD_NEITHER = 3;
  METHOD_BUFFERED = 0;
  FILE_ANY_ACCESS = 0;
  FILE_SPECIAL_ACCESS = 0;
  FILE_READ_ACCESS = 1;
  FILE_WRITE_ACCESS = 2;

  ERROR_JOURNAL_DELETE_IN_PROGRESS = 1178;
  ERROR_JOURNAL_NOT_ACTIVE  = 1179;
  ERROR_JOURNAL_ENTRY_DELETED = 1181;

  FSCTL_GET_OBJECT_ID = $9009c;
  FSCTL_ENUM_USN_DATA = (FILE_DEVICE_FILE_SYSTEM shl 16) or (FILE_ANY_ACCESS shl 14) or (44 shl 2) or METHOD_NEITHER;
  FSCTL_READ_USN_JOURNAL = (FILE_DEVICE_FILE_SYSTEM shl 16) or (FILE_ANY_ACCESS shl 14) or (46 shl 2) or METHOD_NEITHER;
  FSCTL_CREATE_USN_JOURNAL = (FILE_DEVICE_FILE_SYSTEM shl 16) or (FILE_ANY_ACCESS shl 14) or (57 shl 2) or METHOD_NEITHER;
  FSCTL_QUERY_USN_JOURNAL = (FILE_DEVICE_FILE_SYSTEM shl 16) or (FILE_ANY_ACCESS shl 14) or (61 shl 2) or METHOD_BUFFERED;
  FSCTL_DELETE_USN_JOURNAL = (FILE_DEVICE_FILE_SYSTEM shl 16) or (FILE_ANY_ACCESS shl 14) or (62 shl 2) or METHOD_BUFFERED;

  USN_PAGE_SIZE = $1000;
  USN_REASON_DATA_OVERWRITE = $00000001;
  USN_REASON_DATA_EXTEND = $00000002;
  USN_REASON_DATA_TRUNCATION = $00000004;
  USN_REASON_NAMED_DATA_OVERWRITE = $00000010;
  USN_REASON_NAMED_DATA_EXTEND = $00000020;
  USN_REASON_NAMED_DATA_TRUNCATION = $00000040;
  USN_REASON_FILE_CREATE = $00000100;
  USN_REASON_FILE_DELETE = $00000200;
  USN_REASON_EA_CHANGE = $00000400;
  USN_REASON_SECURITY_CHANGE = $00000800;
  USN_REASON_RENAME_OLD_NAME = $00001000;
  USN_REASON_RENAME_NEW_NAME = $00002000;
  USN_REASON_INDEXABLE_CHANGE = $00004000;
  USN_REASON_BASIC_INFO_CHANGE = $00008000;
  USN_REASON_HARD_LINK_CHANGE = $00010000;
  USN_REASON_COMPRESSION_CHANGE = $00020000;
  USN_REASON_ENCRYPTION_CHANGE = $00040000;
  USN_REASON_OBJECT_ID_CHANGE = $00080000;
  USN_REASON_REPARSE_POINT_CHANGE = $00100000;
  USN_REASON_STREAM_CHANGE = $00200000;
  USN_REASON_CLOSE = $80000000;

  USN_DELETE_FLAG_DELETE = $00000001;
  USN_DELETE_FLAG_NOTIFY = $00000002;
  USN_DELETE_VALID_FLAGS = $00000003;

  USNREC_MAJVER_OFFSET = 4;
  USNREC_MINVER_OFFSET = 8; 
  USNREC_FR_OFFSET = 8;
  USNREC_PFR_OFFSET = 16;
  USNREC_USN_OFFSET = 24;
  USNREC_TIMESTAMP_OFFSET = 32;
  USNREC_REASON_OFFSET = 40;
  USNREC_SINFO_OFFSET = 44;
  USNREC_SECID_OFFSET = 48;
  USNREC_FA_OFFSET = 52;
  USNREC_FNL_OFFSET = 56;
  USNREC_FN_OFFSET = 58;

  IOCTL_DISK_BASE = $00000007;
  IOCTL_DISK_GET_PARTITION_INFO = (IOCTL_DISK_BASE shl 16) or (FILE_READ_ACCESS shl 14) or ($0001 shl 2) or METHOD_BUFFERED;
  PARTITION_IFS = $07; 

type
  USN_JOURNAL_DATA = record
    UsnJournalID: UInt64;
    FirstUsn: Int64;
    NextUsn: Int64;
    LowestValidUsn: Int64;
    MaxUsn: Int64;
    MaximumSize: UInt64;
    AllocationDelta: UInt64;
  end;
  TUSNJournalData = USN_JOURNAL_DATA;
  PUSNJournalData = ^TUSNJournalData;

  MFT_ENUM_DATA = record
    StartFileReferenceNumber: UInt64;
    LowUsn: Int64;
    HighUsn: Int64;
  end;
  TMFTEnumData = MFT_ENUM_DATA;
  PMFTEnumData = ^TMFTEnumData;

  CREATE_USN_JOURNAL_DATA = record
    MaximumSize: UInt64;
    AllocationDelta: UInt64;
  end;
  TCreateUSNJournalData = CREATE_USN_JOURNAL_DATA;
  PCreateUSNJournalData = ^TCreateUSNJournalData;

  USN_RECORD = record
    RecordLength: Cardinal;
    MajorVersion: Word;
    MinorVersion: Word;
    FileReferenceNumber: UInt64;
    ParentFileReferenceNumber: UInt64;
    Usn: Int64;
    TimeStamp: LARGE_INTEGER;
    Reason: Cardinal;
    SourceInfo: Cardinal;
    SecurityId: Cardinal;
    FileAttributes: Cardinal;
    FileNameLength: Word;
    FileNameOffset: Word;
    FileName: PWideChar;//   PWChar; [ss]
  end;
  TUSNRecord = USN_RECORD;
  PUSNRecord = ^TUSNRecord;

  TMFTEnumCallback = function(AUSN: PUSNRecord; Extra: Pointer = nil): Boolean of object;
  PUInt64 = ^UInt64;
  EMFTException = class(Exception);

  TUSNRecChangeType = (uceNew, uceDeleted, uceRenamed);


function USNRecFromPointer(const P: Pointer): TUSNRecord;
function GetRootHandle(const Drive: Char): Cardinal;
function CreateUSNJournal(ARootHandle: Cardinal; MaxSize, AllocationDelta: UInt64): Boolean;
function AllocMFTEnumBuffer(ARootHandle: Cardinal; var AMFTEnumBuff: Pointer): boolean;
function EnumMFTEntries(ARootHandle: Cardinal; AMFTEnumBuff: Pointer; EnumCallBack: TMFTEnumCallback;Extra: Pointer = nil): Boolean;

implementation
uses main ;

function USNRecFromPointer(const P: Pointer): TUSNRecord;
var
  PA: PAnsiChar;
begin
  PA := PAnsiChar(P);
  Result.RecordLength := PInteger(PA)^ ;
  Result.MajorVersion := PInteger(PA + USNREC_MAJVER_OFFSET)^;
  Result.MinorVersion := PInteger(PA + USNREC_MINVER_OFFSET)^;
  Result.FileReferenceNumber := PUInt64(PA + USNREC_FR_OFFSET)^;
  Result.ParentFileReferenceNumber := PUInt64(PA + USNREC_PFR_OFFSET)^;
  Result.USN := PInt64(PA + USNREC_USN_OFFSET)^;
  Result.TimeStamp.QuadPart := PInt64(PA + USNREC_TIMESTAMP_OFFSET)^;
  Result.Reason := PCardinal(PA + USNREC_REASON_OFFSET)^;
  Result.SourceInfo := PCardinal(PA + USNREC_SINFO_OFFSET)^;
  Result.SecurityId := PCardinal(PA + USNREc_SECID_OFFSET)^;
  Result.FileAttributes := PCardinal(PA + USNREC_FA_OFFSET)^;
  Result.FileNameLength := PWord(PA + USNREC_FNL_OFFSET)^;
  Result.FileNameOffset := PWord(PA + USNREC_FN_OFFSET)^;
  Result.FileName :=   PWideChar(Integer(P) + Result.FileNameOffset);
  if form_Main.memo1.lines.Count<100 then
    form_Main.memo1.lines.add(Result.FileName+' '+InttoStr(Result.TimeStamp.QuadPart)) ;

end;


// this requires admin privileges
function GetRootHandle(const Drive: Char): Cardinal;
var
  W: WideString;
  RootHandle: Cardinal;
  Flags: Cardinal;
  Access: Cardinal;
begin
  Flags := 0;
  W := '\\.\' + Drive + ':';
  Access := GENERIC_READ;
  RootHandle := CreateFileW(PWChar(W), Access, FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, Flags, 0);
  if RootHandle <> INVALID_HANDLE_VALUE then
    Result := RootHandle
  else
    raise EMFTException.Create(SysErrorMessage(GetLastError));
end;

function CreateUSNJournal(ARootHandle: Cardinal; MaxSize, AllocationDelta: UInt64): Boolean;
var
  BytesRet: Cardinal;
  CreateData: TCreateUSNJournalData;
begin
  CreateData.MaximumSize := MaxSize;
  CreateData.AllocationDelta := AllocationDelta;
  Result := DeviceIoControl(ARootHandle, FSCTL_CREATE_USN_JOURNAL, @CreateData, sizeof(TCreateUSNJournalData), nil, 0, BytesRet, nil);
end;


function AllocMFTEnumBuffer(ARootHandle: Cardinal; var AMFTEnumBuff: Pointer): boolean;
var
  USNBuf: TUSNJournalData;
  ErrCode,BytesRet: Cardinal;
  EnumBuf: PMFTEnumData;
begin
  result:=false;
  if DeviceIoControl(ARootHandle, FSCTL_QUERY_USN_JOURNAL, nil, 0, @USNBuf, sizeof(TUSNJournalData), BytesRet, nil) then
  begin
    GetMem(EnumBuf, sizeof(TMFTEnumData));
    EnumBuf.StartFileReferenceNumber := 0;
    EnumBuf.LowUsn := 0;
    EnumBuf.HighUsn := USNBuf.NextUsn;
    AMFTEnumBuff := EnumBuf;
    result:=true;
    exit;
  end;

  ErrCode:=GetLastError;
  if ErrCode = ERROR_JOURNAL_NOT_ACTIVE then
  begin
    // journal does not exist, create a new1 one
    if CreateUSNJournal(ARootHandle, $10000000, $100000) then
    begin
      if DeviceIoControl(ARootHandle, FSCTL_QUERY_USN_JOURNAL, nil, 0, @USNBuf, sizeof(TUSNJournalData), BytesRet, nil) then
      begin
        GetMem(EnumBuf, sizeof(TMFTEnumData));
        EnumBuf.StartFileReferenceNumber := 0;
        EnumBuf.LowUsn := 0;
        EnumBuf.HighUsn := USNBuf.NextUsn;
        AMFTEnumBuff := EnumBuf;
        result:=true;
        exit;
      end;
      ErrCode:=GetLastError;
    end
    else
    begin
      ErrCode:=GetLastError;
    end;
  end;
end;

//v main function
function EnumMFTEntries(ARootHandle: Cardinal; AMFTEnumBuff: Pointer; EnumCallBack: TMFTEnumCallback;Extra: Pointer): Boolean;
const
  BUF_SIZE = sizeof(UInt64) + $10000;
var
  P: Pointer;
  MFTEnum: Pointer;
  BytesRet: Cardinal;
  PUSN: PUSNRecord;
//  TUSN: TUSNRecord;
begin
  Result := False;
  if (ARootHandle = INVALID_HANDLE_VALUE) or (AMFTEnumBuff = nil) then
    Exit;

  MFTEnum := AMFTEnumBuff;
  GetMem(P, BUF_SIZE);
  try
    ZeroMemory(P, BUF_SIZE);
    while DeviceIoControl(ARootHandle, FSCTL_ENUM_USN_DATA, MFTEnum, sizeof(TMFTEnumData), P, BUF_SIZE, BytesRet, nil) do
    begin
      PUSN := PUSNRecord(Integer(P) + sizeof(Int64));

      while BytesRet > 60 do
      begin
        if (not EnumCallBack(PUSN, Extra)) then
          Exit;
        if PUSN.RecordLength > 0 then
          Dec(BytesRet, PUSN.RecordLength)
        else
          break;
        PUSN := PUSNRecord(Cardinal(PUSN) + PUSN.RecordLength);
        if form_Main.memo1.lines.Count>100 then
          break ;
      end;
      CopyMemory(MFTEnum, P, sizeof(Int64));
    end;
    Result := True;

  finally
    FreeMem(P);
  end;
end;
//^ main function


end.

Solution

  • Time stamps come up 0 because FSCTL_QUERY_USN_JOURNAL does not populate that information. This is not explicitly stated in the control code's documentation and I cannot find any official documentation where it is. But there are places that mention a USN_RECORD is only partially populated. Like in this link which is given from USN_RECORD_V2's documentation, although I'm not even sure what FSCTL the former link is talking about.

    Anyway, this is inline with all the examples I can find including Microsoft's own. Below is a Delphi translation of this code. The USN information retrieved by using a FSCTL_QUERY_USN_JOURNAL control code is used in a DeviceIoControl call using FSCTL_READ_USN_JOURNAL to retrieve detailed information.

    There are few exceptions of a direct translation, one is the retrieval of the file name, which I didn't understand how the C++ code is doing. Another is printing time stamps as this is your requirement. Also, you need to add error checking and protection blocks for resources like the volume handle or memory, etc..

    The code uses the declarations from the helper unit you have included in the question. I checked the time stamps against a 3rd party freeware utility which I found here and they are in accordance.

    program Project1;
    
    {$APPTYPE CONSOLE}
    
    {$R *.res}
    
    uses
      System.SysUtils,
      windows,
      uMFT;
    
    type
      USN = LONGLONG;
    const
      BUF_LEN = 4096;
    var
      hVol: THandle;
      JournalData: TUSNJournalData;
      dwBytes: DWORD;
      dwRetBytes: DWORD;
      ReadData: TReadUSNJournalData;
      i: Integer;
      Buffer: array [0..BUF_LEN - 1] of Byte;
      UsnRecord: PUSNRecord;
      FileName: PWideChar;
      SysTime: TSystemTime;
    begin
      hVol := CreateFile( '\\.\c:', GENERIC_READ,
          FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, 0, 0);
    
      if hVol = INVALID_HANDLE_VALUE then begin
        Writeln(Format('CreateFile failed (%d)', [GetLastError]));
        Exit;
      end;
    
      if not DeviceIoControl(hVol, FSCTL_QUERY_USN_JOURNAL, nil, 0, @JournalData,
          SizeOf(JournalData), dwBytes, nil) then begin
        Writeln(Format('Query journal failed (%d)', [GetLastError]));
        Exit;
      end;
    
      ZeroMemory(@ReadData, SizeOf(ReadData));
      ReadData.ReasonMask := $FFFFFFFF;
      ReadData.UsnJournalID := JournalData.UsnJournalID;
    
      Writeln(Format('Journal ID: %x', [JournalData.UsnJournalID]));
      Writeln(Format('FirstUsn: %x' + sLineBreak, [JournalData.FirstUsn]));
    
      for i := 0 to 10 do begin
        FillChar(Buffer, BUF_LEN, 0);
    
        if not DeviceIoControl(hVol, FSCTL_READ_USN_JOURNAL, @ReadData,
            SizeOf(ReadData), @Buffer, BUF_LEN, dwBytes, nil) then begin
          Writeln(Format('Read journal failed (%d)', [GetLastError]));
          Exit;
        end;
        dwRetBytes := dwBytes - SizeOf(USN);
    
        // Find the first record
        UsnRecord := PUsnRecord(NativeInt(@Buffer) + SizeOf(USN));
    
        Writeln('****************************************');
    
        while dwRetBytes > 0 do begin
    
          Writeln(Format('USN: %x', [UsnRecord.Usn]));
    
          GetMem(FileName, UsnRecord.FileNameLength + SizeOf(Char));
          Move(Pointer(NativeInt(UsnRecord) + UsnRecord.FileNameOffset)^, FileName^,
              UsnRecord.FileNameLength);
          FileName[UsnRecord.FileNameLength div 2] := #0;
          Writeln(Format('File name: %s', [FileName]));
          FreeMem(FileName);
    
          FileTimeToSystemTime(@UsnRecord.TimeStamp, SysTime);
          Writeln(Format('Time stamp: %s', [DateTimeToStr(SystemTimeToDateTime(SysTime))]));
          Writeln(Format('Reason: %x', [UsnRecord.Reason]));
          Writeln;
    
          dwRetBytes := dwRetBytes - UsnRecord.RecordLength;
    
          // Find the next record
          UsnRecord := PUsnRecord(NativeInt(UsnRecord) + UsnRecord.RecordLength);
        end;
      end;
      CloseHandle(hVol);
    
      Writeln;
      Writeln('End of sample');
      Readln;
    end.