Skip to content

Instantly share code, notes, and snippets.

@picheljitsu
Last active August 2, 2024 19:55
Show Gist options
  • Select an option

  • Save picheljitsu/21cea4bb25e58fc579a6e2ef32ead66a to your computer and use it in GitHub Desktop.

Select an option

Save picheljitsu/21cea4bb25e58fc579a6e2ef32ead66a to your computer and use it in GitHub Desktop.
Powershell Diskless MFT Grabber
function Get-RemoteMFT {
<#
.SYNOPSIS
Extracts master file table from volume.
Version: 0.1
Author : Jesse Davis (@secabstraction)
License: BSD 3-Clause
Version: 0.1
Author : Matt Pichelmayer
License: BSD 3-Clause
.DESCRIPTION
This module reads the Master File Table from a remote host and streams it to a local path on the workstation the script is ran from.
.PARAMETER ComputerName
Specify host to retrieve the Master File Table from.
.PARAMETER Volume
Specify a volume to retrieve its master file table.
.PARAMETER FirewallRuleName
Speficy the name of the FirewallRuleName to use when opening a firewall port.
.PARAMETER LPort
Specify a local port to listen on to receive the MFT file transfer.
.EXAMPLE
The following example extracts the master file table from a remote workstation, connects back to this script's workstation and streams the file on port 7777. If an LPort
is not secified, port 2998 is used by default.
PS C:\> Get-RemoteMFT -ComputerName <computer_name> -OutputFilePath "C:\mft.bin" -FirewallRuleName "MFT File Transfer" -Port 7777
.NOTES
This script is a slightly modified version + wrapper for Jesse Davis's Export-MFT (https://gist.github.com/secabstraction/4044f4aadd3ef21f0ca9). It will parse the MFT and
send it over the network to prevent any writes to disk.
The MFT location isn't always fixed on the volume. You should get the starting MFT offset from the boot sector (sector 0 of the volume, you can find the structure online).
The first file in the MFT is the "$MFT" file which is the file record for the entire MFT itself. You can parse the attributes of this file like any other file and get it's
data run list. When you know the size of each fragment in clusters, parse the last cluster for each 1024 byte record of the last fragment (although I believe a fragmented
MFT is rare). The last record in the MFT is the last record in that particular cluster marked "FILE0", if you encounter a null magic number that would be 1024 bytes too far.
Or you can just get the file size from it's attributes and calculate the offset to the end of the MFT based on how many fragments it has. Then subtract 1024 from the offset
and you should be looking at the last file.
.INPUTS
.OUTPUTS
.LINK
Export-MFT: https://gist.github.com/secabstraction/4044f4aadd3ef21f0ca9
Get-RemoteMFT: https://github.com/picheljitsu/Powershell/blob/master/Forensics/Get-RemoteMFT.ps1
#>
[CmdLetBinding()]
Param(
[Parameter(Position = 0)]
[ValidateNotNullOrEmpty()]
[String[]]$ComputerName,
[Parameter()]
[ValidateNotNullOrEmpty()]
[Char]$Volume = 0,
[Parameter()]
[ValidateNotNullOrEmpty()]
[Int]$LPort = 2998,
[Parameter()]
[string]$OutputFilePath = "$($pwd.Path)\$($Computername)_MFT.bin",
[Parameter()]
[String]$FirewallRuleName = "MFT XFER $lport"
)
#Enable verbosity by default
$VerbosePreference = 'Continue'
#Scriptblock to start a tcp server that will be forked into a seperate
$OutputFilePath = Join-Path (Split-Path $OutputFilePath | resolve-path ) (Split-Path $OutputFilePath -Leaf) -ErrorAction Stop
$remote_pssession = New-PSSession -computername $computername -SessionOption (New-PSSessionOption -NoMachineProfile) -ErrorAction Stop
#Powershell Runspace
$ListenerBlock = {
param($Lport, $OutputFilePath)
#Start TCP SERVER
$Tcplistener = New-object System.Net.Sockets.TcpListener $lport
$Tcplistener.Start()
$TcpClient = $Tcplistener.AcceptTcpClient()
$remotesvr = $TcpClient.Client.RemoteEndPoint.Address.IPAddressToString
$TcpNetworkstream = $TCPClient.GetStream()
$Receivebuffer = New-Object Byte[] $TcpClient.ReceiveBufferSize
$OutputFileStream = New-Object IO.FileStream $OutputFilePath ,'Append','Write','Read'
try {
while($TcpClient.Connected){
$Read = $TcpNetworkstream.Read($Receivebuffer, 0, $Receivebuffer.Length)
if($Read -eq 0){ break }
else{
[Array]$Bytesreceived += $Receivebuffer[0..($Read -1)]
[Array]::Clear($Receivebuffer, 0, $Read)
$OutputFileStream.Write($Bytesreceived, 0, $Bytesreceived.Length)
$TcpNetworkstream.Flush()
$Bytesreceived = $null
}
}
}
catch { exit(1) }
$OutputFileStream.Close()
$Tcplistener.Stop()
} #End Listener Block
Write-Verbose "[*] Initializing Listener Runspace..."
$InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$runspacepool = [RunspaceFactory]::CreateRunspacePool($InitialSessionState)
Write-Verbose "[*] Opening Runspace..."
sleep 1
$runspacepool.Open()
$runspace = [PowerShell]::Create()
$runspace.runspacePool = $runspacepool
Write-Verbose "[+] Runspace Open"
sleep 1
$ParamList = @{ "Lport" = $Lport
"OutputFilePath" = $OutputFilePath }
Write-Verbose "[*] Forking Listener to Runspace..."
[void]$runspace.AddScript($ListenerBlock).AddParameters($ParamList)
Write-Verbose "[*] Starting Listener..."
sleep 1
$runspace.begininvoke() | Out-Null
if(-not ($runspace.HadErrors)) {
if($(netstat -ant | findstr $Lport)){ Write-Verbose "[+] Successfully forked TCP listener on port $Lport to background" }
}
else { Write-Verbose "[-] Couldn't start Listener. Exiting." }
#Scriptblock to dump MFT on Remote host
$ScriptBlock = {
Param($calling_host, $Volume, $Lport)
if ($Volume -ne 0) {
$Win32_Volume = Get-WmiObject -Class Win32_Volume -Filter "DriveLetter LIKE '$($Volume):'"
if ($Win32_Volume.FileSystem -ne "NTFS") {
Write-Error "$Volume is not an NTFS filesystem."
break
}
}
else {
$Win32_Volume = Get-WmiObject -Class Win32_Volume -Filter "DriveLetter LIKE '$($env:SystemDrive)'"
if ($Win32_Volume.FileSystem -ne "NTFS") {
Write-Error "$env:SystemDrive is not an NTFS filesystem."
break
}
}
#region WinAPI
$GENERIC_READWRITE = 0x80000000
$FILE_SHARE_READWRITE = 0x02 -bor 0x01
$OPEN_EXISTING = 0x03
$DynAssembly = New-Object System.Reflection.AssemblyName('MFT')
$AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run)
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('InMemory', $false)
$TypeBuilder = $ModuleBuilder.DefineType('kernel32', 'Public, Class')
$DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
$SetLastError = [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')
$SetLastErrorCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($DllImportConstructor,
@('kernel32.dll'),
[Reflection.FieldInfo[]]@($SetLastError),
@($True))
#CreateFile
$PInvokeMethodBuilder = $TypeBuilder.DefinePInvokeMethod('CreateFile', 'kernel32.dll',
([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
[Reflection.CallingConventions]::Standard,
[IntPtr],
[Type[]]@([String], [Int32], [UInt32], [IntPtr], [UInt32], [UInt32], [IntPtr]),
[Runtime.InteropServices.CallingConvention]::Winapi,
[Runtime.InteropServices.CharSet]::Ansi)
$PInvokeMethodBuilder.SetCustomAttribute($SetLastErrorCustomAttribute)
#CloseHandle
$PInvokeMethodBuilder = $TypeBuilder.DefinePInvokeMethod('CloseHandle', 'kernel32.dll',
([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
[Reflection.CallingConventions]::Standard,
[Bool],
[Type[]]@([IntPtr]),
[Runtime.InteropServices.CallingConvention]::Winapi,
[Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethodBuilder.SetCustomAttribute($SetLastErrorCustomAttribute)
$Kernel32 = $TypeBuilder.CreateType()
#endregion WinAPI
#Get handle to volume
if($Volume -ne 0){
$VolumeHandle = $Kernel32::CreateFile(('\\.\' + $Volume + ':'), $GENERIC_READWRITE, $FILE_SHARE_READWRITE, [IntPtr]::Zero, $OPEN_EXISTING, 0, [IntPtr]::Zero)
}
else {
$VolumeHandle = $Kernel32::CreateFile(('\\.\' + $env:SystemDrive), $GENERIC_READWRITE, $FILE_SHARE_READWRITE, [IntPtr]::Zero, $OPEN_EXISTING, 0, [IntPtr]::Zero)
$Volume = ($env:SystemDrive).TrimEnd(':')
}
if ($VolumeHandle -eq -1) {
Write-Error "Unable to obtain read handle for volume."
break
}
# Create a FileStream to read from the volume handle
$FileStream = New-Object IO.FileStream($VolumeHandle, [IO.FileAccess]::Read)
# Read VBR from volume
$VolumeBootRecord = New-Object Byte[](512)
if ($FileStream.Read($VolumeBootRecord, 0, $VolumeBootRecord.Length) -ne 512) { Write-Error "Error reading volume boot record." }
# Parse MFT offset from VBR and set stream to its location
$MftOffset = [Bitconverter]::ToInt32($VolumeBootRecord[0x30..0x37], 0) * 0x1000
$FileStream.Position = $MftOffset
# Read MFT's file record header
$MftFileRecordHeader = New-Object byte[](48)
if ($FileStream.Read($MftFileRecordHeader, 0, $MftFileRecordHeader.Length) -ne $MftFileRecordHeader.Length) { Write-Error "Error reading MFT file record header." }
# Parse values from MFT's file record header
$OffsetToAttributes = [Bitconverter]::ToInt16($MftFileRecordHeader[0x14..0x15], 0)
$AttributesRealSize = [Bitconverter]::ToInt32($MftFileRecordHeader[0x18..0x21], 0)
# Read MFT's full file record
$MftFileRecord = New-Object byte[]($AttributesRealSize)
$FileStream.Position = $MftOffset
if ($FileStream.Read($MftFileRecord, 0, $MftFileRecord.Length) -ne $AttributesRealSize) { Write-Error "Error reading MFT file record." }
# Parse MFT's attributes from file record
$Attributes = New-object byte[]($AttributesRealSize - $OffsetToAttributes)
[Array]::Copy($MftFileRecord, $OffsetToAttributes, $Attributes, 0, $Attributes.Length)
# Find Data attribute
$CurrentOffset = 0
do {
$AttributeType = [Bitconverter]::ToInt32($Attributes[$CurrentOffset..$($CurrentOffset + 3)], 0)
$AttributeSize = [Bitconverter]::ToInt32($Attributes[$($CurrentOffset + 4)..$($CurrentOffset + 7)], 0)
$CurrentOffset += $AttributeSize
} until ($AttributeType -eq 128)
# Parse data attribute from all attributes
$DataAttribute = $Attributes[$($CurrentOffset - $AttributeSize)..$($CurrentOffset - 1)]
# Parse MFT size from data attribute
$MftSize = [Bitconverter]::ToUInt64($DataAttribute[0x30..0x37], 0)
# Parse data runs from data attribute
$OffsetToDataRuns = [Bitconverter]::ToInt16($DataAttribute[0x20..0x21], 0)
$DataRuns = $DataAttribute[$OffsetToDataRuns..$($DataAttribute.Length -1)]
# Convert data run info to string[] for calculations
$DataRunStrings = ([Bitconverter]::ToString($DataRuns)).Split('-')
# Setup to read MFT
$FileStreamOffset = 0
$DataRunStringsOffset = 0
$TotalBytesWritten = 0
$MftData = New-Object byte[](0x1000)
[array]$SendBuffer = @()
# Connect Back to calling host to send MFT
$Tcpclient = New-Object System.Net.Sockets.TcpClient
$Tcpclient.sendbuffersize = 16384
$serverip = (([System.Net.Dns]::GetHostAddresses($calling_host))[0]).IPAddressToString
try{ $Tcpclient.Connect($serverip, $lport)
$TcpNetworkStream = $Tcpclient.GetStream()
do {
$StartBytes = [int]($DataRunStrings[$DataRunStringsOffset][0]).ToString()
$LengthBytes = [int]($DataRunStrings[$DataRunStringsOffset][1]).ToString()
$DataRunStart = "0x"
$DataRunLength = "0x"
for ($i = $StartBytes; $i -gt 0; $i--) { $DataRunStart += $DataRunStrings[($DataRunStringsOffset + $LengthBytes + $i)] }
for ($i = $LengthBytes; $i -gt 0; $i--) { $DataRunLength += $DataRunStrings[($DataRunStringsOffset + $i)] }
$FileStreamOffset += ([int]$DataRunStart * 0x1000)
$FileStream.Position = $FileStreamOffset
for ($i = 1; $i -lt [int]$DataRunLength+1; $i++) {
$readlen = $FileStream.Read($MftData, 0, $MftData.Length)
[array]$SendBuffer += $MftData[0..($readlen-1)]
if ( $readlen -ne $MftData.Length) {
Write-Warning "Possible error reading MFT data on $env:COMPUTERNAME."
}
#Logic to only write to stream only if buffer is full (16384)
#or not divisble by 4096, meaning it's the last write
switch($Sendbuffer.length) {
16384 { $TcpNetworkstream.write($Sendbuffer, 0, $Sendbuffer.Length)
$TcpNetworkstream.flush()
$SendBuffer = $null }
{ ![math]::Equals(($_ % 4092),0) } {
$TcpNetworkstream.write($Sendbuffer, 0, $Sendbuffer.Length)
$TcpNetworkstream.flush()
$SendBuffer = $null }
default { continue }
}
$TotalBytesWritten += $MftData.Length
}
$DataRunStringsOffset += $StartBytes + $LengthBytes + 1
} until ($TotalBytesWritten -eq $MftSize)
#Shutdown the connection
$TcpNetworkstream.Dispose()
$Tcpclient.Close()
#On success return the MFT's size
$MftSize
}
catch { 0 }
}
$calling_host = $env:COMPUTERNAME
netsh advfirewall firewall delete rule name=$FirewallRuleName | Out-Null
netsh advfirewall firewall add rule name=$FirewallRuleName dir=in action=allow protocol=TCP localport=$lport | Out-Null
write-verbose "[+] Added Firewall rule `"$FirewallRuleName`" for port $LPort"
$scriptTime = [Diagnostics.Stopwatch]::StartNew()
Write-Verbose "[+] Execution Start time: $(get-date -Format t)"
Write-Verbose "[*] Writing to $OutputFilePath"
$ReturnedObjects = Invoke-Command -Session $remote_pssession -ScriptBlock $ScriptBlock -ArgumentList @($calling_host,$Volume,$LPort)
if($ReturnedObjects -eq 0){ write-verbose "[-] Failed to transfer MFT."}
else{write-verbose "[+] Successfully copied MFT with a size of $($ReturnedObjects / 1024 / 1024) MB"}
Write-Verbose "[*] Removing Runspace..."
$runspace.Stop()
$runspacepool.Close()
$runspacepool.Dispose()
[GC]::Collect()
$ScriptTime.Stop()
Write-Verbose "[+] Runspace removed."
netsh advfirewall firewall delete rule name=$FirewallRuleName | Out-Null
Write-Verbose "[+] Firewall rule removed"
write-Verbose "[+] Done, execution time: $($ScriptTime.Elapsed)"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment