Guest User

Untitled

a guest
Nov 20th, 2017
121
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 6.88 KB | None | 0 0
  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
Add Comment
Please, Sign In to add comment