--- /dev/null
+#!/usr/bin/ruby
+
+# Kyle C. Hale (c) 2012/2013
+#
+# Ruby implementation of a tool to identify the callbacks
+# in a driver given the source for the driver and it's compiled kernel
+# object. It does this by finding all static functions in the module and scanning
+# the source to identify sites where a static function is being assigned to a variable.
+#
+# Dependencies:
+# json ruby gem
+#
+# By default, all callbacks will be added as valid entry points into the module.
+# For now, entries must manually be added and deleted by module author
+#
+# TODO:
+# - remove hardcoded directories from this and chirsto script
+#
+#
+# NOTES:
+#
+# - .i files are generated to confirm callback functions
+#
+# - source files and makefiles must be in same dir. Wont work for big
+# multi-level hierarchical source trees
+#
+# - this produces JSON output, including valid entry points, which
+# (both entering into callback functions and returning from calls
+# to the kernel) can be modified by driver developer
+#
+# - this modifies the original driver source, as well as the original
+# driver Makefile
+#
+# - You must provide your **GUEST'S** System.map file for this script to reference
+
+require 'optparse'
+require 'rubygems'
+require 'json'
+
+CHRISTO = "/root/kyle/kyle/bnx/christo-s.pl"
+GM_INIT = "v3_guard_mod_init"
+GM_INIT_INTERNAL = "__guard_mod_init_internal"
+GM_MACRO = "GM_ENTRY_REQUEST"
+WRAP_FILE = "entry_wrapper.S"
+
+GUARD_INIT_NR = 0x6000
+GUARD_ENTER_NR = 0x6001
+GUARD_EXIT_NR = 0x6002
+
+
+def replace_callback_assigns(callbacks, file)
+
+end
+
+# ================== OPTIONS =========================
+
+options = {}
+
+optparse = OptionParser.new do|opts|
+
+ opts.banner = "Usage: guard_modules.rb [options] ..."
+
+ options[:verbose] = false
+ opts.on( '-v', '--verbose', 'Allow verbose output') do
+ options[:verbose] = true
+ end
+
+ opts.on( '-h', '--help', 'Show this information') do
+ puts opts
+ exit
+ end
+
+ opts.on('-s', '--source <driver,src,files,...,>', '(C) source code for driver module') do |file|
+ options[:srcfile] = file.split(',')
+ end
+
+ opts.on('-p', '--privs <requested,privileges>', 'Privileges requested. Currently numerical') do |privs|
+ options[:privs] = privs.split(',')
+ end
+
+ opts.on('-k', '--ko DRIVER KO', 'The compiled and linked kernel object for the driver') do |file|
+ options[:ko] = file
+ end
+
+ opts.on('-m', '--makefile MAKEFILE', 'The driver\'s makefile') do |file|
+ options[:mf] = file
+ end
+
+ opts.on('-o', '--output OUTFILE', 'The file to which static module info will be written') do |file|
+ options[:out] = file
+ end
+
+ opts.on('-n', '--name NAME', 'The name of the guarded module') do |name|
+ options[:name] = name
+ end
+
+ options[:instr] = false
+ opts.on('i', '--instrument', 'Dump out the functions which are *NOT* to be instrumented, i.e. those that aren\'t callbacks') do
+ options[:instr] = true
+ end
+end
+
+
+begin
+ optparse.parse!
+ mand = [:ko, :srcfile, :mf, :name]
+ missing = mand.select{ |parm| options[parm].nil? }
+ if not missing.empty?
+ puts "Missing options: #{missing.join(', ')}"
+ puts optparse
+ exit
+ end
+rescue OptionParser::InvalidOption, OptionParser::MissingArgument
+ puts $!.to_s
+ puts optparse
+ exit
+end
+
+# ===================== END OPTIONS ==============================
+
+$mkdir = File.expand_path(File.dirname(options[:mf]))
+$mkbase = File.basename(options[:mf])
+#TODO: this needs to be specified explicitly
+$srcpath = options[:srcfile][0]
+$srcdir = File.expand_path(File.dirname(options[:srcfile][0]))
+$srcbase = File.basename(options[:srcfile][0])
+
+# global list of functions
+# TODO: THIS IS NOT WORKING!!!! e.g., on bnx2_find_max_ring, objdump is not finding static funs
+funcs = `objdump -t #{options[:ko]} | awk '\$3==\"F\" {print \$6}'`
+
+# only static functions
+stats = `objdump -t #{options[:ko]} | awk '\$2==\"l\"&&\$3==\"F\" {print \$6}'`
+
+callbacks = []
+
+puts "Running preprocessor..."
+prep_files = {}
+
+Dir.chdir($mkdir) do
+
+ # generate .i files (preprocessing)
+ ENV['PWD'] = Dir.pwd
+
+ abort("Problem cleaning") if not system "make clean > /dev/null"
+
+ #TODO parameterize this!
+ kerndir = "/v-test/guard_modules/gm_guest/kyle_gl/"
+
+ # run the kernel make process, and repeat the build of each .o with preprocessing directives
+ cmds = `make V=1 2>&1 | grep -P "gcc.*?\\s+\\-o\\s+.*?[^mod]\\.c" | sed 's/\\s\\+-c/ -E/g'| sed 's/\\(-o\\s\\+.*\\?\\)\\.o/\\1.i/g'`
+
+ Dir.chdir(kerndir) do
+ ENV['PWD'] = Dir.pwd
+
+ cmds.each do |i|
+ abort ("Problem compiling.") if not system i
+ end
+ end
+
+ ENV['PWD'] = Dir.pwd
+
+ # read in preprocessed files
+ Dir.glob("#{$mkdir}/*.i", File::FNM_DOTMATCH).each do |fname|
+ prep_files[fname] = IO.readlines(fname)
+ puts "Reading in preprocessed file: #{fname}"
+ if not prep_files[fname]
+ puts "could not open #{fname}"
+ exit
+ end
+ end
+end
+
+ENV['PWD'] = Dir.pwd
+
+stats.each do |stat|
+ s = stat.chomp
+ defs = []
+
+ # look for callback assignments, this tells us that this
+ # static function is indeed being used as a callback
+ prep_files.each_value do |src|
+ tmp_defs = src.grep(/.*=\s*#{s}\s*(;|,)/)
+ defs.concat(tmp_defs) if not tmp_defs.empty?
+ end
+
+ callbacks.push(s) if not defs.empty?
+
+ if (options[:verbose])
+ puts "Possible callback assignment for #{s} : " if not defs.empty?
+ defs.each {|i| print "\t#{i}\n"}
+ end
+
+end
+
+if (options[:instr])
+ f = funcs.split("\n")
+
+ puts "Searching for driver's module_init routine..."
+ mod_init = nil
+
+ # TODO: make this work for multiple files
+ File.open($srcpath, 'r') do |file|
+ new_str = file.read
+ if new_str =~ /module_init\((.*?)\)/
+ puts "Found it, it's #{$1}"
+ mod_init = $1
+ end
+ end
+
+ # back up (TODO: ALL) source/makefiles
+ `cp #{$mkdir}/#{$mkbase} #{$mkdir}/#{$mkbase}.bak`
+ `cp #{options[:srcfile][0]} #{$srcdir}/#{$srcbase}.bak`
+
+ # generate a restore script
+ shell = `which sh`
+ restore = <<-eos
+#!#{shell}
+for i in `ls *.bak | sed 's/\.bak//g'`; do mv $i.bak $i; done
+make clean && make
+rm -f *.i
+eos
+
+ # TODO: WARNING WARNING: assumes source files and makefiles are in same place
+ File.open("#{$srcdir}/gm_restore.sh", "w", 0744) do |file|
+ file.puts restore
+ end
+
+ str = <<-eos
+/* BEGIN GENERATED */
+#define V3_GUARD_ENTER_HCALL_NR #{GUARD_ENTER_NR}
+#define V3_GUARD_EXIT_HCALL_NR #{GUARD_EXIT_NR}
+#define V3_GUARD_INIT_HCALL_NR #{GUARD_INIT_NR}
+#define V3_GUARDED_MODULE_ID 0xA3AEEA3AEEBADBADULL
+#define #{GM_MACRO}(fun) _gm_entry_req_##fun
+int __init #{GM_INIT} (void);
+eos
+
+# for assembly linkage
+callbacks.each do |c|
+ str +=<<-eos "int _gm_entry_req_#{c}(void);\n"
+__asm__(".globl _gm_entry_req_#{c};\\
+ _gm_entry_req_#{c}:\\
+ popq %r11; \\
+ pushq %rax;\\
+ movq $#{GUARD_ENTER_NR}, %rax;\\
+ vmmcall;\\
+ popq %rax;\\
+ callq #{c};\\
+ pushq %rax;\\
+ movq $#{GUARD_EXIT_NR}, %rax;\\
+ vmmcall;\\
+ popq %rax;\\
+ pushq %r11;\\
+ ret;");
+eos
+end
+str += "/* END GENREATED */\n"
+
+end_str = <<-eos
+/* BEGIN GENERATED */
+int #{GM_INIT_INTERNAL} (void) __attribute__((section (".text"),no_instrument_function));
+
+int #{GM_INIT_INTERNAL} (void) {
+ int ret, ret_orig;
+
+ /* GUARD INIT */
+ __asm__ __volatile__ ("vmmcall"
+ :"=a" (ret)
+ :"0" (V3_GUARD_INIT_HCALL_NR), "b" (V3_GUARDED_MODULE_ID));
+
+ if (ret < 0) {
+ printk("Guest GM: error initializing guarded module\\n");
+ } else {
+ printk("Guest GM: successfully initialized guarded module\\n");
+ }
+
+ ret_orig = #{mod_init}();
+
+ if (ret_orig < 0) {
+ printk("Guest GM: error calling original init\\n");
+ } else {
+ printk("Guest GM: successfully called original driver init\\n");
+ }
+
+ /* GUARD EXIT */
+ __asm__ __volatile__("vmmcall" : "=a" (ret) : "0" (V3_GUARD_EXIT_HCALL_NR));
+
+ if (ret < 0) {
+ printk("Guest GM: error doing initial exit\\n");
+ return ret;
+ }
+
+ return ret_orig;
+}
+
+int __init #{GM_INIT} (void) {
+ return #{GM_INIT_INTERNAL}();
+}
+
+/* END GENERATED */
+eos
+
+# GENERATE THE CALLBACK WRAPPERS
+#
+# statements like
+# .callback = myfunc;
+#
+# will be replaced with
+# .callback = GM_ENTRY_REQUEST(myfunc);
+#
+# which expands to
+# .callback = __gm_entry_req_myfunc; (this is an assembly stub)
+#
+
+ callbacks.each do |c|
+ end_str += <<-eos
+ void (*#{c}_ptr)() = #{c};
+eos
+ end
+
+ end_str += "/* END GENERATED */"
+
+ # append instrumentation functions to end of source file
+ new_str = ""
+ File.open($srcpath, "r") do |file|
+ new_str = file.read
+ end
+
+ # fixup module init calls with our own
+ new_str = new_str.gsub(/__init #{mod_init}/, "#{mod_init}")
+ new_str = new_str.gsub(/module_init\(.*?\);/, "module_init(#{GM_INIT});")
+
+ # fixup callback assignments with our macro
+ # TODO: make this work for multiple source files
+ callbacks.each do |c|
+ new_str = new_str.gsub(/(.*=\s*)#{c}(\s*(;|,))/) {
+ "#{$1}#{GM_MACRO}(#{c})#{$2}"
+ }
+ end
+
+ # put these at the top of the file, after the includes
+ new_str = new_str.insert(new_str.rindex(/#include.*?[>"]/), str)
+ new_str += end_str
+
+ File.open($srcpath, "w+") do |file|
+ file.puts new_str
+ end
+
+ Dir.chdir($mkdir) do
+ abort ("Error cleaning") if not system "make clean > /dev/null 2>&1"
+ abort("Error rebuilding module") if not system "make > /dev/null 2>&1"
+ end
+
+end
+
+# if an outfile is specified, output static module info
+# in JSON format
+if (options[:out])
+ # First generate wrapped module
+
+ ko = File.expand_path(options[:ko])
+ puts "Running christo with Kernel object: " + ko
+ christo_output = `#{CHRISTO} --kern #{ko} 2>&1 | grep ACCEPT`
+ out_arr = []
+ christo_output.each_line { |x| out_arr.push(x.chomp.gsub(/\s*ACCEPT\s*/, '')) }
+
+ wrap_name = File.basename(ko).sub(/\.ko$/,'_wrapped.ko')
+
+ ret_points = []
+ out_arr.each do |x|
+ ret_points.push Array[x, `objdump -d #{wrap_name} | sed -n '/__wrap_#{x}>/,/ret/p' | grep vmmcall | tail -1 | cut -f1 -d':' | sed 's/\s//g'`.to_i(16)]
+ end
+
+ abort("Problem building wrapped module") if not system "make > /dev/null 2>&1"
+ static_info = {}
+ static_info['module_name'] = options[:name]
+
+ # get the content hash of .text
+ static_info['content_hash'] = `objdump -h #{wrap_name} | grep -P "\s+\.text\s+" | awk '{print "dd if=#{wrap_name} bs=1 count=$[0x" $3 "] skip=$[0x" $6 "] 2>/dev/null"}' | bash | md5sum - | awk '{print $1}'`.chomp
+ static_info['size'] = `objdump -h #{wrap_name} | grep -P "\s+\.text\s+" | awk '{print $3}'`.to_i(16)
+
+ # find offset to hcall (we've ensured it's in .text)
+ static_info['hcall_offset'] = `objdump -d #{wrap_name} | sed -n '/#{GM_INIT_INTERNAL}/,/ret/p'| grep vmmcall | cut -f1 -d':'|sed 's/\s//g'`.to_i(16)
+
+ # each callback function will have ONE valid entry point, the address of the VMMCALL
+ # in its function body
+ static_info['entry_points'] = callbacks.collect do |c|
+ c_off = `objdump -d #{wrap_name} | sed -n '/_gm_entry_req_#{c}>/,/vmmcall/p' | tail -1 | cut -f1 -d':' | sed 's/\s//g'`.to_i(16)
+ Array[c, c_off]
+ end
+
+ static_info['ret_points'] = ret_points
+ static_info['privileges'] = options[:privs]
+
+ File.open(options[:out], "w") do |file|
+ file.write(JSON.pretty_generate(static_info))
+ end
+
+else
+ callbacks.each {|c| puts c }
+end