daily pastebin goal
16%
SHARE
TWEET

Untitled

a guest Nov 20th, 2017 64 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/usr/bin/env ruby
  2. ###################################################
  3. # ----------------------------------------------- #
  4. # Fuzz Origami Ruby gem with mutated PDF files    #
  5. # ----------------------------------------------- #
  6. #                                                 #
  7. # Each test case is written to 'fuzz.pdf' in the  #
  8. # current working directory.                      #
  9. #                                                 #
  10. # Crashes and the associated backtrace are saved  #
  11. # in the 'crashes' directory in the current       #
  12. # working directory.                              #
  13. #                                                 #
  14. ###################################################
  15. # ~ bcoles
  16.  
  17. require 'date'
  18. require 'origami'
  19. require 'colorize'
  20. require 'fileutils'
  21. require 'timeout'
  22. require 'securerandom'
  23.  
  24. include Origami
  25.  
  26. VERBOSE = false
  27. OUTPUT_DIR = "#{Dir.pwd}/crashes".freeze
  28.  
  29. #
  30. # Show usage
  31. #
  32. def usage
  33.   puts 'Usage: ./fuzz.rb <FILE1> [FILE2] [FILE3] [...]'
  34.   puts 'Example: ./fuzz.rb test/dataset/**.pdf'
  35.   exit 1
  36. end
  37.  
  38. #
  39. # Print status message
  40. #
  41. # @param [String] msg message to print
  42. #
  43. def print_status(msg = '')
  44.   puts '[*] '.blue + msg if VERBOSE
  45. end
  46.  
  47. #
  48. # Print progress messages
  49. #
  50. # @param [String] msg message to print
  51. #
  52. def print_good(msg = '')
  53.   puts '[+] '.green + msg if VERBOSE
  54. end
  55.  
  56. #
  57. # Print error message
  58. #
  59. # @param [String] msg message to print
  60. #
  61. def print_error(msg = '')
  62.   puts '[-] '.red + msg
  63. end
  64.  
  65. #
  66. # Setup environment
  67. #
  68. def setup
  69.   FileUtils.mkdir_p OUTPUT_DIR unless File.directory? OUTPUT_DIR
  70. rescue => e
  71.   print_error "Could not create output directory '#{OUTPUT_DIR}': #{e}"
  72.   exit 1
  73. end
  74.  
  75. #
  76. # Generate a mutated PDF file with a single mitated byte
  77. #
  78. # @param [Path] f path to PDF file
  79. #
  80. def mutate_byte(f)
  81.   data = IO.binread f
  82.   position = SecureRandom.random_number data.size
  83.   new_byte = SecureRandom.random_number 256
  84.   new_data = data.dup.tap { |s| s.setbyte(position, new_byte) }
  85.  
  86.   File.open(@fuzz_outfile, 'w') do |file|
  87.     file.write new_data
  88.   end
  89. end
  90.  
  91. #
  92. # Generate a mutated PDF file with multiple mutated bytes
  93. #
  94. # @param [Path] f path to PDF file
  95. #
  96. def mutate_bytes(f)
  97.   data = IO.binread f
  98.   fuzz_factor = 200
  99.   num_writes = rand((data.size / fuzz_factor.to_f).ceil) + 1
  100.  
  101.   new_data = data.dup
  102.   num_writes.times do
  103.     position = SecureRandom.random_number data.size
  104.     new_byte = SecureRandom.random_number 256
  105.     new_data.tap { |stream| stream.setbyte position, new_byte }
  106.   end
  107.  
  108.   File.open(@fuzz_outfile, 'w') do |file|
  109.     file.write new_data
  110.   end
  111. end
  112.  
  113. #
  114. # Generate a mutated PDF file with all integers replaced by '-1'
  115. #
  116. # @param [Path] f path to PDF file
  117. #
  118. def clobber_integers(f)
  119.   data = IO.binread f
  120.   new_data = data.dup.gsub(/\d/, '-1')
  121.  
  122.   File.open(@fuzz_outfile, 'w') do |file|
  123.     file.write new_data
  124.   end
  125. end
  126.  
  127. #
  128. # Generate a mutated PDF file with all strings 3 characters or longer
  129. # replaced with 2000 'A' characters
  130. #
  131. # @param [Path] f path to PDF file
  132. #
  133. def clobber_strings(f)
  134.   data = IO.binread f
  135.   new_data = data.dup.gsub(/[a-zA-Z]{3,}/, 'A' * 2000)
  136.  
  137.   File.open(@fuzz_outfile, 'w') do |file|
  138.     file.write new_data
  139.   end
  140. end
  141.  
  142. #
  143. # Read a PDF file
  144. #
  145. # @param [String] f path to PDF file
  146. #
  147. def read(f)
  148.   print_status "Processing '#{f}'"
  149.   begin
  150.     pdf = PDF.read f, ignore_errors: false, password: 'invalid'
  151.   rescue Origami => e
  152.     print_status "Could not read PDF '#{f}': PDF is malformed"
  153.     return
  154.   end
  155.   print_good 'Processing complete'
  156.  
  157.   print_status "Parsing '#{f}'"
  158.   begin
  159.     parse pdf
  160.   rescue Origami => e
  161.     print_status "Could not parse PDF '#{f}': PDF is malformed"
  162.     return
  163.   end
  164.   print_good 'Parsing complete'
  165. end
  166.  
  167. #
  168. # Parse PDF
  169. #
  170. def parse(pdf)
  171.   print_status "Version: #{pdf.header.to_f}"
  172.   print_status "Number of revisions: #{pdf.revisions.size}"
  173.   print_status "Number of indirect objects: #{pdf.indirect_objects.size}"
  174.   print_status "Number of pages: #{pdf.pages.count}"
  175.   print_status "Linearized: #{pdf.linearized?}"
  176.   print_status "Encrypted: #{pdf.encrypted?}"
  177.   print_status "Signed: #{pdf.signed?}"
  178.   print_status "Has usage rights: #{pdf.usage_rights?}"
  179.   print_status "Form: #{pdf.form?}"
  180.   print_status "XFA form: #{pdf.xfa_form?}"
  181.   print_status "Document Info: #{pdf.document_info?}"
  182.   print_status "Metadata: #{pdf.metadata?}"
  183.  
  184.   print_status 'Parsing PDF contents...'
  185.   pdf.pages.each_with_index do |page, index|
  186.     page.each_annotation do |a|
  187.     end
  188.     page.each_content_stream do |a|
  189.     end
  190.     page.each_annotation do |a|
  191.     end
  192.     page.each_colorspace do |a|
  193.     end
  194.     page.each_extgstate do |a|
  195.     end
  196.     page.each_pattern do |a|
  197.     end
  198.     page.each_shading do |a|
  199.     end
  200.     page.each_xobject do |a|
  201.     end
  202.     page.each_font do |a|
  203.     end
  204.     page.each_property do |a|
  205.     end
  206.     page.each do |a|
  207.     end
  208.     page.each_key do |a|
  209.     end
  210.     page.each_value do |a|
  211.     end
  212.     page.each_pair do |a|
  213.     end
  214.     page.each_with_index do |a, b|
  215.     end
  216.     page.reverse_each do |a|
  217.     end
  218.     page.each_entry do |a|
  219.     end
  220.     #page.each_resource do |a|
  221.     #end
  222.     #page.each_slice do |a|
  223.     #end
  224.     #page.each_cons do |a|
  225.     #end
  226.     #page.each_with_object do |a|
  227.     #end
  228.   end
  229. end
  230.  
  231. #
  232. # Show summary of crashes
  233. #
  234. def summary
  235.   puts
  236.   puts "Complete! Crashes saved to '#{OUTPUT_DIR}'"
  237.   puts
  238.   puts `/usr/bin/head -n1 #{OUTPUT_DIR}/*.trace` if File.exist? '/usr/bin/head'
  239. end
  240.  
  241. #
  242. # Report error message to STDOUT
  243. # and save fuzz test case and backtrace to OUTPUT_DIR
  244. #
  245. def report_crash(e)
  246.   puts " - #{e.message}"
  247.   puts e.backtrace.first
  248.   fname = "#{DateTime.now.strftime('%Y%m%d%H%M%S%N')}_crash_#{rand(1000)}"
  249.   FileUtils.mv @fuzz_outfile, "#{OUTPUT_DIR}/#{fname}.pdf"
  250.   File.open("#{OUTPUT_DIR}/#{fname}.pdf.trace", 'w') do |file|
  251.     file.write "#{e.message}\n#{e.backtrace.join "\n"}"
  252.   end
  253. end
  254.  
  255. #
  256. # Test Origami with the mutated file
  257. #
  258. def test
  259.   Timeout.timeout(@timeout) do
  260.     read @fuzz_outfile
  261.   end
  262. rescue SystemStackError => e
  263.   report_crash e
  264. rescue Timeout::Error => e
  265.   report_crash e
  266. rescue SyntaxError => e
  267.   report_crash e
  268. rescue => e
  269.   raise e unless e.backtrace.join("\n") =~ %r{gems/origami}
  270.   report_crash e
  271. end
  272.  
  273. #
  274. # Generate random byte mutations and run test
  275. #
  276. # @param [String] f path to PDF file
  277. #
  278. def fuzz_bytes(f)
  279.   iterations = 1000
  280.   1.upto(iterations) do |i|
  281.     print "\r#{(i * 100) / iterations} % (#{i} / #{iterations})"
  282.     mutate_bytes f
  283.     test
  284.   end
  285. end
  286.  
  287. #
  288. # Generate integer mutations and run tests
  289. #
  290. # @param [String] f path to PDF file
  291. #
  292. def fuzz_integers(f)
  293.   clobber_integers f
  294.   test
  295. end
  296.  
  297. #
  298. # Generate string mutations and run tests
  299. #
  300. # @param [String] f path to PDF file
  301. #
  302. def fuzz_strings(f)
  303.   clobber_strings f
  304.   test
  305. end
  306.  
  307. puts '-' * 60
  308. puts '% Fuzzer for Origami Ruby gem'
  309. puts '-' * 60
  310. puts
  311.  
  312. usage if ARGV[0].nil?
  313.  
  314. setup
  315.  
  316. @timeout = 15
  317. @fuzz_outfile = 'fuzz.pdf'
  318.  
  319. trap 'SIGINT' do
  320.   puts
  321.   puts 'Caught interrupt. Exiting...'
  322.   summary
  323.   exit 130
  324. end
  325.  
  326. ARGV.each do |f|
  327.   unless File.exist? f
  328.     print_error "Could not find file '#{f}'"
  329.     next
  330.   end
  331.  
  332.   fuzz_integers f
  333.   fuzz_strings f
  334.   fuzz_bytes f
  335.  
  336.   puts '-' * 60
  337. end
  338.  
  339. summary
RAW Paste Data
Top