Skip to content

Instantly share code, notes, and snippets.

@aogail
Last active December 31, 2015 03:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aogail/7930766 to your computer and use it in GitHub Desktop.
Save aogail/7930766 to your computer and use it in GitHub Desktop.
Attempt to read NTFS streams using BackupRead() and BackupSeek() via FFI.
C:\Development\ruby-ads>ruby streams_ffi.rb
C:/Ruby193/lib/ruby/gems/1.9.1/gems/ffi-1.0.9-x86-mingw32/lib/ffi/platform.rb:27: Use RbConfig instead of obsolete and d
eprecated Config.
CreateFile handle=192
Before BackupRead loop. Context=0, handle=192
After BackupRead(), context=8537608
#<Win::FFI::File::Win32StreamId dw_stream_id=0x1, dw_stream_attributes=0x0, size_low=16, size_high=0, dw_stream_name_siz
e=0, c_stream_name=''>
BackupSeek failed: BackupSeek(192, 16, 0, #<FFI::MemoryPointer address=0x2a7bbf0 size=4>, #<FFI::MemoryPointer address=0
x2a7bc08 size=4>, #<FFI::MemoryPointer address=0x2a7bc20 size=4>). skipped_low=0, skipped_high=0, context=8537608, handl
e=192
Closing BackupRead context
Closing file handle
streams_ffi.rb:195:in `alternate_streams': The operation completed successfully. (RuntimeError)
from streams_ffi.rb:222:in `<main>'
require 'pp'
require 'ffi'
module Win
module FFI
module File
extend ::FFI::Library
ffi_lib 'kernel32'
ffi_convention :stdcall
# CreateFile flags
OPEN_EXISTING = 3
GENERIC_READ = 0x80000000
FILE_SHARE_READ = 0x00000001
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
# Backup Stream IDs
BACKUP_DATA = 0x00000001
BACKUP_EA_DATA = 0x00000002
BACKUP_SECURITY_DATA = 0x00000003
BACKUP_ALTERNATE_DATA = 0x00000004
BACKUP_LINK = 0x00000005
BACKUP_PROPERTY_DATA = 0x00000006
BACKUP_OBJECT_ID = 0x00000007
BACKUP_REPARSE_DATA = 0x00000008
BACKUP_SPARSE_BLOCK = 0x00000009
BACKUP_TXFS_DATA = 0x0000000A
# Stream attributes
STREAM_NORMAL_ATTRIBUTE = 0x00000000
STREAM_MODIFIED_WHEN_READ = 0x00000001
STREAM_CONTAINS_SECURITY = 0x00000002
STREAM_CONTAINS_PROPERTIES = 0x00000004
STREAM_SPARSE_ATTRIBUTE = 0x00000008
# FormatMessage flags
FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200
FORMAT_MESSAGE_FROM_STRING = 0x00000400
FORMAT_MESSAGE_FROM_HMODULE = 0x00000800
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000
FORMAT_MESSAGE_MAX_WIDTH_MASK = 0x000000FF
typedef :uint32, :dword
typedef :uintptr_t, :handle
=begin
typedef struct _WIN32_STREAM_ID {
DWORD dwStreamId;
DWORD dwStreamAttributes;
LARGE_INTEGER Size;
DWORD dwStreamNameSize;
WCHAR cStreamName[ANYSIZE_ARRAY];
} WIN32_STREAM_ID, *LPWIN32_STREAM_ID;
=end
class Win32StreamId < ::FFI::Struct
layout :dw_stream_id, :uint32,
:dw_stream_attributes, :uint32,
# Size is actually a LARGE_INTEGER union,
# but we only need it for BackupSeek(), which
# takes two DWORDs, anyway.
:size_low, :ulong,
:size_high, :long,
:dw_stream_name_size, :uint32,
:c_stream_name, :pointer
def inspect
"#<#{self.class.inspect} dw_stream_id=0x#{self[:dw_stream_id].to_s(16)}, dw_stream_attributes=0x#{self[:dw_stream_attributes].to_s(16)}, size_low=#{self[:size_low]}, size_high=#{self[:size_high]}, dw_stream_name_size=#{self[:dw_stream_name_size]}, c_stream_name='#{name}'>"
end
def name
self[:c_stream_name].read_string(self[:dw_stream_name_size])
end
def size
self[:size_low] + self[:size_high] << 32
end
end
=begin
HANDLE WINAPI CreateFile(
_In_ LPCTSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
);
=end
attach_function :CreateFile, :CreateFileA, [:pointer, :dword, :dword, :pointer, :dword, :dword, :pointer], :handle
=begin
DWORD WINAPI GetLastError(void);
=end
attach_function :GetLastError, :GetLastError, [], :dword
=begin
DWORD WINAPI FormatMessage(
_In_ DWORD dwFlags,
_In_opt_ LPCVOID lpSource,
_In_ DWORD dwMessageId,
_In_ DWORD dwLanguageId,
_Out_ LPTSTR lpBuffer,
_In_ DWORD nSize,
_In_opt_ va_list *Arguments
);
=end
attach_function :FormatMessage, :FormatMessageA, [:dword, :pointer, :dword, :dword, :pointer, :dword, :varargs], :dword
=begin
BOOL WINAPI CloseHandle(
_In_ HANDLE hObject
);
=end
attach_function :CloseHandle, :CloseHandle, [:handle], :bool
=begin
BOOL BackupRead(
_In_ HANDLE hFile,
_Out_ LPBYTE lpBuffer,
_In_ DWORD nNumberOfBytesToRead,
_Out_ LPDWORD lpNumberOfBytesRead,
_In_ BOOL bAbort,
_In_ BOOL bProcessSecurity,
_Out_ LPVOID *lpContext
);
=end
attach_function :BackupRead, :BackupRead, [:handle, :pointer, :dword, :pointer, :bool, :bool, :pointer], :bool
=begin
BOOL BackupSeek(
_In_ HANDLE hFile,
_In_ DWORD dwLowBytesToSeek,
_In_ DWORD dwHighBytesToSeek,
_Out_ LPDWORD lpdwLowByteSeeked,
_Out_ LPDWORD lpdwHighByteSeeked,
_In_ LPVOID *lpContext
);
=end
attach_function :BackupSeek, :BackupSeek, [:handle, :dword, :dword, :pointer, :pointer, :pointer], :bool
end
end
end
class File
include Win::FFI::File
# Read stream information from this file and return an array of
# (name, size) pairs.
def alternate_streams()
handle = CreateFile(
self.path,
GENERIC_READ,
FILE_SHARE_READ,
nil,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
nil
)
$stderr.puts "CreateFile handle=#{handle}"
if handle != 0
streams = []
begin
buf = FFI::MemoryPointer.new(1024)
required_size = FFI::MemoryPointer.new(:uint32)
skipped_low = FFI::MemoryPointer.new(:ulong)
skipped_high = FFI::MemoryPointer.new(:ulong)
context = FFI::MemoryPointer.new(:pointer)
$stderr.puts "Before BackupRead loop. Context=#{context.read_uint32}, handle=#{handle}"
while BackupRead(handle, buf, buf.size, required_size, false, false, context)
$stderr.puts "After BackupRead(), context=#{context.read_uint32}"
info = Win32StreamId.new buf
$stderr.puts info.inspect
case info[:dw_stream_id]
when BACKUP_ALTERNATE_DATA
streams.push [info.name, info.size]
when BACKUP_DATA
streams.push ['<unnamed>', info.size]
else
$stderr.puts "Unknown stream ID: #{info[:dw_stream_id]}"
end
# BackupSeek fails but GetLastERror indicates success.
unless BackupSeek(handle, info[:size_low], info[:size_high], skipped_low, skipped_high, context)
$stderr.puts "BackupSeek failed: BackupSeek(#{handle}, #{info[:size_low]}, #{info[:size_high]}, #{skipped_low}, #{skipped_high}, #{context}). skipped_low=#{skipped_low.read_long}, skipped_high=#{skipped_high.read_long}, context=#{context.read_uint32}, handle=#{handle}"
err = GetLastError()
err_buf = FFI::MemoryPointer.new :char, 1024
num_chars = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY, nil, err, 0, err_buf, err_buf.size, *[:dword, 0])
raise num_chars == 0 ? nil : err_buf.get_bytes(0, num_chars).strip
end
break
end
return streams
ensure
$stderr.puts 'Closing BackupRead context'
BackupRead(handle, nil, 0, nil, true, false, context)
$stderr.puts 'Closing file handle'
CloseHandle(handle)
end
end
end
end
File.open('streams.txt', 'w+') do |f|
f.puts 'unnamed stream'
end
File.open('streams.txt:ads1', 'w+') do |f|
f.puts 'stream one'
end
File.open('streams.txt:ads2', 'w+') do |f|
f.puts 'stream two'
end
pp File.new('streams.txt').alternate_streams
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment