# frozen_string_literal: true # # ronin-post_ex - a Ruby API for Post-Exploitation. # # Copyright (c) 2007-2023 Hal Brodigan (postmodern.mod3 at gmail.com) # # ronin-post_ex is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # ronin-post_ex is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with ronin-post_ex. If not, see . # require 'ronin/post_ex/remote_file/stat' require 'ronin/post_ex/resource' require 'fake_io' require 'set' module Ronin module PostEx # # The {RemoteFile} class represents files on a remote system. {RemoteFile} # requires the API object to define either `file_read` and/or `file_write`. # Additionally, {RemoteFile} can optionally use the `file_open`, # `file_close`, `file_tell`, `file_seek` and `file_stat` methods. # # ## Supported API Methods # # * `file_open(path : String, mode : String) -> Integer` # * `file_read(fd : Integer, length : Integer) -> String | nil` # * `file_write(fd : Integer, pos : Integer, data : String) -> Integer` # * `file_seek(fd : Integer, new_pos : Integer, whence : String)` # * `file_tell(fd : Integer) -> Integer` # * `file_ioctl(fd : Integer, command : String | Array[Integer], argument : Object) -> Integer` # * `file_fcntl(fd : Integer, command : String | Array[Integer], argument : Object) -> Integer` # * `file_stat(fd : Integer) => Hash[Symbol, Object] | nil` # * `file_close(fd : Integer)` # * `fs_readfile(path : String) -> String | nil` # * `fs_stat(path : String) => Hash[Symbol, Object] | nil` # class RemoteFile < Resource include FakeIO # # Creates a new remote controlled File object. # # @param [Sessions::Session#file_read, Sessions::Session#file_write] session # The session object that defines the `file_read` and `file_write` # methods. # # @param [String] path # The path of the remote file. # # @param [String] mode # The mode to open the file in. # # @note # This method may use the `file_open` method, if it is defined by # {#session}. # def initialize(session,path,mode='r') @session = session @path = path.to_s @mode = mode.to_s super() end # # Opens a file. # # @param [Sessions::Session#file_read, Sessions::Session#file_write] session # The session object controlling remote files. # # @param [String] path # The path of the remote file. # # @yield [file] # The given block will be passed the newly created file object. # When the block has returned, the File object will be closed. # # @yieldparam [RemoteFile] # The newly created file object. # # @return [RemoteFile, nil] # If no block is given, then the newly opened remote file object will be # returned. If a block was given, then `nil` will be returned. # def self.open(session,path) io = new(session,path) if block_given? yield(io) io.close return else return io end end # Seeks from beginning of file. SEEK_SET = File::SEEK_SET # Seeks from current position. SEEK_CUR = File::SEEK_CUR # Seeks from end of file. SEEK_END = File::SEEK_END # Seeks to next data. SEEK_DATA = (defined?(File::SEEK_DATA) && File::SEEK_DATA) || 3 # Seeks to next hole. SEEK_HOLE = (defined?(File::SEEK_HOLE) && File::SEEK_HOLE) || 4 # Mapping of `SEEK_*` constants to their String values. # # @api private WHENCE = { SEEK_SET => 'SEEK_SET', SEEK_CUR => 'SEEK_CUR', SEEK_END => 'SEEK_END', SEEK_DATA => 'SEEK_DATA', SEEK_HOLE => 'SEEK_HOLE' } # # Sets the position in the file to read. # # @param [Integer] new_pos # The new position to read from. # # @param [Integer] whence # The origin point to seek from. # # @return [Integer] # The new position within the file. # # @raise [ArgumentError] # An invalid whence value was given. # # @note This method may use the `file_seek` API method, if it is defined # by {#session}. # def seek(new_pos,whence=SEEK_SET) clear_buffer! unless WHENCE.has_key?(whence) raise(ArgumentError,"invalid whence value: #{whence.inspect}") end if @session.respond_to?(:file_seek) @session.file_seek(@fd,new_pos,WHENCE[whence]) end @pos = new_pos end resource_method :seek, [:file_seek] # # The current offset in the file. # # @return [Integer] # The current offset in bytes. # # @note # This method may use the `file_tell` API method, if it is defined by # {#session}. # def tell if @session.respond_to?(:file_tell) @pos = @session.file_tell(@fd) else @pos end end resource_method :tell, [:file_tell] # # Executes a low-level command to control or query the IO stream. # # @param [String, Array] command # The IOCTL command. # # @param [Object] argument # Argument of the command. # # @return [Integer] # The return value from the `ioctl`. # # @raise [NotImplementedError] # The API object does not define `file_ioctl`. # # @raise [RuntimeError] # The `file_ioctl` method requires a file-descriptor. # # @note This method requires the `file_ioctl` API method. # def ioctl(command,argument) unless @session.respond_to?(:file_ioctl) raise(NotImplementedError,"#{@session.inspect} does not define file_ioctl") end if @fd == nil raise(RuntimeError,"file_ioctl requires a file-descriptor") end return @session.file_ioctl(@fd,command,argument) end resource_method :ioctl, [:file_ioctl] # # Executes a low-level command to control or query the file stream. # # @param [String, Array] command # The FCNTL command. # # @param [Object] argument # Argument of the command. # # @return [Integer] # The return value from the `fcntl`. # # @raise [NotImplementedError] # The API object does not define `file_fcntl`. # # @note This method requires the `file_fnctl` API method. # def fcntl(command,argument) unless @session.respond_to?(:file_fcntl) raise(NotImplementedError,"#{@session.inspect} does not define file_fcntl") end if @fd == nil raise(RuntimeError,"file_ioctl requires a file-descriptor") end return @session.file_fcntl(@fd,command,argument) end resource_method :fcntl, [:file_fcntl] # # Re-opens the file. # # @param [String] path # The new path for the file. # # @return [RemoteFile] # The re-opened the file. # # @note # This method may use the `file_close` and `file_open` API methods, # if they are defined by {#session}. # def reopen(path) close @path = path.to_s return open end resource_method :reopen, [:file_close, :file_open] # # The status information for the file. # # @return [Stat] # The status information. # # @note This method relies on the `fs_stat` API method. # def stat if @fd Stat.new(@session, fd: @fd) else Stat.new(@session, path: @path) end end resource_method :stat, [:file_stat] # # Flushes the file. # # @return [self] # # @note This method may use the `file_flush` API method, if it is defined # by {#session}. # def flush if @session.respond_to?(:file_flush) @session.file_flush end return self end # # Flushes the file before closing it. # # @return [nil] # def close flush if @mode.include?('w') super() end # # Inspects the open file. # # @return [String] # The inspected open file. # def inspect "#<#{self.class}:#{@path}>" end private # # Attempts calling `file_open` from the API object to open the remote # file. # # @return [Object] # The file descriptor returned by `file_open`. # # @note # This method may use the `file_open` API method, if {#session} defines # it. # def io_open if @session.respond_to?(:file_open) @session.file_open(@path,@mode) end end resource_method :open # Default block size to read file data with. BLOCK_SIZE = 4096 # # Reads a block from the remote file by calling `file_read` or # `fs_readfile` from the API object. # # @return [String, nil] # A block of data from the file or `nil` if there is no more data to be # read. # # @raise [IOError] # The API object does not define `file_read` or `fs_readfile`. # # @note # This method requires either the `fs_readfile` or `file_read` API # methods. # def io_read if @session.respond_to?(:file_read) @session.file_read(@fd,BLOCK_SIZE) elsif @api.respond_to?(:fs_readfile) @eof = true @api.fs_readfile(@path) else raise(IOError,"#{@session.inspect} does not support reading") end end resource_method :read, [:file_read] # # Writes data to the remote file by calling `file_write` from the # API object. # # @param [String] data # The data to write. # # @return [Integer] # The number of bytes written. # # @raise [IOError] # The API object does not define `file_write`. # # @note This method requires the `file_write` API method. # def io_write(data) if @session.respond_to?(:file_write) @pos += @session.file_write(@fd,@pos,data) else raise(IOError,"#{@session.inspect} does not support writing to files") end end resource_method :write, [:file_write] # # Attempts calling `file_close` from the API object to close # the file. # # @note This method may use the `file_close` method, if {#session} defines # it. # def io_close if @session.respond_to?(:file_close) @session.file_close(@fd) end end resource_method :close end end end