hijack 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +18 -0
- data/README.rdoc +121 -0
- data/Rakefile +4 -0
- data/TODO +2 -0
- data/bin/hijack +26 -0
- data/bin/hijack.compiled.rbc +0 -0
- data/examples/rails_dispatcher.rb +11 -0
- data/lib/hijack/console.rb +137 -0
- data/lib/hijack/gdb.rb +111 -0
- data/lib/hijack/helper.rb +45 -0
- data/lib/hijack/payload.rb +110 -0
- data/lib/hijack/workspace.rb +37 -0
- data/lib/hijack.rb +31 -0
- data/lib/hijack.rbc +0 -0
- data/tasks/gem.rake +41 -0
- data/test/app.rb +7 -0
- data/test/gc.rb +14 -0
- data/test/test.rb +20 -0
- data/test/test.rbc +0 -0
- metadata +73 -0
data/COPYING
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2009 Ian Leitch
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
= Hijack: Provides an irb session to an existing ruby process.
|
2
|
+
|
3
|
+
== Intro
|
4
|
+
|
5
|
+
Hijack allows you to connect to any ruby process and execute code as if it were a normal irb session. Hijack does not require your target process to require any hijacking code, hijack is able to connect to any ruby process. It achieves this by using gdb to inject a payload into the process which starts up a DRb server, hijack then detaches gdb and reconnects via DRb. Please note that gdb will halt your target process while it is attached, though the injection process is very quick and your process should only be halted for a few milliseconds.
|
6
|
+
|
7
|
+
Hijack uses DRb over a unix socket file, so you need to be on the same machine as the process you want to hijack. This is by design for security reasons. You also need to run the hijack client as the same user as the remote process.
|
8
|
+
|
9
|
+
== Using Hijack
|
10
|
+
|
11
|
+
$ hijack 16451
|
12
|
+
=> Hijacked 16451 (my_script.rb) (ruby 1.8.7 [i686-darwin9])
|
13
|
+
>>
|
14
|
+
|
15
|
+
== Using ruby-debug
|
16
|
+
|
17
|
+
Hijack can be used to start ruby-debug in your target process, for example:
|
18
|
+
|
19
|
+
$ hijack 61378
|
20
|
+
=> Hijacked 61378 (/opt/local/bin/thin) (ruby 1.8.7 [i686-darwin9])
|
21
|
+
>> hijack_debug_mode
|
22
|
+
=> true
|
23
|
+
|
24
|
+
We've enabled debug mode, but we still need to insert a breakpoint:
|
25
|
+
|
26
|
+
>> ActionController::Dispatcher.class_eval do
|
27
|
+
>> class << self
|
28
|
+
>> def dispatch_with_debugger(cgi, session_options, output)
|
29
|
+
>> debugger
|
30
|
+
>> dispatch_without_debugger(cgi, session_options, output)
|
31
|
+
>> end
|
32
|
+
>> alias_method :dispatch_without_debugger, :dispatch
|
33
|
+
>> alias_method :dispatch, :dispatch_with_debugger
|
34
|
+
>> end
|
35
|
+
>> end
|
36
|
+
|
37
|
+
Now tell hijack that we can start debugging:
|
38
|
+
|
39
|
+
>> hijack_debug_start
|
40
|
+
Connected.
|
41
|
+
|
42
|
+
Point your browser at http://0.0.0.0:3000 to trigger the breakpoint and ruby-debug's console will appear:
|
43
|
+
|
44
|
+
>> hijack_debug_start
|
45
|
+
Connected.
|
46
|
+
(eval):5
|
47
|
+
(rdb:4) step
|
48
|
+
/Users/ian/Projects/zioko/vendor/rails/actionpack/lib/action_controller/dispatcher.rb:28 new(output).dispatch_cgi(cgi, session_options)
|
49
|
+
(rdb:4) backtrace
|
50
|
+
--> #0 /Users/ian/Projects/zioko/vendor/rails/actionpack/lib/action_controller/dispatcher.rb:28 in 'dispatch_without_debugger'
|
51
|
+
#1 (eval):5 in 'dispatch'
|
52
|
+
#2 /opt/local/lib/ruby/gems/1.8/gems/thin-1.2.2/lib/rack/adapter/rails.rb:81 in 'call'
|
53
|
+
#3 /opt/local/lib/ruby/gems/1.8/gems/thin-1.2.2/lib/rack/adapter/rails.rb:69 in 'call'
|
54
|
+
#4 /opt/local/lib/ruby/gems/1.8/gems/thin-1.2.2/lib/thin/connection.rb:76 in 'pre_process'
|
55
|
+
#5 /opt/local/lib/ruby/gems/1.8/gems/thin-1.2.2/lib/thin/connection.rb:74 in 'pre_process'
|
56
|
+
#6 /opt/local/lib/ruby/gems/1.8/gems/thin-1.2.2/lib/thin/connection.rb:57 in 'process'
|
57
|
+
#7 /opt/local/lib/ruby/gems/1.8/gems/thin-1.2.2/lib/thin/connection.rb:42 in 'receive_data'
|
58
|
+
(rdb:4)
|
59
|
+
|
60
|
+
One caveat is that ruby-debug's 'irb' command will not work because the debug connection is remote, you also can't use hijack's irb console whilst debugging. A future version of hijack will hopefully allow you to switch between the two.
|
61
|
+
|
62
|
+
== Monkey Patching
|
63
|
+
|
64
|
+
Just as in a normal irb session, you can redefine the code running in your target process.
|
65
|
+
|
66
|
+
This example redefines ActionController's dispatcher to print basic request activity (the example can be found in examples/rails_dispatcher.rb).
|
67
|
+
|
68
|
+
Start up a Thin web server:
|
69
|
+
|
70
|
+
$ thin start
|
71
|
+
>> Using rails adapter
|
72
|
+
>> Thin web server (v1.2.2 codename I Find Your Lack of Sauce Disturbing)
|
73
|
+
>> Maximum connections set to 1024
|
74
|
+
>> Listening on 0.0.0.0:3000, CTRL+C to stop
|
75
|
+
|
76
|
+
In another console hijack the Thin process:
|
77
|
+
|
78
|
+
$ ps | grep thin
|
79
|
+
61160 ttys001 0:01.43 /opt/local/bin/ruby /opt/local/bin/thin start
|
80
|
+
|
81
|
+
$ hijack 61160
|
82
|
+
=> Hijacked 61160 (/opt/local/bin/thin) (ruby 1.8.7 [i686-darwin9])
|
83
|
+
>> ActionController::Dispatcher.class_eval do
|
84
|
+
?> class << self
|
85
|
+
>> def dispatch_with_spying(cgi, session_options, output)
|
86
|
+
>> env = cgi.__send__(:env_table)
|
87
|
+
>> puts "#{Time.now.strftime('%Y/%m/%d %H:%M:%S')} - #{env['REMOTE_ADDR']} - #{env['REQUEST_URI']}"
|
88
|
+
>> dispatch_without_spying(cgi, session_options, output)
|
89
|
+
>> end
|
90
|
+
>> alias_method :dispatch_without_spying, :dispatch
|
91
|
+
>> alias_method :dispatch, :dispatch_with_spying
|
92
|
+
>> end
|
93
|
+
>> end
|
94
|
+
|
95
|
+
Point your browser to http://0.0.0.0:3000.
|
96
|
+
|
97
|
+
Back in hijack you'll see your browsing activity:
|
98
|
+
|
99
|
+
2009/08/22 14:24:48 - 127.0.0.1 - /
|
100
|
+
2009/08/22 14:24:53 - 127.0.0.1 - /login
|
101
|
+
2009/08/22 14:24:54 - 127.0.0.1 - /signup
|
102
|
+
|
103
|
+
Instead of pasting your code into hijack, you can pass hijack the -e option execute a local file on the target process.
|
104
|
+
|
105
|
+
== Process Output
|
106
|
+
|
107
|
+
By default hijack will forward your process output to the hijack client. This can get a little messy if your trying to write code at the same time as the target process writes to STDOUT/STDERR. You can mute and unmute the process with:
|
108
|
+
|
109
|
+
>> hijack_mute
|
110
|
+
=> true
|
111
|
+
>> hijack_unmute
|
112
|
+
=> true
|
113
|
+
|
114
|
+
For ease of use, hijack helper methods are discoverable with tab completion. hi<tab><tab> will give you a list of available helpers.
|
115
|
+
|
116
|
+
== Process Mirroring
|
117
|
+
|
118
|
+
DRb cannot dump objects to the hijack client for types that are not loaded in the client process. E.g if the remote process had required ActiveRecord and you tried to dump ActiveRecord::Base back to the client, DRb would instead return a DRb::Unknown object as ActiveRecord
|
119
|
+
isn't loaded in the hijack client.
|
120
|
+
|
121
|
+
To work around this, when hijack connects to a remote process it will inspect all the files required by the process and also attempt to require them itself. This may not work for all object types however so you may still get a warning when an object cannot be dumped.
|
data/Rakefile
ADDED
data/TODO
ADDED
data/bin/hijack
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require File.dirname(__FILE__) + '/../lib/hijack'
|
4
|
+
require 'hijack'
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
usage = 'Usage: hijack [options] <PID>'
|
8
|
+
pid = ARGV.find {|o| o =~ /\d/}
|
9
|
+
new_argv = ARGV.clone
|
10
|
+
new_argv.delete(pid)
|
11
|
+
|
12
|
+
new_argv.options do |opts|
|
13
|
+
opts.banner = usage
|
14
|
+
opts.on("--gdb-debug", "Print gdb activity to the console.") { |v| options[:gdb_debug] = true }
|
15
|
+
opts.on("--mute", "Ignore stdout/stderr writes from the remote process.") { |v| options[:mute] = true }
|
16
|
+
opts.on("-e", "--execute=FILE", String, "Execute the specified file in the remote process and then disconnect.") { |v| options[:execute] = v }
|
17
|
+
opts.on("-h", "--help", "Show this help message.") { puts opts; exit }
|
18
|
+
opts.parse!
|
19
|
+
end
|
20
|
+
|
21
|
+
if pid.nil? || pid.to_s == ''
|
22
|
+
puts usage
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
|
26
|
+
Hijack.start(pid, options)
|
Binary file
|
@@ -0,0 +1,11 @@
|
|
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
|
+
alias_method :dispatch_without_spying, :dispatch
|
9
|
+
alias_method :dispatch, :dispatch_with_spying
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,137 @@
|
|
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
|
+
connect
|
12
|
+
$stdout.write("\b" * str.size)
|
13
|
+
$stdout.flush
|
14
|
+
mirror_process
|
15
|
+
banner
|
16
|
+
execute_file
|
17
|
+
OutputReceiver.start(@remote) unless Hijack.options[:mute]
|
18
|
+
start_irb
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
def check_pid
|
23
|
+
begin
|
24
|
+
Process.kill(0, @pid.to_i)
|
25
|
+
rescue Errno::EPERM
|
26
|
+
puts "=> You do not have the correct permissions to hijack #{@pid}"
|
27
|
+
exit 1
|
28
|
+
rescue Errno::ESRCH
|
29
|
+
puts "=> No such process #{@pid}"
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def connect
|
35
|
+
loop do
|
36
|
+
break if File.exists?(Hijack.socket_path_for(@pid))
|
37
|
+
sleep 0.01
|
38
|
+
end
|
39
|
+
@remote = DRbObject.new(nil, Hijack.socket_for(@pid))
|
40
|
+
end
|
41
|
+
|
42
|
+
module OutputReceiver
|
43
|
+
class << self
|
44
|
+
def mute
|
45
|
+
@mute = true
|
46
|
+
end
|
47
|
+
|
48
|
+
def unmute(remote)
|
49
|
+
start(remote) unless @started
|
50
|
+
@mute = false
|
51
|
+
end
|
52
|
+
|
53
|
+
def write(where, str)
|
54
|
+
Object.const_get(where.upcase).write(str) unless @mute
|
55
|
+
end
|
56
|
+
|
57
|
+
def puts(where, str)
|
58
|
+
Object.const_get(where.upcase).puts(str) unless @mute
|
59
|
+
end
|
60
|
+
|
61
|
+
def start(remote)
|
62
|
+
DRb.start_service(Hijack.socket_for(Process.pid), self)
|
63
|
+
remote.evaluate("__hijack_output_receiver_ready_#{Process.pid}")
|
64
|
+
@started = true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def mirror_process
|
70
|
+
# Attempt to require all files currently loaded by the remote process so DRb can dump as many objects as possible.
|
71
|
+
#
|
72
|
+
# We have to first require everything in reverse order and then in the original order.
|
73
|
+
# This is because when you require file_a.rb which first sets a constant then requires file_b.rb
|
74
|
+
# the $" array will contain file_b.rb before file_a.rb. But if we require file_b.rb before file_a.rb
|
75
|
+
# we'll get a missing constant error.
|
76
|
+
load_path, loaded_files = @remote.evaluate('[$:, $"]')
|
77
|
+
to_load = (loaded_files - $").uniq
|
78
|
+
completion_percentage = 0
|
79
|
+
str = '=> Mirroring: '
|
80
|
+
percent_str = ''
|
81
|
+
$stdout.write(str)
|
82
|
+
$stdout.flush
|
83
|
+
$:.clear
|
84
|
+
$:.push(*load_path)
|
85
|
+
orig_stderr = $stderr
|
86
|
+
$stderr = File.open('/dev/null')
|
87
|
+
(to_load.reverse + to_load).each_with_index do |file, i|
|
88
|
+
begin
|
89
|
+
require file
|
90
|
+
rescue Exception, LoadError
|
91
|
+
end
|
92
|
+
$stdout.write("\b" * percent_str.size)
|
93
|
+
$stdout.flush
|
94
|
+
percent_str = "#{(((i + 1) / (to_load.size * 2).to_f) * 100.0).round}%"
|
95
|
+
$stdout.write(percent_str)
|
96
|
+
$stdout.flush
|
97
|
+
end
|
98
|
+
$stderr = orig_stderr
|
99
|
+
$stdout.write("\b" * (str.size + percent_str.size))
|
100
|
+
$stdout.flush
|
101
|
+
end
|
102
|
+
|
103
|
+
def start_irb
|
104
|
+
ARGV.replace ["--simple-prompt"]
|
105
|
+
IRB.setup(nil)
|
106
|
+
workspace = Hijack::Workspace.new
|
107
|
+
workspace.remote = @remote
|
108
|
+
workspace.pid = @pid
|
109
|
+
irb = IRB::Irb.new(workspace)
|
110
|
+
@CONF = IRB.instance_variable_get(:@CONF)
|
111
|
+
@CONF[:IRB_RC].call irb.context if @CONF[:IRB_RC]
|
112
|
+
@CONF[:MAIN_CONTEXT] = irb.context
|
113
|
+
@CONF[:PROMPT_MODE] = :SIMPLE
|
114
|
+
trap('SIGINT') { irb.signal_handle }
|
115
|
+
catch(:IRB_EXIT) { irb.eval_input }
|
116
|
+
end
|
117
|
+
|
118
|
+
def banner
|
119
|
+
script, ruby_version, platform, hijack_version = @remote.evaluate('[$0, RUBY_VERSION, RUBY_PLATFORM]')
|
120
|
+
puts "=> Hijacked #{@pid} (#{script}) (ruby #{ruby_version} [#{platform}])"
|
121
|
+
end
|
122
|
+
|
123
|
+
def execute_file
|
124
|
+
if Hijack.options[:execute]
|
125
|
+
if File.exists?(Hijack.options[:execute])
|
126
|
+
$stdout.write("=> Executing #{Hijack.options[:execute]}... ")
|
127
|
+
$stdout.flush
|
128
|
+
@remote.evaluate(File.read(Hijack.options[:execute]))
|
129
|
+
puts "done!"
|
130
|
+
exit
|
131
|
+
else
|
132
|
+
puts "=> Can't find #{Hijack.options[:execute]} to execute!"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/lib/hijack/gdb.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# Based on gdb.rb by Jamis Buck, thanks Jamis!
|
2
|
+
|
3
|
+
module Hijack
|
4
|
+
class GDB
|
5
|
+
def initialize(pid)
|
6
|
+
@pid = pid
|
7
|
+
@verbose = Hijack.options[:gdb_debug]
|
8
|
+
@exec_path = File.join(Config::CONFIG['bindir'], Config::CONFIG['RUBY_INSTALL_NAME'] + Config::CONFIG['EXEEXT'])
|
9
|
+
attach
|
10
|
+
end
|
11
|
+
|
12
|
+
def ensure_attached_to_ruby_process
|
13
|
+
unless backtrace.any? {|line| line =~ /rb_/}
|
14
|
+
puts "\n=> #{@pid} doesn't appear to be a Ruby process!"
|
15
|
+
detach
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def ensure_main_thread_not_blocked_by_join
|
21
|
+
if backtrace.any? {|line| line =~ /rb_thread_join/}
|
22
|
+
puts "\n=> Unable to hijack #{@pid} because the main thread is blocked waiting for another thread to join."
|
23
|
+
puts "=> Check that you are using the most recent version of hijack, a newer version may have solved this shortcoming."
|
24
|
+
detach
|
25
|
+
exit 1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def eval(cmd)
|
30
|
+
call("(void)rb_eval_string(#{cmd.strip.gsub(/"/, '\"').inspect})")
|
31
|
+
end
|
32
|
+
|
33
|
+
def detach
|
34
|
+
return unless @gdb
|
35
|
+
exec('detach')
|
36
|
+
exec('quit')
|
37
|
+
@backtrace = nil
|
38
|
+
@gdb.close
|
39
|
+
@gdb = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
def attach
|
44
|
+
loop do
|
45
|
+
@gdb = IO.popen("gdb -q #{@exec_path} #{@pid} 2>&1", 'r+')
|
46
|
+
wait
|
47
|
+
if backtrace.first =~ /Previous frame inner to this frame/
|
48
|
+
detach
|
49
|
+
sleep 0.1
|
50
|
+
else
|
51
|
+
break
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
ensure_attached_to_ruby_process
|
56
|
+
ensure_main_thread_not_blocked_by_join
|
57
|
+
set_trap_pending
|
58
|
+
set_breakpoint
|
59
|
+
continue
|
60
|
+
clear_breakpoint
|
61
|
+
end
|
62
|
+
|
63
|
+
def backtrace
|
64
|
+
@backtrace ||= exec('bt').reverse
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_trap_pending
|
68
|
+
exec("set variable (int)rb_trap_pending=1")
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_breakpoint
|
72
|
+
exec("break rb_trap_exec")
|
73
|
+
end
|
74
|
+
|
75
|
+
def clear_breakpoint
|
76
|
+
exec("clear rb_trap_exec")
|
77
|
+
end
|
78
|
+
|
79
|
+
def continue
|
80
|
+
exec('continue')
|
81
|
+
end
|
82
|
+
|
83
|
+
def call(cmd)
|
84
|
+
exec("call #{cmd}")
|
85
|
+
end
|
86
|
+
|
87
|
+
def exec(str)
|
88
|
+
puts("(gdb) #{str}") if @verbose
|
89
|
+
@gdb.puts(str)
|
90
|
+
wait
|
91
|
+
end
|
92
|
+
|
93
|
+
def wait
|
94
|
+
lines = []
|
95
|
+
line = ''
|
96
|
+
while result = IO.select([@gdb])
|
97
|
+
next if result.empty?
|
98
|
+
c = @gdb.read(1)
|
99
|
+
break if c.nil?
|
100
|
+
line << c
|
101
|
+
break if line == "(gdb) " || line == " >"
|
102
|
+
if line[-1] == ?\n
|
103
|
+
lines << line
|
104
|
+
line = ""
|
105
|
+
end
|
106
|
+
end
|
107
|
+
puts lines.map { |l| "> #{l}" } if @verbose
|
108
|
+
lines
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Hijack
|
2
|
+
module Helper
|
3
|
+
class << self
|
4
|
+
def helpers
|
5
|
+
methods.find_all {|meth| meth =~ /^hijack_/}
|
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(remote)
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def hijack_debug_mode(remote)
|
28
|
+
hijack_mute(remote)
|
29
|
+
require 'rubygems'
|
30
|
+
require 'ruby-debug'
|
31
|
+
remote.evaluate(<<-RB)
|
32
|
+
require 'rubygems'
|
33
|
+
require 'ruby-debug'
|
34
|
+
Debugger.start_remote
|
35
|
+
RB
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def hijack_debug_start(remote)
|
40
|
+
Debugger.start_client
|
41
|
+
true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Hijack
|
2
|
+
class Payload
|
3
|
+
def self.inject(pid)
|
4
|
+
gdb = nil
|
5
|
+
trap('SIGINT') do
|
6
|
+
puts
|
7
|
+
@received_sigint = true
|
8
|
+
end
|
9
|
+
gdb = GDB.new(pid)
|
10
|
+
gdb.eval(payload(pid))
|
11
|
+
gdb.detach
|
12
|
+
exit if @received_sigint
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.payload(pid)
|
16
|
+
<<-RUBY
|
17
|
+
require 'stringio'
|
18
|
+
require 'drb'
|
19
|
+
|
20
|
+
unless defined?(Hijack)
|
21
|
+
module Hijack
|
22
|
+
class OutputCopier
|
23
|
+
def self.remote
|
24
|
+
@remote
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.stop
|
28
|
+
@remote = nil
|
29
|
+
[$stdout, $stderr].each do |io|
|
30
|
+
if io.respond_to?(:write_with_copying)
|
31
|
+
class << io
|
32
|
+
alias_method :write, :write_without_copying
|
33
|
+
remove_method :write_with_copying
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.start(pid)
|
40
|
+
@remote = DRbObject.new(nil, 'drbunix://tmp/hijack.' + pid + '.sock')
|
41
|
+
|
42
|
+
class << $stdout
|
43
|
+
def write_with_copying(str)
|
44
|
+
write_without_copying(str)
|
45
|
+
begin
|
46
|
+
Hijack::OutputCopier.remote.write('stdout', str)
|
47
|
+
rescue Exception
|
48
|
+
Hijack.stop
|
49
|
+
end
|
50
|
+
end
|
51
|
+
alias_method :write_without_copying, :write
|
52
|
+
alias_method :write, :write_with_copying
|
53
|
+
end
|
54
|
+
|
55
|
+
class << $stderr
|
56
|
+
def write_with_copying(str)
|
57
|
+
write_without_copying(str)
|
58
|
+
begin
|
59
|
+
Hijack::OutputCopier.remote.write('stderr', str)
|
60
|
+
rescue Exception
|
61
|
+
Hijack.stop
|
62
|
+
end
|
63
|
+
end
|
64
|
+
alias_method :write_without_copying, :write
|
65
|
+
alias_method :write, :write_with_copying
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Evaluator
|
71
|
+
def initialize(context)
|
72
|
+
@context = context
|
73
|
+
@file = __FILE__
|
74
|
+
end
|
75
|
+
|
76
|
+
def evaluate(rb)
|
77
|
+
if rb =~ /__hijack_output_receiver_ready_([\\d]+)/
|
78
|
+
OutputCopier.start($1)
|
79
|
+
elsif rb =~ /__hijack_get_remote_file_name/
|
80
|
+
@file
|
81
|
+
elsif rb =~ /__hijack_exit/
|
82
|
+
Hijack.stop
|
83
|
+
else
|
84
|
+
@context.instance_eval(rb)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.start(context)
|
90
|
+
return if @service && @service.alive?
|
91
|
+
evaluator = Hijack::Evaluator.new(context)
|
92
|
+
@service = DRb.start_service('#{Hijack.socket_for(pid)}', evaluator)
|
93
|
+
File.chmod(0600, '#{Hijack.socket_path_for(pid)}')
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.stop
|
97
|
+
begin
|
98
|
+
OutputCopier.stop
|
99
|
+
@service.stop_service
|
100
|
+
@service = nil
|
101
|
+
rescue Exception
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
Hijack.start(self)
|
107
|
+
RUBY
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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\./
|
16
|
+
super
|
17
|
+
elsif statements.strip =~ /^exit/
|
18
|
+
remote.evaluate('__hijack_exit') rescue nil
|
19
|
+
super
|
20
|
+
elsif helper = Hijack::Helper.find_helper(statements)
|
21
|
+
Hijack::Helper.send(helper, remote)
|
22
|
+
else
|
23
|
+
begin
|
24
|
+
result = remote.evaluate(statements)
|
25
|
+
rescue DRb::DRbConnError
|
26
|
+
puts "=> Lost connection to #{@pid}!"
|
27
|
+
exit 1
|
28
|
+
end
|
29
|
+
if result.kind_of?(DRb::DRbUnknown)
|
30
|
+
puts "=> Hijack: Unable to dump unknown object type '#{result.name}', try inspecting it instead."
|
31
|
+
return nil
|
32
|
+
end
|
33
|
+
result
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
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
|
data/lib/hijack.rbc
ADDED
Binary file
|
data/tasks/gem.rake
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rake/gempackagetask'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
HIJACK_VERSION = '0.1.9'
|
5
|
+
|
6
|
+
task :clean => :clobber_package
|
7
|
+
|
8
|
+
spec = Gem::Specification.new do |s|
|
9
|
+
s.name = 'hijack'
|
10
|
+
s.version = HIJACK_VERSION
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.summary =
|
13
|
+
s.description = 'Provides an irb session to an existing ruby process.'
|
14
|
+
s.author = "Ian Leitch"
|
15
|
+
s.email = 'ian.leitch@systino.net'
|
16
|
+
s.homepage = 'http://github.com/ileitch/hijack'
|
17
|
+
s.has_rdoc = false
|
18
|
+
s.files = %w(COPYING TODO README.rdoc Rakefile) + Dir.glob("{lib,test,tasks,bin,examples}/**/*")
|
19
|
+
s.bindir = "bin"
|
20
|
+
s.executables = %w( hijack )
|
21
|
+
s.require_path = "lib"
|
22
|
+
end
|
23
|
+
|
24
|
+
Rake::GemPackageTask.new(spec) do |p|
|
25
|
+
p.gem_spec = spec
|
26
|
+
end
|
27
|
+
|
28
|
+
namespace :gem do
|
29
|
+
desc "Update the gemspec for GitHub's gem server"
|
30
|
+
task :github do
|
31
|
+
File.open("hijack.gemspec", 'w') { |f| f << YAML.dump(spec) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
task :install => [:clean, :clobber, :package] do
|
36
|
+
sh "sudo gem install pkg/#{spec.full_name}.gem"
|
37
|
+
end
|
38
|
+
|
39
|
+
task :uninstall => :clean do
|
40
|
+
sh "sudo gem uninstall -v #{HIJACK_VERSION} -x hijack"
|
41
|
+
end
|
data/test/app.rb
ADDED
data/test/gc.rb
ADDED
data/test/test.rb
ADDED
data/test/test.rbc
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hijack
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.9
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ian Leitch
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-23 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Provides an irb session to an existing ruby process.
|
17
|
+
email: ian.leitch@systino.net
|
18
|
+
executables:
|
19
|
+
- hijack
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- COPYING
|
26
|
+
- TODO
|
27
|
+
- README.rdoc
|
28
|
+
- Rakefile
|
29
|
+
- lib/hijack/console.rb
|
30
|
+
- lib/hijack/gdb.rb
|
31
|
+
- lib/hijack/helper.rb
|
32
|
+
- lib/hijack/payload.rb
|
33
|
+
- lib/hijack/workspace.rb
|
34
|
+
- lib/hijack.rb
|
35
|
+
- lib/hijack.rbc
|
36
|
+
- test/app.rb
|
37
|
+
- test/gc.rb
|
38
|
+
- test/test.rb
|
39
|
+
- test/test.rbc
|
40
|
+
- tasks/gem.rake
|
41
|
+
- bin/hijack
|
42
|
+
- bin/hijack.compiled.rbc
|
43
|
+
- examples/rails_dispatcher.rb
|
44
|
+
has_rdoc: true
|
45
|
+
homepage: http://github.com/ileitch/hijack
|
46
|
+
licenses: []
|
47
|
+
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.3.5
|
69
|
+
signing_key:
|
70
|
+
specification_version: 3
|
71
|
+
summary: Provides an irb session to an existing ruby process.
|
72
|
+
test_files: []
|
73
|
+
|