origami 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+