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 +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:
|