origami 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. data/COPYING.LESSER +165 -0
  2. data/README +77 -0
  3. data/VERSION +1 -0
  4. data/bin/config/pdfcop.conf.yml +237 -0
  5. data/bin/gui/about.rb +46 -0
  6. data/bin/gui/config.rb +132 -0
  7. data/bin/gui/file.rb +385 -0
  8. data/bin/gui/hexdump.rb +74 -0
  9. data/bin/gui/hexview.rb +91 -0
  10. data/bin/gui/imgview.rb +72 -0
  11. data/bin/gui/menu.rb +392 -0
  12. data/bin/gui/properties.rb +132 -0
  13. data/bin/gui/signing.rb +635 -0
  14. data/bin/gui/textview.rb +107 -0
  15. data/bin/gui/treeview.rb +409 -0
  16. data/bin/gui/walker.rb +282 -0
  17. data/bin/gui/xrefs.rb +79 -0
  18. data/bin/pdf2graph +121 -0
  19. data/bin/pdf2ruby +353 -0
  20. data/bin/pdfcocoon +104 -0
  21. data/bin/pdfcop +455 -0
  22. data/bin/pdfdecompress +104 -0
  23. data/bin/pdfdecrypt +95 -0
  24. data/bin/pdfencrypt +112 -0
  25. data/bin/pdfextract +221 -0
  26. data/bin/pdfmetadata +123 -0
  27. data/bin/pdfsh +13 -0
  28. data/bin/pdfwalker +7 -0
  29. data/bin/shell/.irbrc +104 -0
  30. data/bin/shell/console.rb +136 -0
  31. data/bin/shell/hexdump.rb +83 -0
  32. data/origami.rb +36 -0
  33. data/origami/3d.rb +239 -0
  34. data/origami/acroform.rb +321 -0
  35. data/origami/actions.rb +299 -0
  36. data/origami/adobe/fdf.rb +259 -0
  37. data/origami/adobe/ppklite.rb +489 -0
  38. data/origami/annotations.rb +775 -0
  39. data/origami/array.rb +187 -0
  40. data/origami/boolean.rb +101 -0
  41. data/origami/catalog.rb +486 -0
  42. data/origami/destinations.rb +213 -0
  43. data/origami/dictionary.rb +188 -0
  44. data/origami/docmdp.rb +96 -0
  45. data/origami/encryption.rb +1293 -0
  46. data/origami/export.rb +283 -0
  47. data/origami/file.rb +222 -0
  48. data/origami/filters.rb +250 -0
  49. data/origami/filters/ascii.rb +189 -0
  50. data/origami/filters/ccitt.rb +515 -0
  51. data/origami/filters/crypt.rb +47 -0
  52. data/origami/filters/dct.rb +61 -0
  53. data/origami/filters/flate.rb +112 -0
  54. data/origami/filters/jbig2.rb +63 -0
  55. data/origami/filters/jpx.rb +53 -0
  56. data/origami/filters/lzw.rb +195 -0
  57. data/origami/filters/predictors.rb +276 -0
  58. data/origami/filters/runlength.rb +117 -0
  59. data/origami/font.rb +209 -0
  60. data/origami/functions.rb +93 -0
  61. data/origami/graphics.rb +33 -0
  62. data/origami/graphics/colors.rb +191 -0
  63. data/origami/graphics/instruction.rb +126 -0
  64. data/origami/graphics/path.rb +154 -0
  65. data/origami/graphics/patterns.rb +180 -0
  66. data/origami/graphics/state.rb +164 -0
  67. data/origami/graphics/text.rb +224 -0
  68. data/origami/graphics/xobject.rb +493 -0
  69. data/origami/header.rb +90 -0
  70. data/origami/linearization.rb +318 -0
  71. data/origami/metadata.rb +114 -0
  72. data/origami/name.rb +170 -0
  73. data/origami/null.rb +75 -0
  74. data/origami/numeric.rb +188 -0
  75. data/origami/obfuscation.rb +233 -0
  76. data/origami/object.rb +527 -0
  77. data/origami/outline.rb +59 -0
  78. data/origami/page.rb +559 -0
  79. data/origami/parser.rb +268 -0
  80. data/origami/parsers/fdf.rb +45 -0
  81. data/origami/parsers/pdf.rb +27 -0
  82. data/origami/parsers/pdf/linear.rb +113 -0
  83. data/origami/parsers/ppklite.rb +86 -0
  84. data/origami/pdf.rb +1144 -0
  85. data/origami/reference.rb +113 -0
  86. data/origami/signature.rb +474 -0
  87. data/origami/stream.rb +575 -0
  88. data/origami/string.rb +416 -0
  89. data/origami/trailer.rb +173 -0
  90. data/origami/webcapture.rb +87 -0
  91. data/origami/xfa.rb +3027 -0
  92. data/origami/xreftable.rb +447 -0
  93. data/templates/patterns.rb +66 -0
  94. data/templates/widgets.rb +173 -0
  95. data/templates/xdp.rb +92 -0
  96. data/tests/dataset/test.dummycrt +28 -0
  97. data/tests/dataset/test.dummykey +27 -0
  98. data/tests/tc_actions.rb +32 -0
  99. data/tests/tc_annotations.rb +85 -0
  100. data/tests/tc_pages.rb +37 -0
  101. data/tests/tc_pdfattach.rb +24 -0
  102. data/tests/tc_pdfencrypt.rb +110 -0
  103. data/tests/tc_pdfnew.rb +32 -0
  104. data/tests/tc_pdfparse.rb +98 -0
  105. data/tests/tc_pdfsig.rb +37 -0
  106. data/tests/tc_streams.rb +129 -0
  107. data/tests/ts_pdf.rb +45 -0
  108. metadata +193 -0
@@ -0,0 +1,455 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ =begin
4
+
5
+ = Author:
6
+ Guillaume Delugré <guillaume/at/security-labs.org>
7
+
8
+ = Info:
9
+ This is a PDF document filtering engine using Origami.
10
+ Security policies are based on a white list of PDF features.
11
+ Default policies details can be found in the default configuration file.
12
+ You can also add your own policy and activate it using the -p switch.
13
+
14
+ = License:
15
+ Origami is free software: you can redistribute it and/or modify
16
+ it under the terms of the GNU Lesser General Public License as published by
17
+ the Free Software Foundation, either version 3 of the License, or
18
+ (at your option) any later version.
19
+
20
+ Origami is distributed in the hope that it will be useful,
21
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ GNU Lesser General Public License for more details.
24
+
25
+ You should have received a copy of the GNU Lesser General Public License
26
+ along with Origami. If not, see <http://www.gnu.org/licenses/>.
27
+
28
+ =end
29
+
30
+ begin
31
+ require 'origami'
32
+ rescue LoadError
33
+ ORIGAMIDIR = "#{File.dirname(__FILE__)}/.."
34
+ $: << ORIGAMIDIR
35
+ require 'origami'
36
+ end
37
+ include Origami
38
+
39
+ require 'optparse'
40
+ require 'yaml'
41
+ require 'rexml/document'
42
+ require 'digest/md5'
43
+
44
+ DEFAULT_CONFIG_FILE = "#{File.dirname(__FILE__)}/config/pdfcop.conf.yml"
45
+ DEFAULT_POLICY = "standard"
46
+ SECURITY_POLICIES = {}
47
+
48
+ def load_config_file(path)
49
+ SECURITY_POLICIES.update(Hash.new(false).update YAML.load(File.read(path)))
50
+ end
51
+
52
+ class OptParser
53
+ BANNER = <<USAGE
54
+ Usage: #{$0} [options] <PDF-file>
55
+ The PDF filtering engine. Scans PDF documents for malicious structures.
56
+ Bug reports or feature requests at: http://origami-pdf.googlecode.com/
57
+
58
+ Options:
59
+ USAGE
60
+
61
+ def self.parse(args)
62
+ options = {:colors => true}
63
+
64
+ opts = OptionParser.new do |opts|
65
+ opts.banner = BANNER
66
+
67
+ opts.on("-o", "--output LOG_FILE", "Output log file (default STDOUT)") do |o|
68
+ options[:output_log] = o
69
+ end
70
+
71
+ opts.on("-c", "--config CONFIG_FILE", "Load security policies from given configuration file") do |cf|
72
+ options[:config_file] = cf
73
+ end
74
+
75
+ opts.on("-p", "--policy POLICY_NAME", "Specify applied policy. Predefined policies: 'none', 'standard', 'strong', 'paranoid'") do |p|
76
+ options[:policy] = p
77
+ end
78
+
79
+ opts.on("-n", "--no-color", "Suppress colored output") do
80
+ options[:colors] = false
81
+ end
82
+
83
+ opts.on_tail("-h", "--help", "Show this message") do
84
+ puts opts
85
+ exit
86
+ end
87
+ end
88
+ opts.parse!(args)
89
+
90
+ options
91
+ end
92
+ end
93
+
94
+ @options = OptParser.parse(ARGV)
95
+ if @options.has_key?(:output_log)
96
+ LOGGER = File.open(@options[:output_log], "a+")
97
+ else
98
+ LOGGER = STDOUT
99
+ end
100
+
101
+ if not @options.has_key?(:policy)
102
+ @options[:policy] = DEFAULT_POLICY
103
+ end
104
+
105
+ load_config_file(@options[:config_file] || DEFAULT_CONFIG_FILE)
106
+ unless SECURITY_POLICIES.has_key?("POLICY_#{@options[:policy].upcase}")
107
+ STDERR.puts "Undeclared policy `#{@options[:policy]}'"
108
+ exit(1)
109
+ end
110
+
111
+ if ARGV.empty?
112
+ STDERR.puts "Error: No filename was specified. #{$0} --help for details."
113
+ exit 1
114
+ else
115
+ TARGET = ARGV.shift
116
+ end
117
+
118
+ def log(str, color = Colors::GREY)
119
+ if @options[:colors]
120
+ colorprint("[#{Time.now}] ", Colors::CYAN, LOGGER)
121
+ colorprint(str, color, LOGGER)
122
+ else
123
+ LOGGER.print("[#{Time.now}] #{str}")
124
+ end
125
+
126
+ LOGGER.puts
127
+ end
128
+
129
+ def reject(cause)
130
+ log("Document rejected by policy `#{@options[:policy]}', caused by #{cause.inspect}.", Colors::RED)
131
+ exit(1)
132
+ end
133
+
134
+ def check_rights(*required_rights)
135
+ current_rights = SECURITY_POLICIES["POLICY_#{@options[:policy].upcase}"]
136
+
137
+ reject(required_rights) if required_rights.any?{|right| current_rights[right.to_s] == false}
138
+ end
139
+
140
+ def analyze_xfa_forms(xfa)
141
+ case xfa
142
+ when Array then
143
+ xml = ""
144
+ i = 0
145
+ xfa.each do |packet|
146
+ if i % 2 == 1
147
+ xml << packet.solve.data
148
+ end
149
+
150
+ i = i + 1
151
+ end
152
+ when Stream then
153
+ xml = xfa.data
154
+ else
155
+ reject("Malformed XFA dictionary")
156
+ end
157
+
158
+ xfadoc = REXML::Document.new(xml)
159
+ REXML::XPath.match(xfadoc, "//script").each do |script|
160
+ case script.attributes["contentType"]
161
+ when "application/x-formcalc" then
162
+ check_rights(:allowFormCalc)
163
+ else
164
+ check_rights(:allowJS)
165
+ end
166
+ end
167
+ end
168
+
169
+ def analyze_annotation(annot, level = 0)
170
+ check_rights(:allowAnnotations)
171
+
172
+ if annot.is_a?(Dictionary) and annot.has_key?(:Subtype)
173
+ case annot[:Subtype].solve.value
174
+ when :FileAttachment then
175
+ check_rights(:allowAttachments, :allowFileAttachmentAnnotation)
176
+
177
+ when :Sound then
178
+ check_rights(:allowSoundAnnotation)
179
+
180
+ when :Movie then
181
+ check_rights(:allowMovieAnnotation)
182
+
183
+ when :Screen then
184
+ check_rights(:allowScreenAnnotation)
185
+
186
+ when :Widget then
187
+ check_rights(:allowAcroforms)
188
+
189
+ when :"3D" then
190
+ check_rights(:allow3DAnnotation)
191
+
192
+ # 3D annotation might pull in JavaScript for real-time driven behavior.
193
+ if annot.has_key?(:"3DD")
194
+ dd = annot[:"3DD"].solve
195
+ u3dstream = nil
196
+
197
+ case dd
198
+ when Stream then
199
+ u3dstream = dd
200
+ when Dictionary then
201
+ u3dstream = dd[:"3DD"]
202
+ end
203
+
204
+ if u3dstream and u3dstream.has_field?(:OnInstantiate)
205
+ check_rights(:allowJS)
206
+
207
+ if annot.has_key?(:"3DA") # is 3d view instantiated automatically?
208
+ u3dactiv = annot[:"3DA"].solve
209
+
210
+ check_rights(:allowJSAtOpening) if u3dactiv.is_a?(Dictionary) and (u3dactiv[:A] == :PO or u3dactiv[:A] == :PV)
211
+ end
212
+ end
213
+ end
214
+
215
+ when :RichMedia then
216
+ check_rights(:allowRichMediaAnnotation)
217
+ end
218
+ end
219
+ end
220
+
221
+ def analyze_page(page, level = 0)
222
+ section_prefix = " " * 2 * level + ">" * (level + 1)
223
+ log(section_prefix + " Inspecting page...")
224
+
225
+ text_prefix = " " * 2 * (level + 1) + "." * (level + 1)
226
+ if page.is_a?(Dictionary)
227
+
228
+ #
229
+ # Checking page additional actions.
230
+ #
231
+ if page.has_key?(:AA)
232
+ if page.AA.is_a?(Dictionary)
233
+ log(text_prefix + " Page has an action dictionary.")
234
+
235
+ aa = PageAdditionalActions.new(page.AA); aa.parent = page.AA.parent
236
+ analyze_action(aa.O, true, level + 1) if aa.has_key?(:O)
237
+ analyze_action(aa.C, false, level + 1) if aa.has_key?(:C)
238
+ end
239
+ end
240
+
241
+ #
242
+ # Looking for page annotations.
243
+ #
244
+ page.each_annot do |annot|
245
+ analyze_annotation(annot, level + 1)
246
+ end
247
+ end
248
+ end
249
+
250
+ def analyze_action(action, triggered_at_opening, level = 0)
251
+ section_prefix = " " * 2 * level + ">" * (level + 1)
252
+ log(section_prefix + " Inspecting action...")
253
+
254
+ text_prefix = " " * 2 * (level + 1) + "." * (level + 1)
255
+ if action.is_a?(Dictionary)
256
+ log(text_prefix + " Found #{action[:S]} action.")
257
+ type = action[:S].is_a?(Reference) ? action[:S].solve : action[:S]
258
+
259
+ case type.value
260
+ when :JavaScript
261
+ check_rights(:allowJS)
262
+ check_rights(:allowJSAtOpening) if triggered_at_opening
263
+
264
+ when :Launch
265
+ check_rights(:allowLaunchAction)
266
+
267
+ when :Named
268
+ check_rights(:allowNamedAction)
269
+
270
+ when :GoTo
271
+ check_rights(:allowGoToAction)
272
+ dest = action[:D].is_a?(Reference) ? action[:D].solve : action[:D]
273
+ if dest.is_a?(Array) and dest.length > 0 and dest.first.is_a?(Reference)
274
+ dest_page = dest.first.solve
275
+ if dest_page.is_a?(Page)
276
+ log(text_prefix + " Destination page found.")
277
+ analyze_page(dest_page, level + 1)
278
+ end
279
+ end
280
+
281
+ when :GoToE
282
+ check_rights(:allowAttachments,:allowGoToEAction)
283
+
284
+ when :GoToR
285
+ check_rights(:allowGoToRAction)
286
+
287
+ when :Thread
288
+ check_rights(:allowGoToRAction) if action.has_key?(:F)
289
+
290
+ when :URI
291
+ check_rights(:allowURIAction)
292
+
293
+ when :SubmitForm
294
+ check_rights(:allowAcroForms,:allowSubmitFormAction)
295
+
296
+ when :ImportData
297
+ check_rights(:allowAcroForms,:allowImportDataAction)
298
+
299
+ when :Rendition
300
+ check_rights(:allowScreenAnnotation,:allowRenditionAction)
301
+
302
+ when :Sound
303
+ check_rights(:allowSoundAnnotation,:allowSoundAction)
304
+
305
+ when :Movie
306
+ check_rights(:allowMovieAnnotation,:allowMovieAction)
307
+
308
+ when :RichMediaExecute
309
+ check_rights(:allowRichMediaAnnotation,:allowRichMediaAction)
310
+
311
+ when :GoTo3DView
312
+ check_rights(:allow3DAnnotation,:allowGoTo3DAction)
313
+ end
314
+
315
+ if action.has_key?(:Next)
316
+ log(text_prefix + "This action is chained to another action!")
317
+ check_rights(:allowChainedActions)
318
+ analyze_action(action.Next)
319
+ end
320
+ elsif action.is_a?(Array)
321
+ dest = action
322
+ if dest.length > 0 and dest.first.is_a?(Reference)
323
+ dest_page = dest.first.solve
324
+ if dest_page.is_a?(Page)
325
+ log(text_prefix + " Destination page found.")
326
+ check_rights(:allowGoToAction)
327
+ analyze_page(dest_page, level + 1)
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+ begin
334
+ log("PDFcop is running on target `#{TARGET}', policy = `#{@options[:policy]}'", Colors::GREEN)
335
+ log(" File size: #{File.size(TARGET)} bytes", Colors::MAGENTA)
336
+ log(" MD5: #{Digest::MD5.hexdigest(File.read(TARGET))}", Colors::MAGENTA)
337
+
338
+ @pdf = PDF.read(TARGET,
339
+ :verbosity => Parser::VERBOSE_QUIET,
340
+ :ignore_errors => SECURITY_POLICIES["POLICY_#{@options[:policy].upcase}"]['allowParserErrors']
341
+ )
342
+
343
+ log("> Inspecting document structure...", Colors::YELLOW)
344
+ if @pdf.is_encrypted?
345
+ log(" . Encryption = YES")
346
+ check_rights(:allowEncryption)
347
+ end
348
+
349
+ log("> Inspecting document catalog...", Colors::YELLOW)
350
+ catalog = @pdf.Catalog
351
+ reject("Invalid document catalog") unless catalog.is_a?(Catalog)
352
+
353
+ if catalog.has_key?(:OpenAction)
354
+ log(" . OpenAction entry = YES")
355
+ check_rights(:allowOpenAction)
356
+ action = catalog.OpenAction
357
+ analyze_action(action, true, 1)
358
+ end
359
+
360
+ if catalog.has_key?(:AA)
361
+ if catalog.AA.is_a?(Dictionary)
362
+ aa = CatalogAdditionalActions.new(catalog.AA); aa.parent = catalog;
363
+ log(" . Additional actions dictionary = YES")
364
+ analyze_action(aa.WC, false, 1) if aa.has_key?(:WC)
365
+ analyze_action(aa.WS, false, 1) if aa.has_key?(:WS)
366
+ analyze_action(aa.DS, false, 1) if aa.has_key?(:DS)
367
+ analyze_action(aa.WP, false, 1) if aa.has_key?(:WP)
368
+ analyze_action(aa.DP, false, 1) if aa.has_key?(:DP)
369
+ end
370
+ end
371
+
372
+ if catalog.has_key?(:AcroForm)
373
+ acroform = catalog.AcroForm
374
+ if acroform.is_a?(Dictionary)
375
+ log(" . AcroForm = YES")
376
+ check_rights(:allowAcroForms)
377
+ if acroform.has_key?(:XFA)
378
+ log(" . XFA = YES")
379
+ check_rights(:allowXFAForms)
380
+
381
+ analyze_xfa_forms(acroform[:XFA].solve)
382
+ end
383
+ end
384
+ end
385
+
386
+ log("> Inspecting JavaScript names directory...", Colors::YELLOW)
387
+ unless @pdf.ls_names(Names::Root::JAVASCRIPT).empty?
388
+ check_rights(:allowJS)
389
+ check_rights(:allowJSAtOpening)
390
+ end
391
+
392
+ log("> Inspecting attachment names directory...", Colors::YELLOW)
393
+ unless @pdf.ls_names(Names::Root::EMBEDDEDFILES).empty?
394
+ check_rights(:allowAttachments)
395
+ end
396
+
397
+ log("> Inspecting document pages...", Colors::YELLOW)
398
+ @pdf.each_page do |page|
399
+ analyze_page(page, 1)
400
+ end
401
+
402
+ log("> Inspecting document streams...", Colors::YELLOW)
403
+ @pdf.indirect_objects.find_all{|obj| obj.is_a?(Stream)}.each do |stream|
404
+ if stream.dictionary.has_key?(:Filter)
405
+ filters = stream.Filter
406
+ filters = [ filters ] if filters.is_a?(Name)
407
+
408
+ if filters.is_a?(Array)
409
+ filters.each do |filter|
410
+ case filter.value
411
+ when :ASCIIHexDecode
412
+ check_rights(:allowASCIIHexFilter)
413
+ when :ASCII85Decode
414
+ check_rights(:allowASCII85Filter)
415
+ when :LZWDecode
416
+ check_rights(:allowLZWFilter)
417
+ when :FlateDecode
418
+ check_rights(:allowFlateDecode)
419
+ when :RunLengthDecode
420
+ check_rights(:allowRunLengthFilter)
421
+ when :CCITTFaxDecode
422
+ check_rights(:allowCCITTFaxFilter)
423
+ when :JBIG2Decode
424
+ check_rights(:allowJBIG2Filter)
425
+ when :DCTDecode
426
+ check_rights(:allowDCTFilter)
427
+ when :JPXDecode
428
+ check_rights(:allowJPXFilter)
429
+ when :Crypt
430
+ check_rights(:allowCryptFilter)
431
+ end
432
+ end
433
+ end
434
+ end
435
+ end
436
+
437
+ #
438
+ # TODO: Detect JS at opening in XFA (check event tag)
439
+ # Check image encoding in XFA ?
440
+ # Only allow valid signed documents ?
441
+ # Recursively scan attached files.
442
+ # On-the-fly injection of prerun JS code to hook vulnerable methods (dynamic exploit detection) ???
443
+ # ...
444
+ #
445
+
446
+ log("Document accepted by policy `#{@options[:policy]}'.", Colors::GREEN)
447
+
448
+ rescue SystemExit
449
+ rescue Exception => e
450
+ log("An error occured during analysis : #{e.class} (#{e.message})")
451
+ reject("Analysis failure")
452
+ ensure
453
+ LOGGER.close
454
+ end
455
+