3 # Kyle C. Hale (c) 2012/2013
5 # Ruby implementation of a tool to identify the callbacks
6 # in a driver given the source for the driver and it's compiled kernel
7 # object. It does this by finding all static functions in the module and scanning
8 # the source to identify sites where a static function is being assigned to a variable.
13 # By default, all callbacks will be added as valid entry points into the module.
14 # For now, entries must manually be added and deleted by module author
17 # - remove hardcoded directories from this and chirsto script
22 # - .i files are generated to confirm callback functions
24 # - source files and makefiles must be in same dir. Wont work for big
25 # multi-level hierarchical source trees
27 # - this produces JSON output, including valid entry points, which
28 # (both entering into callback functions and returning from calls
29 # to the kernel) can be modified by driver developer
31 # - this modifies the original driver source, as well as the original
34 # - You must provide your **GUEST'S** System.map file for this script to reference
40 CHRISTO = "/root/kyle/kyle/bnx/christo-s.pl"
41 GM_INIT = "v3_guard_mod_init"
42 GM_INIT_INTERNAL = "__guard_mod_init_internal"
43 GM_MACRO = "GM_ENTRY_REQUEST"
44 WRAP_FILE = "entry_wrapper.S"
46 GUARD_INIT_NR = 0x6000
47 GUARD_ENTER_NR = 0x6001
48 GUARD_EXIT_NR = 0x6002
51 def replace_callback_assigns(callbacks, file)
55 # ================== OPTIONS =========================
59 optparse = OptionParser.new do|opts|
61 opts.banner = "Usage: guard_modules.rb [options] ..."
63 options[:verbose] = false
64 opts.on( '-v', '--verbose', 'Allow verbose output') do
65 options[:verbose] = true
68 opts.on( '-h', '--help', 'Show this information') do
73 opts.on('-s', '--source <driver,src,files,...,>', '(C) source code for driver module') do |file|
74 options[:srcfile] = file.split(',')
77 opts.on('-p', '--privs <requested,privileges>', 'Privileges requested. Currently numerical') do |privs|
78 options[:privs] = privs.split(',')
81 opts.on('-k', '--ko DRIVER KO', 'The compiled and linked kernel object for the driver') do |file|
85 opts.on('-m', '--makefile MAKEFILE', 'The driver\'s makefile') do |file|
89 opts.on('-o', '--output OUTFILE', 'The file to which static module info will be written') do |file|
93 opts.on('-n', '--name NAME', 'The name of the guarded module') do |name|
97 options[:instr] = false
98 opts.on('i', '--instrument', 'Dump out the functions which are *NOT* to be instrumented, i.e. those that aren\'t callbacks') do
99 options[:instr] = true
106 mand = [:ko, :srcfile, :mf, :name]
107 missing = mand.select{ |parm| options[parm].nil? }
108 if not missing.empty?
109 puts "Missing options: #{missing.join(', ')}"
113 rescue OptionParser::InvalidOption, OptionParser::MissingArgument
119 # ===================== END OPTIONS ==============================
121 $mkdir = File.expand_path(File.dirname(options[:mf]))
122 $mkbase = File.basename(options[:mf])
123 #TODO: this needs to be specified explicitly
124 $srcpath = options[:srcfile][0]
125 $srcdir = File.expand_path(File.dirname(options[:srcfile][0]))
126 $srcbase = File.basename(options[:srcfile][0])
128 # global list of functions
129 # TODO: THIS IS NOT WORKING!!!! e.g., on bnx2_find_max_ring, objdump is not finding static funs
130 funcs = `objdump -t #{options[:ko]} | awk '\$3==\"F\" {print \$6}'`
132 # only static functions
133 stats = `objdump -t #{options[:ko]} | awk '\$2==\"l\"&&\$3==\"F\" {print \$6}'`
137 puts "Running preprocessor..."
142 # generate .i files (preprocessing)
145 abort("Problem cleaning") if not system "make clean > /dev/null"
147 #TODO parameterize this!
148 kerndir = "/v-test/guard_modules/gm_guest/kyle_gl/"
150 # run the kernel make process, and repeat the build of each .o with preprocessing directives
151 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'`
153 Dir.chdir(kerndir) do
157 abort ("Problem compiling.") if not system i
163 # read in preprocessed files
164 Dir.glob("#{$mkdir}/*.i", File::FNM_DOTMATCH).each do |fname|
165 prep_files[fname] = IO.readlines(fname)
166 puts "Reading in preprocessed file: #{fname}"
167 if not prep_files[fname]
168 puts "could not open #{fname}"
180 # look for callback assignments, this tells us that this
181 # static function is indeed being used as a callback
182 prep_files.each_value do |src|
183 tmp_defs = src.grep(/.*=\s*#{s}\s*(;|,)/)
184 defs.concat(tmp_defs) if not tmp_defs.empty?
187 callbacks.push(s) if not defs.empty?
189 if (options[:verbose])
190 puts "Possible callback assignment for #{s} : " if not defs.empty?
191 defs.each {|i| print "\t#{i}\n"}
197 f = funcs.split("\n")
199 puts "Searching for driver's module_init routine..."
202 # TODO: make this work for multiple files
203 File.open($srcpath, 'r') do |file|
205 if new_str =~ /module_init\((.*?)\)/
206 puts "Found it, it's #{$1}"
211 # back up (TODO: ALL) source/makefiles
212 `cp #{$mkdir}/#{$mkbase} #{$mkdir}/#{$mkbase}.bak`
213 `cp #{options[:srcfile][0]} #{$srcdir}/#{$srcbase}.bak`
215 # generate a restore script
219 for i in `ls *.bak | sed 's/\.bak//g'`; do mv $i.bak $i; done
224 # TODO: WARNING WARNING: assumes source files and makefiles are in same place
225 File.open("#{$srcdir}/gm_restore.sh", "w", 0744) do |file|
230 /* BEGIN GENERATED */
231 #define V3_GUARD_ENTER_HCALL_NR #{GUARD_ENTER_NR}
232 #define V3_GUARD_EXIT_HCALL_NR #{GUARD_EXIT_NR}
233 #define V3_GUARD_INIT_HCALL_NR #{GUARD_INIT_NR}
234 #define V3_GUARDED_MODULE_ID 0xA3AEEA3AEEBADBADULL
235 #define #{GM_MACRO}(fun) _gm_entry_req_##fun
236 int __init #{GM_INIT} (void);
239 # for assembly linkage
240 callbacks.each do |c|
241 str +=<<-eos "int _gm_entry_req_#{c}(void);\n"
242 __asm__(".globl _gm_entry_req_#{c};\\
243 _gm_entry_req_#{c}:\\
246 movq $#{GUARD_ENTER_NR}, %rax;\\
251 movq $#{GUARD_EXIT_NR}, %rax;\\
258 str += "/* END GENREATED */\n"
261 /* BEGIN GENERATED */
262 int #{GM_INIT_INTERNAL} (void) __attribute__((section (".text"),no_instrument_function));
264 int #{GM_INIT_INTERNAL} (void) {
268 __asm__ __volatile__ ("vmmcall"
270 :"0" (V3_GUARD_INIT_HCALL_NR), "b" (V3_GUARDED_MODULE_ID));
273 printk("Guest GM: error initializing guarded module\\n");
275 printk("Guest GM: successfully initialized guarded module\\n");
278 ret_orig = #{mod_init}();
281 printk("Guest GM: error calling original init\\n");
283 printk("Guest GM: successfully called original driver init\\n");
287 __asm__ __volatile__("vmmcall" : "=a" (ret) : "0" (V3_GUARD_EXIT_HCALL_NR));
290 printk("Guest GM: error doing initial exit\\n");
297 int __init #{GM_INIT} (void) {
298 return #{GM_INIT_INTERNAL}();
304 # GENERATE THE CALLBACK WRAPPERS
307 # .callback = myfunc;
309 # will be replaced with
310 # .callback = GM_ENTRY_REQUEST(myfunc);
313 # .callback = __gm_entry_req_myfunc; (this is an assembly stub)
316 callbacks.each do |c|
318 void (*#{c}_ptr)() = #{c};
322 end_str += "/* END GENERATED */"
324 # append instrumentation functions to end of source file
326 File.open($srcpath, "r") do |file|
330 # fixup module init calls with our own
331 new_str = new_str.gsub(/__init #{mod_init}/, "#{mod_init}")
332 new_str = new_str.gsub(/module_init\(.*?\);/, "module_init(#{GM_INIT});")
334 # fixup callback assignments with our macro
335 # TODO: make this work for multiple source files
336 callbacks.each do |c|
337 new_str = new_str.gsub(/(.*=\s*)#{c}(\s*(;|,))/) {
338 "#{$1}#{GM_MACRO}(#{c})#{$2}"
342 # put these at the top of the file, after the includes
343 new_str = new_str.insert(new_str.rindex(/#include.*?[>"]/), str)
346 File.open($srcpath, "w+") do |file|
351 abort ("Error cleaning") if not system "make clean > /dev/null 2>&1"
352 abort("Error rebuilding module") if not system "make > /dev/null 2>&1"
357 # if an outfile is specified, output static module info
360 # First generate wrapped module
362 ko = File.expand_path(options[:ko])
363 puts "Running christo with Kernel object: " + ko
364 christo_output = `#{CHRISTO} --kern #{ko} 2>&1 | grep ACCEPT`
366 christo_output.each_line { |x| out_arr.push(x.chomp.gsub(/\s*ACCEPT\s*/, '')) }
368 wrap_name = File.basename(ko).sub(/\.ko$/,'_wrapped.ko')
372 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)]
375 abort("Problem building wrapped module") if not system "make > /dev/null 2>&1"
377 static_info['module_name'] = options[:name]
379 # get the content hash of .text
380 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
381 static_info['size'] = `objdump -h #{wrap_name} | grep -P "\s+\.text\s+" | awk '{print $3}'`.to_i(16)
383 # find offset to hcall (we've ensured it's in .text)
384 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)
386 # each callback function will have ONE valid entry point, the address of the VMMCALL
387 # in its function body
388 static_info['entry_points'] = callbacks.collect do |c|
389 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)
393 static_info['ret_points'] = ret_points
394 static_info['privileges'] = options[:privs]
396 File.open(options[:out], "w") do |file|
397 file.write(JSON.pretty_generate(static_info))
401 callbacks.each {|c| puts c }