emonti-jdi_hook 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ == 1.0.0 / 2009-05-15
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
@@ -0,0 +1,85 @@
1
+ jdi_hook
2
+ by Eric Monti
3
+ http://github.com/emonti/jdi_hook
4
+
5
+ == DESCRIPTION:
6
+
7
+ JdiHook is a ruby-scriptable Java debugger based on and around Sun's Java
8
+ Debugging Interface (JDI) API.
9
+
10
+ == FEATURES/PROBLEMS:
11
+
12
+ * JdiHook aims to provide a scriptable engine around Sun's Java Debugging
13
+ Interface (JDI) using JRuby. The uses of such an engine are intentionally
14
+ open-ended, but the initial rationale was the need for custom instrumentation
15
+ and dynamic analysis tools for Java applications with a reverse engineering
16
+ and vulnerability testing mindset.
17
+
18
+ * You already use JAD or JODE for class decompiling, but you need a quick and
19
+ painless way to observe call trees, hit traces, and other target behaviors
20
+ during runtime. Create bespoke Java runtime debugging scripts just like you
21
+ would using PyDbg or Ragweed on a native target.
22
+
23
+ == SYNOPSIS:
24
+
25
+ require 'rubygems'
26
+ require 'jdi_hook'
27
+
28
+ # Start a java target class. Kind of equivalent to running 'jdb HelloWorld'
29
+ vm = JdiHook.command_line_launch("HelloWorld")
30
+
31
+ # Instantiate and attach debugging event handler. MethodTracer is geared
32
+ # for attaching hooks to method entry and exit events.
33
+ dbg = JdiHook::MethodTracer.new vm, :redirect_stdio => true
34
+
35
+ # Define some handlers for Java method entry and exit events to dump
36
+ # some information about the method invocation. Event handlers for
37
+ # MethodTracer are supplied as Ruby Proc objects (or blocks if you will)
38
+ en_proc = lambda {|this, evt| puts " [*] " << this.notify_entry(evt.method) }
39
+ ex_proc = lambda {|this, evt| puts " [*] " << this.notify_exit(evt.method) }
40
+
41
+ # Configure some event hooks to fire on regex pattern matches by method name
42
+ dbg.meth_hooks = {
43
+ /\.main$/ => { :on_entry => en_proc, :on_exit => ex_proc },
44
+ /.*/ => { :on_entry => en_proc },
45
+ }
46
+
47
+ # "Continue" the target from the debugger
48
+ dbg.go
49
+
50
+
51
+ == REQUIREMENTS:
52
+
53
+ * A JRE (recommend Sun JDK version 1.6+) - http://java.sun.com/javase/downloads/
54
+ * jruby - http://jruby.org
55
+
56
+ == INSTALL:
57
+
58
+ * jruby -S gem sources -a http://gems.github.com # only have to do this once
59
+ * jruby -S gem install emonti-rbkb
60
+
61
+
62
+ == LICENSE:
63
+
64
+ (The MIT License)
65
+
66
+ Copyright (c) 2008 Eric Monti - Matasano Security
67
+
68
+ Permission is hereby granted, free of charge, to any person obtaining
69
+ a copy of this software and associated documentation files (the
70
+ 'Software'), to deal in the Software without restriction, including
71
+ without limitation the rights to use, copy, modify, merge, publish,
72
+ distribute, sublicense, and/or sell copies of the Software, and to
73
+ permit persons to whom the Software is furnished to do so, subject to
74
+ the following conditions:
75
+
76
+ The above copyright notice and this permission notice shall be
77
+ included in all copies or substantial portions of the Software.
78
+
79
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
80
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
81
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
82
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
83
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
84
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
85
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,31 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ Bones.setup
8
+ rescue LoadError
9
+ begin
10
+ load 'tasks/setup.rb'
11
+ rescue LoadError
12
+ raise RuntimeError, '### please install the "bones" gem ###'
13
+ end
14
+ end
15
+
16
+ ensure_in_path 'lib'
17
+ require 'jdi_hook'
18
+
19
+ task :default => 'spec:run'
20
+
21
+ PROJ.name = 'jdi_hook'
22
+ PROJ.authors = 'Eric Monti'
23
+ PROJ.email = 'emonti@matasano.com'
24
+ PROJ.url = 'http://github.com/emonti/jdi_hook'
25
+ PROJ.version = JdiHook::VERSION
26
+ PROJ.rubyforge.name = 'jdi_hook'
27
+ PROJ.readme_file = 'README.rdoc'
28
+
29
+ PROJ.spec.opts << '--color'
30
+
31
+ # EOF
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{jdi_hook}
5
+ s.version = "1.0.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Eric Monti"]
9
+ s.date = %q{2009-05-15}
10
+ s.description = %q{JdiHook is a ruby-scriptable Java debugger based on and around Sun's Java Debugging Interface (JDI) API.}
11
+ s.email = %q{emonti@matasano.com}
12
+ s.extra_rdoc_files = ["History.txt", "README.rdoc"]
13
+ s.files = ["History.txt", "README.rdoc", "Rakefile", "jdi_hook.gemspec", "lib/jdi_hook.rb", "lib/jdi_hook/base_debugger.rb", "lib/jdi_hook/event_thread.rb", "lib/jdi_hook/method_tracer.rb", "lib/jdi_hook/stream_redirect_thread.rb", "samples/base_test.rb", "samples/meth_test.rb", "tasks/ann.rake", "tasks/bones.rake", "tasks/gem.rake", "tasks/git.rake", "tasks/notes.rake", "tasks/post_load.rake", "tasks/rdoc.rake", "tasks/rubyforge.rake", "tasks/setup.rb", "tasks/spec.rake", "tasks/svn.rake", "tasks/test.rake", "tasks/zentest.rake"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://github.com/emonti/jdi_hook}
16
+ s.rdoc_options = ["--main", "README.rdoc"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{jdi_hook}
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = %q{JdiHook is a ruby-scriptable Java debugger based on and around Sun's Java Debugging Interface (JDI) API}
21
+
22
+ if s.respond_to? :specification_version then
23
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
24
+ s.specification_version = 2
25
+
26
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
27
+ s.add_development_dependency(%q<bones>, [">= 2.5.0"])
28
+ else
29
+ s.add_dependency(%q<bones>, [">= 2.5.0"])
30
+ end
31
+ else
32
+ s.add_dependency(%q<bones>, [">= 2.5.0"])
33
+ end
34
+ end
@@ -0,0 +1,166 @@
1
+
2
+ module JdiHook
3
+ VERSION = '1.0.0'
4
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
5
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
6
+
7
+
8
+ include Java
9
+
10
+ include_class [
11
+ "com.sun.jdi.IncompatibleThreadStateException",
12
+ "com.sun.jdi.VMDisconnectedException",
13
+ "com.sun.jdi.InternalException",
14
+ "com.sun.jdi.event.ClassPrepareEvent",
15
+ "com.sun.jdi.event.MethodEntryEvent",
16
+ "com.sun.jdi.event.MethodExitEvent",
17
+ "com.sun.jdi.event.ModificationWatchpointEvent",
18
+ "com.sun.jdi.event.StepEvent",
19
+ "com.sun.jdi.event.ThreadDeathEvent",
20
+ "com.sun.jdi.event.VMDeathEvent",
21
+ "com.sun.jdi.event.VMDisconnectEvent",
22
+ "com.sun.jdi.event.VMStartEvent",
23
+ "com.sun.jdi.request.EventRequest",
24
+ "com.sun.jdi.request.StepRequest",
25
+ "java.io.InputStreamReader",
26
+ "java.lang.InterruptedException",
27
+ "java.util.List",
28
+ "java.util.Map",
29
+ ]
30
+
31
+ ## Class sugar methods
32
+
33
+ # Shorthand for com.sun.jdi.Bootstrap.virtualMachineManager()
34
+ def self.vm_mgr
35
+ com.sun.jdi.Bootstrap.virtualMachineManager()
36
+ end
37
+
38
+ # Shorthand for com.sun.jdi.Bootstrap.virtualMachineManager.allConnectors()
39
+ def self.vm_connectors
40
+ vm_mgr.allConnectors()
41
+ end
42
+
43
+ # Launches a target VM by running it with commandline arguments and
44
+ # attaches to it.
45
+ #
46
+ # Returns: instance of VirtualMachineImpl
47
+ #
48
+ # Arguments:
49
+ # main = String or Array command line for class target
50
+ # o = an optional Hash of parameters:
51
+ # :options = Additional options such as '-classic' (optional)
52
+ # :home = JAVA_HOME path (default: probably your JAVA_HOME)
53
+ # :suspend = whether to suspend the target on start (default: true)
54
+ # :vmexec = what to run through exec (default: java)
55
+ # :quote = the quote char for tokenizing args? (default: ")
56
+ def self.command_line_launch(main, o=nil)
57
+ o ||= {}
58
+ o[:main] = [*main].join(' ')
59
+ o[:options] = [*o[:options]].join(' ') if o[:options]
60
+ con, args = get_connector("com.sun.jdi.CommandLineLaunch", o)
61
+ return con.launch(args)
62
+ end
63
+
64
+ # Attaches to a running VM by process ID on the system.
65
+ #
66
+ # Note: Process attaching by pid only works if both the debugger and target
67
+ # VM are Java 1.6 or higher.
68
+ #
69
+ # Returns: instance of VirtualMachineImpl
70
+ #
71
+ # Arguments:
72
+ # pid = process ID to attach to
73
+ # o = an optional Hash of parameters:
74
+ # :timeout = connection timeout? (optional)
75
+ def self.process_attach(pid, o=nil)
76
+ o ||= {}
77
+ o[:pid] = pid.to_s
78
+ con, args = get_connector("com.sun.jdi.ProcessAttach", o)
79
+ return con.attach(args)
80
+ end
81
+
82
+ # Attaches to a running VM by opening a TCP socket to the target
83
+ #
84
+ # Returns: instance of VirtualMachineImpl
85
+ #
86
+ # Arguments:
87
+ # port = port to connect to
88
+ # o = an optional Hash of parameters:
89
+ # :hostname = host to connect to (default = localhost)
90
+ # :timeout = connection timeout? (optional)
91
+ def self.socket_attach(port, o=nil)
92
+ o ||= {}
93
+ o[:port] = port.to_i
94
+ con, args = get_connector("com.sun.jdi.SocketAttach", o)
95
+ return con.attach(args)
96
+ end
97
+
98
+ # Connects to a running VM by awaiting a connection via TCP socket
99
+ # the target initiates the connection to this listener.
100
+ #
101
+ # Returns: instance of VirtualMachineImpl
102
+ #
103
+ # Arguments:
104
+ # port = port to listen on
105
+ # o = an optional Hash of parameters:
106
+ # :localAddress = address to listen on (default = 0.0.0.0)
107
+ # :timeout = connection timeout? (optional)
108
+ def self.socket_listen(port, o=nil)
109
+ o ||= {}
110
+ o[:port] = port.to_i
111
+ con, args = get_connector("com.sun.jdi.SocketListen", o)
112
+ return con.accept(args)
113
+ end
114
+
115
+ # Finds a connector of the given name from the virtual machine manager
116
+ # and prepares arguments based on its defaultArguments()
117
+ #
118
+ # Returns: an array containing the connector and prepared arguments
119
+ def self.get_connector(name, opts)
120
+ unless con=vm_connectors.find {|c| c.name == name}
121
+ raise "Can't get connector named #{name.inspect}'"
122
+ end
123
+ args = con.defaultArguments()
124
+ opts.each {|k,v| args.get(k.to_s).setValue(v) }
125
+ return [con, args]
126
+ end
127
+
128
+ ##### Cruft added by mr bones:
129
+
130
+ # Returns the version string for the library.
131
+ #
132
+ def self.version
133
+ VERSION
134
+ end
135
+
136
+ # Returns the library path for the module. If any arguments are given,
137
+ # they will be joined to the end of the libray path using
138
+ # <tt>File.join</tt>.
139
+ #
140
+ def self.libpath( *args )
141
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
142
+ end
143
+
144
+ # Returns the lpath for the module. If any arguments are given,
145
+ # they will be joined to the end of the path using
146
+ # <tt>File.join</tt>.
147
+ #
148
+ def self.path( *args )
149
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
150
+ end
151
+
152
+ # Utility method used to require all files ending in .rb that lie in the
153
+ # directory below this file that has the same name as the filename passed
154
+ # in. Optionally, a specific _directory_ name can be passed in such that
155
+ # the _filename_ does not have to be equivalent to the directory.
156
+ #
157
+ def self.require_all_libs_relative_to( fname, dir = nil )
158
+ dir ||= ::File.basename(fname, '.*')
159
+ search_me = ::File.expand_path(
160
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
161
+
162
+ Dir.glob(search_me).sort.each {|rb| require rb}
163
+ end
164
+ end
165
+ JdiHook.require_all_libs_relative_to(__FILE__)
166
+
@@ -0,0 +1,119 @@
1
+ module JdiHook
2
+ # This is a base wrapper class for setting up an event handler for event
3
+ # requests in a debugee target VM.
4
+ #
5
+ # Implementations should override the following callbacks:
6
+ # create_event_requests, receive_event, and cleanup
7
+ class BaseDebugger
8
+ include_class [
9
+ "java.lang.InterruptedException",
10
+ "com.sun.jdi.request.EventRequest",
11
+ ]
12
+
13
+ attr_accessor :class_filters_exc, :class_filters_inc
14
+ attr_reader :vm
15
+
16
+ DEFAULT_EXCLUDES = [
17
+ # Base java/sun stuff to exclude
18
+ "java.*", "javax.*", "sun.*", "com.sun.*",
19
+ # several exclusions for jruby/jirb targets
20
+ "org.jruby.*", "jline.*", "ruby.*", "org.jcodings.*", "jruby.*",
21
+ "org.joni.*"
22
+ ]
23
+
24
+ def initialize(vm, opts={})
25
+ @vm = vm
26
+ @class_filters_exc = opts[:class_filters_exc] || DEFAULT_EXCLUDES
27
+ @class_filters_inc = opts[:class_filters_inc] || Array.new
28
+ @debug_mode = opts[:debug_mode] || 0
29
+ @redirect_stdio = opts[:redirect_stdio]
30
+ end
31
+
32
+ # This method begins the debugging session setting up the event
33
+ # handler and
34
+ def go
35
+ @vm.setDebugTraceMode(@debug_mode)
36
+ create_event_requests(@vm.eventRequestManager() )
37
+ @evt_thread = EventThread.new(self)
38
+ @evt_thread.start()
39
+ if @redirect_stdio
40
+ redirect_target_output($stdout, $stderr)
41
+ end
42
+
43
+ begin
44
+ @vm.resume()
45
+ @evt_thread.join()
46
+ rescue InterruptedException => e
47
+ STDERR.puts "** Got InterruptedException: #{exc}"
48
+ ensure
49
+ cleanup()
50
+ @evt_thread = nil
51
+ end
52
+ end
53
+
54
+ # This method adds class exclusion and inclusion filters to an
55
+ # event request. It should be called from overridden create_event_requests
56
+ # implementations while setting up new event requests for the target VM.
57
+ def filter_classes(req)
58
+ if exc=@class_filters_exc
59
+ exc.each {|e| req.addClassExclusionFilter(e) }
60
+ end
61
+ if inc=@class_filters_inc
62
+ inc.each {|i| req.addClassFilter(i) }
63
+ end
64
+ end
65
+
66
+ # This is a callback to set up event requests.
67
+ # It is called with one argument 'mgr' which is the event request
68
+ # manager for the target VM.
69
+ def create_event_requests(mgr)
70
+ # stub
71
+ end
72
+
73
+ # This is a callback to dispatch incoming events
74
+ # override it to perform whatever specific actions you want based
75
+ # on the event type
76
+ def receive_event(event)
77
+ # stub
78
+ end
79
+
80
+ # This is a callback to handle the end of the debugging session
81
+ # override it to perform any cleanup tasks or wrap up.
82
+ def cleanup()
83
+ # stub
84
+ end
85
+
86
+ # This method starts and joins threads to redirect stderr and stdout from
87
+ # the target process. Capturing IO this way from the target is generally
88
+ # only possible if the target is connected through a command line
89
+ # launch connector.
90
+ #
91
+ # This method should only be called from the 'go' method after the
92
+ # primary event thread has been started but before it has been
93
+ # joined.
94
+ def redirect_target_output(out=$stdout, err=$stderr)
95
+ if process = @vm.process()
96
+ unless @evt_thread and @evt_thread.connected
97
+ raise "the event thread has not yet been started"
98
+ end
99
+ out_thread = StreamRedirectThread.new("target stdout reader",
100
+ process.getInputStream(),
101
+ "Process STDOUT",
102
+ out)
103
+
104
+ err_thread = StreamRedirectThread.new("target stderr reader",
105
+ process.getErrorStream(),
106
+ "Process STDERR",
107
+ err)
108
+
109
+ out_thread.start()
110
+ err_thread.start()
111
+ out_thread.join()
112
+ err_thread.join()
113
+ return [out_thread, err_thread]
114
+ else
115
+ STDERR.puts "WARNING: can't redirect output on this target'"
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,68 @@
1
+ module JdiHook
2
+ class EventThread < java.lang.Thread
3
+ include_class [
4
+ "java.lang.InterruptedException",
5
+ "com.sun.jdi.VMDisconnectedException",
6
+ "com.sun.jdi.event.VMStartEvent",
7
+ "com.sun.jdi.event.VMDeathEvent",
8
+ "com.sun.jdi.event.VMDisconnectEvent",
9
+ ]
10
+
11
+ attr_reader :connected
12
+
13
+ def initialize( handler )
14
+ @handler = handler
15
+ @vm = handler.vm
16
+ end
17
+
18
+ def start(*args)
19
+ @connected = true
20
+ super(*args)
21
+ end
22
+
23
+ def run()
24
+ queue = @vm.eventQueue()
25
+ while @connected
26
+ begin
27
+ events = queue.remove
28
+ events.each do |evt|
29
+ @connected=false if evt.is_a? VMDisconnectEvent
30
+ @handler.receive_event(evt) if @handler.respond_to?(:receive_event)
31
+ end
32
+ events.resume()
33
+ rescue InterruptedException
34
+ # ignore
35
+ rescue VMDisconnectedException
36
+ # A VMDisconnectedException has happened while dealing with
37
+ # another event. We need to bail so that we terminate correctly.
38
+ # XXX do we really need to do this?
39
+ @connected=false
40
+ break
41
+ end
42
+ end
43
+ end
44
+
45
+ # A VMDisconnectedException has happened while dealing with
46
+ # another event. We need to flush the event queue, dealing only
47
+ # with exit events (VMDeath, VMDisconnect) so that we terminate
48
+ # correctly.
49
+ def handleDisconnectedException
50
+ queue = @vm.eventQueue()
51
+ while @connected
52
+ begin
53
+ eventSet = queue.remove()
54
+ eventSet.each do |event|
55
+ if VMDeathEvent === event
56
+ vmDeathEvent(event)
57
+ elsif VMDisconnectEvent === event
58
+ vmDisconnectEvent(event)
59
+ end
60
+ end
61
+ rescue InterruptedException
62
+ # ignore
63
+ end
64
+ end
65
+ end
66
+
67
+ end
68
+ end