Extract deleted file from NTFS using PowerShell (Main)
Here's preparation part
These are web sites that I referred to in implementation.
For the most part I referred to this site, but details are different because some improvements are made based on my analysis to the NTFS format.
After opening NTFS drive, read the bootsector. The bootsector includes information about
- size of sector
- the number of sector in a cluster
- entry point of MFT
$handle = $kernel32::CreateFile(
"\\.\C:",
[system.io.fileaccess]::read,
[system.io.fileshare]::readwrite,
[system.intptr]::zero,
[system.io.filemode]::open,
0,
[system.intptr]::zero
)
if($handle.toint32() -gt 0){
$readsize = 4096
#read drive
$bootsector = read $handle $readsize
#$myutil.dump($bootsector)
#drive info
$sectorsize = $myutil.ReadUInt16($bootsector,0x0b)
$numofsector_in_cluster = $myutil.ReadByte($bootsector,0x0d)
$numofsector = $myutil.ReadUInt64($bootsector,0x28)
$mft = $myutil.ReadUInt64($bootsector,0x30)
$numofsector_in_filerecord = $myutil.ReadByte($bootsector,0x40)
if($numofsector_in_filerecord -gt 0x7f){
$recordsize = [math]::pow(2, 0x0100 - $numofsector_in_filerecord)
} else {
$recordsize = $numofsector_in_filerecord * $sectorsize
}
Then read the MFT (master File Table). MFT includes data attribute field that contains Data RunList to file records.
#seek
$result = seek $handle ($sectorsize * $numofsector_in_cluster * $mft)
#read MFT
$mft_record = read $handle $recordsize
Retreive file record using obtained RunList. This file record includes filename in file name attribute field. Futher more the file record includes entity of data in data attribute field when the file size is relatively small, This is called "Resident".
#read record
$record = getRecord $handle $i $runList $sectorsize $numofsector_in_cluster
if($record.count -eq 0){
continue
}
When the attribute field does not have entity of data, being "Non-Resident", the field includes runlist instead of data. You can access data using the RunList in data attribute field.
ntfs.ps1
#disk reader
$scriptPath = $MyInvocation.MyCommand.Path
$dir = Split-Path -Parent $scriptPath
#import scripts
. ($dir + "\psUtil.ps1")
. ($dir + "\kernel32.ps1")
. ($dir + "\ntfs_functions.ps1")
#$myutil.writeoption = $true
$outfile = "restore.jpg"
$handle = $kernel32::CreateFile(
"\\.\C:",
[system.io.fileaccess]::read,
[system.io.fileshare]::readwrite,
[system.intptr]::zero,
[system.io.filemode]::open,
0,
[system.intptr]::zero
)
if($handle.toint32() -gt 0){
$readsize = 4096
#read drive
$bootsector = read $handle $readsize
#$myutil.dump($bootsector)
#drive info
$sectorsize = $myutil.ReadUInt16($bootsector,0x0b)
$numofsector_in_cluster = $myutil.ReadByte($bootsector,0x0d)
$numofsector = $myutil.ReadUInt64($bootsector,0x28)
$mft = $myutil.ReadUInt64($bootsector,0x30)
$numofsector_in_filerecord = $myutil.ReadByte($bootsector,0x40)
if($numofsector_in_filerecord -gt 0x7f){
$recordsize = [math]::pow(2, 0x0100 - $numofsector_in_filerecord)
} else {
$recordsize = $numofsector_in_filerecord * $sectorsize
}
$myutil.write2host("sectorsize : " + $sectorsize.toString())
$myutil.write2host("recordsize : " + $recordsize.toString())
#seek
$result = seek $handle ($sectorsize * $numofsector_in_cluster * $mft)
#read MFT
$mft_record = read $handle $recordsize
#$myutil.dump($mft_record)
#data attribute
$attr_offset, $attr_len = getAttrOffset $mft_record 0x80
$contentdisksize = $myutil.ReadUInt64($mft_record, $attr_offset + 0x28)
$contentsize = $myutil.ReadUInt64($mft_record, $attr_offset + 0x30)
$recordnumber = [math]::truncate($contentsize/$recordsize)
#get runlist
$runlist = getRunList $mft_record $attr_offset $attr_len
$mode = ""
while($true){
$input = Read-Host "Search[S] / Restore[R] / End[E]"
switch($input.ToString().ToUpper()){
"S" {$mode = "S"}
"R" {$mode = "R"}
"E" {$mode = "E"}
}
if($mode -ne ""){
break
}
}
switch($mode){
"S"
{
for($i = 0; $i -lt $recordnumber; $i++){
#read record
$record = getRecord $handle $i $runList $sectorsize $numofsector_in_cluster
if($record.count -eq 0){
continue
}
$flag = $myutil.ReadUint16($record, 0x16)
switch($flag){
0 {$datatype = "x "}
1 {$datatype = " "}
2 {$datatype = "xd"}
3 {$datatype = " d"}
default{$datatype = "??"}
}
#filename attribute
$attr_offset, $attr_len = getAttrOffset $record 0x30
if(($attr_offset -eq 0) -and ($attr_len -eq 0)){
continue
}
$formcode = $myutil.ReadByte($record, $attr_offset + 0x08)
if($formcode -eq 0){
#Non-Resident
$attr_headersize = $myutil.ReadUint16($record, $attr_offset + 0x14)
$filename_length = $myutil.ReadByte($record, $attr_offset + $attr_headersize + 0x40)
$file_type = $myutil.ReadByte($record, $attr_offset + $attr_headersize + 0x41)
$filenamepos = ($attr_offset + $attr_headersize + 0x42)
$filename = $record[($filenamepos)..($filenamepos + ($filename_length * 2) - 1)]
write-host ($i.tostring() + " " + $datatype + " " + [System.Text.Encoding]::Unicode.GetString($filename))
}
}
}
"R"
{
[uint32]$target = 0
while($true){
$input = Read-Host "Target record number"
[void][int]::TryParse($input.ToString(),[ref]$target)
if($target -gt 0){
break
}
}
set-content $outfile $null
#get target record
$record = getRecord $handle $target $runList $sectorsize $numofsector_in_cluster
#filename attribute
$attr_offset_c, $attr_len_c = getAttrOffset $record 0x20
$attr_offset_d, $attr_len_d = getAttrOffset $record 0x80
if(($attr_offset_d -eq 0) -and ($attr_len_d -eq 0)){
#data attribute not exists
$attr_rocordsize = $myutil.ReadUint32($record, $attr_offset_c + 0x10)
$contentoffset = $myutil.ReadUint16($record, $attr_offset_c + 0x14)
[uint]$p = 0
while($p + 0x1c -le $attr_rocordsize){
#get typeid
$typeid = $myutil.ReadUint32($record, $attr_offset_c + $contentoffset + $p)
if($typeid = 0x80){
#get record including runlist in data attribute
$recordnumber = $myutil.ReadUint32($record, $attr_offset_c + $contentoffset + $p + 0x10)
$forrunlist = getRecord $handle $recordnumber $runList $sectorsize $numofsector_in_cluster
$attr_offset_t, $attr_len_t = getAttrOffset $forrunlist 0x80
#get runlist
$runlist = getRunList $forrunlist $attr_offset_t $attr_len_t
#size of data collected by the runlist
$contentsize = $myutil.ReadUInt64($record, $attr_offset_d + 0x30)
$prev_cluster_offset = 0
foreach($run in $runlist){
#seek to target cluster
$result = seek $handle (($prev_cluster_offset + $run.Offset) * $sectorsize * $numofsector_in_cluster)
for([int64]$i = 0; $i -lt $run.Length; $i++){
#read from cluster
$tmp = read $handle ($sectorsize * $numofsector_in_cluster)
$remain = $contentsize - $data.count
if($remain -gt ($sectorsize * $numofsector_in_cluster)){
add-content $outfile -value $tmp -Encoding byte
} else {
add-content $outfile -value $tmp[0..$remain] -Encoding byte
}
}
#save current cluster
$prev_cluster_offset += $run.Offset
}
}
#move by attrubute record length
$p += $myutil.ReadUint16($record, $attr_offset_c + $contentoffset + $p + 4)
}
} else {
#data attribute exists in target record
$formcode = $myutil.ReadByte($record, $attr_offset_d + 0x08)
if($formcode -eq 0){
#resident
#get data from current MFT
$contentsize = $myutil.ReadUint32($record, $attr_offset_d + 0x10)
$contentoffset = $myutil.ReadUint16($record, $attr_offset_d + 0x14)
$tmp = $record[($attr_offset_d + $contentoffset)..($attr_offset_d + $contentoffset + $contentsize)]
add-content $outfile -value $tmp -Encoding byte
} else {
#non-resident
#get runlist
$runlist = getRunList $record $attr_offset_d $attr_len_d
#size of data collected by the runlist
$contentsize = $myutil.ReadUInt64($record, $attr_offset_d + 0x30)
$prev_cluster_offset = 0
foreach($run in $runlist){
#seek to target cluster
$result = seek $handle (($prev_cluster_offset + $run.Offset) * $sectorsize * $numofsector_in_cluster)
for([int64]$i = 0; $i -lt $run.Length; $i++){
#read from cluster
$tmp = read $handle ($sectorsize * $numofsector_in_cluster)
$remain = $contentsize - $data.count
if($remain -gt ($sectorsize * $numofsector_in_cluster)){
add-content $outfile -value $tmp -Encoding byte
} else {
add-content $outfile -value $tmp[0..$remain] -Encoding byte
}
}
#save current cluster
$prev_cluster_offset += $run.Offset
}
}
}
}
}
#close handle
$result = $kernel32::CloseHandle($handle)
}
ntfs_functions.ps1
# seek to specified point
# $handle : file handle
# $moveto : access point
function seek([IntPtr]$handle, [int64]$moveto){
$longlongptr = [system.runtime.interopservices.marshal]::allochglobal(8)
$result = $kernel32::SetFilePointerEx(
$handle,
$moveto,
$longlongptr,
0 #file_begin
)
$accesspoint = $myutil.ReadUInt64($longlongptr,0)
[system.runtime.interopservices.marshal]::freehglobal($longlongptr)
return $result
}
# read from drive
# $handle : file handle
# $size : read size
function read([IntPtr]$handle,[uint32]$size){
$record = new-object byte[] 0
$buffer = [system.runtime.interopservices.marshal]::allochglobal($size)
$lpsize = [system.runtime.interopservices.marshal]::allochglobal(4)
$result = $kernel32::ReadFile(
$handle,
$buffer,
$size,
$lpsize,
[system.IntPtr]::zero
)
if($result -eq 1){
$readsize = $myutil.ReadUint32($lpsize, 0)
$record = new-object byte[] $readsize
[system.runtime.interopservices.marshal]::Copy($buffer,$record,0,$readsize)
}
[system.runtime.interopservices.marshal]::freehglobal($buffer)
[system.runtime.interopservices.marshal]::freehglobal($lpsize)
return $record
}
# get runlist from Master File Table
# $record : file record
# $attr_offset : offset to a attribute
# $attr_len : length of a attribute
function getRunList([object[]]$record, [uint32]$attr_offset, [uint32]$attr_len){
$runlist = @()
$runlistoffset = $myutil.ReadUInt64($record, $attr_offset + 0x20)
$runlistsize = $attr_len - $runlistoffset
while($true){
$lenLength = $myutil.ReadByte($record, $attr_offset + $runlistoffset) % 0x10
$lenOffset = [math]::truncate($myutil.ReadByte($record, $attr_offset + $runlistoffset) / 0x10)
if(($lenLength -eq 0) -and ($lenOffset -eq 0)){
break
}
if($lenOffset -eq 0){
#sparse file
return @()
}
[int64]$length = 0
[int64]$offsetdiff = 0
for($i = 0; $i -lt $lenLength; $i++){
$byte = $myutil.ReadByte($record, $attr_offset + $runlistoffset + 1 + $i)
$length += $byte * [math]::pow(0x0100, $i)
}
for($i = 0; $i -lt $lenOffset; $i++){
$byte = $myutil.ReadByte($record, $attr_offset + $runlistoffset + 1 + $lenLength + $i)
$offsetdiff += $byte * [math]::pow(0x0100, $i)
if(($i + 1 -eq $lenOffset) -and ($byte -gt 0x7f)){
#if most significant bit is 1, it is treated as backward offset
$offsetdiff = ([math]::pow(0x0100, $lenOffset) - $offsetdiff) * -1
break
}
}
$element = @{Length=$length; Offset=$offsetdiff}
$runlist += $element
$runlistoffset += (1 + $lenLength + $lenOffset)
}
return $runlist
}
# get file record for target record number
# $target : target record number
# $runlist : runlist
# $sectorsize : size of a sector
# $clustersize : number of sector in a cluster
function getRecord([IntPtr]$handle, [uint32]$target, [object[]]$runList, [uint32]$sectorsize, [uint32]$numofsector_in_cluster){
$record = new-object byte[] 0
#num of sector in a record
$numofsector = [math]::truncate($recordsize / $sectorsize)
#virtual sector offset to target record
$sectoroffset = $target * $numofsector
#cluster byte size
$clustersize = ($numofsector_in_cluster * $sectorsize)
for($sector = 0; $sector -lt $numofsector; $sector++){
#cluster number of target record
$cn = [math]::truncate(($sectoroffset + $sector) / $numofsector_in_cluster)
#virtual offset to the record sector in target cluster
$voffset = ($sectoroffset + $sector) * $sectorsize % $clustersize
#virtual cluster number
[int64]$vcn = 0
#offset to target sector (ignore run offset)
[int64]$offset = $sectoroffset + $sector * $sectorsize + $voffset
foreach($run in $runlist){
if($cn -lt $vcn + $run.length){
#reachable by this runlist
#shift by runoffset
$offset += $run.offset * $clustersize
break
} else {
#not reachable by this runlist yet
#shift by runoffset
$offset += $run.offset * $clustersize
}
$vcn += $run.length
}
if($offset -gt 0){
#seek
$result = seek $handle $offset
#read
$tmp = read $handle $recordsize
$record += $tmp
}
}
$head = $record[0..3]
if(($head[0] -ne 0x46) -or ($head[1] -ne 0x49) -or ($head[2] -ne 0x4c) -or ($head[3] -ne 0x45)){
#$myutil.dump($record[0..31])
continue
}
$updateoffset = $myutil.ReadUint16($record, 0x04)
$updatenumber = $myutil.ReadUint16($record,0x06)
#replace with update sequence
for($j = 1; $j -lt $updatenumber; $j++){
$record[$j * $sectorsize - 2] = $myutil.ReadByte($record, $updateoffset + 2 * $j)
$record[$j * $sectorsize - 1] = $myutil.ReadByte($record, $updateoffset + 2 * $j + 1)
}
return $record
}
# get offset to attribute and length of attribute
# $record : file record
# $targetid : target attribute id
function getAttrOffset([object[]]$record, [uint32]$targetid){
$attr_offset = $myutil.ReadUInt16($record,0x14)
while($true){
$typeid = $myutil.ReadUInt32($record, $attr_offset)
$attr_len = $myutil.ReadUInt32($record, $attr_offset + 0x04)
if($typeid -eq $targetid){
return $attr_offset, $attr_len
} elseif($typeid -eq 4294967295) {
#end of attribute area
return 0,0
} else {
$attr_offset += $attr_len
}
}
return 0, 0
}
Profile
I have technical job experience in enbedded software development and server side infrastructure/application engineering.
I'm interested in programming and computer security.
Objective
To write down my technical knowledge in the place where I can access from anywhere.
To share my program source code.
To train my writing skill.
New entries