daily pastebin goal
40%
SHARE
TWEET

Untitled

a guest Nov 20th, 2017 65 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
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top