hackedteam/rcs-db

View on GitHub
scripts/rcs-kill.rb

Summary

Maintainability
D
3 days
Test Coverage
#!/usr/bin/env ruby
# encoding: utf-8

require 'singleton'
require 'pp'
require 'optparse'
require 'openssl'

require 'open-uri'
require "net/http"
require "uri"

$watermark_table = {
  "B3lZ3bup" => "VIRGIN",
  "q6OVLjoD"=>"afp",
  "paEr6KlM"=>"alfahad-prod",
  "MBe5kSWG"=>"alfahad-test",
  "HXcMQKsB"=>"ati",
  "CscR5f7w"=>"azns",
  "fsPcaLii"=>"azure",
  "j5DK3mx1"=>"bhr",
  "JZfKkrNd"=>"bsgo",
  "p8DQ8ZjQ"=>"bull",
  "ev68E732"=>"cba",
  "S0l5nD1a"=>"cis",
  "00yRHOTA"=>"cni-old",
  "XTqDh8yF"=>"cni-prod",
  "hC37bvu2"=>"cni-test",
  "7ux8M2tM"=>"csdn",
  "7UBPM2tM"=>"csdn2",
  "JBq6sMVX"=>"csh-pa",
  "XidiPq2M"=>"csh-vr",
  "2ZaXtINx"=>"cusaem",
  "30UN7R0l"=>"demo1",
  "q4afkQWr"=>"devel",
  "GDWwVyrq"=>"dod",
  "Kwh80g9E"=>"edq",
  "Xt1DW33K"=>"fae-demo",
  "Xt0DW33K"=>"fae-poc",
  "j84fj1Ej"=>"gedp",
  "vIByzgbS"=>"gip",
  "KY4pBxoC"=>"gnse",
  "nFGPKB8T"=>"ida-prod",
  "HtAUfHdq"=>"ida-test",
  "74FFGHrh"=>"insa",
  "45u8wvtB"=>"intech-condor",
  "d4vofCKS"=>"intech-falcon",
  "NO7Sy8tl"=>"intech-trial",
  "hr2Sdm23"=>"katie",
  "j4Dnq4lY"=>"knb",
  "whP1Z114"=>"kvant",
  "ZgLs9Knj"=>"macc",
  "De3elpjn"=>"mcdf",
  "in3r0sCU"=>"mdnp",
  "hrSddKc0"=>"mimy",
  "rMMNNu0g"=>"mkih",
  "169hWMEj"=>"moaca",
  "R4B1diMM"=>"mod",
  "069sWhEj"=>"moi",
  "NnkL7M2C"=>"mxnv",
  "h2zYJ264"=>"niss-01",
  "GErh2CTQ"=>"niss-02",
  "B4y9gjKB"=>"nss",
  "PxL2BITH"=>"orf",
  "WksS4Fba"=>"panp",
  "yIQVWBIW"=>"pcit",
  "tXMxdi5M"=>"pemex",
  "f7Ch9Y1H"=>"pf",
  "kjmljtaV"=>"pgj",
  "25GSdf2h"=>"phoebe-demo",
  "Xn6PbS3f"=>"phoebe-prod",
  "an5GeV3M"=>"phoebe-test",
  "AIQ6WcIW"=>"pmo",
  "kJ3kVZXU"=>"pn",
  "4qXth8Sd"=>"pp",
  "HBsxPyXs"=>"pp-8",
  "Q0BnNWlg"=>"rcmp",
  "8hbGc5FW"=>"rcs-demo",
  "tMkD7I7H"=>"rcs-test",
  "UjEpdaZw"=>"rcs-trial-01",
  "wkVoFkAT"=>"rcs-trial-02",
  "QYkYTGxQ"=>"rcs-trial-03",
  "PMc9ux2v"=>"rcs-trial-04",
  "Czu9PEbg"=>"rcs-trial-05",
  "9RDKyUOg"=>"rcs-trial-06",
  "AATHaG4q"=>"rcs-trial-07",
  "VSAO1uA0"=>"rcs-trial-08",
  "fDlKdSxH"=>"rcs-trial-algeria",
  "5P77pcBK"=>"rcs-trial-bin",
  "vs1tUVQR"=>"rcs-trial-bsd",
  "ZwQYmhgx"=>"rcs-trial-fajar",
  "251zfDbu"=>"rcs-trial-farin",
  "Fiw5Th3z"=>"rcs-trial-indo",
  "VNVj9Fkq"=>"rcs-trial-mns",
  "reQsJlwa"=>"rcs-trial-nice",
  "ioMsWQzR"=>"rcs-trial-pcs",
  "NxdP6SRp"=>"rcs-trial-rs",
  "mERh04Tk"=>"rcs-trial-starlight",
  "DC9RWNCC"=>"rcs-trial-vba",
  "B16S0SHJ"=>"rcsspa",
  "s4twpefL"=>"robotec-a",
  "j6dQqpsj"=>"ros-prod",
  "j5ldda3C"=>"ros-test",
  "R4cCdi5M"=>"scico",
  "igGf3d1j"=>"sduc",
  "lBhEn16q"=>"segob",
  "fj2mO5as"=>"senain",
  "KdQdJeaC"=>"sio-prod",
  "ebXMHVBX"=>"sio-test",
  "WCOUQarb"=>"ska",
  "Xuu5XSXT"=>"ssns",
  "M8GQZoCE"=>"tcc-gid",
  "Ra6jeeCa"=>"thdoc",
  "ZjvOuN3m"=>"tnp",
  "wHvPBn7c"=>"trial-dc",
  "Sg96gC96"=>"uaeaf",
  "ZY4eyq9p"=>"uzc",
  'LOuWAplu'=>'devel',
  # post 9.2
  "JMBjaHT9"=>"afp",
  "uKtfllVy"=>"alfahad-prod",
  "gjThyP6x"=>"alfahad-test",
  "FvcNVhh4"=>"ati",
  "6dNlwbd3"=>"azns",
  "bELzoNUB"=>"azure",
  "HRT7ooCy"=>"bhr",
  "XZQ4uETH"=>"bsgo",
  "hgyWgDBO"=>"bull",
  "hK9z9fCE"=>"cba",
  "xjcFNE3x"=>"cis",
  "D6jwkXiM"=>"cni-old",
  "n065fq82"=>"cni-prod",
  "AkP1iABo"=>"cni-test",
  "SEpHtvjx"=>"csdn",
  "Qf3InaPZ"=>"csdn2",
  "lfFyz17L"=>"csh-pa",
  "3QZEPKxT"=>"csh-vr",
  "qp1RXzTF"=>"cusaem",
  "ItHlAzx7"=>"demo1",
  "sCu1FLd0"=>"devel",
  "YFytsEgx"=>"dod",
  "uBC20d6y"=>"edq",
  "XbzKR2xi"=>"fae-demo",
  "F2TykC1t"=>"fae-poc",
  "UcxYysiy"=>"gedp",
  "AejJIvjh"=>"gip",
  "JYyv8PRP"=>"gnse",
  "WQp8H0vU"=>"ida-prod",
  "mspCmQs7"=>"ida-test",
  "QKPHH8aL"=>"insa",
  "69lnyWt4"=>"intech-condor",
  "xvTxqIj8"=>"intech-falcon",
  "PIP8VeRq"=>"intech-trial",
  "tz43IZFS"=>"katie",
  "I2xKl2NN"=>"knb",
  "Fifn91xX"=>"kvant",
  "V1Bd23qs"=>"macc",
  "gPLCyHmq"=>"mcdf",
  "xMIgwcxM"=>"mdnp",
  "88VAWrW2"=>"mimy",
  "4Zm96GbG"=>"mkih",
  "nqJ7Hx0E"=>"moaca",
  "gf2kuVvC"=>"mod",
  "pcjJbpuN"=>"moi",
  "60jn56GG"=>"mxnv",
  "JuqI8lxZ"=>"niss-01",
  "6XnldpwY"=>"niss-02",
  "V4fxNMpT"=>"nss",
  "rkbTODTo"=>"orf",
  "y46fCsQv"=>"panp",
  "gcXlX0bF"=>"pcit",
  "rU6ako2H"=>"pemex",
  "Oc4mYGlW"=>"pf",
  "dUTdPHoi"=>"pgj",
  "p0eSqRpR"=>"phoebe-demo",
  "XvvK5bxf"=>"phoebe-prod",
  "gjBybHT4"=>"phoebe-test",
  "z6dkDK1m"=>"pmo",
  "GakmF6bY"=>"pn",
  "YISVMvwJ"=>"pp",
  "aU9Uus8y"=>"pp-8",
  "0RVNghu4"=>"rcmp",
  "tnUeCBvA"=>"rcs-demo",
  "l3cxcULy"=>"rcs-test",
  "0lrKKswL"=>"rcs-trial-01",
  "CjKAwAdX"=>"rcs-trial-02",
  "llQkyfgy"=>"rcs-trial-03",
  "vcsq9dzR"=>"rcs-trial-04",
  "AjF89CAW"=>"rcs-trial-05",
  "PiOZput5"=>"rcs-trial-06",
  "sUdNJ65t"=>"rcs-trial-07",
  "kiBRDyfW"=>"rcs-trial-08",
  "izaORtWx"=>"rcs-trial-algeria",
  "01cLnPbg"=>"rcs-trial-bin",
  "jUHSyOZH"=>"rcs-trial-bsd",
  "fhucayKn"=>"rcs-trial-fajar",
  "TOyJPjnj"=>"rcs-trial-farin",
  "UgpRDAXC"=>"rcs-trial-indo",
  "oJRcqn2i"=>"rcs-trial-mns",
  "tV99kp09"=>"rcs-trial-nice",
  "diYggXp4"=>"rcs-trial-pcs",
  "YIdwmJpe"=>"rcs-trial-rs",
  "8o5UNlWr"=>"rcs-trial-starlight",
  "eseuZiGA"=>"rcs-trial-vba",
  "1jQhW8WH"=>"rcsspa",
  "eL70Uaxj"=>"robotec-a",
  "Gfxf8s0N"=>"ros-prod",
  "ClQ8VkTr"=>"ros-test",
  "Y52qYyDx"=>"scico",
  "rjzlYuyq"=>"sduc",
  "DPHvibMh"=>"segob",
  "rIz5GoyV"=>"senain",
  "xrZM6zM0"=>"sio-prod",
  "94qvpnXU"=>"sio-test",
  "S91zzhly"=>"ska",
  "0ruJv3Ae"=>"ssns",
  "ixfjPmvy"=>"tcc-gid",
  "DDeGydVT"=>"thdoc",
  "IdQcUI52"=>"tnp",
  "DNulnqc2"=>"trial-dc",
  "6fHETf1v"=>"uaeaf",
  "jyyaixey"=>"uzc"
  }

class Killer
  include Singleton
  
  def request(url, request)
    Timeout::timeout(10) do
      puts "Connecting to: #{url}"
      http = Net::HTTP.new(url, 80)
      http.send_request('WATCHDOG', "#{request}")
    end
  end
  
  def load_from_file(file)
    entries = []
    File.readlines(file).each do |url|
      url = url.strip
      next if url.start_with? "#"
      entries << url
    end
    return entries
  end

  def analyze_scout_v1(sample)
    # Click to start the program
    marker = "\x43\x00\x6C\x00\x69\x00\x63\x00\x6B\x00\x20\x00\x74\x00\x6F\x00\x20\x00\x73\x00\x74\x00\x61\x00\x72\x00\x74\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x70\x00\x72\x00\x6F\x00\x67\x00\x72\x00\x61\x00\x6D\x00\x00\x00\x00\x00" 
    offset = sample.index(marker) 
    raise "marker for watermark not found" unless offset
    offset += marker.size + 28
    watermark = sample[offset..offset+7]
    puts "WATERMARK: #{watermark} (#{$watermark_table[watermark]})"
    
    # Compositionimage/jpeg
    marker = "\x43\x00\x6F\x00\x6D\x00\x70\x00\x6F\x00\x73\x00\x69\x00\x74\x00\x69\x00\x6F\x00\x6E\x00\x00\x00\x69\x00\x6D\x00\x61\x00\x67\x00\x65\x00\x2F\x00\x6A\x00\x70\x00\x65\x00\x67\x00\x00\x00\x00\x00"
    offset = sample.index(marker) 
    raise "marker for ident not found" unless offset
    offset += marker.size + 12
    ident = sample[offset..offset+14]
    ident[0..3] = "RCS_"
    puts "IDENT: " + ident
    
    # UNKNOWN\.tmp
    marker = "\x55\x00\x4E\x00\x4B\x00\x4E\x00\x4F\x00\x57\x00\x4E\x00\x00\x00\x00\x00\x00\x00\x5C\x00\x00\x00\x2E\x00\x74\x00\x6D\x00\x70\x00\x00\x00\x00\x00"
    offset = sample.index(marker) 
    raise "marker for sync not found" unless offset
    offset += marker.size + 40
    sync = sample[offset..offset+63]
    puts "SYNC ADDRESS: " + sync
  end
  
  def analyze_scout_v2(sample)
    # Click to start the program
    marker = "\x43\x00\x6C\x00\x69\x00\x63\x00\x6B\x00\x20\x00\x74\x00\x6F\x00\x20\x00\x73\x00\x74\x00\x61\x00\x72\x00\x74\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x70\x00\x72\x00\x6F\x00\x67\x00\x72\x00\x61\x00\x6D\x00\x00\x00\x00\x00" 
    offset = sample.index(marker) 
    raise "marker for watermark not found" unless offset
    offset += marker.size + 28
    watermark = sample[offset..offset+7]
    puts "WATERMARK: #{watermark} (#{$watermark_table[watermark]})"
    
    # ExitProcesskernel32
    marker = "\x45\x78\x69\x74\x50\x72\x6F\x63\x65\x73\x73\x00\x6B\x00\x65\x00\x72\x00\x6E\x00\x65\x00\x6C\x00\x33\x00\x32\x00\x00\x00\x00\x00" 
    offset = sample.index(marker) 
    raise "marker for ident not found" unless offset
    offset += marker.size
    ident = sample[offset..offset+14]
    ident[0..3] = "RCS_"
    puts "IDENT: " + ident
    
    # %s\%s.tmp
    marker = "\x25\x00\x73\x00\x5C\x00\x25\x00\x73\x00\x2E\x00\x74\x00\x6D\x00\x70\x00\x00\x00"
    offset = sample.index(marker) 
    raise "marker for sync not found" unless offset
    offset += marker.size
    sync = sample[offset..offset+63]
    puts "SYNC ADDRESS: " + sync
  end

  def analyze_scout_v3(sample)
    # Click to start the program
    marker = "\x43\x00\x6C\x00\x69\x00\x63\x00\x6B\x00\x20\x00\x74\x00\x6F\x00\x20\x00\x73\x00\x74\x00\x61\x00\x72\x00\x74\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x70\x00\x72\x00\x6F\x00\x67\x00\x72\x00\x61\x00\x6D\x00\x00\x00\x00\x00"
    offset = sample.index(marker)
    raise "marker for watermark not found" unless offset
    offset += marker.size + 28
    watermark = sample[offset..offset+7]
    puts "WATERMARK: #{watermark} (#{$watermark_table[watermark]})"

    # ExitProcess
    marker = "\x45\x78\x69\x74\x50\x72\x6F\x63\x65\x73\x73\x00\x00\x00\x00\x00"
    offset = sample.index(marker)
    raise "marker for ident not found" unless offset
    offset += marker.size
    ident = sample[offset..offset+14]
    ident[0..3] = "RCS_"
    puts "IDENT: " + ident

    # elitescoutrecover
    marker = "\x65\x00\x6C\x00\x69\x00\x74\x00\x65\x00\x00\x00\x73\x00\x63\x00\x6F\x00\x75\x00\x74\x00\x00\x00\x72\x00\x65\x00\x63\x00\x6F\x00\x76\x00\x65\x00\x72\x00\x00\x00\x00\x00\x00\x00"
    offset = sample.index(marker)
    raise "marker for sync not found" unless offset
    offset += marker.size
    sync = sample[offset..offset+63]
    puts "SYNC ADDRESS: " + sync
  end

  def analyze_scout_v5(sample)
    analyze_scout_offsets(sample, 0x2E6E4, 0x2e4d0, 0x2E7C0)
  end

  def analyze_scout_v51(sample)
    analyze_scout_offsets(sample, 0x2e724, 0x2e510, 0x2e800)
  end

  def analyze_scout_v6(sample)
    analyze_scout_offsets(sample, 0x2fc6c, 0x2fa94, 0x2f9f0)
  end

  def analyze_scout_offsets(sample, wmark, id, sync)
    offset = wmark
    watermark = sample[offset..offset+7]
    puts "WATERMARK: #{watermark} (#{$watermark_table[watermark]})"

    offset = id
    ident = sample[offset..offset+14]
    ident[0..3] = "RCS_"
    puts "IDENT: " + ident

    offset = sync
    sync = sample[offset..offset+63]
    puts "SYNC ADDRESS: " + sync
  end

  def analyze_unknown(sample)
    offset = nil
    $watermark_table.keys.each do |k|
      offset = sample.index(k)
      if offset
        puts "WATERMARK: #{k} (#{$watermark_table[k]})"
        break
      end
    end
    if offset
      # heuristic (completely unreliable) to find the ident...
      ident = sample.index('0000')
      return unless ident
      ident = sample[ident..ident+11].to_i
      puts "IDENT: RCS_" + ident.to_s.rjust(10, '0') unless ident == 0
    else
      puts "No known watermark found!"
    end
  end

  def compare_offset(binary, offset, value)
    version = binary[offset..offset+3]
    return false if version.nil?
    version = version.unpack('I').first
    version == value
  end

  def detect_version(binary)
    return 1 if compare_offset(binary, 0x20714, 1)
    return 2 if compare_offset(binary, 0x20868, 2)
    return 3 if compare_offset(binary, 0x21518, 3)
    return 5 if compare_offset(binary, 0x21956, 5)
    return 5.1 if compare_offset(binary, 0x21946, 5)
    return 6 if compare_offset(binary, 0x21c52, 6)

    return "unknown"
  end

  def analyze(file)
    sample = File.binread(file)
    version = detect_version(sample)
    puts "SCOUT VERSION: #{version}" #if version.is_a? Fixnum

    case(version)
      when 1
        analyze_scout_v1(sample)
      when 2
        analyze_scout_v2(sample)
      when 3
        analyze_scout_v3(sample)
      when 5
        analyze_scout_v5(sample)
      when 5.1
        analyze_scout_v51(sample)
      when 6
        analyze_scout_v6(sample)
      when 7
        analyze_scout_v7(sample)
      else
        puts "UNKNOWN BINARY, falling back to grep..."
        analyze_unknown(sample)
    end

  end

  def check_collector(url)
    puts
    puts "Checking reply of #{url}"
    http = Net::HTTP.new(url, 80)
    resp = http.send_request('GET', "/")
    raise "No response, probably down" unless resp.kind_of? Net::HTTPResponse
    puts "#{url} replied with #{resp.class}..."
    puts resp.body.inspect
  end

  def get_collector_info(url)
    puts
    puts "Requesting info to #{url}"
    info = request(url, 'CHECK')
    raise "Cannot get info, unsupported command by collector" unless info.kind_of? Net::HTTPOK
    address, watermark, version = info.body.split(' ')
    # we don't have the collector address
    if watermark.nil?
      watermark = address.dup
      address = 'unknown'
    end
    puts "Collector ip address: #{address}"
    puts "Collector watermark: #{watermark} (#{$watermark_table[watermark]})"
    puts "Collector version: #{version || 'unknown'}"

    puts
    puts "Requesting SSL info to #{url}"
    ssl_info = get_ssl_info(url, 443)
    pp ssl_info
  end

  def get_ssl_info(url, port)
    tcp_client = TCPSocket.new url, port
    ssl_client = OpenSSL::SSL::SSLSocket.new tcp_client
    ssl_client.connect
    cert = OpenSSL::X509::Certificate.new(ssl_client.peer_cert)
    ssl_client.sysclose
    tcp_client.close

    info = {}

    info[:issuer] = OpenSSL::X509::Name.new(cert.issuer).to_a
    info[:subject] = OpenSSL::X509::Name.new(cert.subject).to_a
    info[:valid_on] = cert.not_before
    info[:valid_until] = cert.not_after

    info
  end

  def kill_collector(url)
    puts
    puts "Killing #{url}"
    ver = request(url, $local_address)
    raise "Bad response, probably not a collector" unless ver.kind_of? Net::HTTPOK
    raise "Kill command not successful" unless ver.size != 0
    puts "Kill command issued to #{url} (version: #{ver.body})"
  end

  def run(options)

    return analyze(options[:analyze]) if options[:analyze]

    $local_address = options[:ip]
    
    unless options[:ip]
      Timeout::timeout(2) do
        $local_address = open("http://bot.whatismyipaddress.com") {|f| f.read}
      end
    end
    # check if it's a valid ip address
    raise "Invalid local IP" if /(?:[0-9]{1,3}\.){3}[0-9]{1,3}/.match($local_address).nil?
    
    puts "Local IP: #{$local_address}"

    begin
      begin
        collectors = [options[:url]] if options[:url]
        collectors = load_from_file(options[:file]) if options[:file]

        if options[:check]
          collectors.each { |coll| check_collector(coll) }
        end

        if options[:info]
          collectors.each { |coll| get_collector_info(coll) }
        end

        if options[:kill]
          collectors.each { |coll| kill_collector(coll) }
        end

        sleep 1 if options[:loop]
      rescue Interrupt
        puts "User asked to exit. Bye bye!"
        exit!
      rescue Exception => e
        puts "ERROR: #{e.message}"
      end

    end while options[:loop]

  rescue Interrupt
    puts "User asked to exit. Bye bye!"
    exit!
  rescue Exception => e
    puts "ERROR: #{e.message}"
    #puts "TRACE: " + e.backtrace.join("\n")
  end
  
  def self.run!(*argv)

    # This hash will hold all of the options parsed from the command-line by OptionParser.
    options = {}

    optparse = OptionParser.new do |opts|
      opts.banner = "Usage: rcs-killer [options]"

      opts.separator ""
      opts.separator "Collector options:"
      opts.on( '-c', '--check URL', String, 'Check if the collector is up and running' ) do |url|
        options[:check] = true
        options[:url] = url
      end

      opts.on( '-i', '--info URL', String, 'Get info from collector' ) do |url|
        options[:info] = true
        options[:url] = url
      end
      
      opts.on( '-I', '--info-all FILE', String, 'Get info from a list of collectors' ) do |file|
        options[:info] = true
        options[:file] = file
      end
      
      opts.on( '-k', '--kill URL', String, 'Kill the collector' ) do |url|
        options[:kill] = true
        options[:url] = url
      end

      opts.on( '-K', '--kill-all FILE', String, 'Kill a list of collectors' ) do |file|
        options[:kill] = true
        options[:file] = file
      end

      opts.on( '-l', '--loop', 'Loop the requests' ) do
        options[:loop] = true
      end

      opts.on( '-a', '--address IP', String, 'Use this address as source ip' ) do |ip|
        options[:ip] = ip
      end

      opts.separator ""
      opts.separator "Leaked samples:"
      opts.on( '-A', '--analyze FILE', String, 'Get info from a leaked agent' ) do |file|
        options[:analyze] = file
      end

      opts.separator ""
      opts.separator "General options:"
      opts.on( '-v', '--verbose', 'Verbose mode' ) do
        options[:verbose] = true
      end

      # This displays the help screen
      opts.on( '-h', '--help', 'Display this screen' ) do
        puts opts
        return 0
      end
    end

    # do the magic parsing
    optparse.parse(argv)

    # error checking
    abort "Don't know what to do..." unless (options[:info] or options[:check] or options[:kill] or options[:analyze])

    # execute the generator
    return Killer.instance.run(options)
  end

end

if __FILE__ == $0
  Killer.run!(*ARGV)
end