origami 1.2.7 → 2.0.0

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