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 +9 -4
- data/examples/rails_dispatcher.rb +12 -0
- data/lib/hijack.rb +31 -0
- data/lib/hijack/console.rb +139 -0
- data/lib/hijack/gdb.rb +52 -0
- data/lib/hijack/helper.rb +28 -0
- data/lib/hijack/payload.rb +78 -0
- data/lib/hijack/workspace.rb +34 -0
- metadata +10 -1
data/bin/hijack
CHANGED
@@ -1,20 +1,25 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'optparse'
|
3
|
-
|
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.
|
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
|
20
|
+
if pid.nil? || pid.to_s == ''
|
16
21
|
puts usage
|
17
22
|
exit 1
|
18
23
|
end
|
19
24
|
|
20
|
-
Hijack.start(
|
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.
|
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:
|