ileitch-hijack 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/bin/hijack CHANGED
@@ -1,20 +1,25 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'optparse'
3
- $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require File.dirname(__FILE__) + '/../lib/hijack'
4
4
  require 'hijack'
5
5
 
6
6
  options = {}
7
7
  usage = 'Usage: hijack [options] <PID>'
8
- ARGV.clone.options do |opts|
8
+ pid = ARGV.find {|o| o =~ /\d/}
9
+ new_argv = ARGV.clone
10
+ new_argv.delete(pid)
11
+
12
+ new_argv.options do |opts|
9
13
  opts.banner = usage
10
14
  opts.on("--gdb-debug", "Print gdb activity to the console.") { |v| options[:gdb_debug] = v }
15
+ opts.on("-e", "--execute=FILE", String, "Execute the specified file in the remote process.") { |v| options[:execute] = v }
11
16
  opts.on("-h", "--help", "Show this help message.") { puts opts; exit }
12
17
  opts.parse!
13
18
  end
14
19
 
15
- if ARGV.last.nil?
20
+ if pid.nil? || pid.to_s == ''
16
21
  puts usage
17
22
  exit 1
18
23
  end
19
24
 
20
- Hijack.start(ARGV.last, options)
25
+ Hijack.start(pid, options)
@@ -0,0 +1,12 @@
1
+ ActionController::Dispatcher.class_eval do
2
+ class << self
3
+ def dispatch_with_spying(cgi, session_options, output)
4
+ env = cgi.__send__(:env_table)
5
+ puts "#{Time.now.strftime('%Y/%m/%d %H:%M:%S')} - #{env['REMOTE_ADDR']} - #{env['REQUEST_URI']}"
6
+ dispatch_without_spying(cgi, session_options, output)
7
+ end
8
+
9
+ alias_method :dispatch_without_spying, :dispatch
10
+ alias_method :dispatch, :dispatch_with_spying
11
+ end
12
+ end
data/lib/hijack.rb ADDED
@@ -0,0 +1,31 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+ require 'stringio'
3
+ require 'drb'
4
+ require 'drb/unix'
5
+ require 'rbconfig'
6
+ require 'irb'
7
+ require 'irb/completion'
8
+ require 'hijack/console'
9
+ require 'hijack/gdb'
10
+ require 'hijack/payload'
11
+ require 'hijack/helper'
12
+ require 'hijack/workspace'
13
+
14
+ module Hijack
15
+ def self.start(pid, options)
16
+ @@options = options
17
+ Console.new(pid)
18
+ end
19
+
20
+ def self.options
21
+ @@options
22
+ end
23
+
24
+ def self.socket_for(pid)
25
+ "drbunix:/#{socket_path_for(pid)}"
26
+ end
27
+
28
+ def self.socket_path_for(pid)
29
+ "/tmp/hijack.#{pid}.sock"
30
+ end
31
+ end
@@ -0,0 +1,139 @@
1
+ module Hijack
2
+ class Console
3
+ def initialize(pid)
4
+ @pid = pid
5
+ @remote = nil
6
+ check_pid
7
+ str = "=> Hijacking..."
8
+ $stdout.write(str)
9
+ $stdout.flush
10
+ Payload.inject(@pid)
11
+ signal_drb_start
12
+ connect
13
+ $stdout.write("\b" * str.size)
14
+ $stdout.flush
15
+ mirror_process
16
+ banner
17
+ execute_file
18
+ start_output_receiver
19
+ start_irb
20
+ end
21
+
22
+ protected
23
+ def check_pid
24
+ begin
25
+ Process.kill(0, @pid.to_i)
26
+ rescue Errno::EPERM
27
+ puts "=> You do not have the correct permissions to hijack #{@pid}"
28
+ exit 1
29
+ rescue Errno::ESRCH
30
+ puts "=> No such process #{@pid}"
31
+ exit 1
32
+ end
33
+ end
34
+
35
+ def signal_drb_start
36
+ Process.kill('USR1', @pid.to_i)
37
+ loop do
38
+ break if File.exists?(Hijack.socket_path_for(@pid))
39
+ sleep 0.1
40
+ end
41
+ end
42
+
43
+ def connect
44
+ @remote = DRbObject.new(nil, Hijack.socket_for(@pid))
45
+ end
46
+
47
+ module OutputReceiver
48
+ @@mute = false
49
+
50
+ def self.mute
51
+ @@mute = true
52
+ end
53
+
54
+ def self.unmute
55
+ @@mute = false
56
+ end
57
+
58
+ def self.write(where, str)
59
+ Object.const_get(where.upcase).write(str) unless @@mute
60
+ end
61
+
62
+ def self.puts(where, str)
63
+ Object.const_get(where.upcase).puts(str) unless @@mute
64
+ end
65
+ end
66
+
67
+ def start_output_receiver
68
+ DRb.start_service(Hijack.socket_for(Process.pid), OutputReceiver)
69
+ @remote.evaluate("__hijack_output_receiver_ready_#{Process.pid}")
70
+ end
71
+
72
+ def mirror_process
73
+ # Attempt to require all files currently loaded by the remote process so DRb can dump as many objects as possible.
74
+ #
75
+ # We have to first require everything in reverse order and then in the original order.
76
+ # This is because when you require file_a.rb which first sets a constant then requires file_b.rb
77
+ # the $" array will contain file_b.rb before file_a.rb. But if we require file_b.rb before file_a.rb
78
+ # we'll get a missing constant error.
79
+ load_path, loaded_files = @remote.evaluate('[$:, $"]')
80
+ to_load = (loaded_files - $").uniq
81
+ completion_percentage = 0
82
+ str = '=> Mirroring: '
83
+ percent_str = ''
84
+ $stdout.write(str)
85
+ $stdout.flush
86
+ $:.clear
87
+ $:.push(*load_path)
88
+ orig_stderr = $stderr
89
+ $stderr = File.open('/dev/null')
90
+ (to_load.reverse + to_load).each_with_index do |file, i|
91
+ begin
92
+ require file
93
+ rescue Exception, LoadError
94
+ end
95
+ $stdout.write("\b" * percent_str.size)
96
+ $stdout.flush
97
+ percent_str = "#{(((i + 1) / (to_load.size * 2).to_f) * 100.0).round}%"
98
+ $stdout.write(percent_str)
99
+ $stdout.flush
100
+ end
101
+ $stderr = orig_stderr
102
+ $stdout.write("\b" * (str.size + percent_str.size))
103
+ $stdout.flush
104
+ end
105
+
106
+ def start_irb
107
+ ARGV.replace ["--simple-prompt"]
108
+ IRB.setup(nil)
109
+ workspace = Hijack::Workspace.new
110
+ workspace.remote = @remote
111
+ workspace.pid = @pid
112
+ irb = IRB::Irb.new(workspace)
113
+ @CONF = IRB.instance_variable_get(:@CONF)
114
+ @CONF[:IRB_RC].call irb.context if @CONF[:IRB_RC]
115
+ @CONF[:MAIN_CONTEXT] = irb.context
116
+ @CONF[:PROMPT_MODE] = :SIMPLE
117
+ trap('SIGINT') { irb.signal_handle }
118
+ catch(:IRB_EXIT) { irb.eval_input }
119
+ end
120
+
121
+ def banner
122
+ script, ruby_version, platform, hijack_version = @remote.evaluate('[$0, RUBY_VERSION, RUBY_PLATFORM]')
123
+ puts "=> Hijacked #{@pid} (#{script}) (ruby #{ruby_version} [#{platform}])"
124
+ end
125
+
126
+ def execute_file
127
+ if Hijack.options[:execute]
128
+ if File.exists?(Hijack.options[:execute])
129
+ $stdout.write("=> Executing #{Hijack.options[:execute]}... ")
130
+ $stdout.flush
131
+ @remote.evaluate(File.read(Hijack.options[:execute]))
132
+ puts "done!"
133
+ else
134
+ puts "=> Can't find #{Hijack.options[:execute]} to execute!"
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
data/lib/hijack/gdb.rb ADDED
@@ -0,0 +1,52 @@
1
+ # Based on gdb.rb by Jamis Buck, thanks Jamis!
2
+
3
+ module Hijack
4
+ class GDB
5
+ def initialize(pid)
6
+ @verbose = Hijack.options[:gdb_debug]
7
+ exec_path = File.join(Config::CONFIG['bindir'], Config::CONFIG['RUBY_INSTALL_NAME'] + Config::CONFIG['EXEEXT'])
8
+ @gdb = IO.popen("gdb -q #{exec_path} #{pid} 2>&1", 'r+')
9
+ wait
10
+ end
11
+
12
+ def attached_to_ruby_process?
13
+ # TODO: Implement me
14
+ true
15
+ end
16
+
17
+ def eval(cmd)
18
+ call("(void)rb_eval_string(#{cmd.strip.gsub(/"/, '\"').inspect})")
19
+ end
20
+
21
+ def detach
22
+ @gdb.puts('detach')
23
+ wait
24
+ @gdb.puts('quit')
25
+ @gdb.close
26
+ end
27
+
28
+ protected
29
+ def call(cmd)
30
+ puts "(gdb) call #{cmd}" if @verbose
31
+ @gdb.puts("call #{cmd}")
32
+ wait
33
+ end
34
+
35
+ def wait
36
+ lines = []
37
+ line = ''
38
+ while result = IO.select([@gdb])
39
+ next if result.empty?
40
+ c = @gdb.read(1)
41
+ break if c.nil?
42
+ line << c
43
+ break if line == "(gdb) " || line == " >"
44
+ if line[-1] == ?\n
45
+ lines << line
46
+ line = ""
47
+ end
48
+ end
49
+ puts lines.map { |l| "> #{l}" } if @verbose
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,28 @@
1
+ module Hijack
2
+ module Helper
3
+ class << self
4
+ def helpers
5
+ ['hijack_mute', 'hijack_unmute']
6
+ end
7
+
8
+ def find_helper(statements)
9
+ helpers.include?(statements.strip) ? statements.strip : nil
10
+ end
11
+
12
+ def helpers_like(str)
13
+ found = helpers.find_all { |helper| helper =~ Regexp.new(str) }
14
+ found.empty? ? nil : found
15
+ end
16
+
17
+ def hijack_mute(remote)
18
+ Hijack::Console::OutputReceiver.mute
19
+ true
20
+ end
21
+
22
+ def hijack_unmute(remote)
23
+ Hijack::Console::OutputReceiver.unmute
24
+ true
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,78 @@
1
+ module Hijack
2
+ class Payload
3
+ def self.inject(pid)
4
+ gdb = GDB.new(pid)
5
+ unless gdb.attached_to_ruby_process?
6
+ puts "\n=> #{pid} doesn't appear to be a Ruby process!"
7
+ exit 1
8
+ end
9
+ gdb.eval(payload(pid))
10
+ gdb.detach
11
+ end
12
+
13
+ def self.payload(pid)
14
+ <<-EOS
15
+ require 'stringio'
16
+ require 'drb'
17
+
18
+ unless defined?(Hijack)
19
+ module Hijack
20
+ class OutputCopier
21
+ def self.remote
22
+ @@remote
23
+ end
24
+
25
+ def self.start(pid)
26
+ @@remote = DRbObject.new(nil, 'drbunix://tmp/hijack.' + pid + '.sock')
27
+
28
+ class << $stdout
29
+ def write_with_copying(str)
30
+ write_without_copying(str)
31
+ Hijack::OutputCopier.remote.write('stdout', str) rescue nil
32
+ end
33
+ alias_method :write_without_copying, :write
34
+ alias_method :write, :write_with_copying
35
+ end
36
+
37
+ class << $stderr
38
+ def write_with_copying(str)
39
+ write_without_copying(str)
40
+ Hijack::OutputCopier.remote.write('stderr', str) rescue nil
41
+ end
42
+ alias_method :write_without_copying, :write
43
+ alias_method :write, :write_with_copying
44
+ end
45
+ end
46
+ end
47
+
48
+ class Evaluator
49
+ def initialize(context)
50
+ @context = context
51
+ @file = __FILE__
52
+ end
53
+
54
+ def evaluate(rb)
55
+ if rb =~ /__hijack_output_receiver_ready_([\\d]+)/
56
+ OutputCopier.start($1)
57
+ elsif rb =~ /__hijack_get_remote_file_name/
58
+ @file
59
+ else
60
+ @context.instance_eval(rb)
61
+ end
62
+ end
63
+ end
64
+
65
+ def self.start(context)
66
+ return if @service && @service.alive?
67
+ evaluator = Hijack::Evaluator.new(context)
68
+ @service = DRb.start_service('#{Hijack.socket_for(pid)}', evaluator)
69
+ File.chmod(0600, '#{Hijack.socket_path_for(pid)}')
70
+ end
71
+ end
72
+ end
73
+ __hijack_context = self
74
+ Signal.trap('USR1') { Hijack.start(__hijack_context) }
75
+ EOS
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,34 @@
1
+ module Hijack
2
+ HijackCompletionProc = proc {|input|
3
+ bind = IRB.conf[:MAIN_CONTEXT].workspace.binding
4
+ if helpers = Helper.helpers_like(input)
5
+ helpers
6
+ else
7
+ IRB::InputCompletor::CompletionProc.call(input)
8
+ end
9
+ }
10
+ Readline.completion_proc = HijackCompletionProc
11
+
12
+ class Workspace < IRB::WorkSpace
13
+ attr_accessor :remote, :pid
14
+ def evaluate(context, statements, file = __FILE__, line = __LINE__)
15
+ if statements =~ /(IRB\.|exit)/
16
+ super
17
+ elsif helper = Hijack::Helper.find_helper(statements)
18
+ Hijack::Helper.send(helper, remote)
19
+ else
20
+ begin
21
+ result = remote.evaluate(statements)
22
+ rescue DRb::DRbConnError
23
+ puts "=> Lost connection to #{@pid}!"
24
+ exit 1
25
+ end
26
+ if result.kind_of?(DRb::DRbUnknown)
27
+ puts "=> Hijack: Unable to dump unknown object type '#{result.name}', try inspecting it instead."
28
+ return nil
29
+ end
30
+ result
31
+ end
32
+ end
33
+ end
34
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ileitch-hijack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian Leitch
@@ -26,6 +26,15 @@ files:
26
26
  - README.rdoc
27
27
  - Rakefile
28
28
  - TODO
29
+ - lib/hijack
30
+ - lib/hijack.rb
31
+ - lib/hijack/console.rb
32
+ - lib/hijack/gdb.rb
33
+ - lib/hijack/helper.rb
34
+ - lib/hijack/payload.rb
35
+ - lib/hijack/workspace.rb
36
+ - examples/rails_dispatcher.rb
37
+ - bin/hijack
29
38
  has_rdoc: true
30
39
  homepage: http://github.com/ileitch/hijack
31
40
  licenses: