ileitch-hijack 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: