Commit d64c474309c8d6cef972da51053ce3c63301f5dc

Authored by Philippe Lagadec
Committed by GitHub
2 parents 82bb3107 7909381f

Merge branch 'master' into master

Showing 65 changed files with 2463 additions and 677 deletions
.travis.yml 0 → 100644
  1 +language: python
  2 +
  3 +python:
  4 + - "2.7"
  5 + - "3.6"
  6 + - "nightly"
  7 +cache: pip
  8 +script:
  9 + - python setup.py test
... ...
README.md
1 1 python-oletools
2 2 ===============
  3 +[![PyPI](https://img.shields.io/pypi/v/oletools.svg)](https://pypi.python.org/pypi/oletools)
  4 +[![Build Status](https://travis-ci.org/decalage2/oletools.svg?branch=master)](https://travis-ci.org/decalage2/oletools)
3 5  
4 6 [oletools](http://www.decalage.info/python/oletools) is a package of python tools to analyze
5 7 [Microsoft OLE2 files](http://en.wikipedia.org/wiki/Compound_File_Binary_Format)
... ... @@ -22,7 +24,17 @@ Note: python-oletools is not related to OLETools published by BeCubed Software.
22 24 News
23 25 ----
24 26  
25   -- **2016-11-01 v0.50**: all oletools now support python 2 and 3.
  27 +- **2017-06-29 v0.51**:
  28 + - added the [oletools cheatsheet](https://github.com/decalage2/oletools/blob/master/cheatsheet/oletools_cheatsheet.pdf)
  29 + - improved [rtfobj](https://github.com/decalage2/oletools/wiki/rtfobj) to handle malformed RTF files, detect vulnerability CVE-2017-0199
  30 + - olevba: improved deobfuscation and Mac files support
  31 + - [mraptor](https://github.com/decalage2/oletools/wiki/mraptor): added more ActiveX macro triggers
  32 + - added [DocVarDump.vba](https://github.com/decalage2/oletools/blob/master/oletools/DocVarDump.vba) to dump document variables using Word
  33 + - olemap: can now detect and extract [extra data at end of file](http://decalage.info/en/ole_extradata), improved display
  34 + - oledir, olemeta, oletimes: added support for zip files and wildcards
  35 + - many [bugfixes](https://github.com/decalage2/oletools/milestone/3?closed=1) in all the tools
  36 + - improved Python 2+3 support
  37 +- 2016-11-01 v0.50: all oletools now support python 2 and 3.
26 38 - olevba: several bugfixes and improvements.
27 39 - mraptor: improved detection, added mraptor_milter for Sendmail/Postfix integration.
28 40 - rtfobj: brand new RTF parser, obfuscation-aware, improved display, detect
... ... @@ -33,13 +45,6 @@ improved handling of malformed/incomplete documents, improved error handling and
33 45 now returns an exit code based on analysis results, new --relaxed option.
34 46 [rtfobj](https://github.com/decalage2/oletools/wiki/rtfobj): improved parsing to handle obfuscated RTF documents,
35 47 added -d option to set output dir. Moved repository and documentation to GitHub.
36   -- 2016-04-19 v0.46: [olevba](https://github.com/decalage2/oletools/wiki/olevba)
37   -does not deobfuscate VBA expressions by default (much faster), new option --deobf
38   -to enable it. Fixed color display bug on Windows for several tools.
39   -- 2016-04-12 v0.45: improved [rtfobj](https://github.com/decalage2/oletools/wiki/rtfobj)
40   -to handle several [anti-analysis tricks](http://www.decalage.info/rtf_tricks),
41   -improved [olevba](https://github.com/decalage2/oletools/wiki/olevba)
42   -to export results in JSON format.
43 48  
44 49 See the [full changelog](https://github.com/decalage2/oletools/wiki/Changelog) for more information.
45 50  
... ... @@ -67,6 +72,7 @@ Projects using oletools:
67 72  
68 73 oletools are used by a number of projects and online malware analysis services,
69 74 including [Viper](http://viper.li/), [REMnux](https://remnux.org/),
  75 +[FAME](https://certsocietegenerale.github.io/fame/),
70 76 [Hybrid-analysis.com](https://www.hybrid-analysis.com/),
71 77 [Joe Sandbox](https://www.document-analyzer.net/),
72 78 [Deepviz](https://sandbox.deepviz.com/),
... ... @@ -129,7 +135,7 @@ License
129 135 This license applies to the python-oletools package, apart from the thirdparty folder which contains third-party files
130 136 published with their own license.
131 137  
132   -The python-oletools package is copyright (c) 2012-2016 Philippe Lagadec (http://www.decalage.info)
  138 +The python-oletools package is copyright (c) 2012-2017 Philippe Lagadec (http://www.decalage.info)
133 139  
134 140 All rights reserved.
135 141  
... ...
cheatsheet/oletools_cheatsheet.docx 0 → 100644
No preview for this file type
cheatsheet/oletools_cheatsheet.pdf 0 → 100644
No preview for this file type
oletools/DocVarDump.vba 0 → 100644
  1 +' DocVarDump.vba
  2 +'
  3 +' DocVarDump is a VBA macro that can be used to dump the content of all document
  4 +' variables stored in a MS Word document.
  5 +'
  6 +' USAGE:
  7 +' 1. Open the document to be analyzed in MS Word
  8 +' 2. Do NOT click on "Enable Content", to avoid running malicious macros
  9 +' 3. Save the document with a new name, using the DOCX format (not doc, not docm)
  10 +' This will remove all VBA macro code.
  11 +' 4. Close the file, and reopen the DOCX file you just saved
  12 +' 5. Press Alt+F11 to open the VBA Editor
  13 +' 6. Double-click on "This Document" under Project
  14 +' 7. Copy and Paste all the code from DocVarDump.vba
  15 +' 8. Move the cursor on the line "Sub DocVarDump()"
  16 +' 9. Press F5: This should run the code, and create a file "docvardump.txt"
  17 +' containing a hex dump of all document variables.
  18 +'
  19 +' ALTERNATIVE: Open the document in LibreOffice/OpenOffice,
  20 +' then go to File / Properties / Custom Properties
  21 +'
  22 +' Author: Philippe Lagadec - http://www.decalage.info
  23 +' License: BSD, see source code or documentation
  24 +'
  25 +' DocVarDump is part of the python-oletools package:
  26 +' http://www.decalage.info/python/oletools
  27 +
  28 +' CHANGELOG:
  29 +' 2016-09-21 v0.01 PL: - First working version
  30 +' 2017-04-10 v0.02 PL: - Added usage instructions
  31 +
  32 +Sub DocVarDump()
  33 + intFileNum = FreeFile
  34 + FName = Environ("TEMP") & "\docvardump.txt"
  35 + Open FName For Output As intFileNum
  36 + For Each myvar In ActiveDocument.Variables
  37 + Write #intFileNum, "Name = " & myvar.Name
  38 + 'TODO: check VarType, and only use hexdump for strings with non-printable chars
  39 + Write #intFileNum, "Value = " & HexDump(myvar.value)
  40 + Write #intFileNum,
  41 + Next myvar
  42 + Close intFileNum
  43 + Documents.Open (FName)
  44 +End Sub
  45 +
  46 +Function Hex2(value As Integer)
  47 + h = Hex(value)
  48 + If Len(h) < 2 Then
  49 + h = "0" & h
  50 + End If
  51 + Hex2 = h
  52 +End Function
  53 +
  54 +Function HexN(value As Integer, nchars As Integer)
  55 + h = Hex(value)
  56 + Do While Len(h) < nchars
  57 + h = "0" & h
  58 + Loop
  59 + HexN = h
  60 +End Function
  61 +
  62 +Function ReplaceClean1(sText As String)
  63 + Dim J As Integer
  64 + Dim vAddText
  65 +
  66 + vAddText = Array(Chr(129), Chr(141), Chr(143), Chr(144), Chr(157))
  67 + For J = 0 To 31
  68 + sText = Replace(sText, Chr(J), "\x" & Hex2(J))
  69 + Next
  70 + For J = 0 To UBound(vAddText)
  71 + c = vAddText(J)
  72 + a = Asc(c)
  73 + sText = Replace(sText, c, "\x" & Hex2(a))
  74 + Next
  75 + ReplaceClean1 = sText
  76 +End Function
  77 +
  78 +Function ReplaceClean3(sText As String)
  79 + Dim J As Integer
  80 + For J = 0 To 31
  81 + sText = Replace(sText, Chr(J), ".")
  82 + Next
  83 + For J = 127 To 255
  84 + sText = Replace(sText, Chr(J), ".")
  85 + Next
  86 + ReplaceClean3 = sText
  87 +End Function
  88 +
  89 +Function HexBytes(sText As String)
  90 + Dim i As Integer
  91 + HexBytes = ""
  92 + For i = 1 To Len(sText)
  93 + HexBytes = HexBytes & Hex2(Asc(Mid(sText, i))) & " "
  94 + Next
  95 +End Function
  96 +
  97 +
  98 +Function HexDump(sText As String)
  99 + Dim chunk As String
  100 + Dim i As Long
  101 + ' "\" is integer division, "/" is normal division (float)
  102 + nbytes = 8
  103 + nchunks = Len(sText) \ nbytes
  104 + lastchunk = Len(sText) Mod nbytes
  105 + HexDump = ""
  106 + For i = 0 To nchunks - 1
  107 + Offset = HexN(i * nbytes, 8)
  108 + chunk = Mid(sText, i * nbytes + 1, nbytes)
  109 + HexDump = HexDump & Offset & " " & HexBytes(chunk) & " " & ReplaceClean3(chunk) & vbCrLf
  110 + Next i
  111 + 'TODO: LAST CHUNK!
  112 + If lastchunk > 0 Then
  113 + Offset = HexN(nchunks * nbytes, 8)
  114 + chunk = Mid(sText, nchunks * nbytes + 1, lastchunk)
  115 + HexDump = HexDump & Offset & " " & HexBytes(chunk) & " " & ReplaceClean3(chunk) & vbCrLf
  116 + End If
  117 +End Function
... ...
oletools/LICENSE.txt
... ... @@ -3,7 +3,7 @@ LICENSE for the python-oletools package:
3 3 This license applies to the python-oletools package, apart from the thirdparty
4 4 folder which contains third-party files published with their own license.
5 5  
6   -The python-oletools package is copyright (c) 2012-2016 Philippe Lagadec (http://www.decalage.info)
  6 +The python-oletools package is copyright (c) 2012-2017 Philippe Lagadec (http://www.decalage.info)
7 7  
8 8 All rights reserved.
9 9  
... ...
oletools/README.html
... ... @@ -9,12 +9,24 @@
9 9 </head>
10 10 <body>
11 11 <h1 id="python-oletools">python-oletools</h1>
12   -<p><a href="http://www.decalage.info/python/oletools">oletools</a> is a package of python tools to analyze <a href="http://en.wikipedia.org/wiki/Compound_File_Binary_Format">Microsoft OLE2 files</a> (also called Structured Storage, Compound File Binary Format or Compound Document File Format), such as Microsoft Office documents or Outlook messages, mainly for malware analysis, forensics and debugging. It is based on the <a href="http://www.decalage.info/olefile">olefile</a> parser. See <a href="http://www.decalage.info/python/oletools">http://www.decalage.info/python/oletools</a> for more info.</p>
  12 +<p><a href="http://www.decalage.info/python/oletools">oletools</a> is a package of python tools to analyze <a href="http://en.wikipedia.org/wiki/Compound_File_Binary_Format">Microsoft OLE2 files</a> (also called Structured Storage, Compound File Binary Format or Compound Document File Format), such as Microsoft Office documents or Outlook messages, mainly for malware analysis, forensics and debugging. It is based on the <a href="http://www.decalage.info/olefile">olefile</a> parser. See <a href="http://www.decalage.info/python/oletools" class="uri">http://www.decalage.info/python/oletools</a> for more info.</p>
13 13 <p><strong>Quick links:</strong> <a href="http://www.decalage.info/python/oletools">Home page</a> - <a href="https://github.com/decalage2/oletools/wiki/Install">Download/Install</a> - <a href="https://github.com/decalage2/oletools/wiki">Documentation</a> - <a href="https://github.com/decalage2/oletools/issues">Report Issues/Suggestions/Questions</a> - <a href="http://decalage.info/contact">Contact the Author</a> - <a href="https://github.com/decalage2/oletools">Repository</a> - <a href="https://twitter.com/decalage2">Updates on Twitter</a></p>
14 14 <p>Note: python-oletools is not related to OLETools published by BeCubed Software.</p>
15 15 <h2 id="news">News</h2>
16 16 <ul>
17   -<li><strong>2016-11-01 v0.50</strong>: all oletools now support python 2 and 3.
  17 +<li><strong>2017-06-29 v0.51</strong>:
  18 +<ul>
  19 +<li>added the <a href="https://github.com/decalage2/oletools/blob/master/cheatsheet/oletools_cheatsheet.pdf">oletools cheatsheet</a></li>
  20 +<li>improved <a href="https://github.com/decalage2/oletools/wiki/rtfobj">rtfobj</a> to handle malformed RTF files, detect vulnerability CVE-2017-0199</li>
  21 +<li>olevba: improved deobfuscation and Mac files support</li>
  22 +<li><a href="https://github.com/decalage2/oletools/wiki/mraptor">mraptor</a>: added more ActiveX macro triggers</li>
  23 +<li>added <a href="https://github.com/decalage2/oletools/blob/master/oletools/DocVarDump.vba">DocVarDump.vba</a> to dump document variables using Word</li>
  24 +<li>olemap: can now detect and extract <a href="http://decalage.info/en/ole_extradata">extra data at end of file</a>, improved display</li>
  25 +<li>oledir, olemeta, oletimes: added support for zip files and wildcards</li>
  26 +<li>many <a href="https://github.com/decalage2/oletools/milestone/3?closed=1">bugfixes</a> in all the tools</li>
  27 +<li>improved Python 2+3 support</li>
  28 +</ul></li>
  29 +<li>2016-11-01 v0.50: all oletools now support python 2 and 3.
18 30 <ul>
19 31 <li>olevba: several bugfixes and improvements.</li>
20 32 <li>mraptor: improved detection, added mraptor_milter for Sendmail/Postfix integration.</li>
... ... @@ -22,28 +34,9 @@
22 34 <li>setup: now creates handy command-line scripts to run oletools from any directory.</li>
23 35 </ul></li>
24 36 <li>2016-06-10 v0.47: <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a> added PPT97 macros support, improved handling of malformed/incomplete documents, improved error handling and JSON output, now returns an exit code based on analysis results, new --relaxed option. <a href="https://github.com/decalage2/oletools/wiki/rtfobj">rtfobj</a>: improved parsing to handle obfuscated RTF documents, added -d option to set output dir. Moved repository and documentation to GitHub.</li>
25   -<li>2016-04-19 v0.46: <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a> does not deobfuscate VBA expressions by default (much faster), new option --deobf to enable it. Fixed color display bug on Windows for several tools.</li>
26   -<li>2016-04-12 v0.45: improved <a href="https://github.com/decalage2/oletools/wiki/rtfobj">rtfobj</a> to handle several <a href="http://www.decalage.info/rtf_tricks">anti-analysis tricks</a>, improved <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a> to export results in JSON format.</li>
27   -<li>2016-03-11 v0.44: improved <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a> to extract and analyse strings from VBA Forms.</li>
28   -<li>2016-03-04 v0.43: added new tool <a href="https://github.com/decalage2/oletools/wiki/mraptor">MacroRaptor</a> (mraptor) to detect malicious macros, bugfix and slight improvements in <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a>.</li>
29   -<li>2016-02-07 v0.42: added two new tools oledir and olemap, better handling of malformed files and several bugfixes in <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a>, improved display for <a href="https://github.com/decalage2/oletools/wiki/olemeta">olemeta</a>.</li>
30   -<li>2015-09-22 v0.41: added new --reveal option to <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a>, to show the macro code with VBA strings deobfuscated.</li>
31   -<li>2015-09-17 v0.40: Improved macro deobfuscation in <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a>, to decode Hex and Base64 within VBA expressions. Display printable deobfuscated strings by default. Improved the VBA_Parser API. Improved performance. Fixed <a href="https://github.com/decalage2/oletools/issues/23">issue #23</a> with sys.stderr.</li>
32   -<li>2015-06-19 v0.12: <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a> can now deobfuscate VBA expressions with any combination of Chr, Asc, Val, StrReverse, Environ, +, &amp;, using a VBA parser built with <a href="http://pyparsing.wikispaces.com">pyparsing</a>. New options to display only the analysis results or only the macros source code. The analysis is now done on all the VBA modules at once.</li>
33   -<li>2015-05-29 v0.11: Improved parsing of MHTML and ActiveMime/MSO files in <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a>, added several suspicious keywords to VBA scanner (thanks to <span class="citation">@ozhermit</span> and Davy Douhine for the suggestions)</li>
34   -<li>2015-05-06 v0.10: <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a> now supports Word MHTML files with macros, aka &quot;Single File Web Page&quot; (.mht) - see <a href="https://github.com/decalage2/oletools/issues/10">issue #10</a> for more info</li>
35   -<li>2015-03-23 v0.09: <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a> now supports Word 2003 XML files, added anti-sandboxing/VM detection</li>
36   -<li>2015-02-08 v0.08: <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a> can now decode strings obfuscated with Hex/StrReverse/Base64/Dridex and extract IOCs. Added new triage mode, support for non-western codepages with olefile 0.42, improved API and display, several bugfixes.</li>
37   -<li>2015-01-05 v0.07: improved <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a> to detect suspicious keywords and IOCs in VBA macros, can now scan several files and open password-protected zip archives, added a Python API, upgraded OleFileIO_PL to olefile v0.41</li>
38   -<li>2014-08-28 v0.06: added <a href="https://github.com/decalage2/oletools/wiki/olevba">olevba</a>, a new tool to extract VBA Macro source code from MS Office documents (97-2003 and 2007+). Improved <a href="https://github.com/decalage2/oletools/wiki">documentation</a></li>
39   -<li>2013-07-24 v0.05: added new tools <a href="https://github.com/decalage2/oletools/wiki/olemeta">olemeta</a> and <a href="https://github.com/decalage2/oletools/wiki/oletimes">oletimes</a></li>
40   -<li>2013-04-18 v0.04: fixed bug in rtfobj, added documentation for <a href="https://github.com/decalage2/oletools/wiki/rtfobj">rtfobj</a></li>
41   -<li>2012-11-09 v0.03: Improved <a href="https://github.com/decalage2/oletools/wiki/pyxswf">pyxswf</a> to extract Flash objects from RTF</li>
42   -<li>2012-10-29 v0.02: Added <a href="https://github.com/decalage2/oletools/wiki/oleid">oleid</a></li>
43   -<li>2012-10-09 v0.01: Initial version of <a href="https://github.com/decalage2/oletools/wiki/olebrowse">olebrowse</a> and pyxswf</li>
44   -<li>see changelog in source code for more info.</li>
45 37 </ul>
46   -<h2 id="tools-in-python-oletools">Tools in python-oletools:</h2>
  38 +<p>See the <a href="https://github.com/decalage2/oletools/wiki/Changelog">full changelog</a> for more information.</p>
  39 +<h2 id="tools">Tools:</h2>
47 40 <ul>
48 41 <li><a href="https://github.com/decalage2/oletools/wiki/olebrowse">olebrowse</a>: A simple GUI to browse OLE files (e.g. MS Word, Excel, Powerpoint documents), to view and extract individual data streams.</li>
49 42 <li><a href="https://github.com/decalage2/oletools/wiki/oleid">oleid</a>: to analyze OLE files to detect specific characteristics usually found in malicious files.</li>
... ... @@ -59,13 +52,20 @@
59 52 <li>and a few others (coming soon)</li>
60 53 </ul>
61 54 <h2 id="projects-using-oletools">Projects using oletools:</h2>
62   -<p>oletools are used by a number of projects and online malware analysis services, including <a href="http://viper.li/">Viper</a>, <a href="https://remnux.org/">REMnux</a>, <a href="https://www.hybrid-analysis.com/">Hybrid-analysis.com</a>, <a href="https://www.document-analyzer.net/">Joe Sandbox</a>, <a href="https://sandbox.deepviz.com/">Deepviz</a>, <a href="https://github.com/lmco/laikaboss">Laika BOSS</a>, <a href="https://github.com/cuckoosandbox/cuckoo">Cuckoo Sandbox</a>, <a href="https://sandbox.anlyz.io/">Anlyz.io</a>, <a href="https://github.com/bontchev/pcodedmp">pcodedmp</a> and probably <a href="https://www.virustotal.com">VirusTotal</a>. (Please <a href="(http://decalage.info/contact)">contact me</a> if you have or know a project using oletools)</p>
  55 +<p>oletools are used by a number of projects and online malware analysis services, including <a href="http://viper.li/">Viper</a>, <a href="https://remnux.org/">REMnux</a>, <a href="https://certsocietegenerale.github.io/fame/">FAME</a>, <a href="https://www.hybrid-analysis.com/">Hybrid-analysis.com</a>, <a href="https://www.document-analyzer.net/">Joe Sandbox</a>, <a href="https://sandbox.deepviz.com/">Deepviz</a>, <a href="https://github.com/lmco/laikaboss">Laika BOSS</a>, <a href="https://github.com/cuckoosandbox/cuckoo">Cuckoo Sandbox</a>, <a href="https://sandbox.anlyz.io/">Anlyz.io</a>, <a href="https://github.com/decalage2/ViperMonkey">ViperMonkey</a>, <a href="https://github.com/bontchev/pcodedmp">pcodedmp</a>, <a href="https://dridex.malwareconfig.com">dridex.malwareconfig.com</a>, and probably <a href="https://www.virustotal.com">VirusTotal</a>. (Please <a href="(http://decalage.info/contact)">contact me</a> if you have or know a project using oletools)</p>
63 56 <h2 id="download-and-install">Download and Install:</h2>
64   -<p>To use python-oletools from the command line as analysis tools, you may simply <a href="https://github.com/decalage2/oletools/releases">download the latest release archive</a> and extract the files into the directory of your choice.</p>
65   -<p>You may also download the <a href="https://github.com/decalage2/oletools/archive/master.zip">latest development version</a> with the most recent features.</p>
66   -<p>Another possibility is to use a git client to clone the repository (https://github.com/decalage2/oletools.git) into a folder. You can then update it easily in the future.</p>
67   -<p>If you plan to use python-oletools with other Python applications or your own scripts, then the simplest solution is to use &quot;<strong>pip install oletools</strong>&quot; or &quot;<strong>easy_install oletools</strong>&quot; to download and install in one go. Otherwise you may download/extract the zip archive and run &quot;<strong>setup.py install</strong>&quot;.</p>
68   -<p><strong>Important: to update oletools</strong> if it is already installed, you must run <strong>&quot;pip install -U oletools&quot;</strong>, otherwise pip will not update it.</p>
  57 +<p>The recommended way to download and install/update the <strong>latest stable release</strong> of oletools is to use <a href="https://pip.pypa.io/en/stable/installing/">pip</a>:</p>
  58 +<ul>
  59 +<li>On Linux/Mac: <code>sudo -H pip install -U oletools</code></li>
  60 +<li>On Windows: <code>pip install -U oletools</code></li>
  61 +</ul>
  62 +<p>This should automatically create command-line scripts to run each tool from any directory: <code>olevba</code>, <code>mraptor</code>, <code>rtfobj</code>, etc.</p>
  63 +<p>To get the <strong>latest development version</strong> instead:</p>
  64 +<ul>
  65 +<li>On Linux/Mac: <code>sudo -H pip install -U https://github.com/decalage2/oletools/archive/master.zip</code></li>
  66 +<li>On Windows: <code>pip install -U https://github.com/decalage2/oletools/archive/master.zip</code></li>
  67 +</ul>
  68 +<p>See the <a href="https://github.com/decalage2/oletools/wiki/Install">documentation</a> for other installation options.</p>
69 69 <h2 id="documentation">Documentation:</h2>
70 70 <p>The latest version of the documentation can be found <a href="https://github.com/decalage2/oletools/wiki">online</a>, otherwise a copy is provided in the doc subfolder of the package.</p>
71 71 <h2 id="how-to-suggest-improvements-report-issues-or-contribute">How to Suggest Improvements, Report Issues or Contribute:</h2>
... ... @@ -75,7 +75,7 @@
75 75 <p>The code is available in <a href="https://github.com/decalage2/oletools">a GitHub repository</a>. You may use it to submit enhancements using forks and pull requests.</p>
76 76 <h2 id="license">License</h2>
77 77 <p>This license applies to the python-oletools package, apart from the thirdparty folder which contains third-party files published with their own license.</p>
78   -<p>The python-oletools package is copyright (c) 2012-2016 Philippe Lagadec (http://www.decalage.info)</p>
  78 +<p>The python-oletools package is copyright (c) 2012-2017 Philippe Lagadec (http://www.decalage.info)</p>
79 79 <p>All rights reserved.</p>
80 80 <p>Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:</p>
81 81 <ul>
... ...
oletools/README.rst
... ... @@ -26,7 +26,29 @@ Software.
26 26 News
27 27 ----
28 28  
29   -- **2016-11-01 v0.50**: all oletools now support python 2 and 3.
  29 +- **2017-06-29 v0.51**:
  30 +
  31 + - added the `oletools
  32 + cheatsheet <https://github.com/decalage2/oletools/blob/master/cheatsheet/oletools_cheatsheet.pdf>`__
  33 + - improved
  34 + `rtfobj <https://github.com/decalage2/oletools/wiki/rtfobj>`__ to
  35 + handle malformed RTF files, detect vulnerability CVE-2017-0199
  36 + - olevba: improved deobfuscation and Mac files support
  37 + - `mraptor <https://github.com/decalage2/oletools/wiki/mraptor>`__:
  38 + added more ActiveX macro triggers
  39 + - added
  40 + `DocVarDump.vba <https://github.com/decalage2/oletools/blob/master/oletools/DocVarDump.vba>`__
  41 + to dump document variables using Word
  42 + - olemap: can now detect and extract `extra data at end of
  43 + file <http://decalage.info/en/ole_extradata>`__, improved display
  44 + - oledir, olemeta, oletimes: added support for zip files and
  45 + wildcards
  46 + - many
  47 + `bugfixes <https://github.com/decalage2/oletools/milestone/3?closed=1>`__
  48 + in all the tools
  49 + - improved Python 2+3 support
  50 +
  51 +- 2016-11-01 v0.50: all oletools now support python 2 and 3.
30 52  
31 53 - olevba: several bugfixes and improvements.
32 54 - mraptor: improved detection, added mraptor\_milter for
... ... @@ -44,92 +66,13 @@ News
44 66 `rtfobj <https://github.com/decalage2/oletools/wiki/rtfobj>`__:
45 67 improved parsing to handle obfuscated RTF documents, added -d option
46 68 to set output dir. Moved repository and documentation to GitHub.
47   -- 2016-04-19 v0.46:
48   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__ does
49   - not deobfuscate VBA expressions by default (much faster), new option
50   - --deobf to enable it. Fixed color display bug on Windows for several
51   - tools.
52   -- 2016-04-12 v0.45: improved
53   - `rtfobj <https://github.com/decalage2/oletools/wiki/rtfobj>`__ to
54   - handle several `anti-analysis
55   - tricks <http://www.decalage.info/rtf_tricks>`__, improved
56   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__ to
57   - export results in JSON format.
58   -- 2016-03-11 v0.44: improved
59   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__ to
60   - extract and analyse strings from VBA Forms.
61   -- 2016-03-04 v0.43: added new tool
62   - `MacroRaptor <https://github.com/decalage2/oletools/wiki/mraptor>`__
63   - (mraptor) to detect malicious macros, bugfix and slight improvements
64   - in `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__.
65   -- 2016-02-07 v0.42: added two new tools oledir and olemap, better
66   - handling of malformed files and several bugfixes in
67   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__,
68   - improved display for
69   - `olemeta <https://github.com/decalage2/oletools/wiki/olemeta>`__.
70   -- 2015-09-22 v0.41: added new --reveal option to
71   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__, to
72   - show the macro code with VBA strings deobfuscated.
73   -- 2015-09-17 v0.40: Improved macro deobfuscation in
74   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__, to
75   - decode Hex and Base64 within VBA expressions. Display printable
76   - deobfuscated strings by default. Improved the VBA\_Parser API.
77   - Improved performance. Fixed `issue
78   - #23 <https://github.com/decalage2/oletools/issues/23>`__ with
79   - sys.stderr.
80   -- 2015-06-19 v0.12:
81   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__ can
82   - now deobfuscate VBA expressions with any combination of Chr, Asc,
83   - Val, StrReverse, Environ, +, &, using a VBA parser built with
84   - `pyparsing <http://pyparsing.wikispaces.com>`__. New options to
85   - display only the analysis results or only the macros source code. The
86   - analysis is now done on all the VBA modules at once.
87   -- 2015-05-29 v0.11: Improved parsing of MHTML and ActiveMime/MSO files
88   - in `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__,
89   - added several suspicious keywords to VBA scanner (thanks to @ozhermit
90   - and Davy Douhine for the suggestions)
91   -- 2015-05-06 v0.10:
92   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__ now
93   - supports Word MHTML files with macros, aka "Single File Web Page"
94   - (.mht) - see `issue
95   - #10 <https://github.com/decalage2/oletools/issues/10>`__ for more
96   - info
97   -- 2015-03-23 v0.09:
98   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__ now
99   - supports Word 2003 XML files, added anti-sandboxing/VM detection
100   -- 2015-02-08 v0.08:
101   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__ can
102   - now decode strings obfuscated with Hex/StrReverse/Base64/Dridex and
103   - extract IOCs. Added new triage mode, support for non-western
104   - codepages with olefile 0.42, improved API and display, several
105   - bugfixes.
106   -- 2015-01-05 v0.07: improved
107   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__ to
108   - detect suspicious keywords and IOCs in VBA macros, can now scan
109   - several files and open password-protected zip archives, added a
110   - Python API, upgraded OleFileIO\_PL to olefile v0.41
111   -- 2014-08-28 v0.06: added
112   - `olevba <https://github.com/decalage2/oletools/wiki/olevba>`__, a new
113   - tool to extract VBA Macro source code from MS Office documents
114   - (97-2003 and 2007+). Improved
115   - `documentation <https://github.com/decalage2/oletools/wiki>`__
116   -- 2013-07-24 v0.05: added new tools
117   - `olemeta <https://github.com/decalage2/oletools/wiki/olemeta>`__ and
118   - `oletimes <https://github.com/decalage2/oletools/wiki/oletimes>`__
119   -- 2013-04-18 v0.04: fixed bug in rtfobj, added documentation for
120   - `rtfobj <https://github.com/decalage2/oletools/wiki/rtfobj>`__
121   -- 2012-11-09 v0.03: Improved
122   - `pyxswf <https://github.com/decalage2/oletools/wiki/pyxswf>`__ to
123   - extract Flash objects from RTF
124   -- 2012-10-29 v0.02: Added
125   - `oleid <https://github.com/decalage2/oletools/wiki/oleid>`__
126   -- 2012-10-09 v0.01: Initial version of
127   - `olebrowse <https://github.com/decalage2/oletools/wiki/olebrowse>`__
128   - and pyxswf
129   -- see changelog in source code for more info.
130   -
131   -Tools in python-oletools:
132   --------------------------
  69 +
  70 +See the `full
  71 +changelog <https://github.com/decalage2/oletools/wiki/Changelog>`__ for
  72 +more information.
  73 +
  74 +Tools:
  75 +------
133 76  
134 77 - `olebrowse <https://github.com/decalage2/oletools/wiki/olebrowse>`__:
135 78 A simple GUI to browse OLE files (e.g. MS Word, Excel, Powerpoint
... ... @@ -168,41 +111,43 @@ Projects using oletools:
168 111 oletools are used by a number of projects and online malware analysis
169 112 services, including `Viper <http://viper.li/>`__,
170 113 `REMnux <https://remnux.org/>`__,
  114 +`FAME <https://certsocietegenerale.github.io/fame/>`__,
171 115 `Hybrid-analysis.com <https://www.hybrid-analysis.com/>`__, `Joe
172 116 Sandbox <https://www.document-analyzer.net/>`__,
173 117 `Deepviz <https://sandbox.deepviz.com/>`__, `Laika
174 118 BOSS <https://github.com/lmco/laikaboss>`__, `Cuckoo
175 119 Sandbox <https://github.com/cuckoosandbox/cuckoo>`__,
176 120 `Anlyz.io <https://sandbox.anlyz.io/>`__,
177   -`pcodedmp <https://github.com/bontchev/pcodedmp>`__ and probably
178   -`VirusTotal <https://www.virustotal.com>`__. (Please `contact
  121 +`ViperMonkey <https://github.com/decalage2/ViperMonkey>`__,
  122 +`pcodedmp <https://github.com/bontchev/pcodedmp>`__,
  123 +`dridex.malwareconfig.com <https://dridex.malwareconfig.com>`__, and
  124 +probably `VirusTotal <https://www.virustotal.com>`__. (Please `contact
179 125 me <(http://decalage.info/contact)>`__ if you have or know a project
180 126 using oletools)
181 127  
182 128 Download and Install:
183 129 ---------------------
184 130  
185   -To use python-oletools from the command line as analysis tools, you may
186   -simply `download the latest release
187   -archive <https://github.com/decalage2/oletools/releases>`__ and extract
188   -the files into the directory of your choice.
  131 +The recommended way to download and install/update the **latest stable
  132 +release** of oletools is to use
  133 +`pip <https://pip.pypa.io/en/stable/installing/>`__:
  134 +
  135 +- On Linux/Mac: ``sudo -H pip install -U oletools``
  136 +- On Windows: ``pip install -U oletools``
189 137  
190   -You may also download the `latest development
191   -version <https://github.com/decalage2/oletools/archive/master.zip>`__
192   -with the most recent features.
  138 +This should automatically create command-line scripts to run each tool
  139 +from any directory: ``olevba``, ``mraptor``, ``rtfobj``, etc.
193 140  
194   -Another possibility is to use a git client to clone the repository
195   -(https://github.com/decalage2/oletools.git) into a folder. You can then
196   -update it easily in the future.
  141 +To get the **latest development version** instead:
197 142  
198   -If you plan to use python-oletools with other Python applications or
199   -your own scripts, then the simplest solution is to use "**pip install
200   -oletools**\ " or "**easy\_install oletools**\ " to download and install
201   -in one go. Otherwise you may download/extract the zip archive and run
202   -"**setup.py install**\ ".
  143 +- On Linux/Mac:
  144 + ``sudo -H pip install -U https://github.com/decalage2/oletools/archive/master.zip``
  145 +- On Windows:
  146 + ``pip install -U https://github.com/decalage2/oletools/archive/master.zip``
203 147  
204   -**Important: to update oletools** if it is already installed, you must
205   -run **"pip install -U oletools"**, otherwise pip will not update it.
  148 +See the
  149 +`documentation <https://github.com/decalage2/oletools/wiki/Install>`__
  150 +for other installation options.
206 151  
207 152 Documentation:
208 153 --------------
... ... @@ -235,7 +180,7 @@ This license applies to the python-oletools package, apart from the
235 180 thirdparty folder which contains third-party files published with their
236 181 own license.
237 182  
238   -The python-oletools package is copyright (c) 2012-2016 Philippe Lagadec
  183 +The python-oletools package is copyright (c) 2012-2017 Philippe Lagadec
239 184 (http://www.decalage.info)
240 185  
241 186 All rights reserved.
... ...
oletools/doc/Home.html
... ... @@ -8,9 +8,9 @@
8 8 <style type="text/css">code{white-space: pre;}</style>
9 9 </head>
10 10 <body>
11   -<h1 id="python-oletools-v0.50-documentation">python-oletools v0.50 documentation</h1>
  11 +<h1 id="python-oletools-v0.51-documentation">python-oletools v0.51 documentation</h1>
12 12 <p>This is the home page of the documentation for python-oletools. The latest version can be found <a href="https://github.com/decalage2/oletools/wiki">online</a>, otherwise a copy is provided in the doc subfolder of the package.</p>
13   -<p><a href="http://www.decalage.info/python/oletools">python-oletools</a> is a package of python tools to analyze <a href="http://en.wikipedia.org/wiki/Compound_File_Binary_Format">Microsoft OLE2 files</a> (also called Structured Storage, Compound File Binary Format or Compound Document File Format), such as Microsoft Office documents or Outlook messages, mainly for malware analysis, forensics and debugging. It is based on the <a href="http://www.decalage.info/olefile">olefile</a> parser. See <a href="http://www.decalage.info/python/oletools">http://www.decalage.info/python/oletools</a> for more info.</p>
  13 +<p><a href="http://www.decalage.info/python/oletools">python-oletools</a> is a package of python tools to analyze <a href="http://en.wikipedia.org/wiki/Compound_File_Binary_Format">Microsoft OLE2 files</a> (also called Structured Storage, Compound File Binary Format or Compound Document File Format), such as Microsoft Office documents or Outlook messages, mainly for malware analysis, forensics and debugging. It is based on the <a href="http://www.decalage.info/olefile">olefile</a> parser. See <a href="http://www.decalage.info/python/oletools" class="uri">http://www.decalage.info/python/oletools</a> for more info.</p>
14 14 <p><strong>Quick links:</strong> <a href="http://www.decalage.info/python/oletools">Home page</a> - <a href="https://github.com/decalage2/oletools/wiki/Install">Download/Install</a> - <a href="https://github.com/decalage2/oletools/wiki">Documentation</a> - <a href="https://github.com/decalage2/oletools/issues">Report Issues/Suggestions/Questions</a> - <a href="http://decalage.info/contact">Contact the Author</a> - <a href="https://github.com/decalage2/oletools">Repository</a> - <a href="https://twitter.com/decalage2">Updates on Twitter</a></p>
15 15 <p>Note: python-oletools is not related to OLETools published by BeCubed Software.</p>
16 16 <h2 id="tools-in-python-oletools">Tools in python-oletools:</h2>
... ...
oletools/doc/Home.md
1   -python-oletools v0.50 documentation
  1 +python-oletools v0.51 documentation
2 2 ===================================
3 3  
4 4 This is the home page of the documentation for python-oletools. The latest version can be found
... ...
oletools/doc/License.html
... ... @@ -10,7 +10,7 @@
10 10 <body>
11 11 <h1 id="license-for-python-oletools">License for python-oletools</h1>
12 12 <p>This license applies to the <a href="http://www.decalage.info/python/oletools">python-oletools</a> package, apart from the thirdparty folder which contains third-party files published with their own license.</p>
13   -<p>The python-oletools package is copyright (c) 2012-2016 Philippe Lagadec (<a href="http://www.decalage.info">http://www.decalage.info</a>)</p>
  13 +<p>The python-oletools package is copyright (c) 2012-2017 Philippe Lagadec (<a href="http://www.decalage.info" class="uri">http://www.decalage.info</a>)</p>
14 14 <p>All rights reserved.</p>
15 15 <p>Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:</p>
16 16 <ul>
... ... @@ -21,7 +21,7 @@
21 21 <table>
22 22 <tbody>
23 23 <tr class="odd">
24   -<td align="left">License for officeparser</td>
  24 +<td>License for officeparser</td>
25 25 </tr>
26 26 </tbody>
27 27 </table>
... ...
oletools/doc/License.md
... ... @@ -4,7 +4,7 @@ License for python-oletools
4 4 This license applies to the [python-oletools](http://www.decalage.info/python/oletools) package, apart from the
5 5 thirdparty folder which contains third-party files published with their own license.
6 6  
7   -The python-oletools package is copyright (c) 2012-2016 Philippe Lagadec ([http://www.decalage.info](http://www.decalage.info))
  7 +The python-oletools package is copyright (c) 2012-2017 Philippe Lagadec ([http://www.decalage.info](http://www.decalage.info))
8 8  
9 9 All rights reserved.
10 10  
... ...
oletools/doc/mraptor.html
... ... @@ -49,6 +49,7 @@ An exit code is returned based on the analysis result:
49 49 <p><strong>Important</strong>: on Linux/MacOSX, always add double quotes around a file name when you use wildcards such as <code>*</code> and <code>?</code>. Otherwise, the shell may replace the argument with the actual list of files matching the wildcards before starting the script.</p>
50 50 <div class="figure">
51 51 <img src="mraptor1.png" />
  52 +
52 53 </div>
53 54 <h2 id="python-3-support---mraptor3">Python 3 support - mraptor3</h2>
54 55 <p>As of v0.50, mraptor has been ported to Python 3 thanks to <span class="citation">@sebdraven</span>. However, the differences between Python 2 and 3 are significant and for now there is a separate version of mraptor named mraptor3 to be used with Python 3.</p>
... ...
oletools/doc/olebrowse.html
... ... @@ -24,14 +24,17 @@
24 24 <p>Main menu, showing all streams in the OLE file:</p>
25 25 <div class="figure">
26 26 <img src="olebrowse1_menu.png" />
  27 +
27 28 </div>
28 29 <p>Menu with actions for a stream:</p>
29 30 <div class="figure">
30 31 <img src="olebrowse2_stream.png" />
  32 +
31 33 </div>
32 34 <p>Hex view for a stream:</p>
33 35 <div class="figure">
34 36 <img src="olebrowse3_hexview.png" />
  37 +
35 38 </div>
36 39 <hr />
37 40 <h2 id="python-oletools-documentation">python-oletools documentation</h2>
... ...
oletools/doc/oledir.html
... ... @@ -19,6 +19,7 @@
19 19 <pre class="text"><code>oledir.py file.doc</code></pre>
20 20 <div class="figure">
21 21 <img src="oledir.png" />
  22 +
22 23 </div>
23 24 <hr />
24 25 <h2 id="how-to-use-oledir-in-python-applications">How to use oledir in Python applications</h2>
... ...
oletools/doc/oleid.html
... ... @@ -7,23 +7,41 @@
7 7 <title></title>
8 8 <style type="text/css">code{white-space: pre;}</style>
9 9 <style type="text/css">
  10 +div.sourceCode { overflow-x: auto; }
10 11 table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
11 12 margin: 0; padding: 0; vertical-align: baseline; border: none; }
12 13 table.sourceCode { width: 100%; line-height: 100%; }
13 14 td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
14 15 td.sourceCode { padding-left: 5px; }
15   -code > span.kw { color: #007020; font-weight: bold; }
16   -code > span.dt { color: #902000; }
17   -code > span.dv { color: #40a070; }
18   -code > span.bn { color: #40a070; }
19   -code > span.fl { color: #40a070; }
20   -code > span.ch { color: #4070a0; }
21   -code > span.st { color: #4070a0; }
22   -code > span.co { color: #60a0b0; font-style: italic; }
23   -code > span.ot { color: #007020; }
24   -code > span.al { color: #ff0000; font-weight: bold; }
25   -code > span.fu { color: #06287e; }
26   -code > span.er { color: #ff0000; font-weight: bold; }
  16 +code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
  17 +code > span.dt { color: #902000; } /* DataType */
  18 +code > span.dv { color: #40a070; } /* DecVal */
  19 +code > span.bn { color: #40a070; } /* BaseN */
  20 +code > span.fl { color: #40a070; } /* Float */
  21 +code > span.ch { color: #4070a0; } /* Char */
  22 +code > span.st { color: #4070a0; } /* String */
  23 +code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
  24 +code > span.ot { color: #007020; } /* Other */
  25 +code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
  26 +code > span.fu { color: #06287e; } /* Function */
  27 +code > span.er { color: #ff0000; font-weight: bold; } /* Error */
  28 +code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
  29 +code > span.cn { color: #880000; } /* Constant */
  30 +code > span.sc { color: #4070a0; } /* SpecialChar */
  31 +code > span.vs { color: #4070a0; } /* VerbatimString */
  32 +code > span.ss { color: #bb6688; } /* SpecialString */
  33 +code > span.im { } /* Import */
  34 +code > span.va { color: #19177c; } /* Variable */
  35 +code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
  36 +code > span.op { color: #666666; } /* Operator */
  37 +code > span.bu { } /* BuiltIn */
  38 +code > span.ex { } /* Extension */
  39 +code > span.pp { color: #bc7a00; } /* Preprocessor */
  40 +code > span.at { color: #7d9029; } /* Attribute */
  41 +code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
  42 +code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
  43 +code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
  44 +code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
27 45 </style>
28 46 </head>
29 47 <body>
... ... @@ -76,9 +94,9 @@ Filename: word_flash_vba.doc
76 94 +-------------------------------+-----------------------+</code></pre>
77 95 <h2 id="how-to-use-oleid-in-your-python-applications">How to use oleid in your Python applications</h2>
78 96 <p>First, import oletools.oleid, and create an <strong>OleID</strong> object to scan a file:</p>
79   -<pre class="sourceCode python"><code class="sourceCode python"><span class="ch">import</span> oletools.oleid
  97 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">import</span> oletools.oleid
80 98  
81   -oid = oletools.oleid.OleID(filename)</code></pre>
  99 +oid <span class="op">=</span> oletools.oleid.OleID(filename)</code></pre></div>
82 100 <p>Note: filename can be a filename, a file-like object, or a bytes string containing the file to be analyzed.</p>
83 101 <p>Second, call the <strong>check()</strong> method. It returns a list of <strong>Indicator</strong> objects.</p>
84 102 <p>Each Indicator object has the following attributes:</p>
... ... @@ -90,11 +108,11 @@ oid = oletools.oleid.OleID(filename)&lt;/code&gt;&lt;/pre&gt;
90 108 <li><strong>value</strong>: value of the indicator</li>
91 109 </ul>
92 110 <p>For example, the following code displays all the indicators:</p>
93   -<pre class="sourceCode python"><code class="sourceCode python">indicators = oid.check()
94   -<span class="kw">for</span> i in indicators:
95   - <span class="dt">print</span> <span class="st">&#39;Indicator id=</span><span class="ot">%s</span><span class="st"> name=&quot;</span><span class="ot">%s</span><span class="st">&quot; type=</span><span class="ot">%s</span><span class="st"> value=</span><span class="ot">%s</span><span class="st">&#39;</span> % (i.<span class="dt">id</span>, i.name, i.<span class="dt">type</span>, <span class="dt">repr</span>(i.value))
96   - <span class="dt">print</span> <span class="st">&#39;description:&#39;</span>, i.description
97   - <span class="dt">print</span> <span class="st">&#39;&#39;</span></code></pre>
  111 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">indicators <span class="op">=</span> oid.check()
  112 +<span class="cf">for</span> i <span class="kw">in</span> indicators:
  113 + <span class="bu">print</span> <span class="st">&#39;Indicator id=</span><span class="sc">%s</span><span class="st"> name=&quot;</span><span class="sc">%s</span><span class="st">&quot; type=</span><span class="sc">%s</span><span class="st"> value=</span><span class="sc">%s</span><span class="st">&#39;</span> <span class="op">%</span> (i.<span class="bu">id</span>, i.name, i.<span class="bu">type</span>, <span class="bu">repr</span>(i.value))
  114 + <span class="bu">print</span> <span class="st">&#39;description:&#39;</span>, i.description
  115 + <span class="bu">print</span> <span class="st">&#39;&#39;</span></code></pre></div>
98 116 <p>See the source code of oleid.py for more details.</p>
99 117 <hr />
100 118 <h2 id="python-oletools-documentation">python-oletools documentation</h2>
... ...
oletools/doc/olemap.html
... ... @@ -19,9 +19,11 @@
19 19 <pre class="text"><code>olemap.py file.doc</code></pre>
20 20 <div class="figure">
21 21 <img src="olemap1.png" />
  22 +
22 23 </div>
23 24 <div class="figure">
24 25 <img src="olemap2.png" />
  26 +
25 27 </div>
26 28 <hr />
27 29 <h2 id="how-to-use-olemap-in-python-applications">How to use olemap in Python applications</h2>
... ...
oletools/doc/olemeta.html
... ... @@ -16,6 +16,7 @@
16 16 <h3 id="example">Example</h3>
17 17 <div class="figure">
18 18 <img src="olemeta1.png" />
  19 +
19 20 </div>
20 21 <h2 id="how-to-use-olemeta-in-python-applications">How to use olemeta in Python applications</h2>
21 22 <p>TODO</p>
... ...
oletools/doc/olevba.html
... ... @@ -7,23 +7,41 @@
7 7 <title></title>
8 8 <style type="text/css">code{white-space: pre;}</style>
9 9 <style type="text/css">
  10 +div.sourceCode { overflow-x: auto; }
10 11 table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
11 12 margin: 0; padding: 0; vertical-align: baseline; border: none; }
12 13 table.sourceCode { width: 100%; line-height: 100%; }
13 14 td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
14 15 td.sourceCode { padding-left: 5px; }
15   -code > span.kw { color: #007020; font-weight: bold; }
16   -code > span.dt { color: #902000; }
17   -code > span.dv { color: #40a070; }
18   -code > span.bn { color: #40a070; }
19   -code > span.fl { color: #40a070; }
20   -code > span.ch { color: #4070a0; }
21   -code > span.st { color: #4070a0; }
22   -code > span.co { color: #60a0b0; font-style: italic; }
23   -code > span.ot { color: #007020; }
24   -code > span.al { color: #ff0000; font-weight: bold; }
25   -code > span.fu { color: #06287e; }
26   -code > span.er { color: #ff0000; font-weight: bold; }
  16 +code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
  17 +code > span.dt { color: #902000; } /* DataType */
  18 +code > span.dv { color: #40a070; } /* DecVal */
  19 +code > span.bn { color: #40a070; } /* BaseN */
  20 +code > span.fl { color: #40a070; } /* Float */
  21 +code > span.ch { color: #4070a0; } /* Char */
  22 +code > span.st { color: #4070a0; } /* String */
  23 +code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
  24 +code > span.ot { color: #007020; } /* Other */
  25 +code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
  26 +code > span.fu { color: #06287e; } /* Function */
  27 +code > span.er { color: #ff0000; font-weight: bold; } /* Error */
  28 +code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
  29 +code > span.cn { color: #880000; } /* Constant */
  30 +code > span.sc { color: #4070a0; } /* SpecialChar */
  31 +code > span.vs { color: #4070a0; } /* VerbatimString */
  32 +code > span.ss { color: #bb6688; } /* SpecialString */
  33 +code > span.im { } /* Import */
  34 +code > span.va { color: #19177c; } /* Variable */
  35 +code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
  36 +code > span.op { color: #666666; } /* Operator */
  37 +code > span.bu { } /* BuiltIn */
  38 +code > span.ex { } /* Extension */
  39 +code > span.pp { color: #bc7a00; } /* Preprocessor */
  40 +code > span.at { color: #7d9029; } /* Attribute */
  41 +code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
  42 +code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
  43 +code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
  44 +code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
27 45 </style>
28 46 </head>
29 47 <body>
... ... @@ -219,22 +237,22 @@ OLE:MA----- \MalwareZoo\VBA\samples\Word within Word macro auto.doc&lt;/code&gt;&lt;/pre&gt;
219 237 <p>IMPORTANT: olevba is currently under active development, therefore this API is likely to change.</p>
220 238 <h3 id="import-olevba">Import olevba</h3>
221 239 <p>First, import the <strong>oletools.olevba</strong> package, using at least the VBA_Parser and VBA_Scanner classes:</p>
222   -<pre class="sourceCode python"><code class="sourceCode python"><span class="ch">from</span> oletools.olevba <span class="ch">import</span> VBA_Parser, TYPE_OLE, TYPE_OpenXML, TYPE_Word2003_XML, TYPE_MHTML</code></pre>
  240 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> oletools.olevba <span class="im">import</span> VBA_Parser, TYPE_OLE, TYPE_OpenXML, TYPE_Word2003_XML, TYPE_MHTML</code></pre></div>
223 241 <h3 id="parse-a-ms-office-file---vba_parser">Parse a MS Office file - VBA_Parser</h3>
224 242 <p>To parse a file on disk, create an instance of the <strong>VBA_Parser</strong> class, providing the name of the file to open as parameter. For example:</p>
225   -<pre class="sourceCode python"><code class="sourceCode python">vbaparser = VBA_Parser(<span class="st">&#39;my_file_with_macros.doc&#39;</span>)</code></pre>
  243 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">vbaparser <span class="op">=</span> VBA_Parser(<span class="st">&#39;my_file_with_macros.doc&#39;</span>)</code></pre></div>
226 244 <p>The file may also be provided as a bytes string containing its data. In that case, the actual filename must be provided for reference, and the file content with the data parameter. For example:</p>
227   -<pre class="sourceCode python"><code class="sourceCode python">myfile = <span class="st">&#39;my_file_with_macros.doc&#39;</span>
228   -filedata = <span class="dt">open</span>(myfile, <span class="st">&#39;rb&#39;</span>).read()
229   -vbaparser = VBA_Parser(myfile, data=filedata)</code></pre>
  245 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">myfile <span class="op">=</span> <span class="st">&#39;my_file_with_macros.doc&#39;</span>
  246 +filedata <span class="op">=</span> <span class="bu">open</span>(myfile, <span class="st">&#39;rb&#39;</span>).read()
  247 +vbaparser <span class="op">=</span> VBA_Parser(myfile, data<span class="op">=</span>filedata)</code></pre></div>
230 248 <p>VBA_Parser will raise an exception if the file is not a supported format, such as OLE (MS Office 97-2003), OpenXML (MS Office 2007+), MHTML or Word 2003 XML.</p>
231 249 <p>After parsing the file, the attribute <strong>VBA_Parser.type</strong> is a string indicating the file type. It can be either TYPE_OLE, TYPE_OpenXML, TYPE_Word2003_XML or TYPE_MHTML. (constants defined in the olevba module)</p>
232 250 <h3 id="detect-vba-macros">Detect VBA macros</h3>
233 251 <p>The method <strong>detect_vba_macros</strong> of a VBA_Parser object returns True if VBA macros have been found in the file, False otherwise.</p>
234   -<pre class="sourceCode python"><code class="sourceCode python"><span class="kw">if</span> vbaparser.detect_vba_macros():
235   - <span class="dt">print</span> <span class="st">&#39;VBA Macros found&#39;</span>
236   -<span class="kw">else</span>:
237   - <span class="dt">print</span> <span class="st">&#39;No VBA Macros found&#39;</span></code></pre>
  252 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="cf">if</span> vbaparser.detect_vba_macros():
  253 + <span class="bu">print</span> <span class="st">&#39;VBA Macros found&#39;</span>
  254 +<span class="cf">else</span>:
  255 + <span class="bu">print</span> <span class="st">&#39;No VBA Macros found&#39;</span></code></pre></div>
238 256 <p>Note: The detection algorithm looks for streams and storage with specific names in the OLE structure, which works fine for all the supported formats listed above. However, for some formats such as PowerPoint 97-2003, this method will always return False because VBA Macros are stored in a different way which is not yet supported by olevba.</p>
239 257 <p>Moreover, if the file contains an embedded document (e.g. an Excel workbook inserted into a Word document), this method may return True if the embedded document contains VBA Macros, even if the main document does not.</p>
240 258 <h3 id="extract-vba-macro-source-code">Extract VBA Macro Source Code</h3>
... ... @@ -246,13 +264,13 @@ vbaparser = VBA_Parser(myfile, data=filedata)&lt;/code&gt;&lt;/pre&gt;
246 264 <li>vba_code: string containing the VBA source code in clear text</li>
247 265 </ul>
248 266 <p>Example:</p>
249   -<pre class="sourceCode python"><code class="sourceCode python"><span class="kw">for</span> (filename, stream_path, vba_filename, vba_code) in vbaparser.extract_macros():
250   - <span class="dt">print</span> <span class="st">&#39;-&#39;</span>*<span class="dv">79</span>
251   - <span class="dt">print</span> <span class="st">&#39;Filename :&#39;</span>, filename
252   - <span class="dt">print</span> <span class="st">&#39;OLE stream :&#39;</span>, stream_path
253   - <span class="dt">print</span> <span class="st">&#39;VBA filename:&#39;</span>, vba_filename
254   - <span class="dt">print</span> <span class="st">&#39;- &#39;</span>*<span class="dv">39</span>
255   - <span class="dt">print</span> vba_code</code></pre>
  267 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="cf">for</span> (filename, stream_path, vba_filename, vba_code) <span class="kw">in</span> vbaparser.extract_macros():
  268 + <span class="bu">print</span> <span class="st">&#39;-&#39;</span><span class="op">*</span><span class="dv">79</span>
  269 + <span class="bu">print</span> <span class="st">&#39;Filename :&#39;</span>, filename
  270 + <span class="bu">print</span> <span class="st">&#39;OLE stream :&#39;</span>, stream_path
  271 + <span class="bu">print</span> <span class="st">&#39;VBA filename:&#39;</span>, vba_filename
  272 + <span class="bu">print</span> <span class="st">&#39;- &#39;</span><span class="op">*</span><span class="dv">39</span>
  273 + <span class="bu">print</span> vba_code</code></pre></div>
256 274 <p>Alternatively, the VBA_Parser method <strong>extract_all_macros</strong> returns the same results as a list of tuples.</p>
257 275 <h3 id="analyze-vba-source-code">Analyze VBA Source Code</h3>
258 276 <p>Since version 0.40, the VBA_Parser class provides simpler methods than VBA_Scanner to analyze all macros contained in a file:</p>
... ... @@ -265,24 +283,24 @@ vbaparser = VBA_Parser(myfile, data=filedata)&lt;/code&gt;&lt;/pre&gt;
265 283 <li>description provides a description of the keyword. For obfuscated strings, it is the encoded value of the string.</li>
266 284 </ul>
267 285 <p>Example:</p>
268   -<pre class="sourceCode python"><code class="sourceCode python">results = vbaparser.analyze_macros()
269   -<span class="kw">for</span> kw_type, keyword, description in results:
270   - <span class="dt">print</span> <span class="st">&#39;type=</span><span class="ot">%s</span><span class="st"> - keyword=</span><span class="ot">%s</span><span class="st"> - description=</span><span class="ot">%s</span><span class="st">&#39;</span> % (kw_type, keyword, description)</code></pre>
  286 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">results <span class="op">=</span> vbaparser.analyze_macros()
  287 +<span class="cf">for</span> kw_type, keyword, description <span class="kw">in</span> results:
  288 + <span class="bu">print</span> <span class="st">&#39;type=</span><span class="sc">%s</span><span class="st"> - keyword=</span><span class="sc">%s</span><span class="st"> - description=</span><span class="sc">%s</span><span class="st">&#39;</span> <span class="op">%</span> (kw_type, keyword, description)</code></pre></div>
271 289 <p>After calling analyze_macros, the following VBA_Parser attributes also provide the number of items found for each category:</p>
272   -<pre class="sourceCode python"><code class="sourceCode python"><span class="dt">print</span> <span class="st">&#39;AutoExec keywords: </span><span class="ot">%d</span><span class="st">&#39;</span> % vbaparser.nb_autoexec
273   -<span class="dt">print</span> <span class="st">&#39;Suspicious keywords: </span><span class="ot">%d</span><span class="st">&#39;</span> % vbaparser.nb_suspicious
274   -<span class="dt">print</span> <span class="st">&#39;IOCs: </span><span class="ot">%d</span><span class="st">&#39;</span> % vbaparser.nb_iocs
275   -<span class="dt">print</span> <span class="st">&#39;Hex obfuscated strings: </span><span class="ot">%d</span><span class="st">&#39;</span> % vbaparser.nb_hexstrings
276   -<span class="dt">print</span> <span class="st">&#39;Base64 obfuscated strings: </span><span class="ot">%d</span><span class="st">&#39;</span> % vbaparser.nb_base64strings
277   -<span class="dt">print</span> <span class="st">&#39;Dridex obfuscated strings: </span><span class="ot">%d</span><span class="st">&#39;</span> % vbaparser.nb_dridexstrings
278   -<span class="dt">print</span> <span class="st">&#39;VBA obfuscated strings: </span><span class="ot">%d</span><span class="st">&#39;</span> % vbaparser.nb_vbastrings</code></pre>
  290 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="bu">print</span> <span class="st">&#39;AutoExec keywords: </span><span class="sc">%d</span><span class="st">&#39;</span> <span class="op">%</span> vbaparser.nb_autoexec
  291 +<span class="bu">print</span> <span class="st">&#39;Suspicious keywords: </span><span class="sc">%d</span><span class="st">&#39;</span> <span class="op">%</span> vbaparser.nb_suspicious
  292 +<span class="bu">print</span> <span class="st">&#39;IOCs: </span><span class="sc">%d</span><span class="st">&#39;</span> <span class="op">%</span> vbaparser.nb_iocs
  293 +<span class="bu">print</span> <span class="st">&#39;Hex obfuscated strings: </span><span class="sc">%d</span><span class="st">&#39;</span> <span class="op">%</span> vbaparser.nb_hexstrings
  294 +<span class="bu">print</span> <span class="st">&#39;Base64 obfuscated strings: </span><span class="sc">%d</span><span class="st">&#39;</span> <span class="op">%</span> vbaparser.nb_base64strings
  295 +<span class="bu">print</span> <span class="st">&#39;Dridex obfuscated strings: </span><span class="sc">%d</span><span class="st">&#39;</span> <span class="op">%</span> vbaparser.nb_dridexstrings
  296 +<span class="bu">print</span> <span class="st">&#39;VBA obfuscated strings: </span><span class="sc">%d</span><span class="st">&#39;</span> <span class="op">%</span> vbaparser.nb_vbastrings</code></pre></div>
279 297 <h3 id="deobfuscate-vba-macro-source-code">Deobfuscate VBA Macro Source Code</h3>
280 298 <p>The method <strong>reveal</strong> attempts to deobfuscate the macro source code by replacing all the obfuscated strings by their decoded content. Returns a single string.</p>
281 299 <p>Example:</p>
282   -<pre class="sourceCode python"><code class="sourceCode python"><span class="dt">print</span> vbaparser.reveal()</code></pre>
  300 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="bu">print</span> vbaparser.reveal()</code></pre></div>
283 301 <h3 id="close-the-vba_parser">Close the VBA_Parser</h3>
284 302 <p>After usage, it is better to call the <strong>close</strong> method of the VBA_Parser object, to make sure the file is closed, especially if your application is parsing many files.</p>
285   -<pre class="sourceCode python"><code class="sourceCode python">vbaparser.close()</code></pre>
  303 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">vbaparser.close()</code></pre></div>
286 304 <hr />
287 305 <h2 id="deprecated-api">Deprecated API</h2>
288 306 <p>The following methods and functions are still functional, but their usage is not recommended since they have been replaced by better solutions.</p>
... ... @@ -297,54 +315,54 @@ vbaparser = VBA_Parser(myfile, data=filedata)&lt;/code&gt;&lt;/pre&gt;
297 315 <li>description provides a description of the keyword. For obfuscated strings, it is the encoded value of the string.</li>
298 316 </ul>
299 317 <p>Example:</p>
300   -<pre class="sourceCode python"><code class="sourceCode python">vba_scanner = VBA_Scanner(vba_code)
301   -results = vba_scanner.scan(include_decoded_strings=<span class="ot">True</span>)
302   -<span class="kw">for</span> kw_type, keyword, description in results:
303   - <span class="dt">print</span> <span class="st">&#39;type=</span><span class="ot">%s</span><span class="st"> - keyword=</span><span class="ot">%s</span><span class="st"> - description=</span><span class="ot">%s</span><span class="st">&#39;</span> % (kw_type, keyword, description)</code></pre>
  318 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">vba_scanner <span class="op">=</span> VBA_Scanner(vba_code)
  319 +results <span class="op">=</span> vba_scanner.scan(include_decoded_strings<span class="op">=</span><span class="va">True</span>)
  320 +<span class="cf">for</span> kw_type, keyword, description <span class="kw">in</span> results:
  321 + <span class="bu">print</span> <span class="st">&#39;type=</span><span class="sc">%s</span><span class="st"> - keyword=</span><span class="sc">%s</span><span class="st"> - description=</span><span class="sc">%s</span><span class="st">&#39;</span> <span class="op">%</span> (kw_type, keyword, description)</code></pre></div>
304 322 <p>The function <strong>scan_vba</strong> is a shortcut for VBA_Scanner(vba_code).scan():</p>
305   -<pre class="sourceCode python"><code class="sourceCode python">results = scan_vba(vba_code, include_decoded_strings=<span class="ot">True</span>)
306   -<span class="kw">for</span> kw_type, keyword, description in results:
307   - <span class="dt">print</span> <span class="st">&#39;type=</span><span class="ot">%s</span><span class="st"> - keyword=</span><span class="ot">%s</span><span class="st"> - description=</span><span class="ot">%s</span><span class="st">&#39;</span> % (kw_type, keyword, description)</code></pre>
  323 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">results <span class="op">=</span> scan_vba(vba_code, include_decoded_strings<span class="op">=</span><span class="va">True</span>)
  324 +<span class="cf">for</span> kw_type, keyword, description <span class="kw">in</span> results:
  325 + <span class="bu">print</span> <span class="st">&#39;type=</span><span class="sc">%s</span><span class="st"> - keyword=</span><span class="sc">%s</span><span class="st"> - description=</span><span class="sc">%s</span><span class="st">&#39;</span> <span class="op">%</span> (kw_type, keyword, description)</code></pre></div>
308 326 <p><strong>scan_summary</strong> returns a tuple with the number of items found for each category: (autoexec, suspicious, IOCs, hex, base64, dridex).</p>
309 327 <h3 id="detect-auto-executable-macros-deprecated">Detect auto-executable macros (deprecated)</h3>
310 328 <p><strong>Deprecated</strong>: It is preferable to use either scan_vba or VBA_Scanner to get all results at once.</p>
311 329 <p>The function <strong>detect_autoexec</strong> checks if VBA macro code contains specific macro names that will be triggered when the document/workbook is opened, closed, changed, etc.</p>
312 330 <p>It returns a list of tuples containing two strings, the detected keyword, and the description of the trigger. (See the malware example above)</p>
313 331 <p>Sample usage:</p>
314   -<pre class="sourceCode python"><code class="sourceCode python"><span class="ch">from</span> oletools.olevba <span class="ch">import</span> detect_autoexec
315   -autoexec_keywords = detect_autoexec(vba_code)
316   -<span class="kw">if</span> autoexec_keywords:
317   - <span class="dt">print</span> <span class="st">&#39;Auto-executable macro keywords found:&#39;</span>
318   - <span class="kw">for</span> keyword, description in autoexec_keywords:
319   - <span class="dt">print</span> <span class="st">&#39;</span><span class="ot">%s</span><span class="st">: </span><span class="ot">%s</span><span class="st">&#39;</span> % (keyword, description)
320   -<span class="kw">else</span>:
321   - <span class="dt">print</span> <span class="st">&#39;Auto-executable macro keywords: None found&#39;</span></code></pre>
  332 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> oletools.olevba <span class="im">import</span> detect_autoexec
  333 +autoexec_keywords <span class="op">=</span> detect_autoexec(vba_code)
  334 +<span class="cf">if</span> autoexec_keywords:
  335 + <span class="bu">print</span> <span class="st">&#39;Auto-executable macro keywords found:&#39;</span>
  336 + <span class="cf">for</span> keyword, description <span class="kw">in</span> autoexec_keywords:
  337 + <span class="bu">print</span> <span class="st">&#39;</span><span class="sc">%s</span><span class="st">: </span><span class="sc">%s</span><span class="st">&#39;</span> <span class="op">%</span> (keyword, description)
  338 +<span class="cf">else</span>:
  339 + <span class="bu">print</span> <span class="st">&#39;Auto-executable macro keywords: None found&#39;</span></code></pre></div>
322 340 <h3 id="detect-suspicious-vba-keywords-deprecated">Detect suspicious VBA keywords (deprecated)</h3>
323 341 <p><strong>Deprecated</strong>: It is preferable to use either scan_vba or VBA_Scanner to get all results at once.</p>
324 342 <p>The function <strong>detect_suspicious</strong> checks if VBA macro code contains specific keywords often used by malware to act on the system (create files, run commands or applications, write to the registry, etc).</p>
325 343 <p>It returns a list of tuples containing two strings, the detected keyword, and the description of the corresponding malicious behaviour. (See the malware example above)</p>
326 344 <p>Sample usage:</p>
327   -<pre class="sourceCode python"><code class="sourceCode python"><span class="ch">from</span> oletools.olevba <span class="ch">import</span> detect_suspicious
328   -suspicious_keywords = detect_suspicious(vba_code)
329   -<span class="kw">if</span> suspicious_keywords:
330   - <span class="dt">print</span> <span class="st">&#39;Suspicious VBA keywords found:&#39;</span>
331   - <span class="kw">for</span> keyword, description in suspicious_keywords:
332   - <span class="dt">print</span> <span class="st">&#39;</span><span class="ot">%s</span><span class="st">: </span><span class="ot">%s</span><span class="st">&#39;</span> % (keyword, description)
333   -<span class="kw">else</span>:
334   - <span class="dt">print</span> <span class="st">&#39;Suspicious VBA keywords: None found&#39;</span></code></pre>
  345 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> oletools.olevba <span class="im">import</span> detect_suspicious
  346 +suspicious_keywords <span class="op">=</span> detect_suspicious(vba_code)
  347 +<span class="cf">if</span> suspicious_keywords:
  348 + <span class="bu">print</span> <span class="st">&#39;Suspicious VBA keywords found:&#39;</span>
  349 + <span class="cf">for</span> keyword, description <span class="kw">in</span> suspicious_keywords:
  350 + <span class="bu">print</span> <span class="st">&#39;</span><span class="sc">%s</span><span class="st">: </span><span class="sc">%s</span><span class="st">&#39;</span> <span class="op">%</span> (keyword, description)
  351 +<span class="cf">else</span>:
  352 + <span class="bu">print</span> <span class="st">&#39;Suspicious VBA keywords: None found&#39;</span></code></pre></div>
335 353 <h3 id="extract-potential-iocs-deprecated">Extract potential IOCs (deprecated)</h3>
336 354 <p><strong>Deprecated</strong>: It is preferable to use either scan_vba or VBA_Scanner to get all results at once.</p>
337 355 <p>The function <strong>detect_patterns</strong> checks if VBA macro code contains specific patterns of interest, that may be useful for malware analysis and detection (potential Indicators of Compromise): IP addresses, e-mail addresses, URLs, executable file names.</p>
338 356 <p>It returns a list of tuples containing two strings, the pattern type, and the extracted value. (See the malware example above)</p>
339 357 <p>Sample usage:</p>
340   -<pre class="sourceCode python"><code class="sourceCode python"><span class="ch">from</span> oletools.olevba <span class="ch">import</span> detect_patterns
341   -patterns = detect_patterns(vba_code)
342   -<span class="kw">if</span> patterns:
343   - <span class="dt">print</span> <span class="st">&#39;Patterns found:&#39;</span>
344   - <span class="kw">for</span> pattern_type, value in patterns:
345   - <span class="dt">print</span> <span class="st">&#39;</span><span class="ot">%s</span><span class="st">: </span><span class="ot">%s</span><span class="st">&#39;</span> % (pattern_type, value)
346   -<span class="kw">else</span>:
347   - <span class="dt">print</span> <span class="st">&#39;Patterns: None found&#39;</span></code></pre>
  358 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> oletools.olevba <span class="im">import</span> detect_patterns
  359 +patterns <span class="op">=</span> detect_patterns(vba_code)
  360 +<span class="cf">if</span> patterns:
  361 + <span class="bu">print</span> <span class="st">&#39;Patterns found:&#39;</span>
  362 + <span class="cf">for</span> pattern_type, value <span class="kw">in</span> patterns:
  363 + <span class="bu">print</span> <span class="st">&#39;</span><span class="sc">%s</span><span class="st">: </span><span class="sc">%s</span><span class="st">&#39;</span> <span class="op">%</span> (pattern_type, value)
  364 +<span class="cf">else</span>:
  365 + <span class="bu">print</span> <span class="st">&#39;Patterns: None found&#39;</span></code></pre></div>
348 366 <hr />
349 367 <h2 id="python-oletools-documentation">python-oletools documentation</h2>
350 368 <ul>
... ...
oletools/doc/rtfobj.html
... ... @@ -7,23 +7,41 @@
7 7 <title></title>
8 8 <style type="text/css">code{white-space: pre;}</style>
9 9 <style type="text/css">
  10 +div.sourceCode { overflow-x: auto; }
10 11 table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
11 12 margin: 0; padding: 0; vertical-align: baseline; border: none; }
12 13 table.sourceCode { width: 100%; line-height: 100%; }
13 14 td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
14 15 td.sourceCode { padding-left: 5px; }
15   -code > span.kw { color: #007020; font-weight: bold; }
16   -code > span.dt { color: #902000; }
17   -code > span.dv { color: #40a070; }
18   -code > span.bn { color: #40a070; }
19   -code > span.fl { color: #40a070; }
20   -code > span.ch { color: #4070a0; }
21   -code > span.st { color: #4070a0; }
22   -code > span.co { color: #60a0b0; font-style: italic; }
23   -code > span.ot { color: #007020; }
24   -code > span.al { color: #ff0000; font-weight: bold; }
25   -code > span.fu { color: #06287e; }
26   -code > span.er { color: #ff0000; font-weight: bold; }
  16 +code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
  17 +code > span.dt { color: #902000; } /* DataType */
  18 +code > span.dv { color: #40a070; } /* DecVal */
  19 +code > span.bn { color: #40a070; } /* BaseN */
  20 +code > span.fl { color: #40a070; } /* Float */
  21 +code > span.ch { color: #4070a0; } /* Char */
  22 +code > span.st { color: #4070a0; } /* String */
  23 +code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
  24 +code > span.ot { color: #007020; } /* Other */
  25 +code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
  26 +code > span.fu { color: #06287e; } /* Function */
  27 +code > span.er { color: #ff0000; font-weight: bold; } /* Error */
  28 +code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
  29 +code > span.cn { color: #880000; } /* Constant */
  30 +code > span.sc { color: #4070a0; } /* SpecialChar */
  31 +code > span.vs { color: #4070a0; } /* VerbatimString */
  32 +code > span.ss { color: #bb6688; } /* SpecialString */
  33 +code > span.im { } /* Import */
  34 +code > span.va { color: #19177c; } /* Variable */
  35 +code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
  36 +code > span.op { color: #666666; } /* Operator */
  37 +code > span.bu { } /* BuiltIn */
  38 +code > span.ex { } /* Extension */
  39 +code > span.pp { color: #bc7a00; } /* Preprocessor */
  40 +code > span.at { color: #7d9029; } /* Attribute */
  41 +code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
  42 +code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
  43 +code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
  44 +code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
27 45 </style>
28 46 </head>
29 47 <body>
... ... @@ -57,6 +75,7 @@ Options:
57 75 <p>When an OLE Package object contains an executable file or script, it is highlighted as such. For example:</p>
58 76 <div class="figure">
59 77 <img src="rtfobj1.png" />
  78 +
60 79 </div>
61 80 <p>To extract an object or file, use the option -s followed by the object number as shown in the table.</p>
62 81 <p>Example:</p>
... ... @@ -67,9 +86,9 @@ Options:
67 86 <h3 id="deprecated-api-still-functional">Deprecated API (still functional):</h3>
68 87 <p>rtf_iter_objects(filename) is an iterator which yields a tuple (index, orig_len, object) providing the index of each hexadecimal stream in the RTF file, and the corresponding decoded object.</p>
69 88 <p>Example:</p>
70   -<pre class="sourceCode python"><code class="sourceCode python"><span class="ch">from</span> oletools <span class="ch">import</span> rtfobj
71   -<span class="kw">for</span> index, orig_len, data in rtfobj.rtf_iter_objects(<span class="st">&quot;myfile.rtf&quot;</span>):
72   - <span class="dt">print</span>(<span class="st">&#39;found object size </span><span class="ot">%d</span><span class="st"> at index </span><span class="ot">%08X</span><span class="st">&#39;</span> % (<span class="dt">len</span>(data), index))</code></pre>
  89 +<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> oletools <span class="im">import</span> rtfobj
  90 +<span class="cf">for</span> index, orig_len, data <span class="kw">in</span> rtfobj.rtf_iter_objects(<span class="st">&quot;myfile.rtf&quot;</span>):
  91 + <span class="bu">print</span>(<span class="st">&#39;found object size </span><span class="sc">%d</span><span class="st"> at index </span><span class="sc">%08X</span><span class="st">&#39;</span> <span class="op">%</span> (<span class="bu">len</span>(data), index))</code></pre></div>
73 92 <hr />
74 93 <h2 id="python-oletools-documentation">python-oletools documentation</h2>
75 94 <ul>
... ...
oletools/ezhexviewer.py
... ... @@ -16,7 +16,7 @@ Usage in a python application:
16 16  
17 17 ezhexviewer project website: http://www.decalage.info/python/ezhexviewer
18 18  
19   -ezhexviewer is copyright (c) 2012-2016, Philippe Lagadec (http://www.decalage.info)
  19 +ezhexviewer is copyright (c) 2012-2017, Philippe Lagadec (http://www.decalage.info)
20 20 All rights reserved.
21 21  
22 22 Redistribution and use in source and binary forms, with or without modification,
... ... @@ -46,16 +46,32 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
46 46 # 2012-10-04 v0.02 PL: - added license
47 47 # 2016-09-06 v0.50 PL: - added main function for entry points in setup.py
48 48 # 2016-10-26 PL: - fixed to run on Python 2+3
  49 +# 2017-03-23 v0.51 PL: - fixed display of control characters (issue #151)
  50 +# 2017-04-26 PL: - fixed absolute imports (issue #141)
49 51  
50   -__version__ = '0.50'
  52 +__version__ = '0.51'
51 53  
52   -#------------------------------------------------------------------------------
  54 +#-----------------------------------------------------------------------------
53 55 # TODO:
54 56 # + options to set title and msg
55 57  
  58 +# === IMPORTS ================================================================
  59 +
  60 +import sys, os
  61 +
  62 +# IMPORTANT: it should be possible to run oletools directly as scripts
  63 +# in any directory without installing them with pip or setup.py.
  64 +# In that case, relative imports are NOT usable.
  65 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  66 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  67 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  68 +# print('_thismodule_dir = %r' % _thismodule_dir)
  69 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  70 +# print('_parent_dir = %r' % _thirdparty_dir)
  71 +if not _parent_dir in sys.path:
  72 + sys.path.insert(0, _parent_dir)
56 73  
57   -from thirdparty.easygui import easygui
58   -import sys
  74 +from oletools.thirdparty.easygui import easygui
59 75  
60 76 # === PYTHON 2+3 SUPPORT ======================================================
61 77  
... ... @@ -106,7 +122,7 @@ def bchr(x):
106 122 # PSF license: http://docs.python.org/license.html
107 123 # Copyright (c) 2001-2012 Python Software Foundation; All Rights Reserved
108 124  
109   -FILTER = b''.join([(len(repr(bchr(x)))<=4 and x != 0x0A) and bchr(x) or b'.' for x in range(256)])
  125 +FILTER = b''.join([(len(repr(bchr(x)))<=4 and x>=0x20) and bchr(x) or b'.' for x in range(256)])
110 126  
111 127 def hexdump3(src, length=8, startindex=0):
112 128 """
... ... @@ -154,4 +170,4 @@ def main():
154 170  
155 171  
156 172 if __name__ == '__main__':
157   - main()
158 173 \ No newline at end of file
  174 + main()
... ...
oletools/mraptor.py
... ... @@ -22,7 +22,7 @@ http://www.decalage.info/python/oletools
22 22  
23 23 # === LICENSE ==================================================================
24 24  
25   -# MacroRaptor is copyright (c) 2016 Philippe Lagadec (http://www.decalage.info)
  25 +# MacroRaptor is copyright (c) 2016-2017 Philippe Lagadec (http://www.decalage.info)
26 26 # All rights reserved.
27 27 #
28 28 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -55,6 +55,7 @@ http://www.decalage.info/python/oletools
55 55 # 2016-09-05 PL: - added Document_BeforeClose keyword for MS Publisher (.pub)
56 56 # 2016-10-25 PL: - fixed print for Python 3
57 57 # 2016-12-21 v0.51 PL: - added more ActiveX macro triggers
  58 +# 2017-03-08 PL: - fixed absolute imports
58 59  
59 60 __version__ = '0.51'
60 61  
... ... @@ -64,12 +65,24 @@ __version__ = &#39;0.51&#39;
64 65  
65 66 #--- IMPORTS ------------------------------------------------------------------
66 67  
67   -import sys, logging, optparse, re
  68 +import sys, logging, optparse, re, os
68 69  
69   -from thirdparty.xglob import xglob
70   -from thirdparty.tablestream import tablestream
  70 +# IMPORTANT: it should be possible to run oletools directly as scripts
  71 +# in any directory without installing them with pip or setup.py.
  72 +# In that case, relative imports are NOT usable.
  73 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  74 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  75 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  76 +# print('_thismodule_dir = %r' % _thismodule_dir)
  77 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  78 +# print('_parent_dir = %r' % _thirdparty_dir)
  79 +if not _parent_dir in sys.path:
  80 + sys.path.insert(0, _parent_dir)
71 81  
72   -import olevba
  82 +from oletools.thirdparty.xglob import xglob
  83 +from oletools.thirdparty.tablestream import tablestream
  84 +
  85 +from oletools import olevba
73 86  
74 87 # === LOGGING =================================================================
75 88  
... ... @@ -228,7 +241,7 @@ def main():
228 241 'critical': logging.CRITICAL
229 242 }
230 243  
231   - usage = 'usage: %prog [options] <filename> [filename2 ...]'
  244 + usage = 'usage: mraptor [options] <filename> [filename2 ...]'
232 245 parser = optparse.OptionParser(usage=usage)
233 246 parser.add_option("-r", action="store_true", dest="recursive",
234 247 help='find files recursively in subdirectories.')
... ... @@ -247,6 +260,8 @@ def main():
247 260  
248 261 # Print help if no arguments are passed
249 262 if len(args) == 0:
  263 + print('MacroRaptor %s - http://decalage.info/python/oletools' % __version__)
  264 + print('This is work in progress, please report issues at %s' % URL_ISSUES)
250 265 print(__doc__)
251 266 parser.print_help()
252 267 print('\nAn exit code is returned based on the analysis result:')
... ...
oletools/mraptor3.py
... ... @@ -11,6 +11,7 @@ Supported formats:
11 11 - PowerPoint 97-2003 (.ppt), PowerPoint 2007+ (.pptm, .ppsm)
12 12 - Word 2003 XML (.xml)
13 13 - Word/Excel Single File Web Page / MHTML (.mht)
  14 +- Publisher (.pub)
14 15  
15 16 Author: Philippe Lagadec - http://www.decalage.info
16 17 License: BSD, see source code or documentation
... ... @@ -21,7 +22,7 @@ http://www.decalage.info/python/oletools
21 22  
22 23 # === LICENSE ==================================================================
23 24  
24   -# MacroRaptor is copyright (c) 2016 Philippe Lagadec (http://www.decalage.info)
  25 +# MacroRaptor is copyright (c) 2016-2017 Philippe Lagadec (http://www.decalage.info)
25 26 # All rights reserved.
26 27 #
27 28 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -52,8 +53,10 @@ http://www.decalage.info/python/oletools
52 53 # 2016-03-08 v0.04 PL: - collapse long lines before analysis
53 54 # 2016-07-19 v0.50 SL: - converted to Python 3
54 55 # 2016-08-26 PL: - changed imports for Python 3
  56 +# 2017-04-26 v0.51 PL: - fixed absolute imports (issue #141)
  57 +# 2017-06-29 PL: - synced with mraptor.py 0.51
55 58  
56   -__version__ = '0.50py3'
  59 +__version__ = '0.51'
57 60  
58 61 #------------------------------------------------------------------------------
59 62 # TODO:
... ... @@ -61,15 +64,25 @@ __version__ = &#39;0.50py3&#39;
61 64  
62 65 #--- IMPORTS ------------------------------------------------------------------
63 66  
64   -import sys, logging, optparse, re
  67 +import sys, os, logging, optparse, re
65 68  
66   -from thirdparty.xglob import xglob
  69 +# IMPORTANT: it should be possible to run oletools directly as scripts
  70 +# in any directory without installing them with pip or setup.py.
  71 +# In that case, relative imports are NOT usable.
  72 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  73 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  74 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  75 +# print('_thismodule_dir = %r' % _thismodule_dir)
  76 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  77 +# print('_parent_dir = %r' % _thirdparty_dir)
  78 +if not _parent_dir in sys.path:
  79 + sys.path.insert(0, _parent_dir)
67 80  
68   -# import the python 3 version of tablestream:
69   -from thirdparty.tablestream import tablestream
  81 +from oletools.thirdparty.xglob import xglob
  82 +from oletools.thirdparty.tablestream import tablestream
70 83  
71 84 # import the python 3 version of olevba
72   -import olevba3 as olevba
  85 +from oletools import olevba3 as olevba
73 86  
74 87 # === LOGGING =================================================================
75 88  
... ... @@ -86,15 +99,24 @@ MSG_ISSUES = &#39;Please report this issue on %s&#39; % URL_ISSUES
86 99  
87 100 # 'AutoExec', 'AutoOpen', 'Auto_Open', 'AutoClose', 'Auto_Close', 'AutoNew', 'AutoExit',
88 101 # 'Document_Open', 'DocumentOpen',
89   -# 'Document_Close', 'DocumentBeforeClose',
  102 +# 'Document_Close', 'DocumentBeforeClose', 'Document_BeforeClose',
90 103 # 'DocumentChange','Document_New',
91 104 # 'NewDocument'
92 105 # 'Workbook_Open', 'Workbook_Close',
  106 +# *_Painted such as InkPicture1_Painted
  107 +# *_GotFocus|LostFocus|MouseHover for other ActiveX objects
  108 +# reference: http://www.greyhathacker.net/?p=948
93 109  
94 110 # TODO: check if line also contains Sub or Function
95 111 re_autoexec = re.compile(r'(?i)\b(?:Auto(?:Exec|_?Open|_?Close|Exit|New)' +
96   - r'|Document(?:_?Open|_Close|BeforeClose|Change|_New)' +
97   - r'|NewDocument|Workbook(?:_Open|_Activate|_Close))\b')
  112 + r'|Document(?:_?Open|_Close|_?BeforeClose|Change|_New)' +
  113 + r'|NewDocument|Workbook(?:_Open|_Activate|_Close)' +
  114 + r'|\w+_(?:Painted|Painting|GotFocus|LostFocus|MouseHover' +
  115 + r'|Layout|Click|Change|Resize|BeforeNavigate2|BeforeScriptExecute' +
  116 + r'|DocumentComplete|DownloadBegin|DownloadComplete|FileDownload' +
  117 + r'|NavigateComplete2|NavigateError|ProgressChange|PropertyChange' +
  118 + r'|SetSecureLockIcon|StatusTextChange|TitleChange|MouseMove' +
  119 + r'|MouseEnter|MouseLeave|))\b')
98 120  
99 121 # MS-VBAL 5.4.5.1 Open Statement:
100 122 RE_OPEN_WRITE = r'(?:\bOpen\b[^\n]+\b(?:Write|Append|Binary|Output|Random)\b)'
... ... @@ -238,6 +260,8 @@ def main():
238 260  
239 261 # Print help if no arguments are passed
240 262 if len(args) == 0:
  263 + print('MacroRaptor %s - http://decalage.info/python/oletools' % __version__)
  264 + print('This is work in progress, please report issues at %s' % URL_ISSUES)
241 265 print(__doc__)
242 266 parser.print_help()
243 267 print('\nAn exit code is returned based on the analysis result:')
... ...
oletools/mraptor_milter.py
... ... @@ -24,7 +24,7 @@ http://www.decalage.info/python/oletools
24 24  
25 25 # === LICENSE ==================================================================
26 26  
27   -# mraptor_milter is copyright (c) 2016 Philippe Lagadec (http://www.decalage.info)
  27 +# mraptor_milter is copyright (c) 2016-2017 Philippe Lagadec (http://www.decalage.info)
28 28 # All rights reserved.
29 29 #
30 30 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -53,8 +53,9 @@ http://www.decalage.info/python/oletools
53 53 # - archive each e-mail to a file before filtering
54 54 # 2016-08-30 v0.03 PL: - added daemonize to run as a Unix daemon
55 55 # 2016-09-06 v0.50 PL: - fixed issue #20, is_zipfile on Python 2.6
  56 +# 2017-04-26 v0.51 PL: - fixed absolute imports (issue #141)
56 57  
57   -__version__ = '0.50'
  58 +__version__ = '0.51'
58 59  
59 60 # --- TODO -------------------------------------------------------------------
60 61  
... ... @@ -81,6 +82,18 @@ import StringIO
81 82  
82 83 from socket import AF_INET6
83 84  
  85 +# IMPORTANT: it should be possible to run oletools directly as scripts
  86 +# in any directory without installing them with pip or setup.py.
  87 +# In that case, relative imports are NOT usable.
  88 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  89 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  90 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  91 +# print('_thismodule_dir = %r' % _thismodule_dir)
  92 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  93 +# print('_parent_dir = %r' % _thirdparty_dir)
  94 +if not _parent_dir in sys.path:
  95 + sys.path.insert(0, _parent_dir)
  96 +
84 97 from oletools import olevba, mraptor
85 98  
86 99 from Milter.utils import parse_addr
... ... @@ -137,6 +150,7 @@ log.setLevel(logging.CRITICAL+1)
137 150 # === CLASSES ================================================================
138 151  
139 152 # Inspired from https://github.com/jmehnle/pymilter/blob/master/milter-template.py
  153 +# TODO: check https://github.com/sdgathman/pymilter which looks more recent
140 154  
141 155 class MacroRaptorMilter(Milter.Base):
142 156 '''
... ...
oletools/msodde.py 0 → 100644
  1 +#!/usr/bin/env python
  2 +"""
  3 +msodde.py
  4 +
  5 +msodde is a script to parse MS Office documents
  6 +(e.g. Word, Excel), to detect and extract DDE links.
  7 +
  8 +Supported formats:
  9 +- Word 97-2003 (.doc, .dot), Word 2007+ (.docx, .dotx, .docm, .dotm)
  10 +
  11 +Author: Philippe Lagadec - http://www.decalage.info
  12 +License: BSD, see source code or documentation
  13 +
  14 +msodde is part of the python-oletools package:
  15 +http://www.decalage.info/python/oletools
  16 +"""
  17 +
  18 +# === LICENSE ==================================================================
  19 +
  20 +# msodde is copyright (c) 2017 Philippe Lagadec (http://www.decalage.info)
  21 +# All rights reserved.
  22 +#
  23 +# Redistribution and use in source and binary forms, with or without modification,
  24 +# are permitted provided that the following conditions are met:
  25 +#
  26 +# * Redistributions of source code must retain the above copyright notice, this
  27 +# list of conditions and the following disclaimer.
  28 +# * Redistributions in binary form must reproduce the above copyright notice,
  29 +# this list of conditions and the following disclaimer in the documentation
  30 +# and/or other materials provided with the distribution.
  31 +#
  32 +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  33 +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  34 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  35 +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  36 +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  37 +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  38 +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  39 +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  40 +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  41 +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42 +
  43 +from __future__ import print_function
  44 +
  45 +#------------------------------------------------------------------------------
  46 +# CHANGELOG:
  47 +# 2017-10-18 v0.52 PL: - first version
  48 +# 2017-10-20 PL: - fixed issue #202 (handling empty xml tags)
  49 +# 2017-10-23 ES: - add check for fldSimple codes
  50 +# 2017-10-24 ES: - group tags and track begin/end tags to keep DDE strings together
  51 +# 2017-10-25 CH: - add json output
  52 +# 2017-10-25 CH: - parse doc
  53 +# PL: - added logging
  54 +
  55 +__version__ = '0.52dev4'
  56 +
  57 +#------------------------------------------------------------------------------
  58 +# TODO: field codes can be in headers/footers/comments - parse these
  59 +# TODO: add xlsx support
  60 +
  61 +#------------------------------------------------------------------------------
  62 +# REFERENCES:
  63 +
  64 +
  65 +#--- IMPORTS ------------------------------------------------------------------
  66 +
  67 +# import lxml or ElementTree for XML parsing:
  68 +try:
  69 + # lxml: best performance for XML processing
  70 + import lxml.etree as ET
  71 +except ImportError:
  72 + import xml.etree.cElementTree as ET
  73 +
  74 +import argparse
  75 +import zipfile
  76 +import os
  77 +import sys
  78 +import json
  79 +import logging
  80 +
  81 +# little hack to allow absolute imports even if oletools is not installed
  82 +# Copied from olevba.py
  83 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  84 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  85 +if not _parent_dir in sys.path:
  86 + sys.path.insert(0, _parent_dir)
  87 +
  88 +from oletools.thirdparty import olefile
  89 +
  90 +# === PYTHON 2+3 SUPPORT ======================================================
  91 +
  92 +if sys.version_info[0] >= 3:
  93 + unichr = chr
  94 +
  95 +# === CONSTANTS ==============================================================
  96 +
  97 +
  98 +NS_WORD = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
  99 +NO_QUOTES = False
  100 +# XML tag for 'w:instrText'
  101 +TAG_W_INSTRTEXT = '{%s}instrText' % NS_WORD
  102 +TAG_W_FLDSIMPLE = '{%s}fldSimple' % NS_WORD
  103 +TAG_W_FLDCHAR = '{%s}fldChar' % NS_WORD
  104 +TAG_W_P = "{%s}p" % NS_WORD
  105 +TAG_W_R = "{%s}r" % NS_WORD
  106 +ATTR_W_INSTR = '{%s}instr' % NS_WORD
  107 +ATTR_W_FLDCHARTYPE = '{%s}fldCharType' % NS_WORD
  108 +LOCATIONS = ['word/document.xml','word/endnotes.xml','word/footnotes.xml','word/header1.xml','word/footer1.xml','word/header2.xml','word/footer2.xml','word/comments.xml']
  109 +
  110 +# banner to be printed at program start
  111 +BANNER = """msodde %s - http://decalage.info/python/oletools
  112 +THIS IS WORK IN PROGRESS - Check updates regularly!
  113 +Please report any issue at https://github.com/decalage2/oletools/issues
  114 +""" % __version__
  115 +
  116 +BANNER_JSON = dict(type='meta', version=__version__, name='msodde',
  117 + link='http://decalage.info/python/oletools',
  118 + message='THIS IS WORK IN PROGRESS - Check updates regularly! '
  119 + 'Please report any issue at '
  120 + 'https://github.com/decalage2/oletools/issues')
  121 +
  122 +# === LOGGING =================================================================
  123 +
  124 +DEFAULT_LOG_LEVEL = "warning" # Default log level
  125 +LOG_LEVELS = {
  126 + 'debug': logging.DEBUG,
  127 + 'info': logging.INFO,
  128 + 'warning': logging.WARNING,
  129 + 'error': logging.ERROR,
  130 + 'critical': logging.CRITICAL
  131 +}
  132 +
  133 +class NullHandler(logging.Handler):
  134 + """
  135 + Log Handler without output, to avoid printing messages if logging is not
  136 + configured by the main application.
  137 + Python 2.7 has logging.NullHandler, but this is necessary for 2.6:
  138 + see https://docs.python.org/2.6/library/logging.html#configuring-logging-for-a-library
  139 + """
  140 + def emit(self, record):
  141 + pass
  142 +
  143 +def get_logger(name, level=logging.CRITICAL+1):
  144 + """
  145 + Create a suitable logger object for this module.
  146 + The goal is not to change settings of the root logger, to avoid getting
  147 + other modules' logs on the screen.
  148 + If a logger exists with same name, reuse it. (Else it would have duplicate
  149 + handlers and messages would be doubled.)
  150 + The level is set to CRITICAL+1 by default, to avoid any logging.
  151 + """
  152 + # First, test if there is already a logger with the same name, else it
  153 + # will generate duplicate messages (due to duplicate handlers):
  154 + if name in logging.Logger.manager.loggerDict:
  155 + #NOTE: another less intrusive but more "hackish" solution would be to
  156 + # use getLogger then test if its effective level is not default.
  157 + logger = logging.getLogger(name)
  158 + # make sure level is OK:
  159 + logger.setLevel(level)
  160 + return logger
  161 + # get a new logger:
  162 + logger = logging.getLogger(name)
  163 + # only add a NullHandler for this logger, it is up to the application
  164 + # to configure its own logging:
  165 + logger.addHandler(NullHandler())
  166 + logger.setLevel(level)
  167 + return logger
  168 +
  169 +# a global logger object used for debugging:
  170 +log = get_logger('msodde')
  171 +
  172 +
  173 +# === UNICODE IN PY2 =========================================================
  174 +
  175 +def ensure_stdout_handles_unicode():
  176 + """ Ensure stdout can handle unicode by wrapping it if necessary
  177 +
  178 + Required e.g. if output of this script is piped or redirected in a linux
  179 + shell, since then sys.stdout.encoding is ascii and cannot handle
  180 + print(unicode). In that case we need to find some compatible encoding and
  181 + wrap sys.stdout into a encoder following (many thanks!)
  182 + https://stackoverflow.com/a/1819009 or https://stackoverflow.com/a/20447935
  183 +
  184 + Can be undone by setting sys.stdout = sys.__stdout__
  185 + """
  186 + import codecs
  187 + import locale
  188 +
  189 + # do not re-wrap
  190 + if isinstance(sys.stdout, codecs.StreamWriter):
  191 + return
  192 +
  193 + # try to find encoding for sys.stdout
  194 + encoding = None
  195 + try:
  196 + encoding = sys.stdout.encoding # variable encoding might not exist
  197 + except Exception:
  198 + pass
  199 +
  200 + if encoding not in (None, '', 'ascii'):
  201 + return # no need to wrap
  202 +
  203 + # try to find an encoding that can handle unicode
  204 + try:
  205 + encoding = locale.getpreferredencoding()
  206 + except Exception:
  207 + pass
  208 +
  209 + # fallback if still no encoding available
  210 + if encoding in (None, '', 'ascii'):
  211 + encoding = 'utf8'
  212 +
  213 + # logging is probably not initialized yet, but just in case
  214 + log.debug('wrapping sys.stdout with encoder using {0}'.format(encoding))
  215 +
  216 + wrapper = codecs.getwriter(encoding)
  217 + sys.stdout = wrapper(sys.stdout)
  218 +
  219 +
  220 +ensure_stdout_handles_unicode() # e.g. for print(text) in main()
  221 +
  222 +
  223 +# === ARGUMENT PARSING =======================================================
  224 +
  225 +class ArgParserWithBanner(argparse.ArgumentParser):
  226 + """ Print banner before showing any error """
  227 + def error(self, message):
  228 + print(BANNER)
  229 + super(ArgParserWithBanner, self).error(message)
  230 +
  231 +
  232 +def existing_file(filename):
  233 + """ called by argument parser to see whether given file exists """
  234 + if not os.path.exists(filename):
  235 + raise argparse.ArgumentTypeError('File {0} does not exist.'
  236 + .format(filename))
  237 + return filename
  238 +
  239 +
  240 +def process_args(cmd_line_args=None):
  241 + """ parse command line arguments (given ones or per default sys.argv) """
  242 + parser = ArgParserWithBanner(description='A python tool to detect and extract DDE links in MS Office files')
  243 + parser.add_argument("filepath", help="path of the file to be analyzed",
  244 + type=existing_file, metavar='FILE')
  245 + parser.add_argument("--json", '-j', action='store_true',
  246 + help="Output in json format. Do not use with -ldebug")
  247 + parser.add_argument("--nounquote", help="don't unquote values",action='store_true')
  248 + parser.add_argument('-l', '--loglevel', dest="loglevel", action="store", default=DEFAULT_LOG_LEVEL,
  249 + help="logging level debug/info/warning/error/critical (default=%(default)s)")
  250 +
  251 + return parser.parse_args(cmd_line_args)
  252 +
  253 +
  254 +# === FUNCTIONS ==============================================================
  255 +
  256 +# from [MS-DOC], section 2.8.25 (PlcFld):
  257 +# A field consists of two parts: field instructions and, optionally, a result. All fields MUST begin with
  258 +# Unicode character 0x0013 with sprmCFSpec applied with a value of 1. This is the field begin
  259 +# character. All fields MUST end with a Unicode character 0x0015 with sprmCFSpec applied with a value
  260 +# of 1. This is the field end character. If the field has a result, then there MUST be a Unicode character
  261 +# 0x0014 with sprmCFSpec applied with a value of 1 somewhere between the field begin character and
  262 +# the field end character. This is the field separator. The field result is the content between the field
  263 +# separator and the field end character. The field instructions are the content between the field begin
  264 +# character and the field separator, if one is present, or between the field begin character and the field
  265 +# end character if no separator is present. The field begin character, field end character, and field
  266 +# separator are collectively referred to as field characters.
  267 +
  268 +
  269 +def process_ole_field(data):
  270 + """ check if field instructions start with DDE
  271 +
  272 + expects unicode input, returns unicode output (empty if not dde) """
  273 + #log.debug('processing field \'{0}\''.format(data))
  274 +
  275 + if data.lstrip().lower().startswith(u'dde'):
  276 + #log.debug('--> is DDE!')
  277 + return data
  278 + elif data.lstrip().lower().startswith(u'\x00d\x00d\x00e\x00'):
  279 + return data
  280 + else:
  281 + return u''
  282 +
  283 +
  284 +OLE_FIELD_START = 0x13
  285 +OLE_FIELD_SEP = 0x14
  286 +OLE_FIELD_END = 0x15
  287 +OLE_FIELD_MAX_SIZE = 1000 # max field size to analyze, rest is ignored
  288 +
  289 +
  290 +def process_ole_stream(stream):
  291 + """ find dde links in single ole stream
  292 +
  293 + since ole file stream are subclasses of io.BytesIO, they are buffered, so
  294 + reading char-wise is not that bad performanc-wise """
  295 +
  296 + have_start = False
  297 + have_sep = False
  298 + field_contents = None
  299 + result_parts = []
  300 + max_size_exceeded = False
  301 + idx = -1
  302 + while True:
  303 + idx += 1
  304 + char = stream.read(1) # loop over every single byte
  305 + if len(char) == 0:
  306 + break
  307 + else:
  308 + char = ord(char)
  309 +
  310 + if char == OLE_FIELD_START:
  311 + have_start = True
  312 + have_sep = False
  313 + max_size_exceeded = False
  314 + field_contents = u''
  315 + continue
  316 + elif not have_start:
  317 + continue
  318 +
  319 + # now we are after start char but not at end yet
  320 + if char == OLE_FIELD_SEP:
  321 + have_sep = True
  322 + elif char == OLE_FIELD_END:
  323 + # have complete field now, process it
  324 + result_parts.append(process_ole_field(field_contents))
  325 +
  326 + # re-set variables for next field
  327 + have_start = False
  328 + have_sep = False
  329 + field_contents = None
  330 + elif not have_sep:
  331 + # check that array does not get too long by accident
  332 + if max_size_exceeded:
  333 + pass
  334 + elif len(field_contents) > OLE_FIELD_MAX_SIZE:
  335 + log.debug('field exceeds max size of {0}. Ignore rest'
  336 + .format(OLE_FIELD_MAX_SIZE))
  337 + max_size_exceeded = True
  338 +
  339 + # appending a raw byte to a unicode string here. Not clean but
  340 + # all we do later is check for the ascii-sequence 'DDE' later...
  341 + elif char < 128:
  342 + field_contents += unichr(char)
  343 + else:
  344 + field_contents += u'?'
  345 + log.debug('Checked {0} characters, found {1} fields'
  346 + .format(idx, len(result_parts)))
  347 +
  348 + return result_parts
  349 +
  350 +
  351 +def process_ole_storage(ole):
  352 + """ process a "directory" inside an ole file; recursive """
  353 + results = []
  354 + for st in ole.listdir(streams=True, storages=True):
  355 + st_type = ole.get_type(st)
  356 + if st_type == olefile.STGTY_STREAM: # a stream
  357 + stream = None
  358 + links = []
  359 + try:
  360 + stream = ole.openstream(st)
  361 + log.debug('Checking stream {0}'.format(st))
  362 + links = process_ole_stream(stream)
  363 + except Exception:
  364 + raise
  365 + finally:
  366 + if stream:
  367 + stream.close()
  368 + if links:
  369 + results.extend(links)
  370 + elif st_type == olefile.STGTY_STORAGE: # a storage
  371 + log.debug('Checking storage {0}'.format(st))
  372 + links = process_ole_storage(st)
  373 + if links:
  374 + results.extend(links)
  375 + else:
  376 + log.info('unexpected type {0} for entry {1}. Ignore it'
  377 + .format(st_type, st))
  378 + continue
  379 + return results
  380 +
  381 +
  382 +def process_ole(filepath):
  383 + """
  384 + find dde links in ole file
  385 +
  386 + like process_xml, returns a concatenated unicode string of dde links or
  387 + empty if none were found. dde-links will still being with the dde[auto] key
  388 + word (possibly after some whitespace)
  389 + """
  390 + log.debug('process_ole')
  391 + ole = olefile.OleFileIO(filepath, path_encoding=None)
  392 + text_parts = process_ole_storage(ole)
  393 +
  394 + # mimic behaviour of process_openxml: combine links to single text string
  395 + return u'\n'.join(text_parts)
  396 +
  397 +
  398 +def process_openxml(filepath):
  399 + log.debug('process_openxml')
  400 + all_fields = []
  401 + z = zipfile.ZipFile(filepath)
  402 + for filepath in z.namelist():
  403 + if filepath in LOCATIONS:
  404 + data = z.read(filepath)
  405 + fields = process_xml(data)
  406 + if len(fields) > 0:
  407 + #print ('DDE Links in %s:'%filepath)
  408 + #for f in fields:
  409 + # print(f)
  410 + all_fields.extend(fields)
  411 + z.close()
  412 + return u'\n'.join(all_fields)
  413 +
  414 +def process_xml(data):
  415 + # parse the XML data:
  416 + root = ET.fromstring(data)
  417 + fields = []
  418 + ddetext = u''
  419 + level = 0
  420 + # find all the tags 'w:p':
  421 + # parse each for begin and end tags, to group DDE strings
  422 + # fldChar can be in either a w:r element, floating alone in the w:p or spread accross w:p tags
  423 + # escape DDE if quoted etc
  424 + # (each is a chunk of a DDE link)
  425 +
  426 + for subs in root.iter(TAG_W_P):
  427 + elem = None
  428 + for e in subs:
  429 + #check if w:r and if it is parse children elements to pull out the first FLDCHAR or INSTRTEXT
  430 + if e.tag == TAG_W_R:
  431 + for child in e:
  432 + if child.tag == TAG_W_FLDCHAR or child.tag == TAG_W_INSTRTEXT:
  433 + elem = child
  434 + break
  435 + else:
  436 + elem = e
  437 + #this should be an error condition
  438 + if elem is None:
  439 + continue
  440 +
  441 + #check if FLDCHARTYPE and whether "begin" or "end" tag
  442 + if elem.attrib.get(ATTR_W_FLDCHARTYPE) is not None:
  443 + if elem.attrib[ATTR_W_FLDCHARTYPE] == "begin":
  444 + level += 1
  445 + if elem.attrib[ATTR_W_FLDCHARTYPE] == "end":
  446 + level -= 1
  447 + if level == 0 or level == -1 : # edge-case where level becomes -1
  448 + fields.append(ddetext)
  449 + ddetext = u''
  450 + level = 0 # reset edge-case
  451 +
  452 + # concatenate the text of the field, if present:
  453 + if elem.tag == TAG_W_INSTRTEXT and elem.text is not None:
  454 + #expand field code if QUOTED
  455 + ddetext += unquote(elem.text)
  456 +
  457 + for elem in root.iter(TAG_W_FLDSIMPLE):
  458 + # concatenate the attribute of the field, if present:
  459 + if elem.attrib is not None:
  460 + fields.append(elem.attrib[ATTR_W_INSTR])
  461 +
  462 + return fields
  463 +
  464 +def unquote(field):
  465 + if "QUOTE" not in field or NO_QUOTES:
  466 + return field
  467 + #split into components
  468 + parts = field.strip().split(" ")
  469 + ddestr = ""
  470 + for p in parts[1:]:
  471 + try:
  472 + ch = chr(int(p))
  473 + except ValueError:
  474 + ch = p
  475 + ddestr += ch
  476 + return ddestr
  477 +
  478 +
  479 +def process_file(filepath):
  480 + """ decides to either call process_openxml or process_ole """
  481 + if olefile.isOleFile(filepath):
  482 + return process_ole(filepath)
  483 + else:
  484 + return process_openxml(filepath)
  485 +
  486 +
  487 +#=== MAIN =================================================================
  488 +
  489 +def main(cmd_line_args=None):
  490 + """ Main function, called if this file is called as a script
  491 +
  492 + Optional argument: command line arguments to be forwarded to ArgumentParser
  493 + in process_args. Per default (cmd_line_args=None), sys.argv is used. Option
  494 + mainly added for unit-testing
  495 + """
  496 + args = process_args(cmd_line_args)
  497 +
  498 + # Setup logging to the console:
  499 + # here we use stdout instead of stderr by default, so that the output
  500 + # can be redirected properly.
  501 + logging.basicConfig(level=LOG_LEVELS[args.loglevel], stream=sys.stdout,
  502 + format='%(levelname)-8s %(message)s')
  503 + # enable logging in the modules:
  504 + log.setLevel(logging.NOTSET)
  505 +
  506 + if args.json and args.loglevel.lower() == 'debug':
  507 + log.warning('Debug log output will not be json-compatible!')
  508 +
  509 + if args.nounquote :
  510 + global NO_QUOTES
  511 + NO_QUOTES = True
  512 +
  513 + if args.json:
  514 + jout = []
  515 + jout.append(BANNER_JSON)
  516 + else:
  517 + # print banner with version
  518 + print(BANNER)
  519 +
  520 + if not args.json:
  521 + print('Opening file: %s' % args.filepath)
  522 +
  523 + text = ''
  524 + return_code = 1
  525 + try:
  526 + text = process_file(args.filepath)
  527 + return_code = 0
  528 + except Exception as exc:
  529 + if args.json:
  530 + jout.append(dict(type='error', error=type(exc).__name__,
  531 + message=str(exc))) # strange: str(exc) is enclosed in ""
  532 + else:
  533 + raise
  534 +
  535 + if args.json:
  536 + for line in text.splitlines():
  537 + if line.strip():
  538 + jout.append(dict(type='dde-link', link=line.strip()))
  539 + json.dump(jout, sys.stdout, check_circular=False, indent=4)
  540 + print() # add a newline after closing "]"
  541 + return return_code # required if we catch an exception in json-mode
  542 + else:
  543 + print ('DDE Links:')
  544 + print(text)
  545 +
  546 + return return_code
  547 +
  548 +
  549 +if __name__ == '__main__':
  550 + sys.exit(main())
... ...
oletools/olebrowse.py
... ... @@ -12,7 +12,7 @@ olebrowse project website: http://www.decalage.info/python/olebrowse
12 12 olebrowse is part of the python-oletools package:
13 13 http://www.decalage.info/python/oletools
14 14  
15   -olebrowse is copyright (c) 2012-2015, Philippe Lagadec (http://www.decalage.info)
  15 +olebrowse is copyright (c) 2012-2017, Philippe Lagadec (http://www.decalage.info)
16 16 All rights reserved.
17 17  
18 18 Redistribution and use in source and binary forms, with or without modification,
... ... @@ -36,12 +36,13 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36 36 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 37 """
38 38  
39   -__version__ = '0.02'
40   -
41 39 #------------------------------------------------------------------------------
42 40 # CHANGELOG:
43 41 # 2012-09-17 v0.01 PL: - first version
44 42 # 2014-11-29 v0.02 PL: - use olefile instead of OleFileIO_PL
  43 +# 2017-04-26 v0.51 PL: - fixed absolute imports (issue #141)
  44 +
  45 +__version__ = '0.51'
45 46  
46 47 #------------------------------------------------------------------------------
47 48 # TODO:
... ... @@ -51,10 +52,25 @@ __version__ = &#39;0.02&#39;
51 52 # - for a stream, display info: size, path, etc
52 53 # - stream info: magic, entropy, ... ?
53 54  
  55 +# === IMPORTS ================================================================
  56 +
54 57 import optparse, sys, os
55   -from thirdparty.easygui import easygui
56   -import thirdparty.olefile as olefile
57   -import ezhexviewer
  58 +
  59 +# IMPORTANT: it should be possible to run oletools directly as scripts
  60 +# in any directory without installing them with pip or setup.py.
  61 +# In that case, relative imports are NOT usable.
  62 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  63 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  64 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  65 +# print('_thismodule_dir = %r' % _thismodule_dir)
  66 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  67 +# print('_parent_dir = %r' % _thirdparty_dir)
  68 +if not _parent_dir in sys.path:
  69 + sys.path.insert(0, _parent_dir)
  70 +
  71 +from oletools.thirdparty.easygui import easygui
  72 +from oletools.thirdparty import olefile
  73 +from oletools import ezhexviewer
58 74  
59 75 ABOUT = '~ About olebrowse'
60 76 QUIT = '~ Quit'
... ...
oletools/oledir.py
... ... @@ -2,7 +2,7 @@
2 2 """
3 3 oledir.py
4 4  
5   -oledir parses OLE files to display technical information about its directory
  5 +oledir parses OLE files to display technical information about their directory
6 6 entries, including deleted/orphan streams/storages and unused entries.
7 7  
8 8 Author: Philippe Lagadec - http://www.decalage.info
... ... @@ -14,7 +14,7 @@ http://www.decalage.info/python/oletools
14 14  
15 15 #=== LICENSE ==================================================================
16 16  
17   -# oledir is copyright (c) 2015-2016 Philippe Lagadec (http://www.decalage.info)
  17 +# oledir is copyright (c) 2015-2017 Philippe Lagadec (http://www.decalage.info)
18 18 # All rights reserved.
19 19 #
20 20 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -37,6 +37,7 @@ http://www.decalage.info/python/oletools
37 37 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
38 38 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39 39  
  40 +from __future__ import print_function
40 41  
41 42 #------------------------------------------------------------------------------
42 43 # CHANGELOG:
... ... @@ -45,8 +46,10 @@ http://www.decalage.info/python/oletools
45 46 # 2016-01-13 v0.03 PL: - replaced prettytable by tablestream, added colors
46 47 # 2016-07-20 v0.50 SL: - added Python 3 support
47 48 # 2016-08-09 PL: - fixed issue #77 (imports from thirdparty dir)
  49 +# 2017-03-08 v0.51 PL: - fixed absolute imports, added optparse
  50 +# - added support for zip files and wildcards
48 51  
49   -__version__ = '0.50'
  52 +__version__ = '0.51'
50 53  
51 54 #------------------------------------------------------------------------------
52 55 # TODO:
... ... @@ -55,12 +58,22 @@ __version__ = &#39;0.50&#39;
55 58  
56 59 # === IMPORTS ================================================================
57 60  
58   -import sys, os
  61 +import sys, os, optparse
59 62  
60   -# add the thirdparty subfolder to sys.path (absolute+normalized path):
  63 +# IMPORTANT: it should be possible to run oletools directly as scripts
  64 +# in any directory without installing them with pip or setup.py.
  65 +# In that case, relative imports are NOT usable.
  66 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  67 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
61 68 _thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
62 69 # print('_thismodule_dir = %r' % _thismodule_dir)
63   -# assumption: the thirdparty dir is a subfolder:
  70 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  71 +# print('_parent_dir = %r' % _parent_dir)
  72 +if not _parent_dir in sys.path:
  73 + sys.path.insert(0, _parent_dir)
  74 +
  75 +# we also need the thirdparty dir for colorclass
  76 +# TODO: remove colorclass from thirdparty, make it a dependency
64 77 _thirdparty_dir = os.path.normpath(os.path.join(_thismodule_dir, 'thirdparty'))
65 78 # print('_thirdparty_dir = %r' % _thirdparty_dir)
66 79 if not _thirdparty_dir in sys.path:
... ... @@ -72,12 +85,15 @@ import colorclass
72 85 if os.name == 'nt':
73 86 colorclass.Windows.enable(auto_colors=True)
74 87  
75   -import olefile
76   -from tablestream import tablestream
  88 +from oletools.thirdparty import olefile
  89 +from oletools.thirdparty.tablestream import tablestream
  90 +from oletools.thirdparty.xglob import xglob
77 91  
78 92  
79 93 # === CONSTANTS ==============================================================
80 94  
  95 +BANNER = 'oledir %s - http://decalage.info/python/oletools' % __version__
  96 +
81 97 STORAGE_NAMES = {
82 98 olefile.STGTY_EMPTY: 'Empty',
83 99 olefile.STGTY_STORAGE: 'Storage',
... ... @@ -115,72 +131,104 @@ def sid_display(sid):
115 131 # === MAIN ===================================================================
116 132  
117 133 def main():
  134 + usage = 'usage: oledir [options] <filename> [filename2 ...]'
  135 + parser = optparse.OptionParser(usage=usage)
  136 + parser.add_option("-r", action="store_true", dest="recursive",
  137 + help='find files recursively in subdirectories.')
  138 + parser.add_option("-z", "--zip", dest='zip_password', type='str', default=None,
  139 + help='if the file is a zip archive, open all files from it, using the provided password (requires Python 2.6+)')
  140 + parser.add_option("-f", "--zipfname", dest='zip_fname', type='str', default='*',
  141 + help='if the file is a zip archive, file(s) to be opened within the zip. Wildcards * and ? are supported. (default:*)')
  142 + # parser.add_option('-l', '--loglevel', dest="loglevel", action="store", default=DEFAULT_LOG_LEVEL,
  143 + # help="logging level debug/info/warning/error/critical (default=%default)")
  144 +
  145 + # TODO: add logfile option
  146 +
  147 + (options, args) = parser.parse_args()
  148 +
  149 + # Print help if no arguments are passed
  150 + if len(args) == 0:
  151 + print(BANNER)
  152 + print(__doc__)
  153 + parser.print_help()
  154 + sys.exit()
  155 +
118 156 # print banner with version
119   - print('oledir %s - http://decalage.info/python/oletools' % __version__)
  157 + print(BANNER)
120 158  
121 159 if os.name == 'nt':
122 160 colorclass.Windows.enable(auto_colors=True, reset_atexit=True)
123 161  
124   - fname = sys.argv[1]
125   - print('OLE directory entries in file %s:' % fname)
126   - ole = olefile.OleFileIO(fname)
127   - # ole.dumpdirectory()
128   -
129   - # t = prettytable.PrettyTable(('id', 'Status', 'Type', 'Name', 'Left', 'Right', 'Child', '1st Sect', 'Size'))
130   - # t.align = 'l'
131   - # t.max_width['id'] = 4
132   - # t.max_width['Status'] = 6
133   - # t.max_width['Type'] = 10
134   - # t.max_width['Name'] = 10
135   - # t.max_width['Left'] = 5
136   - # t.max_width['Right'] = 5
137   - # t.max_width['Child'] = 5
138   - # t.max_width['1st Sect'] = 8
139   - # t.max_width['Size'] = 6
140   -
141   - table = tablestream.TableStream(column_width=[4, 6, 7, 22, 5, 5, 5, 8, 6],
142   - header_row=('id', 'Status', 'Type', 'Name', 'Left', 'Right', 'Child', '1st Sect', 'Size'),
143   - style=tablestream.TableStyleSlim)
144   -
145   - # TODO: read ALL the actual directory entries from the directory stream, because olefile does not!
146   - # TODO: OR fix olefile!
147   - # TODO: olefile should store or give access to the raw direntry data on demand
148   - # TODO: oledir option to hexdump the raw direntries
149   - # TODO: olefile should be less picky about incorrect directory structures
150   -
151   - for id in range(len(ole.direntries)):
152   - d = ole.direntries[id]
153   - if d is None:
154   - # this direntry is not part of the tree: either unused or an orphan
155   - d = ole._load_direntry(id) #ole.direntries[id]
156   - # print('%03d: %s *** ORPHAN ***' % (id, d.name))
157   - if d.entry_type == olefile.STGTY_EMPTY:
158   - status = 'unused'
159   - else:
160   - status = 'ORPHAN'
  162 + for container, filename, data in xglob.iter_files(args, recursive=options.recursive,
  163 + zip_password=options.zip_password, zip_fname=options.zip_fname):
  164 + # ignore directory names stored in zip files:
  165 + if container and filename.endswith('/'):
  166 + continue
  167 + full_name = '%s in %s' % (filename, container) if container else filename
  168 + print('OLE directory entries in file %s:' % full_name)
  169 + if data is not None:
  170 + # data extracted from zip file
  171 + ole = olefile.OleFileIO(data)
161 172 else:
162   - # print('%03d: %s' % (id, d.name))
163   - status = '<Used>'
164   - if d.name.startswith('\x00'):
165   - # this may happen with unused entries, the name may be filled with zeroes
166   - name = ''
167   - else:
168   - # handle non-printable chars using repr(), remove quotes:
169   - name = repr(d.name)[1:-1]
170   - left = sid_display(d.sid_left)
171   - right = sid_display(d.sid_right)
172   - child = sid_display(d.sid_child)
173   - entry_type = STORAGE_NAMES.get(d.entry_type, 'Unknown')
174   - etype_color = STORAGE_COLORS.get(d.entry_type, 'red')
175   - status_color = STATUS_COLORS.get(status, 'red')
176   -
177   - # print(' type=%7s sid_left=%s sid_right=%s sid_child=%s'
178   - # %(entry_type, left, right, child))
179   - # t.add_row((id, status, entry_type, name, left, right, child, hex(d.isectStart), d.size))
180   - table.write_row((id, status, entry_type, name, left, right, child, '%X' % d.isectStart, d.size),
181   - colors=(None, status_color, etype_color, None, None, None, None, None, None))
182   - ole.close()
183   - # print t
  173 + # normal filename
  174 + ole = olefile.OleFileIO(filename)
  175 + # ole.dumpdirectory()
  176 +
  177 + # t = prettytable.PrettyTable(('id', 'Status', 'Type', 'Name', 'Left', 'Right', 'Child', '1st Sect', 'Size'))
  178 + # t.align = 'l'
  179 + # t.max_width['id'] = 4
  180 + # t.max_width['Status'] = 6
  181 + # t.max_width['Type'] = 10
  182 + # t.max_width['Name'] = 10
  183 + # t.max_width['Left'] = 5
  184 + # t.max_width['Right'] = 5
  185 + # t.max_width['Child'] = 5
  186 + # t.max_width['1st Sect'] = 8
  187 + # t.max_width['Size'] = 6
  188 +
  189 + table = tablestream.TableStream(column_width=[4, 6, 7, 22, 5, 5, 5, 8, 6],
  190 + header_row=('id', 'Status', 'Type', 'Name', 'Left', 'Right', 'Child', '1st Sect', 'Size'),
  191 + style=tablestream.TableStyleSlim)
  192 +
  193 + # TODO: read ALL the actual directory entries from the directory stream, because olefile does not!
  194 + # TODO: OR fix olefile!
  195 + # TODO: olefile should store or give access to the raw direntry data on demand
  196 + # TODO: oledir option to hexdump the raw direntries
  197 + # TODO: olefile should be less picky about incorrect directory structures
  198 +
  199 + for id in range(len(ole.direntries)):
  200 + d = ole.direntries[id]
  201 + if d is None:
  202 + # this direntry is not part of the tree: either unused or an orphan
  203 + d = ole._load_direntry(id) #ole.direntries[id]
  204 + # print('%03d: %s *** ORPHAN ***' % (id, d.name))
  205 + if d.entry_type == olefile.STGTY_EMPTY:
  206 + status = 'unused'
  207 + else:
  208 + status = 'ORPHAN'
  209 + else:
  210 + # print('%03d: %s' % (id, d.name))
  211 + status = '<Used>'
  212 + if d.name.startswith('\x00'):
  213 + # this may happen with unused entries, the name may be filled with zeroes
  214 + name = ''
  215 + else:
  216 + # handle non-printable chars using repr(), remove quotes:
  217 + name = repr(d.name)[1:-1]
  218 + left = sid_display(d.sid_left)
  219 + right = sid_display(d.sid_right)
  220 + child = sid_display(d.sid_child)
  221 + entry_type = STORAGE_NAMES.get(d.entry_type, 'Unknown')
  222 + etype_color = STORAGE_COLORS.get(d.entry_type, 'red')
  223 + status_color = STATUS_COLORS.get(status, 'red')
  224 +
  225 + # print(' type=%7s sid_left=%s sid_right=%s sid_child=%s'
  226 + # %(entry_type, left, right, child))
  227 + # t.add_row((id, status, entry_type, name, left, right, child, hex(d.isectStart), d.size))
  228 + table.write_row((id, status, entry_type, name, left, right, child, '%X' % d.isectStart, d.size),
  229 + colors=(None, status_color, etype_color, None, None, None, None, None, None))
  230 + ole.close()
  231 + # print t
184 232  
185 233  
186 234 if __name__ == '__main__':
... ...
oletools/oleid.py
... ... @@ -18,7 +18,7 @@ http://www.decalage.info/python/oletools
18 18  
19 19 #=== LICENSE =================================================================
20 20  
21   -# oleid is copyright (c) 2012-2016, Philippe Lagadec (http://www.decalage.info)
  21 +# oleid is copyright (c) 2012-2017, Philippe Lagadec (http://www.decalage.info)
22 22 # All rights reserved.
23 23 #
24 24 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -53,6 +53,7 @@ from __future__ import print_function
53 53 # 2014-11-30 v0.03 PL: - improved output with prettytable
54 54 # 2016-10-25 v0.50 PL: - fixed print and bytes strings for Python 3
55 55 # 2016-12-12 v0.51 PL: - fixed relative imports for Python 3 (issue #115)
  56 +# 2017-04-26 PL: - fixed absolute imports (issue #141)
56 57  
57 58 __version__ = '0.51'
58 59  
... ... @@ -77,19 +78,20 @@ __version__ = &#39;0.51&#39;
77 78  
78 79 import optparse, sys, os, re, zlib, struct
79 80  
80   -try:
81   - # Relative imports (only works when imported from package):
82   - from .thirdparty import olefile
83   - from .thirdparty.prettytable import prettytable
84   -except:
85   - # if it does not work, fall back to absolute imports:
86   - # add this module's folder to sys.path (absolute+normalized path):
87   - _thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
88   - if not _thismodule_dir in sys.path:
89   - sys.path.insert(0, _thismodule_dir)
90   - # absolute imports:
91   - from thirdparty import olefile
92   - from thirdparty.prettytable import prettytable
  81 +# IMPORTANT: it should be possible to run oletools directly as scripts
  82 +# in any directory without installing them with pip or setup.py.
  83 +# In that case, relative imports are NOT usable.
  84 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  85 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  86 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  87 +# print('_thismodule_dir = %r' % _thismodule_dir)
  88 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  89 +# print('_parent_dir = %r' % _thirdparty_dir)
  90 +if not _parent_dir in sys.path:
  91 + sys.path.insert(0, _parent_dir)
  92 +
  93 +from oletools.thirdparty import olefile
  94 +from oletools.thirdparty.prettytable import prettytable
93 95  
94 96  
95 97  
... ...
oletools/olemap.py
... ... @@ -13,7 +13,7 @@ http://www.decalage.info/python/oletools
13 13  
14 14 #=== LICENSE ==================================================================
15 15  
16   -# olemap is copyright (c) 2015-2016 Philippe Lagadec (http://www.decalage.info)
  16 +# olemap is copyright (c) 2015-2017 Philippe Lagadec (http://www.decalage.info)
17 17 # All rights reserved.
18 18 #
19 19 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -43,25 +43,45 @@ http://www.decalage.info/python/oletools
43 43 # 2016-01-13 v0.02 PL: - improved display with tablestream, added colors
44 44 # 2016-07-20 v0.50 SL: - added Python 3 support
45 45 # 2016-09-05 PL: - added main entry point for setup.py
  46 +# 2017-03-20 v0.51 PL: - fixed absolute imports, added optparse
  47 +# - added support for zip files and wildcards
  48 +# - improved MiniFAT display with tablestream
  49 +# 2017-03-21 PL: - added header display
  50 +# - added options --header, --fat and --minifat
  51 +# 2017-03-22 PL: - added extra data detection, completed header display
  52 +# 2017-03-23 PL: - only display the header by default
  53 +# - added option --exdata to display extra data in hex
46 54  
47   -__version__ = '0.50'
  55 +
  56 +__version__ = '0.51'
48 57  
49 58 #------------------------------------------------------------------------------
50 59 # TODO:
51 60  
52 61 # === IMPORTS ================================================================
53 62  
54   -import sys
55   -from thirdparty.olefile import olefile
56   -from thirdparty.tablestream import tablestream
  63 +import sys, os, optparse, binascii
57 64  
  65 +# IMPORTANT: it should be possible to run oletools directly as scripts
  66 +# in any directory without installing them with pip or setup.py.
  67 +# In that case, relative imports are NOT usable.
  68 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  69 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  70 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  71 +# print('_thismodule_dir = %r' % _thismodule_dir)
  72 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  73 +# print('_parent_dir = %r' % _thirdparty_dir)
  74 +if not _parent_dir in sys.path:
  75 + sys.path.insert(0, _parent_dir)
58 76  
  77 +from oletools.thirdparty.olefile import olefile
  78 +from oletools.thirdparty.tablestream import tablestream
  79 +from oletools.thirdparty.xglob import xglob
  80 +from oletools.ezhexviewer import hexdump3
59 81  
60   -def sid_display(sid):
61   - if sid == olefile.NOSTREAM:
62   - return None
63   - else:
64   - return sid
  82 +# === CONSTANTS ==============================================================
  83 +
  84 +BANNER = 'olemap %s - http://decalage.info/python/oletools' % __version__
65 85  
66 86 STORAGE_NAMES = {
67 87 olefile.STGTY_EMPTY: 'Empty',
... ... @@ -88,37 +108,188 @@ FAT_COLORS = {
88 108 }
89 109  
90 110  
91   -# === MAIN ===================================================================
  111 +# === FUNCTIONS ==============================================================
92 112  
93   -def main():
94   - # print banner with version
95   - print('olemap %s - http://decalage.info/python/oletools' % __version__)
  113 +def sid_display(sid):
  114 + if sid == olefile.NOSTREAM:
  115 + return None
  116 + else:
  117 + return sid
  118 +
  119 +
  120 +def show_header(ole, extra_data=False):
  121 + print("OLE HEADER:")
  122 + t = tablestream.TableStream([24, 16, 79-(4+24+16)], header_row=['Attribute', 'Value', 'Description'])
  123 + t.write_row(['OLE Signature (hex)', binascii.b2a_hex(ole.header_signature).upper(), 'Should be D0CF11E0A1B11AE1'])
  124 + t.write_row(['Header CLSID (hex)', binascii.b2a_hex(ole.header_clsid).upper(), 'Should be 0'])
  125 + t.write_row(['Minor Version', '%04X' % ole.minor_version, 'Should be 003E'])
  126 + t.write_row(['Major Version', '%04X' % ole.dll_version, 'Should be 3 or 4'])
  127 + t.write_row(['Byte Order', '%04X' % ole.byte_order, 'Should be FFFE (little endian)'])
  128 + t.write_row(['Sector Shift', '%04X' % ole.sector_shift, 'Should be 0009 or 000C'])
  129 + t.write_row(['# of Dir Sectors', ole.num_dir_sectors, 'Should be 0 if major version is 3'])
  130 + t.write_row(['# of FAT Sectors', ole.num_fat_sectors, ''])
  131 + t.write_row(['First Dir Sector', '%08X' % ole.first_dir_sector, '(hex)'])
  132 + t.write_row(['Transaction Sig Number', ole.transaction_signature_number, 'Should be 0'])
  133 + t.write_row(['MiniStream cutoff', ole.mini_stream_cutoff_size, 'Should be 4096 bytes'])
  134 + t.write_row(['First MiniFAT Sector', '%08X' % ole.first_mini_fat_sector, '(hex)'])
  135 + t.write_row(['# of MiniFAT Sectors', ole.num_mini_fat_sectors, ''])
  136 + t.write_row(['First DIFAT Sector', '%08X' % ole.first_difat_sector, '(hex)'])
  137 + t.write_row(['# of DIFAT Sectors', ole.num_difat_sectors, ''])
  138 + t.close()
  139 + print('')
  140 + print("CALCULATED ATTRIBUTES:")
  141 + t = tablestream.TableStream([24, 16, 79-(4+24+16)], header_row=['Attribute', 'Value', 'Description'])
  142 + t.write_row(['Sector Size (bytes)', ole.sector_size, 'Should be 512 or 4096 bytes'])
  143 + t.write_row(['Actual File Size (bytes)', ole._filesize, 'Real file size on disk'])
  144 + num_sectors_per_fat_sector = ole.sector_size/4
  145 + num_sectors_in_fat = num_sectors_per_fat_sector * ole.num_fat_sectors
  146 + # Need to add one sector for the header:
  147 + max_filesize_fat = (num_sectors_in_fat + 1) * ole.sector_size
  148 + t.write_row(['Max File Size in FAT', max_filesize_fat, 'Max file size covered by FAT'])
  149 + if ole._filesize > max_filesize_fat:
  150 + extra_size_beyond_fat = ole._filesize - max_filesize_fat
  151 + color = 'red'
  152 + else:
  153 + extra_size_beyond_fat = 0
  154 + color = None
  155 + t.write_row(['Extra data beyond FAT', extra_size_beyond_fat, 'Only if file is larger than FAT coverage'],
  156 + colors=[color, color, color])
  157 + # Find the last used sector:
  158 + # By default, it's the last sector in the FAT
  159 + last_used_sector = len(ole.fat)-1
  160 + for i in range(len(ole.fat)-1, 0, -1):
  161 + last_used_sector = i
  162 + if ole.fat[i] != olefile.FREESECT:
  163 + break
  164 + # Extra data would start at the next sector
  165 + offset_extra_data = ole.sectorsize * (last_used_sector + 2)
  166 + t.write_row(['Extra data offset in FAT', '%08X' % offset_extra_data, 'Offset of the 1st free sector at end of FAT'])
  167 + extra_data_size = ole._filesize - offset_extra_data
  168 + color = 'red' if extra_data_size > 0 else None
  169 + t.write_row(['Extra data size', extra_data_size, 'Size of data starting at the 1st free sector at end of FAT'],
  170 + colors=[color, color, color])
  171 + t.close()
  172 + print('')
96 173  
97   - fname = sys.argv[1]
98   - ole = olefile.OleFileIO(fname)
  174 + if extra_data:
  175 + # hex dump of extra data
  176 + print('HEX DUMP OF EXTRA DATA:\n')
  177 + if extra_data_size <= 0:
  178 + print('No extra data found at end of file.')
  179 + else:
  180 + ole.fp.seek(offset_extra_data)
  181 + # read until end of file:
  182 + exdata = ole.fp.read()
  183 + assert len(exdata) == extra_data_size
  184 + print(hexdump3(exdata, length=16, startindex=offset_extra_data))
  185 + print('')
99 186  
  187 +
  188 +def show_fat(ole):
100 189 print('FAT:')
101 190 t = tablestream.TableStream([8, 12, 8, 8], header_row=['Sector #', 'Type', 'Offset', 'Next #'])
102   - for i in range(ole.nb_sect):
  191 + for i in range(len(ole.fat)):
103 192 fat_value = ole.fat[i]
104 193 fat_type = FAT_TYPES.get(fat_value, '<Data>')
105 194 color_type = FAT_COLORS.get(fat_value, FAT_COLORS['default'])
106 195 # compute offset based on sector size:
107   - offset = ole.sectorsize * (i+1)
  196 + offset = ole.sectorsize * (i + 1)
108 197 # print '%8X: %-12s offset=%08X next=%8X' % (i, fat_type, 0, fat_value)
109 198 t.write_row(['%8X' % i, fat_type, '%08X' % offset, '%8X' % fat_value],
110   - colors=[None, color_type, None, None])
  199 + colors=[None, color_type, None, None])
  200 + t.close()
111 201 print('')
112 202  
  203 +
  204 +def show_minifat(ole):
113 205 print('MiniFAT:')
114 206 # load MiniFAT if it wasn't already done:
115 207 ole.loadminifat()
  208 + t = tablestream.TableStream([8, 12, 8, 8], header_row=['Sector #', 'Type', 'Offset', 'Next #'])
116 209 for i in range(len(ole.minifat)):
117 210 fat_value = ole.minifat[i]
118 211 fat_type = FAT_TYPES.get(fat_value, 'Data')
119   - print('%8X: %-12s offset=%08X next=%8X' % (i, fat_type, 0, fat_value))
  212 + color_type = FAT_COLORS.get(fat_value, FAT_COLORS['default'])
  213 + # TODO: compute offset
  214 + # print('%8X: %-12s offset=%08X next=%8X' % (i, fat_type, 0, fat_value))
  215 + t.write_row(['%8X' % i, fat_type, 'N/A', '%8X' % fat_value],
  216 + colors=[None, color_type, None, None])
  217 + t.close()
  218 + print('')
  219 +
  220 +# === MAIN ===================================================================
  221 +
  222 +def main():
  223 + usage = 'usage: olemap [options] <filename> [filename2 ...]'
  224 + parser = optparse.OptionParser(usage=usage)
  225 + parser.add_option("-r", action="store_true", dest="recursive",
  226 + help='find files recursively in subdirectories.')
  227 + parser.add_option("-z", "--zip", dest='zip_password', type='str', default=None,
  228 + help='if the file is a zip archive, open all files from it, using the provided password (requires Python 2.6+)')
  229 + parser.add_option("-f", "--zipfname", dest='zip_fname', type='str', default='*',
  230 + help='if the file is a zip archive, file(s) to be opened within the zip. Wildcards * and ? are supported. (default:*)')
  231 + # parser.add_option('-l', '--loglevel', dest="loglevel", action="store", default=DEFAULT_LOG_LEVEL,
  232 + # help="logging level debug/info/warning/error/critical (default=%default)")
  233 + parser.add_option("--header", action="store_true", dest="header",
  234 + help='Display the OLE header (default: yes)')
  235 + parser.add_option("--fat", action="store_true", dest="fat",
  236 + help='Display the FAT (default: no)')
  237 + parser.add_option("--minifat", action="store_true", dest="minifat",
  238 + help='Display the MiniFAT (default: no)')
  239 + parser.add_option('-x', "--exdata", action="store_true", dest="extra_data",
  240 + help='Display a hex dump of extra data at end of file')
  241 +
  242 + # TODO: add logfile option
  243 +
  244 + (options, args) = parser.parse_args()
  245 +
  246 + # Print help if no arguments are passed
  247 + if len(args) == 0:
  248 + print(BANNER)
  249 + print(__doc__)
  250 + parser.print_help()
  251 + sys.exit()
  252 +
  253 + # if no display option is provided, set defaults:
  254 + default_options = False
  255 + if not (options.header or options.fat or options.minifat):
  256 + options.header = True
  257 + # options.fat = True
  258 + # options.minifat = True
  259 + default_options = True
  260 +
  261 + # print banner with version
  262 + print(BANNER)
  263 +
  264 + for container, filename, data in xglob.iter_files(args, recursive=options.recursive,
  265 + zip_password=options.zip_password, zip_fname=options.zip_fname):
  266 + # TODO: handle xglob errors
  267 + # ignore directory names stored in zip files:
  268 + if container and filename.endswith('/'):
  269 + continue
  270 + full_name = '%s in %s' % (filename, container) if container else filename
  271 + print("-" * 79)
  272 + print('FILE: %s\n' % full_name)
  273 + if data is not None:
  274 + # data extracted from zip file
  275 + ole = olefile.OleFileIO(data)
  276 + else:
  277 + # normal filename
  278 + ole = olefile.OleFileIO(filename)
  279 +
  280 + if options.header:
  281 + show_header(ole, extra_data=options.extra_data)
  282 + if options.fat:
  283 + show_fat(ole)
  284 + if options.minifat:
  285 + show_minifat(ole)
  286 +
  287 + ole.close()
  288 +
  289 + # if no display option is provided, print a tip:
  290 + if default_options:
  291 + print('To display the FAT or MiniFAT structures, use options --fat or --minifat, and -h for help.')
120 292  
121   - ole.close()
122 293  
123 294 if __name__ == '__main__':
124 295 main()
... ...
oletools/olemeta.py
... ... @@ -15,7 +15,7 @@ http://www.decalage.info/python/oletools
15 15  
16 16 #=== LICENSE =================================================================
17 17  
18   -# olemeta is copyright (c) 2013-2016, Philippe Lagadec (http://www.decalage.info)
  18 +# olemeta is copyright (c) 2013-2017, Philippe Lagadec (http://www.decalage.info)
19 19 # All rights reserved.
20 20 #
21 21 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -47,31 +47,42 @@ http://www.decalage.info/python/oletools
47 47 # 2016-09-06 v0.50 PL: - added main entry point for setup.py
48 48 # 2016-10-25 PL: - fixed print for Python 3
49 49 # 2016-10-28 PL: - removed the UTF8 codec for console display
  50 +# 2017-04-26 v0.51 PL: - fixed absolute imports (issue #141)
  51 +# 2017-05-04 PL: - added optparse and xglob (issue #141)
50 52  
51   -__version__ = '0.50'
  53 +__version__ = '0.51'
52 54  
53 55 #------------------------------------------------------------------------------
54 56 # TODO:
55   -# + optparse
56 57 # + nicer output: table with fixed columns, datetime, etc
57 58 # + CSV output
58 59 # + option to only show available properties (by default)
  60 +# + display codepage names
59 61  
60 62 #=== IMPORTS =================================================================
61 63  
62   -import sys, codecs
63   -import thirdparty.olefile as olefile
64   -from thirdparty.tablestream import tablestream
  64 +import sys, os, optparse
65 65  
  66 +# IMPORTANT: it should be possible to run oletools directly as scripts
  67 +# in any directory without installing them with pip or setup.py.
  68 +# In that case, relative imports are NOT usable.
  69 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  70 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  71 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  72 +# print('_thismodule_dir = %r' % _thismodule_dir)
  73 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  74 +# print('_parent_dir = %r' % _thirdparty_dir)
  75 +if not _parent_dir in sys.path:
  76 + sys.path.insert(0, _parent_dir)
66 77  
67   -#=== MAIN =================================================================
  78 +from oletools.thirdparty import olefile
  79 +from oletools.thirdparty import xglob
  80 +from oletools.thirdparty.tablestream import tablestream
68 81  
69   -def main():
70   - try:
71   - ole = olefile.OleFileIO(sys.argv[1])
72   - except IndexError:
73   - sys.exit(__doc__)
74 82  
  83 +#=== MAIN =================================================================
  84 +
  85 +def process_ole(ole):
75 86 # parse and display metadata:
76 87 meta = ole.get_metadata()
77 88  
... ... @@ -114,7 +125,53 @@ def main():
114 125 t.write_row([prop, value], colors=[None, 'yellow'])
115 126 t.close()
116 127  
117   - ole.close()
  128 +
  129 +# === MAIN ===================================================================
  130 +
  131 +def main():
  132 + # print banner with version
  133 + print('olemeta %s - http://decalage.info/python/oletools' % __version__)
  134 + print ('THIS IS WORK IN PROGRESS - Check updates regularly!')
  135 + print ('Please report any issue at https://github.com/decalage2/oletools/issues')
  136 +
  137 + usage = 'usage: olemeta [options] <filename> [filename2 ...]'
  138 + parser = optparse.OptionParser(usage=usage)
  139 + parser.add_option("-r", action="store_true", dest="recursive",
  140 + help='find files recursively in subdirectories.')
  141 + parser.add_option("-z", "--zip", dest='zip_password', type='str', default=None,
  142 + help='if the file is a zip archive, open all files from it, using the provided password (requires Python 2.6+)')
  143 + parser.add_option("-f", "--zipfname", dest='zip_fname', type='str', default='*',
  144 + help='if the file is a zip archive, file(s) to be opened within the zip. Wildcards * and ? are supported. (default:*)')
  145 +
  146 + # TODO: add logfile option
  147 + # parser.add_option('-l', '--loglevel', dest="loglevel", action="store", default=DEFAULT_LOG_LEVEL,
  148 + # help="logging level debug/info/warning/error/critical (default=%default)")
  149 +
  150 + (options, args) = parser.parse_args()
  151 +
  152 + # Print help if no arguments are passed
  153 + if len(args) == 0:
  154 + print(__doc__)
  155 + parser.print_help()
  156 + sys.exit()
  157 +
  158 + for container, filename, data in xglob.iter_files(args, recursive=options.recursive,
  159 + zip_password=options.zip_password, zip_fname=options.zip_fname):
  160 + # TODO: handle xglob errors
  161 + # ignore directory names stored in zip files:
  162 + if container and filename.endswith('/'):
  163 + continue
  164 + full_name = '%s in %s' % (filename, container) if container else filename
  165 + print("=" * 79)
  166 + print('FILE: %s\n' % full_name)
  167 + if data is not None:
  168 + # data extracted from zip file
  169 + ole = olefile.OleFileIO(data)
  170 + else:
  171 + # normal filename
  172 + ole = olefile.OleFileIO(filename)
  173 + process_ole(ole)
  174 + ole.close()
118 175  
119 176 if __name__ == '__main__':
120   - main()
121 177 \ No newline at end of file
  178 + main()
... ...
oletools/oleobj.py
... ... @@ -15,7 +15,7 @@ http://www.decalage.info/python/oletools
15 15  
16 16 # === LICENSE ==================================================================
17 17  
18   -# oleobj is copyright (c) 2015-2016 Philippe Lagadec (http://www.decalage.info)
  18 +# oleobj is copyright (c) 2015-2017 Philippe Lagadec (http://www.decalage.info)
19 19 # All rights reserved.
20 20 #
21 21 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -47,6 +47,7 @@ http://www.decalage.info/python/oletools
47 47 # 2016-07-19 PL: - fixed Python 2.6-7 support
48 48 # 2016-11-17 v0.51 PL: - fixed OLE native object extraction
49 49 # 2016-11-18 PL: - added main for setup.py entry point
  50 +# 2017-05-03 PL: - fixed absolute imports (issue #141)
50 51  
51 52 __version__ = '0.51'
52 53  
... ... @@ -70,8 +71,20 @@ __version__ = &#39;0.51&#39;
70 71  
71 72 import logging, struct, optparse, os, re, sys
72 73  
73   -from thirdparty.olefile import olefile
74   -from thirdparty.xglob import xglob
  74 +# IMPORTANT: it should be possible to run oletools directly as scripts
  75 +# in any directory without installing them with pip or setup.py.
  76 +# In that case, relative imports are NOT usable.
  77 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  78 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  79 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  80 +# print('_thismodule_dir = %r' % _thismodule_dir)
  81 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  82 +# print('_parent_dir = %r' % _thirdparty_dir)
  83 +if not _parent_dir in sys.path:
  84 + sys.path.insert(0, _parent_dir)
  85 +
  86 +from oletools.thirdparty.olefile import olefile
  87 +from oletools.thirdparty.xglob import xglob
75 88  
76 89 # === LOGGING =================================================================
77 90  
... ... @@ -114,6 +127,14 @@ def get_logger(name, level=logging.CRITICAL+1):
114 127 # a global logger object used for debugging:
115 128 log = get_logger('oleobj')
116 129  
  130 +def enable_logging():
  131 + """
  132 + Enable logging for this module (disabled by default).
  133 + This will set the module-specific logger level to NOTSET, which
  134 + means the main application controls the actual logging level.
  135 + """
  136 + log.setLevel(logging.NOTSET)
  137 +
117 138  
118 139 # === CONSTANTS ==============================================================
119 140  
... ... @@ -290,6 +311,9 @@ class OleObject (object):
290 311 :param data: bytes, OLE 1.0 Object structure containing an OLE object
291 312 :return:
292 313 """
  314 + # from ezhexviewer import hexdump3
  315 + # print("Parsing OLE object data:")
  316 + # print(hexdump3(data, length=16))
293 317 # Header: see MS-OLEDS 2.2.4 ObjectHeader
294 318 self.ole_version, data = read_uint32(data)
295 319 self.format_id, data = read_uint32(data)
... ...
oletools/oletimes.py
... ... @@ -16,7 +16,7 @@ http://www.decalage.info/python/oletools
16 16  
17 17 #=== LICENSE =================================================================
18 18  
19   -# oletimes is copyright (c) 2013-2016, Philippe Lagadec (http://www.decalage.info)
  19 +# oletimes is copyright (c) 2013-2017, Philippe Lagadec (http://www.decalage.info)
20 20 # All rights reserved.
21 21 #
22 22 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -48,61 +48,109 @@ http://www.decalage.info/python/oletools
48 48 # 2014-11-30 v0.03 PL: - improved output with prettytable
49 49 # 2016-07-20 v0.50 SL: - added Python 3 support
50 50 # 2016-09-05 PL: - added main entry point for setup.py
  51 +# 2017-05-03 v0.51 PL: - fixed absolute imports (issue #141)
  52 +# 2017-05-04 PL: - added optparse and xglob (issue #141)
51 53  
52   -__version__ = '0.50'
  54 +__version__ = '0.51'
53 55  
54 56 #------------------------------------------------------------------------------
55 57 # TODO:
56   -# + optparse
57 58 # + nicer output: table with fixed columns, datetime, etc
58 59 # + CSV output
59 60 # + option to only show available timestamps (by default?)
60 61  
61 62 #=== IMPORTS =================================================================
62 63  
63   -import sys, datetime
64   -import thirdparty.olefile as olefile
65   -from thirdparty.prettytable import prettytable
  64 +import sys, os, optparse
66 65  
  66 +# IMPORTANT: it should be possible to run oletools directly as scripts
  67 +# in any directory without installing them with pip or setup.py.
  68 +# In that case, relative imports are NOT usable.
  69 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  70 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  71 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  72 +# print('_thismodule_dir = %r' % _thismodule_dir)
  73 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  74 +# print('_parent_dir = %r' % _thirdparty_dir)
  75 +if not _parent_dir in sys.path:
  76 + sys.path.insert(0, _parent_dir)
67 77  
68   -# === MAIN ===================================================================
  78 +from oletools.thirdparty import olefile
  79 +from oletools.thirdparty import xglob
  80 +from oletools.thirdparty.prettytable import prettytable
69 81  
70   -def main():
71   - # print banner with version
72   - print('oletimes %s - http://decalage.info/python/oletools' % __version__)
73 82  
74   - try:
75   - ole = olefile.OleFileIO(sys.argv[1])
76   - except IndexError:
77   - sys.exit(__doc__)
  83 +# === FUNCTIONS ==============================================================
78 84  
79   - def dt2str (dt):
80   - """
81   - Convert a datetime object to a string for display, without microseconds
  85 +def dt2str(dt):
  86 + """
  87 + Convert a datetime object to a string for display, without microseconds
82 88  
83   - :param dt: datetime.datetime object, or None
84   - :return: str, or None
85   - """
86   - if dt is None:
87   - return None
88   - dt = dt.replace(microsecond = 0)
89   - return str(dt)
  89 + :param dt: datetime.datetime object, or None
  90 + :return: str, or None
  91 + """
  92 + if dt is None:
  93 + return None
  94 + dt = dt.replace(microsecond=0)
  95 + return str(dt)
90 96  
  97 +
  98 +def process_ole(ole):
91 99 t = prettytable.PrettyTable(['Stream/Storage name', 'Modification Time', 'Creation Time'])
92 100 t.align = 'l'
93 101 t.max_width = 26
94   - #t.border = False
95   -
96   - #print'- Root mtime=%s ctime=%s' % (ole.root.getmtime(), ole.root.getctime())
97 102 t.add_row(('Root', dt2str(ole.root.getmtime()), dt2str(ole.root.getctime())))
98   -
99 103 for obj in ole.listdir(streams=True, storages=True):
100   - #print '- %s: mtime=%s ctime=%s' % (repr('/'.join(obj)), ole.getmtime(obj), ole.getctime(obj))
101 104 t.add_row((repr('/'.join(obj)), dt2str(ole.getmtime(obj)), dt2str(ole.getctime(obj))))
102   -
103 105 print(t)
104 106  
105   - ole.close()
  107 +
  108 +# === MAIN ===================================================================
  109 +
  110 +def main():
  111 + # print banner with version
  112 + print('oletimes %s - http://decalage.info/python/oletools' % __version__)
  113 + print ('THIS IS WORK IN PROGRESS - Check updates regularly!')
  114 + print ('Please report any issue at https://github.com/decalage2/oletools/issues')
  115 +
  116 + usage = 'usage: oletimes [options] <filename> [filename2 ...]'
  117 + parser = optparse.OptionParser(usage=usage)
  118 + parser.add_option("-r", action="store_true", dest="recursive",
  119 + help='find files recursively in subdirectories.')
  120 + parser.add_option("-z", "--zip", dest='zip_password', type='str', default=None,
  121 + help='if the file is a zip archive, open all files from it, using the provided password (requires Python 2.6+)')
  122 + parser.add_option("-f", "--zipfname", dest='zip_fname', type='str', default='*',
  123 + help='if the file is a zip archive, file(s) to be opened within the zip. Wildcards * and ? are supported. (default:*)')
  124 +
  125 + # TODO: add logfile option
  126 + # parser.add_option('-l', '--loglevel', dest="loglevel", action="store", default=DEFAULT_LOG_LEVEL,
  127 + # help="logging level debug/info/warning/error/critical (default=%default)")
  128 +
  129 + (options, args) = parser.parse_args()
  130 +
  131 + # Print help if no arguments are passed
  132 + if len(args) == 0:
  133 + print(__doc__)
  134 + parser.print_help()
  135 + sys.exit()
  136 +
  137 + for container, filename, data in xglob.iter_files(args, recursive=options.recursive,
  138 + zip_password=options.zip_password, zip_fname=options.zip_fname):
  139 + # TODO: handle xglob errors
  140 + # ignore directory names stored in zip files:
  141 + if container and filename.endswith('/'):
  142 + continue
  143 + full_name = '%s in %s' % (filename, container) if container else filename
  144 + print("=" * 79)
  145 + print('FILE: %s\n' % full_name)
  146 + if data is not None:
  147 + # data extracted from zip file
  148 + ole = olefile.OleFileIO(data)
  149 + else:
  150 + # normal filename
  151 + ole = olefile.OleFileIO(filename)
  152 + process_ole(ole)
  153 + ole.close()
106 154  
107 155 if __name__ == '__main__':
108 156 main()
... ...
oletools/olevba.py
... ... @@ -26,7 +26,7 @@ https://github.com/unixfreak0037/officeparser
26 26  
27 27 # === LICENSE ==================================================================
28 28  
29   -# olevba is copyright (c) 2014-2016 Philippe Lagadec (http://www.decalage.info)
  29 +# olevba is copyright (c) 2014-2017 Philippe Lagadec (http://www.decalage.info)
30 30 # All rights reserved.
31 31 #
32 32 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -188,8 +188,18 @@ from __future__ import print_function
188 188 # 2016-09-12 PL: - enabled packrat to improve pyparsing performance
189 189 # 2016-10-25 PL: - fixed raise and print statements for Python 3
190 190 # 2016-11-03 v0.51 PL: - added EnumDateFormats and EnumSystemLanguageGroupsW
191   -
192   -__version__ = '0.51a'
  191 +# 2017-02-07 PL: - temporary fix for issue #132
  192 +# - added keywords for Mac-specific macros (issue #130)
  193 +# 2017-03-08 PL: - fixed absolute imports
  194 +# 2017-03-16 PL: - fixed issues #148 and #149 for option --reveal
  195 +# 2017-05-19 PL: - added enable_logging to fix issue #154
  196 +# 2017-05-31 c1fe: - PR #135 fixing issue #132 for some Mac files
  197 +# 2017-06-08 PL: - fixed issue #122 Chr() with negative numbers
  198 +# 2017-06-15 PL: - deobfuscation line by line to handle large files
  199 +# 2017-07-11 v0.52 PL: - raise exception instead of sys.exit (issue #180)
  200 +# 2017-11-08 VB: - PR #124 adding user form parsing (Vincent Brillault)
  201 +
  202 +__version__ = '0.52dev3'
193 203  
194 204 #------------------------------------------------------------------------------
195 205 # TODO:
... ... @@ -223,7 +233,9 @@ __version__ = &#39;0.51a&#39;
223 233  
224 234 #--- IMPORTS ------------------------------------------------------------------
225 235  
226   -import sys, logging
  236 +import sys
  237 +import os
  238 +import logging
227 239 import struct
228 240 import cStringIO
229 241 import math
... ... @@ -256,15 +268,28 @@ except ImportError:
256 268  
257 269 from oleform import extract_OleFormVariables
258 270  
259   -import thirdparty.olefile as olefile
260   -from thirdparty.prettytable import prettytable
261   -from thirdparty.xglob import xglob, PathNotFoundException
262   -from thirdparty.pyparsing.pyparsing import \
  271 +# IMPORTANT: it should be possible to run oletools directly as scripts
  272 +# in any directory without installing them with pip or setup.py.
  273 +# In that case, relative imports are NOT usable.
  274 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  275 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  276 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  277 +# print('_thismodule_dir = %r' % _thismodule_dir)
  278 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  279 +# print('_parent_dir = %r' % _thirdparty_dir)
  280 +if not _parent_dir in sys.path:
  281 + sys.path.insert(0, _parent_dir)
  282 +
  283 +from oletools.thirdparty import olefile
  284 +from oletools.thirdparty.prettytable import prettytable
  285 +from oletools.thirdparty.xglob import xglob, PathNotFoundException
  286 +from oletools.thirdparty.pyparsing.pyparsing import \
263 287 CaselessKeyword, CaselessLiteral, Combine, Forward, Literal, \
264 288 Optional, QuotedString,Regex, Suppress, Word, WordStart, \
265 289 alphanums, alphas, hexnums,nums, opAssoc, srange, \
266 290 infixNotation, ParserElement
267   -import ppt_parser
  291 +from oletools import ppt_parser
  292 +
268 293  
269 294 # monkeypatch email to fix issue #32:
270 295 # allow header lines without ":"
... ... @@ -330,6 +355,18 @@ def get_logger(name, level=logging.CRITICAL+1):
330 355 log = get_logger('olevba')
331 356  
332 357  
  358 +def enable_logging():
  359 + """
  360 + Enable logging for this module (disabled by default).
  361 + This will set the module-specific logger level to NOTSET, which
  362 + means the main application controls the actual logging level.
  363 + """
  364 + log.setLevel(logging.NOTSET)
  365 + # Also enable logging in the ppt_parser module:
  366 + ppt_parser.enable_logging()
  367 +
  368 +
  369 +
333 370 #=== EXCEPTIONS ==============================================================
334 371  
335 372 class OlevbaBaseException(Exception):
... ... @@ -387,10 +424,17 @@ class UnexpectedDataError(OlevbaBaseException):
387 424 """ raised when parsing is strict (=not relaxed) and data is unexpected """
388 425  
389 426 def __init__(self, stream_path, variable, expected, value):
  427 + if isinstance(expected, int):
  428 + es = '{0:04X}'.format(expected)
  429 + elif isinstance(expected, tuple):
  430 + es = ','.join('{0:04X}'.format(e) for e in expected)
  431 + es = '({0})'.format(es)
  432 + else:
  433 + raise ValueError('Unknown type encountered: {0}'.format(type(expected)))
390 434 super(UnexpectedDataError, self).__init__(
391 435 'Unexpected value in {0} for variable {1}: '
392   - 'expected {2:04X} but found {3:04X}!'
393   - .format(stream_path, variable, expected, value))
  436 + 'expected {2} but found {3:04X}!'
  437 + .format(stream_path, variable, es, value))
394 438 self.stream_path = stream_path
395 439 self.variable = variable
396 440 self.expected = expected
... ... @@ -520,6 +564,11 @@ SUSPICIOUS_KEYWORDS = {
520 564 'May run an executable file or a system command':
521 565 ('Shell', 'vbNormal', 'vbNormalFocus', 'vbHide', 'vbMinimizedFocus', 'vbMaximizedFocus', 'vbNormalNoFocus',
522 566 'vbMinimizedNoFocus', 'WScript.Shell', 'Run', 'ShellExecute'),
  567 + # MacScript: see https://msdn.microsoft.com/en-us/library/office/gg264812.aspx
  568 + 'May run an executable file or a system command on a Mac':
  569 + ('MacScript',),
  570 + 'May run an executable file or a system command on a Mac (if combined with libc.dylib)':
  571 + ('system', 'popen', r'exec[lv][ep]?'),
523 572 #Shell: http://msdn.microsoft.com/en-us/library/office/gg278437%28v=office.15%29.aspx
524 573 #WScript.Shell+Run sample: http://pastebin.com/Z4TMyuq6
525 574 'May run PowerShell commands':
... ... @@ -550,8 +599,11 @@ SUSPICIOUS_KEYWORDS = {
550 599 'May enumerate application windows (if combined with Shell.Application object)':
551 600 ('Windows', 'FindWindow'),
552 601 'May run code from a DLL':
553   - #TODO: regex to find declare+lib on same line
  602 + #TODO: regex to find declare+lib on same line - see mraptor
554 603 ('Lib',),
  604 + 'May run code from a library on a Mac':
  605 + #TODO: regex to find declare+lib on same line - see mraptor
  606 + ('libc.dylib', 'dylib'),
555 607 'May inject code into another process':
556 608 ('CreateThread', 'VirtualAlloc', # (issue #9) suggested by Davy Douhine - used by MSF payload
557 609 'VirtualAllocEx', 'RtlMoveMemory',
... ... @@ -723,7 +775,7 @@ class VbaExpressionString(str):
723 775 # NOTE: here Combine() is required to avoid spaces between elements
724 776 # NOTE: here WordStart is necessary to avoid matching a number preceded by
725 777 # letters or underscore (e.g. "VBT1" or "ABC_34"), when using scanString
726   -decimal_literal = Combine(WordStart(vba_identifier_chars) + Word(nums)
  778 +decimal_literal = Combine(Optional('-') + WordStart(vba_identifier_chars) + Word(nums)
727 779 + Suppress(Optional(Word('%&^', exact=1))))
728 780 decimal_literal.setParseAction(lambda t: int(t[0]))
729 781  
... ... @@ -1411,15 +1463,27 @@ def _extract_vba(ole, vba_root, project_path, dir_path, relaxed=False):
1411 1463 reference_sizeof_name = struct.unpack("<L", dir_stream.read(4))[0]
1412 1464 reference_name = dir_stream.read(reference_sizeof_name)
1413 1465 reference_reserved = struct.unpack("<H", dir_stream.read(2))[0]
1414   - if reference_reserved not in (0x003E, 0x000D):
1415   - raise UnexpectedDataError(dir_path, 'REFERENCE_Reserved',
1416   - (0x003E, 0x000D), reference_reserved)
1417   - reference_sizeof_name_unicode = struct.unpack("<L", dir_stream.read(4))[0]
1418   - reference_name_unicode = dir_stream.read(reference_sizeof_name_unicode)
1419   - unused = reference_id
1420   - unused = reference_name
1421   - unused = reference_name_unicode
1422   - continue
  1466 + # According to [MS-OVBA] 2.3.4.2.2.2 REFERENCENAME Record:
  1467 + # "Reserved (2 bytes): MUST be 0x003E. MUST be ignored."
  1468 + # So let's ignore it, otherwise it crashes on some files (issue #132)
  1469 + # PR #135 by @c1fe:
  1470 + # contrary to the specification I think that the unicode name
  1471 + # is optional. if reference_reserved is not 0x003E I think it
  1472 + # is actually the start of another REFERENCE record
  1473 + # at least when projectsyskind_syskind == 0x02 (Macintosh)
  1474 + if reference_reserved == 0x003E:
  1475 + #if reference_reserved not in (0x003E, 0x000D):
  1476 + # raise UnexpectedDataError(dir_path, 'REFERENCE_Reserved',
  1477 + # 0x0003E, reference_reserved)
  1478 + reference_sizeof_name_unicode = struct.unpack("<L", dir_stream.read(4))[0]
  1479 + reference_name_unicode = dir_stream.read(reference_sizeof_name_unicode)
  1480 + unused = reference_id
  1481 + unused = reference_name
  1482 + unused = reference_name_unicode
  1483 + continue
  1484 + else:
  1485 + check = reference_reserved
  1486 + log.debug("reference type = {0:04X}".format(check))
1423 1487  
1424 1488 if check == 0x0033:
1425 1489 # REFERENCEORIGINAL (followed by REFERENCECONTROL)
... ... @@ -1451,15 +1515,16 @@ def _extract_vba(ole, vba_root, project_path, dir_path, relaxed=False):
1451 1515 referencecontrol_namerecordextended_name = dir_stream.read(
1452 1516 referencecontrol_namerecordextended_sizeof_name)
1453 1517 referencecontrol_namerecordextended_reserved = struct.unpack("<H", dir_stream.read(2))[0]
1454   - check_value('REFERENCECONTROL_NameRecordExtended_Reserved', 0x003E,
1455   - referencecontrol_namerecordextended_reserved)
1456   - referencecontrol_namerecordextended_sizeof_name_unicode = struct.unpack("<L", dir_stream.read(4))[0]
1457   - referencecontrol_namerecordextended_name_unicode = dir_stream.read(
1458   - referencecontrol_namerecordextended_sizeof_name_unicode)
1459   - referencecontrol_reserved3 = struct.unpack("<H", dir_stream.read(2))[0]
1460   - unused = referencecontrol_namerecordextended_id
1461   - unused = referencecontrol_namerecordextended_name
1462   - unused = referencecontrol_namerecordextended_name_unicode
  1518 + if referencecontrol_namerecordextended_reserved == 0x003E:
  1519 + referencecontrol_namerecordextended_sizeof_name_unicode = struct.unpack("<L", dir_stream.read(4))[0]
  1520 + referencecontrol_namerecordextended_name_unicode = dir_stream.read(
  1521 + referencecontrol_namerecordextended_sizeof_name_unicode)
  1522 + referencecontrol_reserved3 = struct.unpack("<H", dir_stream.read(2))[0]
  1523 + unused = referencecontrol_namerecordextended_id
  1524 + unused = referencecontrol_namerecordextended_name
  1525 + unused = referencecontrol_namerecordextended_name_unicode
  1526 + else:
  1527 + referencecontrol_reserved3 = referencecontrol_namerecordextended_reserved
1463 1528 else:
1464 1529 referencecontrol_reserved3 = check2
1465 1530  
... ... @@ -1513,7 +1578,9 @@ def _extract_vba(ole, vba_root, project_path, dir_path, relaxed=False):
1513 1578 continue
1514 1579  
1515 1580 log.error('invalid or unknown check Id {0:04X}'.format(check))
1516   - sys.exit(0)
  1581 + # raise an exception instead of stopping abruptly (issue #180)
  1582 + raise UnexpectedDataError(dir_path, 'reference type', (0x0F, 0x16, 0x33, 0x2F, 0x0D, 0x0E), check)
  1583 + #sys.exit(0)
1517 1584  
1518 1585 projectmodules_id = check #struct.unpack("<H", dir_stream.read(2))[0]
1519 1586 check_value('PROJECTMODULES_Id', 0x000F, projectmodules_id)
... ... @@ -1865,7 +1932,8 @@ def detect_dridex_strings(vba_code):
1865 1932 :param vba_code: str, VBA source code
1866 1933 :return: list of str tuples (encoded string, decoded string)
1867 1934 """
1868   - from thirdparty.DridexUrlDecoder.DridexUrlDecoder import DridexUrlDecode
  1935 + # TODO: move this at the beginning of script
  1936 + from oletools.thirdparty.DridexUrlDecoder.DridexUrlDecoder import DridexUrlDecode
1869 1937  
1870 1938 results = []
1871 1939 found = set()
... ... @@ -1900,23 +1968,25 @@ def detect_vba_strings(vba_code):
1900 1968 # we must expand tabs to have the same string as pyparsing.
1901 1969 # Otherwise, start and end offsets are incorrect.
1902 1970 vba_code = vba_code.expandtabs()
1903   - for tokens, start, end in vba_expr_str.scanString(vba_code):
1904   - encoded = vba_code[start:end]
1905   - decoded = tokens[0]
1906   - if isinstance(decoded, VbaExpressionString):
1907   - # This is a VBA expression, not a simple string
1908   - # print 'VBA EXPRESSION: encoded=%r => decoded=%r' % (encoded, decoded)
1909   - # remove parentheses and quotes from original string:
1910   - # if encoded.startswith('(') and encoded.endswith(')'):
1911   - # encoded = encoded[1:-1]
1912   - # if encoded.startswith('"') and encoded.endswith('"'):
1913   - # encoded = encoded[1:-1]
1914   - # avoid duplicates and simple strings:
1915   - if encoded not in found and decoded != encoded:
1916   - results.append((encoded, decoded))
1917   - found.add(encoded)
1918   - # else:
1919   - # print 'VBA STRING: encoded=%r => decoded=%r' % (encoded, decoded)
  1971 + # Split the VBA code line by line to avoid MemoryError on large scripts:
  1972 + for vba_line in vba_code.splitlines():
  1973 + for tokens, start, end in vba_expr_str.scanString(vba_line):
  1974 + encoded = vba_line[start:end]
  1975 + decoded = tokens[0]
  1976 + if isinstance(decoded, VbaExpressionString):
  1977 + # This is a VBA expression, not a simple string
  1978 + # print 'VBA EXPRESSION: encoded=%r => decoded=%r' % (encoded, decoded)
  1979 + # remove parentheses and quotes from original string:
  1980 + # if encoded.startswith('(') and encoded.endswith(')'):
  1981 + # encoded = encoded[1:-1]
  1982 + # if encoded.startswith('"') and encoded.endswith('"'):
  1983 + # encoded = encoded[1:-1]
  1984 + # avoid duplicates and simple strings:
  1985 + if encoded not in found and decoded != encoded:
  1986 + results.append((encoded, decoded))
  1987 + found.add(encoded)
  1988 + # else:
  1989 + # print 'VBA STRING: encoded=%r => decoded=%r' % (encoded, decoded)
1920 1990 return results
1921 1991  
1922 1992  
... ... @@ -1957,19 +2027,19 @@ def json2ascii(json_obj, encoding=&#39;utf8&#39;, errors=&#39;replace&#39;):
1957 2027 return json_obj
1958 2028  
1959 2029  
1960   -_have_printed_json_start = False
1961   -
1962   -def print_json(json_dict=None, _json_is_last=False, **json_parts):
  2030 +def print_json(json_dict=None, _json_is_first=False, _json_is_last=False,
  2031 + **json_parts):
1963 2032 """ line-wise print of json.dumps(json2ascii(..)) with options and indent+1
1964 2033  
1965 2034 can use in two ways:
1966 2035 (1) print_json(some_dict)
1967 2036 (2) print_json(key1=value1, key2=value2, ...)
1968 2037  
  2038 + :param bool _json_is_first: set to True only for very first entry to complete
  2039 + the top-level json-list
1969 2040 :param bool _json_is_last: set to True only for very last entry to complete
1970 2041 the top-level json-list
1971 2042 """
1972   - global _have_printed_json_start
1973 2043  
1974 2044 if json_dict and json_parts:
1975 2045 raise ValueError('Invalid json argument: want either single dict or '
... ... @@ -1981,9 +2051,8 @@ def print_json(json_dict=None, _json_is_last=False, **json_parts):
1981 2051 if json_parts:
1982 2052 json_dict = json_parts
1983 2053  
1984   - if not _have_printed_json_start:
  2054 + if _json_is_first:
1985 2055 print('[')
1986   - _have_printed_json_start = True
1987 2056  
1988 2057 lines = json.dumps(json2ascii(json_dict), check_circular=False,
1989 2058 indent=4, ensure_ascii=False).splitlines()
... ... @@ -2483,7 +2552,6 @@ class VBA_Parser(object):
2483 2552 """
2484 2553  
2485 2554 log.info('Check whether OLE file is PPT')
2486   - ppt_parser.enable_logging()
2487 2555 try:
2488 2556 ppt = ppt_parser.PptParser(self.ole_file, fast_fail=True)
2489 2557 for vba_data in ppt.iter_vba_data():
... ... @@ -2493,7 +2561,7 @@ class VBA_Parser(object):
2493 2561 self.ole_file.close() # just in case
2494 2562 self.ole_file = None # required to make other methods look at ole_subfiles
2495 2563 self.type = TYPE_PPT
2496   - except Exception as exc:
  2564 + except (ppt_parser.PptUnexpectedData, ValueError) as exc:
2497 2565 if self.container == 'PptParser':
2498 2566 # this is a subfile of a ppt --> to be expected that is no ppt
2499 2567 log.debug('PPT subfile is not a PPT file')
... ... @@ -2704,12 +2772,16 @@ class VBA_Parser(object):
2704 2772 vba_stream_ids = set()
2705 2773 for vba_root, project_path, dir_path in self.vba_projects:
2706 2774 # extract all VBA macros from that VBA root storage:
2707   - for stream_path, vba_filename, vba_code in \
2708   - _extract_vba(self.ole_file, vba_root, project_path,
2709   - dir_path, self.relaxed):
2710   - # store direntry ids in a set:
2711   - vba_stream_ids.add(self.ole_file._find(stream_path))
2712   - yield (self.filename, stream_path, vba_filename, vba_code)
  2775 + # The function _extract_vba may fail on some files (issue #132)
  2776 + try:
  2777 + for stream_path, vba_filename, vba_code in \
  2778 + _extract_vba(self.ole_file, vba_root, project_path,
  2779 + dir_path, self.relaxed):
  2780 + # store direntry ids in a set:
  2781 + vba_stream_ids.add(self.ole_file._find(stream_path))
  2782 + yield (self.filename, stream_path, vba_filename, vba_code)
  2783 + except Exception as e:
  2784 + log.exception('Error in _extract_vba')
2713 2785 # Also look for VBA code in any stream including orphans
2714 2786 # (happens in some malformed files)
2715 2787 ole = self.ole_file
... ... @@ -2796,14 +2868,23 @@ class VBA_Parser(object):
2796 2868 # based on the length of the encoded string, in reverse order:
2797 2869 analysis = sorted(analysis, key=lambda type_decoded_encoded: len(type_decoded_encoded[2]), reverse=True)
2798 2870 # normally now self.vba_code_all_modules contains source code from all modules
2799   - deobf_code = self.vba_code_all_modules
  2871 + # Need to collapse long lines:
  2872 + deobf_code = vba_collapse_long_lines(self.vba_code_all_modules)
  2873 + deobf_code = filter_vba(deobf_code)
2800 2874 for kw_type, decoded, encoded in analysis:
2801 2875 if kw_type == 'VBA string':
2802 2876 #print '%3d occurences: %r => %r' % (deobf_code.count(encoded), encoded, decoded)
2803 2877 # need to add double quotes around the decoded strings
2804 2878 # after escaping double-quotes as double-double-quotes for VBA:
2805 2879 decoded = decoded.replace('"', '""')
2806   - deobf_code = deobf_code.replace(encoded, '"%s"' % decoded)
  2880 + decoded = '"%s"' % decoded
  2881 + # if the encoded string is enclosed in parentheses,
  2882 + # keep them in the decoded version:
  2883 + if encoded.startswith('(') and encoded.endswith(')'):
  2884 + decoded = '(%s)' % decoded
  2885 + deobf_code = deobf_code.replace(encoded, decoded)
  2886 + # # TODO: there is a bug somewhere which creates double returns '\r\r'
  2887 + # deobf_code = deobf_code.replace('\r\r', '\r')
2807 2888 return deobf_code
2808 2889 #TODO: repasser l'analyse plusieurs fois si des chaines hex ou base64 sont revelees
2809 2890  
... ... @@ -3213,10 +3294,9 @@ class VBA_Parser_CLI(VBA_Parser):
3213 3294  
3214 3295 #=== MAIN =====================================================================
3215 3296  
3216   -def main():
3217   - """
3218   - Main function, called when olevba is run from the command line
3219   - """
  3297 +def parse_args(cmd_line_args=None):
  3298 + """ parse command line arguments (given ones or per default sys.argv) """
  3299 +
3220 3300 DEFAULT_LOG_LEVEL = "warning" # Default log level
3221 3301 LOG_LEVELS = {
3222 3302 'debug': logging.DEBUG,
... ... @@ -3226,7 +3306,7 @@ def main():
3226 3306 'critical': logging.CRITICAL
3227 3307 }
3228 3308  
3229   - usage = 'usage: %prog [options] <filename> [filename2 ...]'
  3309 + usage = 'usage: olevba [options] <filename> [filename2 ...]'
3230 3310 parser = optparse.OptionParser(usage=usage)
3231 3311 # parser.add_option('-o', '--outfile', dest='outfile',
3232 3312 # help='output file')
... ... @@ -3268,26 +3348,43 @@ def main():
3268 3348 parser.add_option('--relaxed', dest="relaxed", action="store_true", default=False,
3269 3349 help="Do not raise errors if opening of substream fails")
3270 3350  
3271   - (options, args) = parser.parse_args()
  3351 + (options, args) = parser.parse_args(cmd_line_args)
3272 3352  
3273 3353 # Print help if no arguments are passed
3274 3354 if len(args) == 0:
  3355 + print('olevba %s - http://decalage.info/python/oletools' % __version__)
3275 3356 print(__doc__)
3276 3357 parser.print_help()
3277 3358 sys.exit(RETURN_WRONG_ARGS)
3278 3359  
  3360 + options.loglevel = LOG_LEVELS[options.loglevel]
  3361 +
  3362 + return options, args
  3363 +
  3364 +
  3365 +def main(cmd_line_args=None):
  3366 + """
  3367 + Main function, called when olevba is run from the command line
  3368 +
  3369 + Optional argument: command line arguments to be forwarded to ArgumentParser
  3370 + in process_args. Per default (cmd_line_args=None), sys.argv is used. Option
  3371 + mainly added for unit-testing
  3372 + """
  3373 +
  3374 + options, args = parse_args(cmd_line_args)
  3375 +
3279 3376 # provide info about tool and its version
3280 3377 if options.output_mode == 'json':
3281   - # prints opening [
  3378 + # print first json entry with meta info and opening '['
3282 3379 print_json(script_name='olevba', version=__version__,
3283 3380 url='http://decalage.info/python/oletools',
3284   - type='MetaInformation')
  3381 + type='MetaInformation', _json_is_first=True)
3285 3382 else:
3286 3383 print('olevba %s - http://decalage.info/python/oletools' % __version__)
3287 3384  
3288   - logging.basicConfig(level=LOG_LEVELS[options.loglevel], format='%(levelname)-8s %(message)s')
  3385 + logging.basicConfig(level=options.loglevel, format='%(levelname)-8s %(message)s')
3289 3386 # enable logging in the modules:
3290   - log.setLevel(logging.NOTSET)
  3387 + enable_logging()
3291 3388  
3292 3389 # Old display with number of items detected:
3293 3390 # print '%-8s %-7s %-7s %-7s %-7s %-7s' % ('Type', 'Macros', 'AutoEx', 'Susp.', 'IOCs', 'HexStr')
... ...
oletools/olevba3.py
... ... @@ -26,7 +26,7 @@ https://github.com/unixfreak0037/officeparser
26 26  
27 27 # === LICENSE ==================================================================
28 28  
29   -# olevba is copyright (c) 2014-2016 Philippe Lagadec (http://www.decalage.info)
  29 +# olevba is copyright (c) 2014-2017 Philippe Lagadec (http://www.decalage.info)
30 30 # All rights reserved.
31 31 #
32 32 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -189,8 +189,9 @@ from __future__ import print_function
189 189 # 2016-10-25 PL: - fixed raise and print statements for Python 3
190 190 # 2016-10-25 PL: - fixed regex bytes strings (PR/issue #100)
191 191 # 2016-11-03 v0.51 PL: - added EnumDateFormats and EnumSystemLanguageGroupsW
  192 +# 2017-04-26 PL: - fixed absolute imports
192 193  
193   -__version__ = '0.51a'
  194 +__version__ = '0.51'
194 195  
195 196 #------------------------------------------------------------------------------
196 197 # TODO:
... ... @@ -224,7 +225,7 @@ __version__ = &#39;0.51a&#39;
224 225  
225 226 #--- IMPORTS ------------------------------------------------------------------
226 227  
227   -import sys, logging
  228 +import sys, logging, os
228 229 import struct
229 230 from _io import StringIO,BytesIO
230 231 import math
... ... @@ -255,14 +256,26 @@ except ImportError:
255 256 + "see http://codespeak.net/lxml " \
256 257 + "or http://effbot.org/zone/element-index.htm")
257 258  
258   -import oletools.thirdparty.olefile as olefile
  259 +# IMPORTANT: it should be possible to run oletools directly as scripts
  260 +# in any directory without installing them with pip or setup.py.
  261 +# In that case, relative imports are NOT usable.
  262 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  263 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  264 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  265 +# print('_thismodule_dir = %r' % _thismodule_dir)
  266 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  267 +# print('_parent_dir = %r' % _thirdparty_dir)
  268 +if not _parent_dir in sys.path:
  269 + sys.path.insert(0, _parent_dir)
  270 +
  271 +from oletools.thirdparty import olefile
259 272 from oletools.thirdparty.prettytable import prettytable
260 273 from oletools.thirdparty.xglob import xglob, PathNotFoundException
261 274 from oletools.thirdparty.pyparsing.pyparsing import \
262 275 CaselessKeyword, CaselessLiteral, Combine, Forward, Literal, \
263 276 Optional, QuotedString,Regex, Suppress, Word, WordStart, \
264 277 alphanums, alphas, hexnums,nums, opAssoc, srange, \
265   - infixNotation
  278 + infixNotation, ParserElement
266 279 import oletools.ppt_parser as ppt_parser
267 280  
268 281 # monkeypatch email to fix issue #32:
... ... @@ -287,6 +300,25 @@ else:
287 300 # xrange is now called range:
288 301 xrange = range
289 302  
  303 +
  304 +# === PYTHON 3.0 - 3.4 SUPPORT ======================================================
  305 +
  306 +# From https://gist.github.com/ynkdir/867347/c5e188a4886bc2dd71876c7e069a7b00b6c16c61
  307 +
  308 +if sys.version_info >= (3, 0) and sys.version_info < (3, 5):
  309 + import codecs
  310 +
  311 + _backslashreplace_errors = codecs.lookup_error("backslashreplace")
  312 +
  313 + def backslashreplace_errors(exc):
  314 + if isinstance(exc, UnicodeDecodeError):
  315 + u = "".join("\\x{0:02x}".format(c) for c in exc.object[exc.start:exc.end])
  316 + return (u, exc.end)
  317 + return _backslashreplace_errors(exc)
  318 +
  319 + codecs.register_error("backslashreplace", backslashreplace_errors)
  320 +
  321 +
290 322 # === LOGGING =================================================================
291 323  
292 324 class NullHandler(logging.Handler):
... ... @@ -1535,7 +1567,7 @@ def _extract_vba(ole, vba_root, project_path, dir_path, relaxed=False):
1535 1567 modulename_id = struct.unpack("<H", dir_stream.read(2))[0]
1536 1568 check_value('MODULENAME_Id', 0x0019, modulename_id)
1537 1569 modulename_sizeof_modulename = struct.unpack("<L", dir_stream.read(4))[0]
1538   - modulename_modulename = dir_stream.read(modulename_sizeof_modulename)
  1570 + modulename_modulename = dir_stream.read(modulename_sizeof_modulename).decode('utf-8', 'backslashreplace')
1539 1571 # TODO: preset variables to avoid "referenced before assignment" errors
1540 1572 modulename_unicode_modulename_unicode = ''
1541 1573 # account for optional sections
... ... @@ -1781,7 +1813,7 @@ def detect_suspicious(vba_code, obfuscation=None):
1781 1813 for description, keywords in SUSPICIOUS_KEYWORDS.items():
1782 1814 for keyword in keywords:
1783 1815 # search using regex to detect word boundaries:
1784   - match = re.search(r'(?i)\b' + keyword + r'\b', vba_code)
  1816 + match = re.search(r'(?i)\b' + re.escape(keyword) + r'\b', vba_code)
1785 1817 if match:
1786 1818 #if keyword.lower() in vba_code:
1787 1819 found_keyword = match.group()
... ... @@ -1824,7 +1856,7 @@ def detect_hex_strings(vba_code):
1824 1856 value = match.group()
1825 1857 if value not in found:
1826 1858 decoded = binascii.unhexlify(value)
1827   - results.append((value, decoded.decode('utf-8','replace')))
  1859 + results.append((value, decoded.decode('utf-8', 'backslashreplace')))
1828 1860 found.add(value)
1829 1861 return results
1830 1862  
... ... @@ -1956,20 +1988,19 @@ def json2ascii(json_obj, encoding=&#39;utf8&#39;, errors=&#39;replace&#39;):
1956 1988 return json_obj
1957 1989  
1958 1990  
1959   -_have_printed_json_start = False
1960   -
1961   -def print_json(json_dict=None, _json_is_last=False, **json_parts):
  1991 +def print_json(json_dict=None, _json_is_first=False, _json_is_last=False,
  1992 + **json_parts):
1962 1993 """ line-wise print of json.dumps(json2ascii(..)) with options and indent+1
1963 1994  
1964 1995 can use in two ways:
1965 1996 (1) print_json(some_dict)
1966 1997 (2) print_json(key1=value1, key2=value2, ...)
1967 1998  
  1999 + :param bool _json_is_first: set to True only for very first entry to complete
  2000 + the top-level json-list
1968 2001 :param bool _json_is_last: set to True only for very last entry to complete
1969 2002 the top-level json-list
1970 2003 """
1971   - global _have_printed_json_start
1972   -
1973 2004 if json_dict and json_parts:
1974 2005 raise ValueError('Invalid json argument: want either single dict or '
1975 2006 'key=value parts but got both)')
... ... @@ -1980,9 +2011,8 @@ def print_json(json_dict=None, _json_is_last=False, **json_parts):
1980 2011 if json_parts:
1981 2012 json_dict = json_parts
1982 2013  
1983   - if not _have_printed_json_start:
  2014 + if _json_is_first:
1984 2015 print('[')
1985   - _have_printed_json_start = True
1986 2016  
1987 2017 lines = json.dumps(json2ascii(json_dict), check_circular=False,
1988 2018 indent=4, ensure_ascii=False).splitlines()
... ... @@ -2007,6 +2037,8 @@ class VBA_Scanner(object):
2007 2037  
2008 2038 :param vba_code: str, VBA source code to be analyzed
2009 2039 """
  2040 + if isinstance(vba_code, bytes):
  2041 + vba_code = vba_code.decode('utf-8', 'backslashreplace')
2010 2042 # join long lines ending with " _":
2011 2043 self.code = vba_collapse_long_lines(vba_code)
2012 2044 self.code_hex = ''
... ... @@ -2084,7 +2116,7 @@ class VBA_Scanner(object):
2084 2116 (self.code_vba, 'VBA expression'),
2085 2117 ):
2086 2118 if isinstance(code,bytes):
2087   - code=code.decode('utf-8','replace')
  2119 + code=code.decode('utf-8','backslashreplace')
2088 2120 self.autoexec_keywords += detect_autoexec(code, obfuscation)
2089 2121 self.suspicious_keywords += detect_suspicious(code, obfuscation)
2090 2122 self.iocs += detect_patterns(code, obfuscation)
... ... @@ -2411,7 +2443,7 @@ class VBA_Parser(object):
2411 2443 log.info('Opening MHTML file %s' % self.filename)
2412 2444 try:
2413 2445 if isinstance(data,bytes):
2414   - data = data.decode('utf8', 'replace')
  2446 + data = data.decode('utf8', 'backslashreplace')
2415 2447 # parse the MIME content
2416 2448 # remove any leading whitespace or newline (workaround for issue in email package)
2417 2449 stripped_data = data.lstrip('\r\n\t ')
... ... @@ -2514,7 +2546,7 @@ class VBA_Parser(object):
2514 2546 log.info('Opening text file %s' % self.filename)
2515 2547 # directly store the source code:
2516 2548 if isinstance(data,bytes):
2517   - data=data.decode('utf8','replace')
  2549 + data=data.decode('utf8','backslashreplace')
2518 2550 self.vba_code_all_modules = data
2519 2551 self.contains_macros = True
2520 2552 # set type only if parsing succeeds
... ... @@ -2671,7 +2703,7 @@ class VBA_Parser(object):
2671 2703 log.debug('%r...[much more data]...%r' % (data[:100], data[-50:]))
2672 2704 else:
2673 2705 log.debug(repr(data))
2674   - if 'Attribut' in data.decode('utf-8','ignore'):
  2706 + if 'Attribut' in data.decode('utf-8', 'ignore'):
2675 2707 log.debug('Found VBA compressed code')
2676 2708 self.contains_macros = True
2677 2709 except IOError as exc:
... ... @@ -3026,7 +3058,7 @@ class VBA_Parser_CLI(VBA_Parser):
3026 3058 if hide_attributes:
3027 3059 # hide attribute lines:
3028 3060 if isinstance(vba_code,bytes):
3029   - vba_code =vba_code.decode('utf-8','replace')
  3061 + vba_code =vba_code.decode('utf-8','backslashreplace')
3030 3062 vba_code_filtered = filter_vba(vba_code)
3031 3063 else:
3032 3064 vba_code_filtered = vba_code
... ... @@ -3105,9 +3137,12 @@ class VBA_Parser_CLI(VBA_Parser):
3105 3137 if self.detect_vba_macros():
3106 3138 for (subfilename, stream_path, vba_filename, vba_code) in self.extract_all_macros():
3107 3139 curr_macro = {}
  3140 + if isinstance(vba_code, bytes):
  3141 + vba_code = vba_code.decode('utf-8', 'backslashreplace')
  3142 +
3108 3143 if hide_attributes:
3109 3144 # hide attribute lines:
3110   - vba_code_filtered = filter_vba(vba_code.decode('utf-8','replace'))
  3145 + vba_code_filtered = filter_vba(vba_code)
3111 3146 else:
3112 3147 vba_code_filtered = vba_code
3113 3148  
... ... @@ -3198,10 +3233,9 @@ class VBA_Parser_CLI(VBA_Parser):
3198 3233  
3199 3234 #=== MAIN =====================================================================
3200 3235  
3201   -def main():
3202   - """
3203   - Main function, called when olevba is run from the command line
3204   - """
  3236 +def parse_args(cmd_line_args=None):
  3237 + """ parse command line arguments (given ones or per default sys.argv) """
  3238 +
3205 3239 DEFAULT_LOG_LEVEL = "warning" # Default log level
3206 3240 LOG_LEVELS = {
3207 3241 'debug': logging.DEBUG,
... ... @@ -3253,7 +3287,7 @@ def main():
3253 3287 parser.add_option('--relaxed', dest="relaxed", action="store_true", default=False,
3254 3288 help="Do not raise errors if opening of substream fails")
3255 3289  
3256   - (options, args) = parser.parse_args()
  3290 + (options, args) = parser.parse_args(cmd_line_args)
3257 3291  
3258 3292 # Print help if no arguments are passed
3259 3293 if len(args) == 0:
... ... @@ -3261,16 +3295,32 @@ def main():
3261 3295 parser.print_help()
3262 3296 sys.exit(RETURN_WRONG_ARGS)
3263 3297  
  3298 + options.loglevel = LOG_LEVELS[options.loglevel]
  3299 +
  3300 + return options, args
  3301 +
  3302 +
  3303 +def main(cmd_line_args=None):
  3304 + """
  3305 + Main function, called when olevba is run from the command line
  3306 +
  3307 + Optional argument: command line arguments to be forwarded to ArgumentParser
  3308 + in process_args. Per default (cmd_line_args=None), sys.argv is used. Option
  3309 + mainly added for unit-testing
  3310 + """
  3311 +
  3312 + options, args = parse_args(cmd_line_args)
  3313 +
3264 3314 # provide info about tool and its version
3265 3315 if options.output_mode == 'json':
3266   - # prints opening [
  3316 + # print first json entry with meta info and opening '['
3267 3317 print_json(script_name='olevba', version=__version__,
3268 3318 url='http://decalage.info/python/oletools',
3269   - type='MetaInformation')
  3319 + type='MetaInformation', _json_is_first=True)
3270 3320 else:
3271 3321 print('olevba %s - http://decalage.info/python/oletools' % __version__)
3272 3322  
3273   - logging.basicConfig(level=LOG_LEVELS[options.loglevel], format='%(levelname)-8s %(message)s')
  3323 + logging.basicConfig(level=options.loglevel, format='%(levelname)-8s %(message)s')
3274 3324 # enable logging in the modules:
3275 3325 log.setLevel(logging.NOTSET)
3276 3326  
... ...
oletools/ppt_parser.py
... ... @@ -15,6 +15,8 @@ References:
15 15  
16 16 # === LICENSE =================================================================
17 17 # TODO
  18 +
  19 +
18 20 #------------------------------------------------------------------------------
19 21 # TODO:
20 22 # - make stream optional in PptUnexpectedData
... ... @@ -22,14 +24,16 @@ References:
22 24 # - license
23 25 # - make buffered stream from output of iterative_decompress
24 26 # - maybe can merge the 2 decorators into 1? (with_opened_main_stream)
25   -#
  27 +
  28 +
26 29 # CHANGELOG:
27 30 # 2016-05-04 v0.01 CH: - start parsing "Current User" stream
28 31 # 2016-07-20 v0.50 SL: - added Python 3 support
29 32 # 2016-09-13 PL: - fixed olefile import for Python 2+3
30 33 # - fixed format strings for Python 2.6 (issue #75)
  34 +# 2017-04-23 v0.51 PL: - fixed absolute imports and issue #101
31 35  
32   -__version__ = '0.50'
  36 +__version__ = '0.51'
33 37  
34 38  
35 39 # --- IMPORTS ------------------------------------------------------------------
... ... @@ -39,15 +43,21 @@ import logging
39 43 import struct
40 44 import traceback
41 45 import os
  46 +import zlib
42 47  
43   -try:
44   - # absolute import when oletools is installed
45   - import oletools.thirdparty.olefile as olefile
46   -except:
47   - # relative import otherwise
48   - import thirdparty.olefile as olefile
  48 +# IMPORTANT: it should be possible to run oletools directly as scripts
  49 +# in any directory without installing them with pip or setup.py.
  50 +# In that case, relative imports are NOT usable.
  51 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  52 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  53 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  54 +# print('_thismodule_dir = %r' % _thismodule_dir)
  55 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  56 +# print('_parent_dir = %r' % _thirdparty_dir)
  57 +if not _parent_dir in sys.path:
  58 + sys.path.insert(0, _parent_dir)
49 59  
50   -import zlib
  60 +from oletools.thirdparty.olefile import olefile
51 61  
52 62  
53 63 # a global logger object used for debugging:
... ... @@ -1130,7 +1140,7 @@ class PptParser(object):
1130 1140 log.debug('using open OleFileIO')
1131 1141 self.ole = ole
1132 1142 else:
1133   - log.debug('Opening file ' + ole)
  1143 + log.debug('Opening file {0}'.format(ole))
1134 1144 self.ole = olefile.OleFileIO(ole)
1135 1145  
1136 1146 self.fast_fail = fast_fail
... ...
oletools/pyxswf.py
... ... @@ -74,10 +74,11 @@ __version__ = &#39;0.50&#39;
74 74  
75 75 #=== IMPORTS =================================================================
76 76  
77   -import optparse, sys, os, rtfobj
  77 +import optparse, sys, os
  78 +from . import rtfobj
78 79 from io import BytesIO
79   -from thirdparty.xxxswf import xxxswf
80   -import thirdparty.olefile as olefile
  80 +from .thirdparty.xxxswf import xxxswf
  81 +from .thirdparty import olefile
81 82  
82 83  
83 84 #=== MAIN =================================================================
... ...
oletools/rtfobj.py
... ... @@ -17,7 +17,7 @@ http://www.decalage.info/python/oletools
17 17  
18 18 #=== LICENSE =================================================================
19 19  
20   -# rtfobj is copyright (c) 2012-2016, Philippe Lagadec (http://www.decalage.info)
  20 +# rtfobj is copyright (c) 2012-2017, Philippe Lagadec (http://www.decalage.info)
21 21 # All rights reserved.
22 22 #
23 23 # Redistribution and use in source and binary forms, with or without modification,
... ... @@ -65,8 +65,20 @@ http://www.decalage.info/python/oletools
65 65 # 2016-08-09 PL: - fixed issue #78, improved regex
66 66 # 2016-09-06 PL: - fixed issue #83, backward compatible API
67 67 # 2016-11-17 v0.51 PL: - updated call to oleobj.OleNativeStream
68   -
69   -__version__ = '0.51'
  68 +# 2017-03-12 PL: - fixed imports for Python 2+3
  69 +# - fixed hex decoding bug in RtfObjParser (issue #103)
  70 +# 2017-03-29 PL: - fixed RtfParser to handle issue #152 (control word with
  71 +# long parameter)
  72 +# 2017-04-11 PL: - added detection of the OLE2Link vulnerability CVE-2017-0199
  73 +# 2017-05-04 PL: - fixed issue #164 to handle linked OLE objects
  74 +# 2017-06-08 PL: - fixed issue/PR #143: bin object with negative length
  75 +# 2017-06-29 PL: - temporary fix for issue #178
  76 +# 2017-07-14 v0.51.1 PL: - disabled logging of each control word (issue #184)
  77 +# 2017-07-24 PL: - fixed call to RtfParser._end_of_file (issue #185)
  78 +# - ignore optional space after \bin (issue #185)
  79 +# 2017-09-06 PL: - fixed issue #196: \pxe is not a destination
  80 +
  81 +__version__ = '0.51.1dev4'
70 82  
71 83 # ------------------------------------------------------------------------------
72 84 # TODO:
... ... @@ -83,12 +95,22 @@ __version__ = &#39;0.51&#39;
83 95 import re, os, sys, binascii, logging, optparse
84 96 import os.path
85 97  
86   -from thirdparty.xglob import xglob
87   -from oleobj import OleObject, OleNativeStream
88   -import oleobj
89   -
90   -from thirdparty.tablestream import tablestream
91   -
  98 +# IMPORTANT: it should be possible to run oletools directly as scripts
  99 +# in any directory without installing them with pip or setup.py.
  100 +# In that case, relative imports are NOT usable.
  101 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  102 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  103 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  104 +# print('_thismodule_dir = %r' % _thismodule_dir)
  105 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  106 +# print('_parent_dir = %r' % _thirdparty_dir)
  107 +if not _parent_dir in sys.path:
  108 + sys.path.insert(0, _parent_dir)
  109 +
  110 +from oletools.thirdparty.xglob import xglob
  111 +from oletools.thirdparty.tablestream import tablestream
  112 +from oletools.oleobj import OleObject, OleNativeStream
  113 +from oletools import oleobj
92 114  
93 115 # === LOGGING =================================================================
94 116  
... ... @@ -174,8 +196,8 @@ ASCII_NAME = b&#39;([a-zA-Z]{1,250})&#39;
174 196 # SIGNED_INTEGER = r'(-?\d{1,250})'
175 197 SIGNED_INTEGER = b'(-?\\d+)'
176 198  
177   -# Note for issue #78: need to match "\A-" not followed by digits
178   -CONTROL_WORD = b'(?:\\\\' + ASCII_NAME + b'(?:' + SIGNED_INTEGER + b'(?=[^0-9])|(?=[^a-zA-Z0-9])))'
  199 +# Note for issue #78: need to match "\A-" not followed by digits (or the end of string)
  200 +CONTROL_WORD = b'(?:\\\\' + ASCII_NAME + b'(?:' + SIGNED_INTEGER + b'(?=[^0-9])|(?=[^a-zA-Z0-9])|$))'
179 201  
180 202 re_control_word = re.compile(CONTROL_WORD)
181 203  
... ... @@ -262,7 +284,11 @@ DESTINATION_CONTROL_WORDS = frozenset((
262 284 b"mzeroAsc", b"mzeroDesc", b"mzeroWid", b"nesttableprops", b"nexctfile", b"nonesttables", b"objalias", b"objclass",
263 285 b"objdata", b"object", b"objname", b"objsect", b"objtime", b"oldcprops", b"oldpprops", b"oldsprops", b"oldtprops",
264 286 b"oleclsid", b"operator", b"panose", b"password", b"passwordhash", b"pgp", b"pgptbl", b"picprop", b"pict", b"pn", b"pnseclvl",
265   - b"pntext", b"pntxta", b"pntxtb", b"printim", b"private", b"propname", b"protend", b"protstart", b"protusertbl", b"pxe",
  287 + b"pntext", b"pntxta", b"pntxtb", b"printim",
  288 + # It seems \private should not be treated as a destination (issue #178)
  289 + # Same for \pxe (issue #196)
  290 + # b"private", b"pxe",
  291 + b"propname", b"protend", b"protstart", b"protusertbl",
266 292 b"result", b"revtbl", b"revtim", b"rsidtbl", b"rtf", b"rxe", b"shp", b"shpgrp", b"shpinst", b"shppict", b"shprslt", b"shptxt",
267 293 b"sn", b"sp", b"staticval", b"stylesheet", b"subject", b"sv", b"svb", b"tc", b"template", b"themedata", b"title", b"txe", b"ud",
268 294 b"upr", b"userprops", b"wgrffmtfilter", b"windowcaption", b"writereservation", b"writereservhash", b"xe", b"xform",
... ... @@ -311,10 +337,16 @@ class Destination(object):
311 337  
312 338 class RtfParser(object):
313 339 """
314   - Very simple generic RTF parser
  340 + Very simple but robust generic RTF parser, designed to handle
  341 + malformed malicious RTF as MS Word does
315 342 """
316 343  
317 344 def __init__(self, data):
  345 + """
  346 + RtfParser constructor.
  347 +
  348 + :param data: bytes object containing the RTF data to be parsed
  349 + """
318 350 self.data = data
319 351 self.index = 0
320 352 self.size = len(data)
... ... @@ -325,42 +357,66 @@ class RtfParser(object):
325 357 self.current_destination = document_destination
326 358  
327 359 def parse(self):
  360 + """
  361 + Parse the RTF data
  362 +
  363 + :return: nothing
  364 + """
  365 + # Start at beginning of data
328 366 self.index = 0
  367 + # Loop until the end
329 368 while self.index < self.size:
330 369 if self.data[self.index] == BRACE_OPEN:
  370 + # Found an opening brace "{": Start of a group
331 371 self._open_group()
332 372 self.index += 1
333 373 continue
334 374 if self.data[self.index] == BRACE_CLOSE:
  375 + # Found a closing brace "}": End of a group
335 376 self._close_group()
336 377 self.index += 1
337 378 continue
338 379 if self.data[self.index] == BACKSLASH:
339   - m = re_control_word.match(self.data, self.index)
  380 + # Found a backslash "\": Start of a control word or control symbol
  381 + # Use a regex to extract the control word name if present:
  382 + # NOTE: the full length of the control word + its optional integer parameter
  383 + # is limited by MS Word at 253 characters, so we have to run the regex
  384 + # on a cropped string:
  385 + data_cropped = self.data[self.index:]
  386 + if len(data_cropped)>253:
  387 + data_cropped = data_cropped[:254]
  388 + # append a space so that the regex can check the following character:
  389 + data_cropped += b' '
  390 + # m = re_control_word.match(self.data, self.index, self.index+253)
  391 + m = re_control_word.match(data_cropped)
340 392 if m:
341 393 cword = m.group(1)
342 394 param = None
343 395 if len(m.groups()) > 1:
344 396 param = m.group(2)
345   - # log.debug('control word %r at index %Xh - cword=%r param=%r' % (m.group(), self.index, cword, param))
  397 + # log.debug('control word at index %Xh - cword=%r param=%r %r' % (self.index, cword, param, m.group()))
346 398 self._control_word(m, cword, param)
347 399 self.index += len(m.group())
348 400 # if it's \bin, call _bin after updating index
349 401 if cword == b'bin':
350 402 self._bin(m, param)
351 403 continue
  404 + # Otherwise, it may be a control symbol:
352 405 m = re_control_symbol.match(self.data, self.index)
353 406 if m:
354 407 self.control_symbol(m)
355 408 self.index += len(m.group())
356 409 continue
  410 + # Otherwise, this is plain text:
  411 + # Use a regex to match all characters until the next brace or backslash:
357 412 m = re_text.match(self.data, self.index)
358 413 if m:
359 414 self._text(m)
360 415 self.index += len(m.group())
361 416 continue
362 417 raise RuntimeError('Should not have reached this point - index=%Xh' % self.index)
363   - self.end_of_file()
  418 + # call _end_of_file to make sure all groups are closed properly
  419 + self._end_of_file()
364 420  
365 421  
366 422 def _open_group(self):
... ... @@ -430,6 +486,8 @@ class RtfParser(object):
430 486  
431 487 def _control_word(self, matchobject, cword, param):
432 488 #log.debug('control word %r at index %Xh' % (matchobject.group(), self.index))
  489 + # TODO: according to RTF specs v1.9.1, "Destination changes are legal only immediately after an opening brace ({)"
  490 + # (not counting the special control symbol \*, of course)
433 491 if cword in DESTINATION_CONTROL_WORDS:
434 492 # log.debug('%r is a destination control word: starting a new destination' % cword)
435 493 self._open_destination(matchobject, cword)
... ... @@ -454,9 +512,19 @@ class RtfParser(object):
454 512  
455 513 def _bin(self, matchobject, param):
456 514 binlen = int(param)
  515 + # handle negative length
  516 + if binlen < 0:
  517 + log.warn('Detected anti-analysis trick: \\bin object with negative length at index %X' % self.index)
  518 + # binlen = int(param.strip('-'))
  519 + # According to my tests, if the bin length is negative,
  520 + # it should be treated as a null length:
  521 + binlen=0
  522 + # ignore optional space after \bin
  523 + if self.data[self.index] == ' ':
  524 + log.debug('\\bin: ignoring whitespace before data')
  525 + self.index += 1
457 526 log.debug('\\bin: reading %d bytes of binary data' % binlen)
458   - # TODO: handle optional space?
459   - # TODO: handle negative length, and length greater than data
  527 + # TODO: handle length greater than data
460 528 bindata = self.data[self.index:self.index + binlen]
461 529 self.index += binlen
462 530 self.bin(bindata)
... ... @@ -468,7 +536,7 @@ class RtfParser(object):
468 536 # log.debug('%Xh Reached End of File')
469 537 # close any group/destination that is still open:
470 538 while self.group_level > 0:
471   - # log.debug('Group Level = %d, closing group' % self.group_level)
  539 + log.debug('Group Level = %d, closing group' % self.group_level)
472 540 self._close_group()
473 541 self.end_of_file()
474 542  
... ... @@ -517,6 +585,7 @@ class RtfObjParser(RtfParser):
517 585 self.objects = []
518 586  
519 587 def open_destination(self, destination):
  588 + # TODO: detect when the destination is within an objdata, report as obfuscation
520 589 if destination.cword == b'objdata':
521 590 log.debug('*** Start object data at index %Xh' % destination.start)
522 591  
... ... @@ -530,10 +599,10 @@ class RtfObjParser(RtfParser):
530 599 # Filter out all whitespaces first (just ignored):
531 600 hexdata1 = destination.data.translate(None, b' \t\r\n\f\v')
532 601 # Then filter out any other non-hex character:
533   - hexdata = re.sub(b'[^a-hA-H0-9]', b'', hexdata1)
  602 + hexdata = re.sub(b'[^a-fA-F0-9]', b'', hexdata1)
534 603 if len(hexdata) < len(hexdata1):
535 604 # this is only for debugging:
536   - nonhex = re.sub(b'[a-hA-H0-9]', b'', hexdata1)
  605 + nonhex = re.sub(b'[a-fA-F0-9]', b'', hexdata1)
537 606 log.debug('Found non-hex chars in hexdata: %r' % nonhex)
538 607 # MS Word accepts an extra hex digit, so we need to trim it if present:
539 608 if len(hexdata) & 1:
... ... @@ -573,6 +642,7 @@ class RtfObjParser(RtfParser):
573 642 # TODO: extract useful cwords such as objclass
574 643 # TODO: keep track of cwords inside objdata, because it is unusual and indicates potential obfuscation
575 644 # TODO: same with control symbols, and opening bracket
  645 + # log.debug('- Control word "%s", param=%s, level=%d' % (cword, param, self.group_level))
576 646 pass
577 647  
578 648  
... ... @@ -649,11 +719,22 @@ def process_file(container, filename, data, output_dir=None, save_object=False):
649 719 rtfp = RtfObjParser(data)
650 720 rtfp.parse()
651 721 for rtfobj in rtfp.objects:
  722 + ole_color = None
652 723 pkg_color = None
653 724 if rtfobj.is_ole:
654   - ole_column = 'format_id: %d\n' % rtfobj.format_id
  725 + ole_column = 'format_id: %d ' % rtfobj.format_id
  726 + if rtfobj.format_id == oleobj.OleObject.TYPE_EMBEDDED:
  727 + ole_column += '(Embedded)\n'
  728 + elif rtfobj.format_id == oleobj.OleObject.TYPE_LINKED:
  729 + ole_column += '(Linked)\n'
  730 + else:
  731 + ole_column += '(Unknown)\n'
655 732 ole_column += 'class name: %r\n' % rtfobj.class_name
656   - ole_column += 'data size: %d' % rtfobj.oledata_size
  733 + # if the object is linked and not embedded, data_size=None:
  734 + if rtfobj.oledata_size is None:
  735 + ole_column += 'data size: N/A'
  736 + else:
  737 + ole_column += 'data size: %d' % rtfobj.oledata_size
657 738 if rtfobj.is_package:
658 739 pkg_column = 'Filename: %r\n' % rtfobj.filename
659 740 pkg_column += 'Source path: %r\n' % rtfobj.src_path
... ... @@ -667,6 +748,11 @@ def process_file(container, filename, data, output_dir=None, save_object=False):
667 748 pkg_column += '\nEXECUTABLE FILE'
668 749 else:
669 750 pkg_column = 'Not an OLE Package'
  751 + # Detect OLE2Link exploit
  752 + # http://www.kb.cert.org/vuls/id/921560
  753 + if rtfobj.class_name == 'OLE2Link':
  754 + ole_color = 'red'
  755 + ole_column += '\nPossibly an exploit for the OLE2Link vulnerability (VU#921560, CVE-2017-0199)'
670 756 else:
671 757 pkg_column = ''
672 758 ole_column = 'Not a well-formed OLE object'
... ... @@ -676,7 +762,7 @@ def process_file(container, filename, data, output_dir=None, save_object=False):
676 762 '%08Xh' % rtfobj.start,
677 763 ole_column,
678 764 pkg_column
679   - ), colors=(None, None, None, pkg_color)
  765 + ), colors=(None, None, ole_color, pkg_color)
680 766 )
681 767 tstream.write_sep()
682 768 if save_object:
... ... @@ -703,7 +789,8 @@ def process_file(container, filename, data, output_dir=None, save_object=False):
703 789 fname = '%s_object_%08X.noname' % (fname_prefix, rtfobj.start)
704 790 print(' saving to file %s' % fname)
705 791 open(fname, 'wb').write(rtfobj.olepkgdata)
706   - elif rtfobj.is_ole:
  792 + # When format_id=TYPE_LINKED, oledata_size=None
  793 + elif rtfobj.is_ole and rtfobj.oledata_size is not None:
707 794 print('Saving file embedded in OLE object #%d:' % i)
708 795 print(' format_id = %d' % rtfobj.format_id)
709 796 print(' class name = %r' % rtfobj.class_name)
... ... @@ -789,7 +876,7 @@ def main():
789 876 format='%(levelname)-8s %(message)s')
790 877 # enable logging in the modules:
791 878 log.setLevel(logging.NOTSET)
792   - oleobj.log.setLevel(logging.NOTSET)
  879 + oleobj.enable_logging()
793 880  
794 881 for container, filename, data in xglob.iter_files(args, recursive=options.recursive,
795 882 zip_password=options.zip_password, zip_fname=options.zip_fname):
... ...
oletools/thirdparty/olefile/olefile.py
1   -#!/usr/bin/env python
  1 +"""
  2 +olefile (formerly OleFileIO_PL)
2 3  
3   -# olefile (formerly OleFileIO_PL)
4   -#
5   -# Module to read/write Microsoft OLE2 files (also called Structured Storage or
6   -# Microsoft Compound Document File Format), such as Microsoft Office 97-2003
7   -# documents, Image Composer and FlashPix files, Outlook messages, ...
8   -# This version is compatible with Python 2.6+ and 3.x
9   -#
10   -# Project website: http://www.decalage.info/olefile
11   -#
12   -# olefile is copyright (c) 2005-2016 Philippe Lagadec (http://www.decalage.info)
13   -#
14   -# olefile is based on the OleFileIO module from the PIL library v1.1.6
15   -# See: http://www.pythonware.com/products/pil/index.htm
16   -#
17   -# The Python Imaging Library (PIL) is
18   -# Copyright (c) 1997-2005 by Secret Labs AB
19   -# Copyright (c) 1995-2005 by Fredrik Lundh
20   -#
21   -# See source code and LICENSE.txt for information on usage and redistribution.
  4 +Module to read/write Microsoft OLE2 files (also called Structured Storage or
  5 +Microsoft Compound Document File Format), such as Microsoft Office 97-2003
  6 +documents, Image Composer and FlashPix files, Outlook messages, ...
  7 +This version is compatible with Python 2.6+ and 3.x
  8 +
  9 +Project website: https://www.decalage.info/olefile
  10 +
  11 +olefile is copyright (c) 2005-2017 Philippe Lagadec
  12 +(https://www.decalage.info)
22 13  
  14 +olefile is based on the OleFileIO module from the PIL library v1.1.7
  15 +See: http://www.pythonware.com/products/pil/index.htm
  16 +and http://svn.effbot.org/public/tags/pil-1.1.7/PIL/OleFileIO.py
  17 +
  18 +The Python Imaging Library (PIL) is
  19 +Copyright (c) 1997-2009 by Secret Labs AB
  20 +Copyright (c) 1995-2009 by Fredrik Lundh
  21 +
  22 +See source code and LICENSE.txt for information on usage and redistribution.
  23 +"""
23 24  
24 25 # Since OleFileIO_PL v0.30, only Python 2.6+ and 3.x is supported
25 26 # This import enables print() as a function rather than a keyword
... ... @@ -28,14 +29,10 @@
28 29 from __future__ import print_function # This version of olefile requires Python 2.6+ or 3.x.
29 30  
30 31  
31   -__author__ = "Philippe Lagadec"
32   -__date__ = "2016-04-26"
33   -__version__ = '0.44'
34   -
35 32 #--- LICENSE ------------------------------------------------------------------
36 33  
37   -# olefile (formerly OleFileIO_PL) is copyright (c) 2005-2016 Philippe Lagadec
38   -# (http://www.decalage.info)
  34 +# olefile (formerly OleFileIO_PL) is copyright (c) 2005-2017 Philippe Lagadec
  35 +# (https://www.decalage.info)
39 36 #
40 37 # All rights reserved.
41 38 #
... ... @@ -66,8 +63,8 @@ __version__ = &#39;0.44&#39;
66 63 # Imaging Library (PIL) published by Fredrik Lundh under the following license:
67 64  
68 65 # The Python Imaging Library (PIL) is
69   -# Copyright (c) 1997-2005 by Secret Labs AB
70   -# Copyright (c) 1995-2005 by Fredrik Lundh
  66 +# Copyright (c) 1997-2009 by Secret Labs AB
  67 +# Copyright (c) 1995-2009 by Fredrik Lundh
71 68 #
72 69 # By obtaining, using, and/or copying this software and/or its associated
73 70 # documentation, you agree that you have read, understood, and will comply with
... ... @@ -138,7 +135,7 @@ __version__ = &#39;0.44&#39;
138 135 # 2009-12-11 v0.20 PL: - bugfix in OleFileIO.open when filename is not plain str
139 136 # 2010-01-22 v0.21 PL: - added support for big-endian CPUs such as PowerPC Macs
140 137 # 2012-02-16 v0.22 PL: - fixed bug in getproperties, patch by chuckleberryfinn
141   -# (https://bitbucket.org/decalage/olefileio_pl/issue/7)
  138 +# (https://github.com/decalage2/olefile/issues/7)
142 139 # - added close method to OleFileIO (fixed issue #2)
143 140 # 2012-07-25 v0.23 PL: - added support for file-like objects (patch by mete0r_kr)
144 141 # 2013-05-05 v0.24 PL: - getproperties: added conversion from filetime to python
... ... @@ -196,6 +193,16 @@ __version__ = &#39;0.44&#39;
196 193 # 2016-04-27 - added support for incomplete streams and incorrect
197 194 # directory entries (to read malformed documents)
198 195 # 2016-05-04 - fixed slight bug in OleStream
  196 +# 2016-11-27 DR: - added method to get the clsid of a storage/stream
  197 +# (Daniel Roethlisberger)
  198 +# 2017-05-31 v0.45 BS: - PR #114 from oletools to handle excessive number of
  199 +# properties:
  200 +# https://github.com/decalage2/oletools/pull/114
  201 +# 2017-07-11 PL: - ignore incorrect ByteOrder (issue #70)
  202 +
  203 +__date__ = "2017-07-11"
  204 +__version__ = '0.45dev2'
  205 +__author__ = "Philippe Lagadec"
199 206  
200 207 #-----------------------------------------------------------------------------
201 208 # TODO (for version 1.0):
... ... @@ -223,7 +230,7 @@ __version__ = &#39;0.44&#39;
223 230 # - see also original notes and FIXME below
224 231 # - remove all obsolete FIXMEs
225 232 # - OleMetadata: fix version attrib according to
226   -# http://msdn.microsoft.com/en-us/library/dd945671%28v=office.12%29.aspx
  233 +# https://msdn.microsoft.com/en-us/library/dd945671%28v=office.12%29.aspx
227 234  
228 235 # IDEAS:
229 236 # - in OleFileIO._open and OleStream, use size=None instead of 0x7FFFFFFF for
... ... @@ -238,8 +245,8 @@ __version__ = &#39;0.44&#39;
238 245 # - create a simple OLE explorer with wxPython
239 246  
240 247 # FUTURE EVOLUTIONS to add write support:
241   -# see issue #6 on Bitbucket:
242   -# https://bitbucket.org/decalage/olefileio_pl/issue/6/improve-olefileio_pl-to-write-ole-files
  248 +# see issue #6 on GitHub:
  249 +# https://github.com/decalage2/olefile/issues/6
243 250  
244 251 #-----------------------------------------------------------------------------
245 252 # NOTES from PIL 1.1.6:
... ... @@ -268,6 +275,10 @@ __version__ = &#39;0.44&#39;
268 275  
269 276 #------------------------------------------------------------------------------
270 277  
  278 +__all__ = ['isOleFile', 'OleFileIO', 'OleMetadata', 'enable_logging',
  279 + 'MAGIC', 'STGTY_EMPTY',
  280 + 'STGTY_STREAM', 'STGTY_STORAGE', 'STGTY_ROOT', 'STGTY_PROPERTY',
  281 + 'STGTY_LOCKBYTES', 'MINIMAL_OLEFILE_SIZE', 'NOSTREAM']
271 282  
272 283 import io
273 284 import sys
... ... @@ -317,17 +328,10 @@ else:
317 328  
318 329 #[PL] These workarounds were inspired from the Path module
319 330 # (see http://www.jorendorff.com/articles/python/path/)
320   -#TODO: test with old Python versions
321   -
322   -# Pre-2.3 workaround for basestring.
323 331 try:
324 332 basestring
325 333 except NameError:
326   - try:
327   - # is Unicode supported (Python >2.0 or >1.6 ?)
328   - basestring = (str, unicode)
329   - except NameError:
330   - basestring = str
  334 + basestring = str
331 335  
332 336 #[PL] Experimental setting: if True, OLE filenames will be kept in Unicode
333 337 # if False (default PIL behaviour), all filenames are converted to Latin-1.
... ... @@ -395,27 +399,27 @@ def enable_logging():
395 399  
396 400 #=== CONSTANTS ===============================================================
397 401  
398   -# magic bytes that should be at the beginning of every OLE file:
  402 +#: magic bytes that should be at the beginning of every OLE file:
399 403 MAGIC = b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1'
400 404  
401 405 #[PL]: added constants for Sector IDs (from AAF specifications)
402   -MAXREGSECT = 0xFFFFFFFA # (-6) maximum SECT
403   -DIFSECT = 0xFFFFFFFC # (-4) denotes a DIFAT sector in a FAT
404   -FATSECT = 0xFFFFFFFD # (-3) denotes a FAT sector in a FAT
405   -ENDOFCHAIN = 0xFFFFFFFE # (-2) end of a virtual stream chain
406   -FREESECT = 0xFFFFFFFF # (-1) unallocated sector
  406 +MAXREGSECT = 0xFFFFFFFA #: (-6) maximum SECT
  407 +DIFSECT = 0xFFFFFFFC #: (-4) denotes a DIFAT sector in a FAT
  408 +FATSECT = 0xFFFFFFFD #: (-3) denotes a FAT sector in a FAT
  409 +ENDOFCHAIN = 0xFFFFFFFE #: (-2) end of a virtual stream chain
  410 +FREESECT = 0xFFFFFFFF #: (-1) unallocated sector
407 411  
408 412 #[PL]: added constants for Directory Entry IDs (from AAF specifications)
409   -MAXREGSID = 0xFFFFFFFA # (-6) maximum directory entry ID
410   -NOSTREAM = 0xFFFFFFFF # (-1) unallocated directory entry
  413 +MAXREGSID = 0xFFFFFFFA #: (-6) maximum directory entry ID
  414 +NOSTREAM = 0xFFFFFFFF #: (-1) unallocated directory entry
411 415  
412 416 #[PL] object types in storage (from AAF specifications)
413   -STGTY_EMPTY = 0 # empty directory entry (according to OpenOffice.org doc)
414   -STGTY_STORAGE = 1 # element is a storage object
415   -STGTY_STREAM = 2 # element is a stream object
416   -STGTY_LOCKBYTES = 3 # element is an ILockBytes object
417   -STGTY_PROPERTY = 4 # element is an IPropertyStorage object
418   -STGTY_ROOT = 5 # element is a root storage
  417 +STGTY_EMPTY = 0 #: empty directory entry
  418 +STGTY_STORAGE = 1 #: element is a storage object
  419 +STGTY_STREAM = 2 #: element is a stream object
  420 +STGTY_LOCKBYTES = 3 #: element is an ILockBytes object
  421 +STGTY_PROPERTY = 4 #: element is an IPropertyStorage object
  422 +STGTY_ROOT = 5 #: element is a root storage
419 423  
420 424 # Unknown size for a stream (used by OleStream):
421 425 UNKNOWN_SIZE = 0x7FFFFFFF
... ... @@ -472,7 +476,13 @@ def isOleFile (filename):
472 476 """
473 477 Test if a file is an OLE container (according to the magic bytes in its header).
474 478  
475   - :param filename: string-like or file-like object, OLE file to parse
  479 + .. note::
  480 + This function only checks the first 8 bytes of the file, not the
  481 + rest of the OLE structure.
  482 +
  483 + .. versionadded:: 0.16
  484 +
  485 + :param filename: filename, contents or file-like object of the OLE file (string-like or file-like object)
476 486  
477 487 - if filename is a string smaller than 1536 bytes, it is the path
478 488 of the file to open. (bytes or unicode string)
... ... @@ -481,7 +491,9 @@ def isOleFile (filename):
481 491 - if filename is a file-like object (with read and seek methods),
482 492 it is parsed as-is.
483 493  
  494 + :type filename: bytes or str or unicode or file
484 495 :returns: True if OLE, False otherwise.
  496 + :rtype: bool
485 497 """
486 498 # check if filename is a string-like or file-like object:
487 499 if hasattr(filename, 'read'):
... ... @@ -494,7 +506,8 @@ def isOleFile (filename):
494 506 header = filename[:len(MAGIC)]
495 507 else:
496 508 # string-like object: filename of file on disk
497   - header = open(filename, 'rb').read(len(MAGIC))
  509 + with open(filename, 'rb') as fp:
  510 + header = fp.read(len(MAGIC))
498 511 if header == MAGIC:
499 512 return True
500 513 else:
... ... @@ -511,8 +524,6 @@ else:
511 524 return c if c.__class__ is int else c[0]
512 525  
513 526  
514   -#TODO: replace i16 and i32 with more readable struct.unpack equivalent?
515   -
516 527 def i16(c, o = 0):
517 528 """
518 529 Converts a 2-bytes (16 bits) string to an integer.
... ... @@ -520,7 +531,7 @@ def i16(c, o = 0):
520 531 :param c: string containing bytes to convert
521 532 :param o: offset of bytes to convert in string
522 533 """
523   - return i8(c[o]) | (i8(c[o+1])<<8)
  534 + return struct.unpack("<H", c[o:o+2])[0]
524 535  
525 536  
526 537 def i32(c, o = 0):
... ... @@ -530,10 +541,7 @@ def i32(c, o = 0):
530 541 :param c: string containing bytes to convert
531 542 :param o: offset of bytes to convert in string
532 543 """
533   -## return int(ord(c[o])+(ord(c[o+1])<<8)+(ord(c[o+2])<<16)+(ord(c[o+3])<<24))
534   -## # [PL]: added int() because "<<" gives long int since Python 2.4
535   - # copied from Pillow's _binary:
536   - return i8(c[o]) | (i8(c[o+1])<<8) | (i8(c[o+2])<<16) | (i8(c[o+3])<<24)
  544 + return struct.unpack("<I", c[o:o+4])[0]
537 545  
538 546  
539 547 def _clsid(clsid):
... ... @@ -558,7 +566,7 @@ def filetime2datetime(filetime):
558 566 convert FILETIME (64 bits int) to Python datetime.datetime
559 567 """
560 568 # TODO: manage exception when microseconds is too large
561   - # inspired from http://code.activestate.com/recipes/511425-filetime-to-datetime/
  569 + # inspired from https://code.activestate.com/recipes/511425-filetime-to-datetime/
562 570 _FILETIME_null_date = datetime.datetime(1601, 1, 1, 0, 0, 0)
563 571 #log.debug('timedelta days=%d' % (filetime//(10*1000000*3600*24)))
564 572 return _FILETIME_null_date + datetime.timedelta(microseconds=filetime//10)
... ... @@ -585,17 +593,19 @@ class OleMetadata:
585 593 OLE file.
586 594  
587 595 References for SummaryInformation stream:
588   - - http://msdn.microsoft.com/en-us/library/dd942545.aspx
589   - - http://msdn.microsoft.com/en-us/library/dd925819%28v=office.12%29.aspx
590   - - http://msdn.microsoft.com/en-us/library/windows/desktop/aa380376%28v=vs.85%29.aspx
591   - - http://msdn.microsoft.com/en-us/library/aa372045.aspx
592   - - http://sedna-soft.de/summary-information-stream/
593   - - http://poi.apache.org/apidocs/org/apache/poi/hpsf/SummaryInformation.html
  596 +
  597 + - https://msdn.microsoft.com/en-us/library/dd942545.aspx
  598 + - https://msdn.microsoft.com/en-us/library/dd925819%28v=office.12%29.aspx
  599 + - https://msdn.microsoft.com/en-us/library/windows/desktop/aa380376%28v=vs.85%29.aspx
  600 + - https://msdn.microsoft.com/en-us/library/aa372045.aspx
  601 + - http://sedna-soft.de/articles/summary-information-stream/
  602 + - https://poi.apache.org/apidocs/org/apache/poi/hpsf/SummaryInformation.html
594 603  
595 604 References for DocumentSummaryInformation stream:
596   - - http://msdn.microsoft.com/en-us/library/dd945671%28v=office.12%29.aspx
597   - - http://msdn.microsoft.com/en-us/library/windows/desktop/aa380374%28v=vs.85%29.aspx
598   - - http://poi.apache.org/apidocs/org/apache/poi/hpsf/DocumentSummaryInformation.html
  605 +
  606 + - https://msdn.microsoft.com/en-us/library/dd945671%28v=office.12%29.aspx
  607 + - https://msdn.microsoft.com/en-us/library/windows/desktop/aa380374%28v=vs.85%29.aspx
  608 + - https://poi.apache.org/apidocs/org/apache/poi/hpsf/DocumentSummaryInformation.html
599 609  
600 610 new in version 0.25
601 611 """
... ... @@ -676,7 +686,7 @@ class OleMetadata:
676 686 def parse_properties(self, olefile):
677 687 """
678 688 Parse standard properties of an OLE file, from the streams
679   - "\x05SummaryInformation" and "\x05DocumentSummaryInformation",
  689 + ``\\x05SummaryInformation`` and ``\\x05DocumentSummaryInformation``,
680 690 if present.
681 691 Properties are converted to strings, integers or python datetime objects.
682 692 If a property is not present, its value is set to None.
... ... @@ -851,14 +861,14 @@ class OleStream(io.BytesIO):
851 861 elif unknown_size:
852 862 # actual stream size was not known, now we know the size of read
853 863 # data:
854   - log.debug('Read data of length %d, the stream size was unkown' % len(data))
  864 + log.debug('Read data of length %d, the stream size was unknown' % len(data))
855 865 self.size = len(data)
856 866 else:
857 867 # read data is less than expected:
858 868 log.debug('Read data of length %d, less than expected stream size %d' % (len(data), size))
859 869 # TODO: provide details in exception message
860   - self.ole._raise_defect(DEFECT_INCORRECT, 'OLE stream size is less than declared')
861 870 self.size = len(data)
  871 + self.ole._raise_defect(DEFECT_INCORRECT, 'OLE stream size is less than declared')
862 872 # when all data is read in memory, BytesIO constructor is called
863 873 io.BytesIO.__init__(self, data)
864 874 # Then the OleStream object can be used as a read-only file object.
... ... @@ -1023,7 +1033,7 @@ class OleDirectoryEntry:
1023 1033 Walk through red-black tree of children of this directory entry to add
1024 1034 all of them to the kids list. (recursive method)
1025 1035  
1026   - :param child_sid : index of child directory entry to use, or None when called
  1036 + :param child_sid: index of child directory entry to use, or None when called
1027 1037 first time for the root. (only used during recursion)
1028 1038 """
1029 1039 log.debug('append_kids: child_sid=%d' % child_sid)
... ... @@ -1188,8 +1198,8 @@ class OleFileIO:
1188 1198 """
1189 1199 # minimal level for defects to be raised as exceptions:
1190 1200 self._raise_defects_level = raise_defects
1191   - # list of defects/issues not raised as exceptions:
1192   - # tuples of (exception type, message)
  1201 + #: list of defects/issues not raised as exceptions:
  1202 + #: tuples of (exception type, message)
1193 1203 self.parsing_issues = []
1194 1204 self.write_mode = write_mode
1195 1205 self.path_encoding = path_encoding
... ... @@ -1386,7 +1396,7 @@ class OleFileIO:
1386 1396 log.debug( "Byte Order = %X (expected: FFFE)" % self.byte_order )
1387 1397 if self.byte_order != 0xFFFE:
1388 1398 # For now only common little-endian documents are handled correctly
1389   - self._raise_defect(DEFECT_FATAL, "incorrect ByteOrder in OLE header")
  1399 + self._raise_defect(DEFECT_INCORRECT, "incorrect ByteOrder in OLE header")
1390 1400 # TODO: add big-endian support for documents created on Mac ?
1391 1401 # But according to [MS-CFB] ? v20140502, ByteOrder MUST be 0xFFFE.
1392 1402 self.sector_size = 2**self.sector_shift
... ... @@ -2081,6 +2091,21 @@ class OleFileIO:
2081 2091 return False
2082 2092  
2083 2093  
  2094 + def getclsid(self, filename):
  2095 + """
  2096 + Return clsid of a stream/storage.
  2097 +
  2098 + :param filename: path of stream/storage in storage tree. (see openstream for
  2099 + syntax)
  2100 + :returns: Empty string if clsid is null, a printable representation of the clsid otherwise
  2101 +
  2102 + new in version 0.44
  2103 + """
  2104 + sid = self._find(filename)
  2105 + entry = self.direntries[sid]
  2106 + return entry.clsid
  2107 +
  2108 +
2084 2109 def getmtime(self, filename):
2085 2110 """
2086 2111 Return modification time of a stream/storage.
... ... @@ -2201,7 +2226,10 @@ class OleFileIO:
2201 2226 self._raise_defect(DEFECT_INCORRECT, msg, type(exc))
2202 2227 return data
2203 2228  
2204   - for i in range(num_props):
  2229 + # clamp num_props based on the data length
  2230 + num_props = min(num_props, len(s) / 8)
  2231 +
  2232 + for i in iterrange(num_props):
2205 2233 property_id = 0 # just in case of an exception
2206 2234 try:
2207 2235 property_id = i32(s, 8+i*8)
... ... @@ -2222,12 +2250,12 @@ class OleFileIO:
2222 2250 elif property_type in (VT_I4, VT_INT, VT_ERROR):
2223 2251 # VT_I4: 32-bit signed integer
2224 2252 # VT_ERROR: HRESULT, similar to 32-bit signed integer,
2225   - # see http://msdn.microsoft.com/en-us/library/cc230330.aspx
  2253 + # see https://msdn.microsoft.com/en-us/library/cc230330.aspx
2226 2254 value = i32(s, offset+4)
2227 2255 elif property_type in (VT_UI4, VT_UINT): # 4-byte unsigned integer
2228 2256 value = i32(s, offset+4) # FIXME
2229 2257 elif property_type in (VT_BSTR, VT_LPSTR):
2230   - # CodePageString, see http://msdn.microsoft.com/en-us/library/dd942354.aspx
  2258 + # CodePageString, see https://msdn.microsoft.com/en-us/library/dd942354.aspx
2231 2259 # size is a 32 bits integer, including the null terminator, and
2232 2260 # possibly trailing or embedded null chars
2233 2261 #TODO: if codepage is unicode, the string should be converted as such
... ... @@ -2237,12 +2265,12 @@ class OleFileIO:
2237 2265 value = value.replace(b'\x00', b'')
2238 2266 elif property_type == VT_BLOB:
2239 2267 # binary large object (BLOB)
2240   - # see http://msdn.microsoft.com/en-us/library/dd942282.aspx
  2268 + # see https://msdn.microsoft.com/en-us/library/dd942282.aspx
2241 2269 count = i32(s, offset+4)
2242 2270 value = s[offset+8:offset+8+count]
2243 2271 elif property_type == VT_LPWSTR:
2244 2272 # UnicodeString
2245   - # see http://msdn.microsoft.com/en-us/library/dd942313.aspx
  2273 + # see https://msdn.microsoft.com/en-us/library/dd942313.aspx
2246 2274 # "the string should NOT contain embedded or additional trailing
2247 2275 # null characters."
2248 2276 count = i32(s, offset+4)
... ... @@ -2255,7 +2283,7 @@ class OleFileIO:
2255 2283 log.debug('Converting property #%d to python datetime, value=%d=%fs'
2256 2284 %(property_id, value, float(value)/10000000))
2257 2285 # convert FILETIME to Python datetime.datetime
2258   - # inspired from http://code.activestate.com/recipes/511425-filetime-to-datetime/
  2286 + # inspired from https://code.activestate.com/recipes/511425-filetime-to-datetime/
2259 2287 _FILETIME_null_date = datetime.datetime(1601, 1, 1, 0, 0, 0)
2260 2288 log.debug('timedelta days=%d' % (value//(10*1000000*3600*24)))
2261 2289 value = _FILETIME_null_date + datetime.timedelta(microseconds=value//10)
... ... @@ -2269,12 +2297,12 @@ class OleFileIO:
2269 2297 value = _clsid(s[offset+4:offset+20])
2270 2298 elif property_type == VT_CF:
2271 2299 # PropertyIdentifier or ClipboardData??
2272   - # see http://msdn.microsoft.com/en-us/library/dd941945.aspx
  2300 + # see https://msdn.microsoft.com/en-us/library/dd941945.aspx
2273 2301 count = i32(s, offset+4)
2274 2302 value = s[offset+8:offset+8+count]
2275 2303 elif property_type == VT_BOOL:
2276 2304 # VARIANT_BOOL, 16 bits bool, 0x0000=Fals, 0xFFFF=True
2277   - # see http://msdn.microsoft.com/en-us/library/cc237864.aspx
  2305 + # see https://msdn.microsoft.com/en-us/library/cc237864.aspx
2278 2306 value = bool(i16(s, offset+4))
2279 2307 else:
2280 2308 value = None # everything else yields "None"
... ... @@ -2282,13 +2310,13 @@ class OleFileIO:
2282 2310  
2283 2311 # missing: VT_EMPTY, VT_NULL, VT_R4, VT_R8, VT_CY, VT_DATE,
2284 2312 # VT_DECIMAL, VT_I1, VT_I8, VT_UI8,
2285   - # see http://msdn.microsoft.com/en-us/library/dd942033.aspx
  2313 + # see https://msdn.microsoft.com/en-us/library/dd942033.aspx
2286 2314  
2287 2315 # FIXME: add support for VT_VECTOR
2288 2316 # VT_VECTOR is a 32 uint giving the number of items, followed by
2289 2317 # the items in sequence. The VT_VECTOR value is combined with the
2290 2318 # type of items, e.g. VT_VECTOR|VT_BSTR
2291   - # see http://msdn.microsoft.com/en-us/library/dd942011.aspx
  2319 + # see https://msdn.microsoft.com/en-us/library/dd942011.aspx
2292 2320  
2293 2321 #print("%08x" % property_id, repr(value), end=" ")
2294 2322 #print("(%s)" % VT[i32(s, offset) & 0xFFF])
... ... @@ -2344,7 +2372,7 @@ if __name__ == &quot;__main__&quot;:
2344 2372  
2345 2373 (options, args) = parser.parse_args()
2346 2374  
2347   - print('olefile version %s %s - http://www.decalage.info/en/olefile\n' % (__version__, __date__))
  2375 + print('olefile version %s %s - https://www.decalage.info/en/olefile\n' % (__version__, __date__))
2348 2376  
2349 2377 # Print help if no arguments are passed
2350 2378 if len(args) == 0:
... ...
oletools/thirdparty/tablestream/tablestream.py
... ... @@ -281,6 +281,9 @@ class TableStream(object):
281 281 """
282 282 shortcut for self.outfile.write()
283 283 """
  284 + # TODO temporary bugfix for Unicode replacement character FFFD, that triggers an
  285 + # exception when printing to the console on Windows 10 (CP850)
  286 + s = s.replace(u"\uFFFD", '')
284 287 self.outfile.write(s)
285 288  
286 289 def write_row(self, row, last=False, colors=None):
... ...
setup.py
... ... @@ -22,6 +22,7 @@ to install this package.
22 22 # 2016-07-29 PL: - use setuptools if available
23 23 # 2016-09-05 PL: - added more entry points
24 24 # 2017-01-18 v0.51 PL: - added package zipfile27 (issue #121)
  25 +# 2017-10-18 v0.52 PL: - added msodde
25 26  
26 27 #--- TODO ---------------------------------------------------------------------
27 28  
... ... @@ -41,11 +42,11 @@ import os, fnmatch
41 42 #--- METADATA -----------------------------------------------------------------
42 43  
43 44 name = "oletools"
44   -version = '0.51a1'
  45 +version = '0.52dev3'
45 46 desc = "Python tools to analyze security characteristics of MS Office and OLE files (also called Structured Storage, Compound File Binary Format or Compound Document File Format), for Malware Analysis and Incident Response #DFIR"
46 47 long_desc = open('oletools/README.rst').read()
47 48 author = "Philippe Lagadec"
48   -author_email = "decalage at laposte dot net"
  49 +author_email = "nospam@decalage.info"
49 50 url = "http://www.decalage.info/python/oletools"
50 51 license = "BSD"
51 52 download_url = "https://github.com/decalage2/oletools/releases"
... ... @@ -281,6 +282,7 @@ entry_points = {
281 282 'pyxswf=oletools.pyxswf:main',
282 283 'rtfobj=oletools.rtfobj:main',
283 284 'oleobj=oletools.oleobj:main',
  285 + 'msodde=oletools.msodde:main',
284 286 ],
285 287 }
286 288  
... ... @@ -312,6 +314,7 @@ def main():
312 314 download_url=download_url,
313 315 # data_files=data_files,
314 316 entry_points=entry_points,
  317 + test_suite="tests",
315 318 # scripts=scripts,
316 319 )
317 320  
... ...
tests/__init__.py 0 → 100644
tests/howto_add_unittests.txt 0 → 100644
  1 +Howto: Add unittests
  2 +--------------------
  3 +
  4 +Note: The following are just guidelines to help inexperienced users create unit
  5 +tests. The python unittest library (see
  6 +https://docs.python.org/2/library/unittest.html) offers much more flexibility
  7 +than described here.
  8 +
  9 +For helping python's unittest to discover your tests, do the following:
  10 +
  11 +* create a subdirectory within oletools/tests/
  12 + - The directory name must be a valid python package name,
  13 + so must not include '-', for example
  14 + - e.g. oletools/tests/my_feature
  15 +
  16 +* Create a __init__.py inside that directory
  17 + - can be empty but must be there
  18 +
  19 +* Copy the unittest_template.py into your test directory
  20 +
  21 +* Rename your copy of the template to fit its purpose
  22 + - file name must start with 'test' and end with '.py'
  23 + - e.g. oletools/tests/my_feature/test_bla.py
  24 +
  25 +* Create python code inside that directory
  26 + - classes names must start with Test and must be subclasses
  27 + of Unittest.TestCase
  28 + - test functions inside your test cases must start with test_
  29 + - see unittest_template.py for examples
  30 +
  31 +* If your unit test requires test files, put them into a subdir
  32 + of oletools/tests/test-data with some name that clarifies what
  33 + tests it belongs to
  34 + - e.g. oletools/tests/test-data/my_feature/example.doc
  35 + - Do not add files with actual evil malware macros! Only harmless
  36 + test data!
  37 +
  38 +* Test that unittests work by running from the oletools base dir:
  39 + python -m unittest discover -v
  40 +
  41 +* Re-test with python2 and python3 (if possible)
... ...
tests/json/__init__.py 0 → 100644
tests/json/test_output.py 0 → 100644
  1 +""" Test validity of json output
  2 +
  3 +Some scripts have a json output flag. Verify that at default log levels output
  4 +can be captured as-is and parsed by a json parser -- at least if the scripts
  5 +return 0
  6 +"""
  7 +
  8 +import unittest
  9 +import sys
  10 +import json
  11 +import os
  12 +from os.path import join
  13 +from oletools import msodde
  14 +from tests.test_utils import OutputCapture, DATA_BASE_DIR
  15 +
  16 +if sys.version_info[0] <= 2:
  17 + from oletools import olevba
  18 +else:
  19 + from oletools import olevba3 as olevba
  20 +
  21 +
  22 +class TestValidJson(unittest.TestCase):
  23 + """ Ensure that script output is valid json (if return code is 0) """
  24 +
  25 + def iter_test_files(self):
  26 + """ Iterate over all test files in DATA_BASE_DIR """
  27 + for dirpath, _, filenames in os.walk(DATA_BASE_DIR):
  28 + for filename in filenames:
  29 + yield join(dirpath, filename)
  30 +
  31 + def run_and_parse(self, program, args, print_output=False):
  32 + """ run single program with single file and parse output """
  33 + return_code = None
  34 + with OutputCapture() as capturer: # capture stdout
  35 + try:
  36 + return_code = program(args)
  37 + except Exception:
  38 + return_code = 1 # would result in non-zero exit
  39 + except SystemExit as se:
  40 + return_code = se.code or 0 # se.code can be None
  41 + if return_code is not 0:
  42 + if print_output:
  43 + print('Command failed ({0}) -- not parsing output'
  44 + .format(return_code))
  45 + return [] # no need to test
  46 +
  47 + self.assertNotEqual(return_code, None,
  48 + msg='self-test fail: return_code not set')
  49 +
  50 + # now test output
  51 + if print_output:
  52 + print(capturer.buffer.getvalue())
  53 + capturer.buffer.seek(0) # re-set position in file-like stream
  54 + try:
  55 + json_data = json.load(capturer.buffer)
  56 + except ValueError:
  57 + self.fail('Invalid json:\n' + capturer.buffer.getvalue())
  58 + self.assertNotEqual(len(json_data), 0, msg='Output was empty')
  59 + return json_data
  60 +
  61 + def run_all_files(self, program, args_without_filename, print_output=False):
  62 + """ run test for a single program over all test files """
  63 + n_files = 0
  64 + for testfile in self.iter_test_files(): # loop over all input
  65 + args = args_without_filename + [testfile, ]
  66 + self.run_and_parse(program, args, print_output)
  67 + n_files += 1
  68 + self.assertNotEqual(n_files, 0,
  69 + msg='self-test fail: No test files found')
  70 +
  71 + def test_msodde(self):
  72 + """ Test msodde.py """
  73 + self.run_all_files(msodde.main, ['-j', ])
  74 +
  75 + def test_olevba(self):
  76 + """ Test olevba.py with default args """
  77 + self.run_all_files(olevba.main, ['-j', ])
  78 +
  79 + def test_olevba_analysis(self):
  80 + """ Test olevba.py with -a """
  81 + self.run_all_files(olevba.main, ['-j', '-a', ])
  82 +
  83 + def test_olevba_recurse(self):
  84 + """ Test olevba.py with -r """
  85 + json_data = self.run_and_parse(olevba.main,
  86 + ['-j', '-r', join(DATA_BASE_DIR, '*')])
  87 + self.assertNotEqual(len(json_data), 0,
  88 + msg='olevba[3] returned non-zero or no output')
  89 + self.assertNotEqual(json_data[-1]['n_processed'], 0,
  90 + msg='self-test fail: No test files found!')
  91 +
  92 +
  93 +# just in case somebody calls this file as a script
  94 +if __name__ == '__main__':
  95 + unittest.main()
... ...
tests/msodde_doc/__init__.py 0 → 100644
tests/msodde_doc/test_basic.py 0 → 100644
  1 +""" Test some basic behaviour of msodde.py
  2 +
  3 +Ensure that
  4 +- doc and docx are read without error
  5 +- garbage returns error return status
  6 +- dde-links are found where appropriate
  7 +"""
  8 +
  9 +from __future__ import print_function
  10 +
  11 +import unittest
  12 +from oletools import msodde
  13 +from tests.test_utils import OutputCapture, DATA_BASE_DIR as BASE_DIR
  14 +import shlex
  15 +from os.path import join
  16 +from traceback import print_exc
  17 +
  18 +
  19 +class TestReturnCode(unittest.TestCase):
  20 +
  21 + def test_valid_doc(self):
  22 + """ check that a valid doc file leads to 0 exit status """
  23 + for filename in ('dde-test-from-office2003', 'dde-test-from-office2016',
  24 + 'harmless-clean', 'dde-test-from-office2013-utf_16le-korean'):
  25 + self.do_test_validity(join(BASE_DIR, 'msodde-doc',
  26 + filename + '.doc'))
  27 +
  28 + def test_valid_docx(self):
  29 + """ check that a valid docx file leads to 0 exit status """
  30 + for filename in 'dde-test', 'harmless-clean':
  31 + self.do_test_validity(join(BASE_DIR, 'msodde-doc',
  32 + filename + '.docx'))
  33 +
  34 + def test_valid_docm(self):
  35 + """ check that a valid docm file leads to 0 exit status """
  36 + for filename in 'dde-test', 'harmless-clean':
  37 + self.do_test_validity(join(BASE_DIR, 'msodde-doc',
  38 + filename + '.docm'))
  39 +
  40 + def test_invalid_other(self):
  41 + """ check that xml do not work yet """
  42 + for extn in '-2003.xml', '.xml':
  43 + self.do_test_validity(join(BASE_DIR, 'msodde-doc',
  44 + 'harmless-clean' + extn), True)
  45 +
  46 + def test_invalid_none(self):
  47 + """ check that no file argument leads to non-zero exit status """
  48 + self.do_test_validity('', True)
  49 +
  50 + def test_invalid_empty(self):
  51 + """ check that empty file argument leads to non-zero exit status """
  52 + self.do_test_validity(join(BASE_DIR, 'basic/empty'), True)
  53 +
  54 + def test_invalid_text(self):
  55 + """ check that text file argument leads to non-zero exit status """
  56 + self.do_test_validity(join(BASE_DIR, 'basic/text'), True)
  57 +
  58 + def do_test_validity(self, args, expect_error=False):
  59 + """ helper for test_valid_doc[x] """
  60 + args = shlex.split(args)
  61 + return_code = -1
  62 + have_exception = False
  63 + try:
  64 + return_code = msodde.main(args)
  65 + except Exception:
  66 + have_exception = True
  67 + print_exc()
  68 + except SystemExit as se: # sys.exit() was called
  69 + return_code = se.code
  70 + if se.code is None:
  71 + return_code = 0
  72 +
  73 + self.assertEqual(expect_error, have_exception or (return_code != 0),
  74 + msg='Args={0}, expect={1}, exc={2}, return={3}'
  75 + .format(args, expect_error, have_exception,
  76 + return_code))
  77 +
  78 +
  79 +class TestDdeInDoc(unittest.TestCase):
  80 +
  81 + def get_dde_from_output(self, capturer):
  82 + """ helper to read dde links from captured output """
  83 + have_start_line = False
  84 + result = []
  85 + for line in capturer:
  86 + if not line.strip():
  87 + continue # skip empty lines
  88 + if have_start_line:
  89 + result.append(line)
  90 + elif line == 'DDE Links:':
  91 + have_start_line = True
  92 +
  93 + self.assertTrue(have_start_line) # ensure output was complete
  94 + return result
  95 +
  96 + def test_with_dde(self):
  97 + """ check that dde links appear on stdout """
  98 + with OutputCapture() as capturer:
  99 + msodde.main([join(BASE_DIR, 'msodde-doc',
  100 + 'dde-test-from-office2003.doc')])
  101 + self.assertNotEqual(len(self.get_dde_from_output(capturer)), 0,
  102 + msg='Found no dde links in output for doc file')
  103 +
  104 + def test_no_dde(self):
  105 + """ check that no dde links appear on stdout """
  106 + with OutputCapture() as capturer:
  107 + msodde.main([join(BASE_DIR, 'msodde-doc', 'harmless-clean.doc')])
  108 + self.assertEqual(len(self.get_dde_from_output(capturer)), 0,
  109 + msg='Found dde links in output for doc file')
  110 +
  111 + def test_with_dde_utf16le(self):
  112 + """ check that dde links appear on stdout """
  113 + with OutputCapture() as capturer:
  114 + msodde.main([join(BASE_DIR, 'msodde-doc',
  115 + 'dde-test-from-office2013-utf_16le-korean.doc')])
  116 + self.assertNotEqual(len(self.get_dde_from_output(capturer)), 0,
  117 + msg='Found no dde links in output for doc file')
  118 +
  119 +
  120 +if __name__ == '__main__':
  121 + unittest.main()
... ...
tests/rtfobj/__init__.py 0 → 100644
tests/rtfobj/test_issue_185.py 0 → 100644
  1 +import unittest, sys, os
  2 +
  3 +from tests.test_utils import testdata_reader
  4 +from oletools import rtfobj
  5 +
  6 +class TestRtfObjIssue185(unittest.TestCase):
  7 + def test_skip_space_after_bin_control_word(self):
  8 + data = testdata_reader.read('rtfobj/issue_185.rtf')
  9 + rtfp = rtfobj.RtfObjParser(data)
  10 + rtfp.parse()
  11 + objects = rtfp.objects
  12 +
  13 + self.assertTrue(len(objects) == 1)
  14 +
  15 +if __name__ == '__main__':
  16 + unittest.main()
... ...
tests/test-data/basic/empty 0 → 100644
tests/test-data/basic/text 0 → 100644
  1 +bla
... ...
tests/test-data/msodde-doc/dde-test-from-office2003.doc 0 → 100644
No preview for this file type
tests/test-data/msodde-doc/dde-test-from-office2013-utf_16le-korean.doc 0 → 100644
No preview for this file type
tests/test-data/msodde-doc/dde-test-from-office2016.doc 0 → 100644
No preview for this file type
tests/test-data/msodde-doc/dde-test.docm 0 → 100644
No preview for this file type
tests/test-data/msodde-doc/dde-test.docx 0 → 100644
No preview for this file type
tests/test-data/msodde-doc/harmless-clean-2003.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  2 +<?mso-application progid="Word.Document"?>
  3 +<w:wordDocument xmlns:aml="http://schemas.microsoft.com/aml/2001/core" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml" xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wsp="http://schemas.microsoft.com/office/word/2003/wordml/sp2" xmlns:sl="http://schemas.microsoft.com/schemaLibrary/2003/core" w:macrosPresent="no" w:embeddedObjPresent="no" w:ocxPresent="no" xml:space="preserve"><w:ignoreSubtree w:val="http://schemas.microsoft.com/office/word/2003/wordml/sp2"/><o:DocumentProperties><o:Author>user</o:Author><o:LastAuthor>user</o:LastAuthor><o:Revision>2</o:Revision><o:TotalTime>0</o:TotalTime><o:Created>2017-10-26T09:10:00Z</o:Created><o:LastSaved>2017-10-26T09:10:00Z</o:LastSaved><o:Pages>1</o:Pages><o:Words>39</o:Words><o:Characters>250</o:Characters><o:Lines>2</o:Lines><o:Paragraphs>1</o:Paragraphs><o:CharactersWithSpaces>288</o:CharactersWithSpaces><o:Version>16</o:Version></o:DocumentProperties><w:fonts><w:defaultFonts w:ascii="Calibri" w:fareast="Calibri" w:h-ansi="Calibri" w:cs="Times New Roman"/><w:font w:name="Times New Roman"><w:panose-1 w:val="02020603050405020304"/><w:charset w:val="00"/><w:family w:val="Roman"/><w:pitch w:val="variable"/><w:sig w:usb-0="E0002EFF" w:usb-1="C000785B" w:usb-2="00000009" w:usb-3="00000000" w:csb-0="000001FF" w:csb-1="00000000"/></w:font><w:font w:name="Times New Roman"><w:panose-1 w:val="02020603050405020304"/><w:charset w:val="00"/><w:family w:val="Roman"/><w:pitch w:val="variable"/><w:sig w:usb-0="E0002EFF" w:usb-1="C000785B" w:usb-2="00000009" w:usb-3="00000000" w:csb-0="000001FF" w:csb-1="00000000"/></w:font><w:font w:name="Calibri"><w:panose-1 w:val="020F0502020204030204"/><w:charset w:val="00"/><w:family w:val="Swiss"/><w:pitch w:val="variable"/><w:sig w:usb-0="E0002AFF" w:usb-1="C000247B" w:usb-2="00000009" w:usb-3="00000000" w:csb-0="000001FF" w:csb-1="00000000"/></w:font><w:font w:name="Calibri Light"><w:panose-1 w:val="020F0302020204030204"/><w:charset w:val="00"/><w:family w:val="Swiss"/><w:pitch w:val="variable"/><w:sig w:usb-0="E0002AFF" w:usb-1="C000247B" w:usb-2="00000009" w:usb-3="00000000" w:csb-0="000001FF" w:csb-1="00000000"/></w:font><w:font w:name="Algerian"><w:panose-1 w:val="04020705040A02060702"/><w:charset w:val="00"/><w:family w:val="Decorative"/><w:pitch w:val="variable"/><w:sig w:usb-0="00000003" w:usb-1="00000000" w:usb-2="00000000" w:usb-3="00000000" w:csb-0="00000001" w:csb-1="00000000"/></w:font></w:fonts><w:styles><w:versionOfBuiltInStylenames w:val="7"/><w:latentStyles w:defLockedState="off" w:latentStyleCount="375"><w:lsdException w:name="Normal"/><w:lsdException w:name="heading 1"/><w:lsdException w:name="heading 2"/><w:lsdException w:name="heading 3"/><w:lsdException w:name="heading 4"/><w:lsdException w:name="heading 5"/><w:lsdException w:name="heading 6"/><w:lsdException w:name="heading 7"/><w:lsdException w:name="heading 8"/><w:lsdException w:name="heading 9"/><w:lsdException w:name="index 1"/><w:lsdException w:name="index 2"/><w:lsdException w:name="index 3"/><w:lsdException w:name="index 4"/><w:lsdException w:name="index 5"/><w:lsdException w:name="index 6"/><w:lsdException w:name="index 7"/><w:lsdException w:name="index 8"/><w:lsdException w:name="index 9"/><w:lsdException w:name="toc 1"/><w:lsdException w:name="toc 2"/><w:lsdException w:name="toc 3"/><w:lsdException w:name="toc 4"/><w:lsdException w:name="toc 5"/><w:lsdException w:name="toc 6"/><w:lsdException w:name="toc 7"/><w:lsdException w:name="toc 8"/><w:lsdException w:name="toc 9"/><w:lsdException w:name="Normal Indent"/><w:lsdException w:name="footnote text"/><w:lsdException w:name="annotation text"/><w:lsdException w:name="header"/><w:lsdException w:name="footer"/><w:lsdException w:name="index heading"/><w:lsdException w:name="caption"/><w:lsdException w:name="table of figures"/><w:lsdException w:name="envelope address"/><w:lsdException w:name="envelope return"/><w:lsdException w:name="footnote reference"/><w:lsdException w:name="annotation reference"/><w:lsdException w:name="line number"/><w:lsdException w:name="page number"/><w:lsdException w:name="endnote reference"/><w:lsdException w:name="endnote text"/><w:lsdException w:name="table of authorities"/><w:lsdException w:name="macro"/><w:lsdException w:name="toa heading"/><w:lsdException w:name="List"/><w:lsdException w:name="List Bullet"/><w:lsdException w:name="List Number"/><w:lsdException w:name="List 2"/><w:lsdException w:name="List 3"/><w:lsdException w:name="List 4"/><w:lsdException w:name="List 5"/><w:lsdException w:name="List Bullet 2"/><w:lsdException w:name="List Bullet 3"/><w:lsdException w:name="List Bullet 4"/><w:lsdException w:name="List Bullet 5"/><w:lsdException w:name="List Number 2"/><w:lsdException w:name="List Number 3"/><w:lsdException w:name="List Number 4"/><w:lsdException w:name="List Number 5"/><w:lsdException w:name="Title"/><w:lsdException w:name="Closing"/><w:lsdException w:name="Signature"/><w:lsdException w:name="Default Paragraph Font"/><w:lsdException w:name="Body Text"/><w:lsdException w:name="Body Text Indent"/><w:lsdException w:name="List Continue"/><w:lsdException w:name="List Continue 2"/><w:lsdException w:name="List Continue 3"/><w:lsdException w:name="List Continue 4"/><w:lsdException w:name="List Continue 5"/><w:lsdException w:name="Message Header"/><w:lsdException w:name="Subtitle"/><w:lsdException w:name="Salutation"/><w:lsdException w:name="Date"/><w:lsdException w:name="Body Text First Indent"/><w:lsdException w:name="Body Text First Indent 2"/><w:lsdException w:name="Note Heading"/><w:lsdException w:name="Body Text 2"/><w:lsdException w:name="Body Text 3"/><w:lsdException w:name="Body Text Indent 2"/><w:lsdException w:name="Body Text Indent 3"/><w:lsdException w:name="Block Text"/><w:lsdException w:name="Hyperlink"/><w:lsdException w:name="FollowedHyperlink"/><w:lsdException w:name="Strong"/><w:lsdException w:name="Emphasis"/><w:lsdException w:name="Document Map"/><w:lsdException w:name="Plain Text"/><w:lsdException w:name="E-mail Signature"/><w:lsdException w:name="HTML Top of Form"/><w:lsdException w:name="HTML Bottom of Form"/><w:lsdException w:name="Normal (Web)"/><w:lsdException w:name="HTML Acronym"/><w:lsdException w:name="HTML Address"/><w:lsdException w:name="HTML Cite"/><w:lsdException w:name="HTML Code"/><w:lsdException w:name="HTML Definition"/><w:lsdException w:name="HTML Keyboard"/><w:lsdException w:name="HTML Preformatted"/><w:lsdException w:name="HTML Sample"/><w:lsdException w:name="HTML Typewriter"/><w:lsdException w:name="HTML Variable"/><w:lsdException w:name="Normal Table"/><w:lsdException w:name="annotation subject"/><w:lsdException w:name="No List"/><w:lsdException w:name="Outline List 1"/><w:lsdException w:name="Outline List 2"/><w:lsdException w:name="Outline List 3"/><w:lsdException w:name="Table Simple 1"/><w:lsdException w:name="Table Simple 2"/><w:lsdException w:name="Table Simple 3"/><w:lsdException w:name="Table Classic 1"/><w:lsdException w:name="Table Classic 2"/><w:lsdException w:name="Table Classic 3"/><w:lsdException w:name="Table Classic 4"/><w:lsdException w:name="Table Colorful 1"/><w:lsdException w:name="Table Colorful 2"/><w:lsdException w:name="Table Colorful 3"/><w:lsdException w:name="Table Columns 1"/><w:lsdException w:name="Table Columns 2"/><w:lsdException w:name="Table Columns 3"/><w:lsdException w:name="Table Columns 4"/><w:lsdException w:name="Table Columns 5"/><w:lsdException w:name="Table Grid 1"/><w:lsdException w:name="Table Grid 2"/><w:lsdException w:name="Table Grid 3"/><w:lsdException w:name="Table Grid 4"/><w:lsdException w:name="Table Grid 5"/><w:lsdException w:name="Table Grid 6"/><w:lsdException w:name="Table Grid 7"/><w:lsdException w:name="Table Grid 8"/><w:lsdException w:name="Table List 1"/><w:lsdException w:name="Table List 2"/><w:lsdException w:name="Table List 3"/><w:lsdException w:name="Table List 4"/><w:lsdException w:name="Table List 5"/><w:lsdException w:name="Table List 6"/><w:lsdException w:name="Table List 7"/><w:lsdException w:name="Table List 8"/><w:lsdException w:name="Table 3D effects 1"/><w:lsdException w:name="Table 3D effects 2"/><w:lsdException w:name="Table 3D effects 3"/><w:lsdException w:name="Table Contemporary"/><w:lsdException w:name="Table Elegant"/><w:lsdException w:name="Table Professional"/><w:lsdException w:name="Table Subtle 1"/><w:lsdException w:name="Table Subtle 2"/><w:lsdException w:name="Table Web 1"/><w:lsdException w:name="Table Web 2"/><w:lsdException w:name="Table Web 3"/><w:lsdException w:name="Balloon Text"/><w:lsdException w:name="Table Grid"/><w:lsdException w:name="Table Theme"/><w:lsdException w:name="Placeholder Text"/><w:lsdException w:name="No Spacing"/><w:lsdException w:name="Light Shading"/><w:lsdException w:name="Light List"/><w:lsdException w:name="Light Grid"/><w:lsdException w:name="Medium Shading 1"/><w:lsdException w:name="Medium Shading 2"/><w:lsdException w:name="Medium List 1"/><w:lsdException w:name="Medium List 2"/><w:lsdException w:name="Medium Grid 1"/><w:lsdException w:name="Medium Grid 2"/><w:lsdException w:name="Medium Grid 3"/><w:lsdException w:name="Dark List"/><w:lsdException w:name="Colorful Shading"/><w:lsdException w:name="Colorful List"/><w:lsdException w:name="Colorful Grid"/><w:lsdException w:name="Light Shading Accent 1"/><w:lsdException w:name="Light List Accent 1"/><w:lsdException w:name="Light Grid Accent 1"/><w:lsdException w:name="Medium Shading 1 Accent 1"/><w:lsdException w:name="Medium Shading 2 Accent 1"/><w:lsdException w:name="Medium List 1 Accent 1"/><w:lsdException w:name="Revision"/><w:lsdException w:name="List Paragraph"/><w:lsdException w:name="Quote"/><w:lsdException w:name="Intense Quote"/><w:lsdException w:name="Medium List 2 Accent 1"/><w:lsdException w:name="Medium Grid 1 Accent 1"/><w:lsdException w:name="Medium Grid 2 Accent 1"/><w:lsdException w:name="Medium Grid 3 Accent 1"/><w:lsdException w:name="Dark List Accent 1"/><w:lsdException w:name="Colorful Shading Accent 1"/><w:lsdException w:name="Colorful List Accent 1"/><w:lsdException w:name="Colorful Grid Accent 1"/><w:lsdException w:name="Light Shading Accent 2"/><w:lsdException w:name="Light List Accent 2"/><w:lsdException w:name="Light Grid Accent 2"/><w:lsdException w:name="Medium Shading 1 Accent 2"/><w:lsdException w:name="Medium Shading 2 Accent 2"/><w:lsdException w:name="Medium List 1 Accent 2"/><w:lsdException w:name="Medium List 2 Accent 2"/><w:lsdException w:name="Medium Grid 1 Accent 2"/><w:lsdException w:name="Medium Grid 2 Accent 2"/><w:lsdException w:name="Medium Grid 3 Accent 2"/><w:lsdException w:name="Dark List Accent 2"/><w:lsdException w:name="Colorful Shading Accent 2"/><w:lsdException w:name="Colorful List Accent 2"/><w:lsdException w:name="Colorful Grid Accent 2"/><w:lsdException w:name="Light Shading Accent 3"/><w:lsdException w:name="Light List Accent 3"/><w:lsdException w:name="Light Grid Accent 3"/><w:lsdException w:name="Medium Shading 1 Accent 3"/><w:lsdException w:name="Medium Shading 2 Accent 3"/><w:lsdException w:name="Medium List 1 Accent 3"/><w:lsdException w:name="Medium List 2 Accent 3"/><w:lsdException w:name="Medium Grid 1 Accent 3"/><w:lsdException w:name="Medium Grid 2 Accent 3"/><w:lsdException w:name="Medium Grid 3 Accent 3"/><w:lsdException w:name="Dark List Accent 3"/><w:lsdException w:name="Colorful Shading Accent 3"/><w:lsdException w:name="Colorful List Accent 3"/><w:lsdException w:name="Colorful Grid Accent 3"/><w:lsdException w:name="Light Shading Accent 4"/><w:lsdException w:name="Light List Accent 4"/><w:lsdException w:name="Light Grid Accent 4"/><w:lsdException w:name="Medium Shading 1 Accent 4"/><w:lsdException w:name="Medium Shading 2 Accent 4"/><w:lsdException w:name="Medium List 1 Accent 4"/><w:lsdException w:name="Medium List 2 Accent 4"/><w:lsdException w:name="Medium Grid 1 Accent 4"/><w:lsdException w:name="Medium Grid 2 Accent 4"/><w:lsdException w:name="Medium Grid 3 Accent 4"/><w:lsdException w:name="Dark List Accent 4"/><w:lsdException w:name="Colorful Shading Accent 4"/><w:lsdException w:name="Colorful List Accent 4"/><w:lsdException w:name="Colorful Grid Accent 4"/><w:lsdException w:name="Light Shading Accent 5"/><w:lsdException w:name="Light List Accent 5"/><w:lsdException w:name="Light Grid Accent 5"/><w:lsdException w:name="Medium Shading 1 Accent 5"/><w:lsdException w:name="Medium Shading 2 Accent 5"/><w:lsdException w:name="Medium List 1 Accent 5"/><w:lsdException w:name="Medium List 2 Accent 5"/><w:lsdException w:name="Medium Grid 1 Accent 5"/><w:lsdException w:name="Medium Grid 2 Accent 5"/><w:lsdException w:name="Medium Grid 3 Accent 5"/><w:lsdException w:name="Dark List Accent 5"/><w:lsdException w:name="Colorful Shading Accent 5"/><w:lsdException w:name="Colorful List Accent 5"/><w:lsdException w:name="Colorful Grid Accent 5"/><w:lsdException w:name="Light Shading Accent 6"/><w:lsdException w:name="Light List Accent 6"/><w:lsdException w:name="Light Grid Accent 6"/><w:lsdException w:name="Medium Shading 1 Accent 6"/><w:lsdException w:name="Medium Shading 2 Accent 6"/><w:lsdException w:name="Medium List 1 Accent 6"/><w:lsdException w:name="Medium List 2 Accent 6"/><w:lsdException w:name="Medium Grid 1 Accent 6"/><w:lsdException w:name="Medium Grid 2 Accent 6"/><w:lsdException w:name="Medium Grid 3 Accent 6"/><w:lsdException w:name="Dark List Accent 6"/><w:lsdException w:name="Colorful Shading Accent 6"/><w:lsdException w:name="Colorful List Accent 6"/><w:lsdException w:name="Colorful Grid Accent 6"/><w:lsdException w:name="Subtle Emphasis"/><w:lsdException w:name="Intense Emphasis"/><w:lsdException w:name="Subtle Reference"/><w:lsdException w:name="Intense Reference"/><w:lsdException w:name="Book Title"/><w:lsdException w:name="Bibliography"/><w:lsdException w:name="TOC Heading"/><w:lsdException w:name="Plain Table 1"/><w:lsdException w:name="Plain Table 2"/><w:lsdException w:name="Plain Table 3"/><w:lsdException w:name="Plain Table 4"/><w:lsdException w:name="Plain Table 5"/><w:lsdException w:name="Grid Table Light"/><w:lsdException w:name="Grid Table 1 Light"/><w:lsdException w:name="Grid Table 2"/><w:lsdException w:name="Grid Table 3"/><w:lsdException w:name="Grid Table 4"/><w:lsdException w:name="Grid Table 5 Dark"/><w:lsdException w:name="Grid Table 6 Colorful"/><w:lsdException w:name="Grid Table 7 Colorful"/><w:lsdException w:name="Grid Table 1 Light Accent 1"/><w:lsdException w:name="Grid Table 2 Accent 1"/><w:lsdException w:name="Grid Table 3 Accent 1"/><w:lsdException w:name="Grid Table 4 Accent 1"/><w:lsdException w:name="Grid Table 5 Dark Accent 1"/><w:lsdException w:name="Grid Table 6 Colorful Accent 1"/><w:lsdException w:name="Grid Table 7 Colorful Accent 1"/><w:lsdException w:name="Grid Table 1 Light Accent 2"/><w:lsdException w:name="Grid Table 2 Accent 2"/><w:lsdException w:name="Grid Table 3 Accent 2"/><w:lsdException w:name="Grid Table 4 Accent 2"/><w:lsdException w:name="Grid Table 5 Dark Accent 2"/><w:lsdException w:name="Grid Table 6 Colorful Accent 2"/><w:lsdException w:name="Grid Table 7 Colorful Accent 2"/><w:lsdException w:name="Grid Table 1 Light Accent 3"/><w:lsdException w:name="Grid Table 2 Accent 3"/><w:lsdException w:name="Grid Table 3 Accent 3"/><w:lsdException w:name="Grid Table 4 Accent 3"/><w:lsdException w:name="Grid Table 5 Dark Accent 3"/><w:lsdException w:name="Grid Table 6 Colorful Accent 3"/><w:lsdException w:name="Grid Table 7 Colorful Accent 3"/><w:lsdException w:name="Grid Table 1 Light Accent 4"/><w:lsdException w:name="Grid Table 2 Accent 4"/><w:lsdException w:name="Grid Table 3 Accent 4"/><w:lsdException w:name="Grid Table 4 Accent 4"/><w:lsdException w:name="Grid Table 5 Dark Accent 4"/><w:lsdException w:name="Grid Table 6 Colorful Accent 4"/><w:lsdException w:name="Grid Table 7 Colorful Accent 4"/><w:lsdException w:name="Grid Table 1 Light Accent 5"/><w:lsdException w:name="Grid Table 2 Accent 5"/><w:lsdException w:name="Grid Table 3 Accent 5"/><w:lsdException w:name="Grid Table 4 Accent 5"/><w:lsdException w:name="Grid Table 5 Dark Accent 5"/><w:lsdException w:name="Grid Table 6 Colorful Accent 5"/><w:lsdException w:name="Grid Table 7 Colorful Accent 5"/><w:lsdException w:name="Grid Table 1 Light Accent 6"/><w:lsdException w:name="Grid Table 2 Accent 6"/><w:lsdException w:name="Grid Table 3 Accent 6"/><w:lsdException w:name="Grid Table 4 Accent 6"/><w:lsdException w:name="Grid Table 5 Dark Accent 6"/><w:lsdException w:name="Grid Table 6 Colorful Accent 6"/><w:lsdException w:name="Grid Table 7 Colorful Accent 6"/><w:lsdException w:name="List Table 1 Light"/><w:lsdException w:name="List Table 2"/><w:lsdException w:name="List Table 3"/><w:lsdException w:name="List Table 4"/><w:lsdException w:name="List Table 5 Dark"/><w:lsdException w:name="List Table 6 Colorful"/><w:lsdException w:name="List Table 7 Colorful"/><w:lsdException w:name="List Table 1 Light Accent 1"/><w:lsdException w:name="List Table 2 Accent 1"/><w:lsdException w:name="List Table 3 Accent 1"/><w:lsdException w:name="List Table 4 Accent 1"/><w:lsdException w:name="List Table 5 Dark Accent 1"/><w:lsdException w:name="List Table 6 Colorful Accent 1"/><w:lsdException w:name="List Table 7 Colorful Accent 1"/><w:lsdException w:name="List Table 1 Light Accent 2"/><w:lsdException w:name="List Table 2 Accent 2"/><w:lsdException w:name="List Table 3 Accent 2"/><w:lsdException w:name="List Table 4 Accent 2"/><w:lsdException w:name="List Table 5 Dark Accent 2"/><w:lsdException w:name="List Table 6 Colorful Accent 2"/><w:lsdException w:name="List Table 7 Colorful Accent 2"/><w:lsdException w:name="List Table 1 Light Accent 3"/><w:lsdException w:name="List Table 2 Accent 3"/><w:lsdException w:name="List Table 3 Accent 3"/><w:lsdException w:name="List Table 4 Accent 3"/><w:lsdException w:name="List Table 5 Dark Accent 3"/><w:lsdException w:name="List Table 6 Colorful Accent 3"/><w:lsdException w:name="List Table 7 Colorful Accent 3"/><w:lsdException w:name="List Table 1 Light Accent 4"/><w:lsdException w:name="List Table 2 Accent 4"/><w:lsdException w:name="List Table 3 Accent 4"/><w:lsdException w:name="List Table 4 Accent 4"/><w:lsdException w:name="List Table 5 Dark Accent 4"/><w:lsdException w:name="List Table 6 Colorful Accent 4"/><w:lsdException w:name="List Table 7 Colorful Accent 4"/><w:lsdException w:name="List Table 1 Light Accent 5"/><w:lsdException w:name="List Table 2 Accent 5"/><w:lsdException w:name="List Table 3 Accent 5"/><w:lsdException w:name="List Table 4 Accent 5"/><w:lsdException w:name="List Table 5 Dark Accent 5"/><w:lsdException w:name="List Table 6 Colorful Accent 5"/><w:lsdException w:name="List Table 7 Colorful Accent 5"/><w:lsdException w:name="List Table 1 Light Accent 6"/><w:lsdException w:name="List Table 2 Accent 6"/><w:lsdException w:name="List Table 3 Accent 6"/><w:lsdException w:name="List Table 4 Accent 6"/><w:lsdException w:name="List Table 5 Dark Accent 6"/><w:lsdException w:name="List Table 6 Colorful Accent 6"/><w:lsdException w:name="List Table 7 Colorful Accent 6"/><w:lsdException w:name="Mention"/><w:lsdException w:name="Smart Hyperlink"/><w:lsdException w:name="Hashtag"/><w:lsdException w:name="Unresolved Mention"/></w:latentStyles><w:style w:type="paragraph" w:default="on" w:styleId="Standard"><w:name w:val="Normal"/><wx:uiName wx:val="Standard"/><w:pPr><w:spacing w:after="160" w:line="259" w:line-rule="auto"/></w:pPr><w:rPr><wx:font wx:val="Calibri"/><w:sz w:val="22"/><w:sz-cs w:val="22"/><w:lang w:val="DE" w:fareast="EN-US" w:bidi="AR-SA"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="berschrift1"><w:name w:val="heading 1"/><wx:uiName wx:val="Überschrift 1"/><w:basedOn w:val="Standard"/><w:next w:val="Standard"/><w:link w:val="berschrift1Zchn"/><w:rsid w:val="00601692"/><w:pPr><w:keepNext/><w:keepLines/><w:spacing w:before="240" w:after="0"/><w:outlineLvl w:val="0"/></w:pPr><w:rPr><w:rFonts w:ascii="Calibri Light" w:fareast="Times New Roman" w:h-ansi="Calibri Light"/><wx:font wx:val="Calibri Light"/><w:color w:val="2F5496"/><w:sz w:val="32"/><w:sz-cs w:val="32"/></w:rPr></w:style><w:style w:type="character" w:default="on" w:styleId="Absatz-Standardschriftart"><w:name w:val="Default Paragraph Font"/><wx:uiName wx:val="Absatz-Standardschriftart"/></w:style><w:style w:type="table" w:default="on" w:styleId="NormaleTabelle"><w:name w:val="Normal Table"/><wx:uiName wx:val="Normale Tabelle"/><w:rPr><wx:font wx:val="Calibri"/><w:lang w:val="DE" w:fareast="DE" w:bidi="AR-SA"/></w:rPr><w:tblPr><w:tblInd w:w="0" w:type="dxa"/><w:tblCellMar><w:top w:w="0" w:type="dxa"/><w:left w:w="108" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="108" w:type="dxa"/></w:tblCellMar></w:tblPr></w:style><w:style w:type="list" w:default="on" w:styleId="KeineListe"><w:name w:val="No List"/><wx:uiName wx:val="Keine Liste"/></w:style><w:style w:type="character" w:styleId="berschrift1Zchn"><w:name w:val="Überschrift 1 Zchn"/><w:link w:val="berschrift1"/><w:rsid w:val="00601692"/><w:rPr><w:rFonts w:ascii="Calibri Light" w:fareast="Times New Roman" w:h-ansi="Calibri Light" w:cs="Times New Roman"/><w:color w:val="2F5496"/><w:sz w:val="32"/><w:sz-cs w:val="32"/></w:rPr></w:style></w:styles><w:shapeDefaults><o:shapedefaults v:ext="edit" spidmax="1026"/><o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout></w:shapeDefaults><w:docPr><w:view w:val="print"/><w:zoom w:percent="100"/><w:doNotEmbedSystemFonts/><w:proofState w:spelling="clean" w:grammar="clean"/><w:defaultTabStop w:val="708"/><w:hyphenationZone w:val="425"/><w:punctuationKerning/><w:characterSpacingControl w:val="DontCompress"/><w:optimizeForBrowser/><w:allowPNG/><w:validateAgainstSchema/><w:saveInvalidXML w:val="off"/><w:ignoreMixedContent w:val="off"/><w:alwaysShowPlaceholderText w:val="off"/><w:compat><w:breakWrappedTables/><w:snapToGridInCell/><w:wrapTextWithPunct/><w:useAsianBreakRules/><w:dontGrowAutofit/></w:compat><wsp:rsids><wsp:rsidRoot wsp:val="00601692"/><wsp:rsid wsp:val="002341F3"/><wsp:rsid wsp:val="00601692"/><wsp:rsid wsp:val="00972907"/><wsp:rsid wsp:val="00FC3A9D"/></wsp:rsids></w:docPr><w:body><wx:sect><wx:sub-section><w:p wsp:rsidR="00601692" wsp:rsidRDefault="00601692" wsp:rsidP="00601692"><w:pPr><w:pStyle w:val="berschrift1"/><w:rPr><w:lang w:val="EN-US"/></w:rPr></w:pPr><w:r><w:rPr><w:lang w:val="EN-US"/></w:rPr><w:t>Test</w:t></w:r></w:p><w:p wsp:rsidR="00601692" wsp:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="EN-US"/></w:rPr></w:pPr></w:p><w:p wsp:rsidR="00FC3A9D" wsp:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="EN-US"/></w:rPr></w:pPr><w:r><w:rPr><w:lang w:val="EN-US"/></w:rPr><w:t>This is a harmless test document.</w:t></w:r></w:p><w:p wsp:rsidR="00601692" wsp:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="EN-US"/></w:rPr></w:pPr></w:p><w:p wsp:rsidR="00601692" wsp:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="EN-US"/></w:rPr></w:pPr><w:r><w:rPr><w:lang w:val="EN-US"/></w:rPr><w:t>It contains neither macros nor </w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:rPr><w:lang w:val="EN-US"/></w:rPr><w:t>dde</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:rPr><w:lang w:val="EN-US"/></w:rPr><w:t> links nor embedded viruses nor links to evil web pages. Not even a single insult. Boring!</w:t></w:r></w:p><w:p wsp:rsidR="00601692" wsp:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="EN-US"/></w:rPr></w:pPr></w:p><w:p wsp:rsidR="00601692" wsp:rsidRPr="00601692" wsp:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="EN-US"/></w:rPr></w:pPr><w:r><w:rPr><w:lang w:val="EN-US"/></w:rPr><w:t>Just to make things slightly interesting, however, we add some </w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:rPr><w:lang w:val="EN-US"/></w:rPr><w:t>ünicöde-ßtringß</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:rPr><w:lang w:val="EN-US"/></w:rPr><w:t> and different text </w:t></w:r><w:r wsp:rsidRPr="00601692"><w:rPr><w:sz w:val="32"/><w:sz-cs w:val="32"/><w:lang w:val="EN-US"/></w:rPr><w:t>sizes</w:t></w:r><w:r><w:rPr><w:lang w:val="EN-US"/></w:rPr><w:t>, </w:t></w:r><w:r wsp:rsidRPr="00601692"><w:rPr><w:color w:val="C00000"/><w:lang w:val="EN-US"/></w:rPr><w:t>colors </w:t></w:r><w:r><w:rPr><w:lang w:val="EN-US"/></w:rPr><w:t>and </w:t></w:r><w:r wsp:rsidRPr="00601692"><w:rPr><w:rFonts w:ascii="Algerian" w:h-ansi="Algerian"/><wx:font wx:val="Algerian"/><w:lang w:val="EN-US"/></w:rPr><w:t>fonts</w:t></w:r></w:p></wx:sub-section><w:sectPr wsp:rsidR="00601692" wsp:rsidRPr="00601692"><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1417" w:right="1417" w:bottom="1134" w:left="1417" w:header="708" w:footer="708" w:gutter="0"/><w:cols w:space="708"/><w:docGrid w:line-pitch="360"/></w:sectPr></wx:sect></w:body></w:wordDocument>
0 4 \ No newline at end of file
... ...
tests/test-data/msodde-doc/harmless-clean.doc 0 → 100644
No preview for this file type
tests/test-data/msodde-doc/harmless-clean.docm 0 → 100644
No preview for this file type
tests/test-data/msodde-doc/harmless-clean.docx 0 → 100644
No preview for this file type
tests/test-data/msodde-doc/harmless-clean.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  2 +<?mso-application progid="Word.Document"?>
  3 +<pkg:package xmlns:pkg="http://schemas.microsoft.com/office/2006/xmlPackage"><pkg:part pkg:name="/_rels/.rels" pkg:contentType="application/vnd.openxmlformats-package.relationships+xml" pkg:padding="512"><pkg:xmlData><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships></pkg:xmlData></pkg:part><pkg:part pkg:name="/word/_rels/document.xml.rels" pkg:contentType="application/vnd.openxmlformats-package.relationships+xml" pkg:padding="256"><pkg:xmlData><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/><Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/><Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/></Relationships></pkg:xmlData></pkg:part><pkg:part pkg:name="/word/document.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"><pkg:xmlData><w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid wp14"><w:body><w:p w:rsidR="00601692" w:rsidRDefault="00601692" w:rsidP="00601692"><w:pPr><w:pStyle w:val="berschrift1"/><w:rPr><w:lang w:val="en-US"/></w:rPr></w:pPr><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t>Test</w:t></w:r></w:p><w:p w:rsidR="00601692" w:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="en-US"/></w:rPr></w:pPr></w:p><w:p w:rsidR="00FC3A9D" w:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="en-US"/></w:rPr></w:pPr><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t>This is a harmless test document.</w:t></w:r></w:p><w:p w:rsidR="00601692" w:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="en-US"/></w:rPr></w:pPr></w:p><w:p w:rsidR="00601692" w:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="en-US"/></w:rPr></w:pPr><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t xml:space="preserve">It contains neither macros nor </w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t>dde</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t xml:space="preserve"> links nor embedded viruses nor links to evil web pages. Not even a single insult. Boring!</w:t></w:r></w:p><w:p w:rsidR="00601692" w:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="en-US"/></w:rPr></w:pPr></w:p><w:p w:rsidR="00601692" w:rsidRPr="00601692" w:rsidRDefault="00601692"><w:pPr><w:rPr><w:lang w:val="en-US"/></w:rPr></w:pPr><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t>Just to make things sli</w:t></w:r><w:bookmarkStart w:id="0" w:name="_GoBack"/><w:bookmarkEnd w:id="0"/><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t xml:space="preserve">ghtly interesting, however, we add some </w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t>ünicöde-ßtringß</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t xml:space="preserve"> and different text </w:t></w:r><w:r w:rsidRPr="00601692"><w:rPr><w:sz w:val="32"/><w:szCs w:val="32"/><w:lang w:val="en-US"/></w:rPr><w:t>sizes</w:t></w:r><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t xml:space="preserve">, </w:t></w:r><w:r w:rsidRPr="00601692"><w:rPr><w:color w:val="C00000"/><w:lang w:val="en-US"/></w:rPr><w:t xml:space="preserve">colors </w:t></w:r><w:r><w:rPr><w:lang w:val="en-US"/></w:rPr><w:t xml:space="preserve">and </w:t></w:r><w:r w:rsidRPr="00601692"><w:rPr><w:rFonts w:ascii="Algerian" w:hAnsi="Algerian"/><w:lang w:val="en-US"/></w:rPr><w:t>fonts</w:t></w:r></w:p><w:sectPr w:rsidR="00601692" w:rsidRPr="00601692"><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1417" w:right="1417" w:bottom="1134" w:left="1417" w:header="708" w:footer="708" w:gutter="0"/><w:cols w:space="708"/><w:docGrid w:linePitch="360"/></w:sectPr></w:body></w:document></pkg:xmlData></pkg:part><pkg:part pkg:name="/word/theme/theme1.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.theme+xml"><pkg:xmlData><a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="44546A"/></a:dk2><a:lt2><a:srgbClr val="E7E6E6"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="5B9BD5"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin typeface="Calibri Light" panose="020F0302020204030204"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="游ゴシック Light"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="等线 Light"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Times New Roman"/><a:font script="Hebr" typeface="Times New Roman"/><a:font script="Thai" typeface="Angsana New"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="MoolBoran"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Times New Roman"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/><a:font script="Armn" typeface="Arial"/><a:font script="Bugi" typeface="Leelawadee UI"/><a:font script="Bopo" typeface="Microsoft JhengHei"/><a:font script="Java" typeface="Javanese Text"/><a:font script="Lisu" typeface="Segoe UI"/><a:font script="Mymr" typeface="Myanmar Text"/><a:font script="Nkoo" typeface="Ebrima"/><a:font script="Olck" typeface="Nirmala UI"/><a:font script="Osma" typeface="Ebrima"/><a:font script="Phag" typeface="Phagspa"/><a:font script="Syrn" typeface="Estrangelo Edessa"/><a:font script="Syrj" typeface="Estrangelo Edessa"/><a:font script="Syre" typeface="Estrangelo Edessa"/><a:font script="Sora" typeface="Nirmala UI"/><a:font script="Tale" typeface="Microsoft Tai Le"/><a:font script="Talu" typeface="Microsoft New Tai Lue"/><a:font script="Tfng" typeface="Ebrima"/></a:majorFont><a:minorFont><a:latin typeface="Calibri" panose="020F0502020204030204"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="游明朝"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="等线"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Arial"/><a:font script="Hebr" typeface="Arial"/><a:font script="Thai" typeface="Cordia New"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="DaunPenh"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Arial"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/><a:font script="Armn" typeface="Arial"/><a:font script="Bugi" typeface="Leelawadee UI"/><a:font script="Bopo" typeface="Microsoft JhengHei"/><a:font script="Java" typeface="Javanese Text"/><a:font script="Lisu" typeface="Segoe UI"/><a:font script="Mymr" typeface="Myanmar Text"/><a:font script="Nkoo" typeface="Ebrima"/><a:font script="Olck" typeface="Nirmala UI"/><a:font script="Osma" typeface="Ebrima"/><a:font script="Phag" typeface="Phagspa"/><a:font script="Syrn" typeface="Estrangelo Edessa"/><a:font script="Syrj" typeface="Estrangelo Edessa"/><a:font script="Syre" typeface="Estrangelo Edessa"/><a:font script="Sora" typeface="Nirmala UI"/><a:font script="Tale" typeface="Microsoft Tai Le"/><a:font script="Talu" typeface="Microsoft New Tai Lue"/><a:font script="Tfng" typeface="Ebrima"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:lumMod val="110000"/><a:satMod val="105000"/><a:tint val="67000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="103000"/><a:tint val="73000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="109000"/><a:tint val="81000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:satMod val="103000"/><a:lumMod val="102000"/><a:tint val="94000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:satMod val="110000"/><a:lumMod val="100000"/><a:shade val="100000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="99000"/><a:satMod val="120000"/><a:shade val="78000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln><a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln><a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="57150" dist="19050" dir="5400000" algn="ctr" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="63000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="93000"/><a:satMod val="150000"/><a:shade val="98000"/><a:lumMod val="102000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:tint val="98000"/><a:satMod val="130000"/><a:shade val="90000"/><a:lumMod val="103000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="63000"/><a:satMod val="120000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults/><a:extraClrSchemeLst/><a:extLst><a:ext uri="{05A4C25C-085E-4340-85A3-A5531E510DB2}"><thm15:themeFamily xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" name="Office Theme" id="{62F939B6-93AF-4DB8-9C6B-D6C7DFDC589F}" vid="{4A3C46E8-61CC-4603-A589-7422A47A8E4A}"/></a:ext></a:extLst></a:theme></pkg:xmlData></pkg:part><pkg:part pkg:name="/word/settings.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"><pkg:xmlData><w:settings xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" mc:Ignorable="w14 w15 w16se w16cid"><w:zoom w:percent="100"/><w:proofState w:spelling="clean" w:grammar="clean"/><w:doNotTrackMoves/><w:defaultTabStop w:val="708"/><w:hyphenationZone w:val="425"/><w:characterSpacingControl w:val="doNotCompress"/><w:compat><w:useNormalStyleForList/><w:doNotUseIndentAsNumberingTabStop/><w:useAltKinsokuLineBreakRules/><w:allowSpaceOfSameStyleInTable/><w:doNotSuppressIndentation/><w:doNotAutofitConstrainedTables/><w:autofitToFirstFixedWidthCell/><w:displayHangulFixedWidth/><w:splitPgBreakAndParaMark/><w:doNotVertAlignCellWithSp/><w:doNotBreakConstrainedForcedTable/><w:doNotVertAlignInTxbx/><w:useAnsiKerningPairs/><w:cachedColBalance/><w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="11"/><w:compatSetting w:name="allowHyphenationAtTrackBottom" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/><w:compatSetting w:name="useWord2013TrackBottomHyphenation" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/></w:compat><w:rsids><w:rsidRoot w:val="00601692"/><w:rsid w:val="00601692"/><w:rsid w:val="00972907"/><w:rsid w:val="00FC3A9D"/></w:rsids><m:mathPr><m:mathFont m:val="Cambria Math"/><m:brkBin m:val="before"/><m:brkBinSub m:val="--"/><m:smallFrac m:val="0"/><m:dispDef/><m:lMargin m:val="0"/><m:rMargin m:val="0"/><m:defJc m:val="centerGroup"/><m:wrapIndent m:val="1440"/><m:intLim m:val="subSup"/><m:naryLim m:val="undOvr"/></m:mathPr><w:themeFontLang w:val="de-DE"/><w:clrSchemeMapping w:bg1="light1" w:t1="dark1" w:bg2="light2" w:t2="dark2" w:accent1="accent1" w:accent2="accent2" w:accent3="accent3" w:accent4="accent4" w:accent5="accent5" w:accent6="accent6" w:hyperlink="hyperlink" w:followedHyperlink="followedHyperlink"/><w:shapeDefaults><o:shapedefaults v:ext="edit" spidmax="1026"/><o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout></w:shapeDefaults><w:decimalSymbol w:val=","/><w:listSeparator w:val=";"/><w14:docId w14:val="047552AE"/><w15:chartTrackingRefBased/><w15:docId w15:val="{C5480CB3-6457-4809-B8A6-E3BEF53BE60F}"/></w:settings></pkg:xmlData></pkg:part><pkg:part pkg:name="/word/fontTable.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"><pkg:xmlData><w:fonts xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" mc:Ignorable="w14 w15 w16se w16cid"><w:font w:name="Calibri"><w:panose1 w:val="020F0502020204030204"/><w:charset w:val="00"/><w:family w:val="swiss"/><w:pitch w:val="variable"/><w:sig w:usb0="E0002AFF" w:usb1="C000247B" w:usb2="00000009" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/></w:font><w:font w:name="Times New Roman"><w:panose1 w:val="02020603050405020304"/><w:charset w:val="00"/><w:family w:val="roman"/><w:pitch w:val="variable"/><w:sig w:usb0="E0002EFF" w:usb1="C000785B" w:usb2="00000009" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/></w:font><w:font w:name="Calibri Light"><w:panose1 w:val="020F0302020204030204"/><w:charset w:val="00"/><w:family w:val="swiss"/><w:pitch w:val="variable"/><w:sig w:usb0="E0002AFF" w:usb1="C000247B" w:usb2="00000009" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/></w:font><w:font w:name="Algerian"><w:panose1 w:val="04020705040A02060702"/><w:charset w:val="00"/><w:family w:val="decorative"/><w:pitch w:val="variable"/><w:sig w:usb0="00000003" w:usb1="00000000" w:usb2="00000000" w:usb3="00000000" w:csb0="00000001" w:csb1="00000000"/></w:font></w:fonts></pkg:xmlData></pkg:part><pkg:part pkg:name="/word/webSettings.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"><pkg:xmlData><w:webSettings xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" mc:Ignorable="w14 w15 w16se w16cid"><w:optimizeForBrowser/><w:allowPNG/></w:webSettings></pkg:xmlData></pkg:part><pkg:part pkg:name="/docProps/app.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" pkg:padding="256"><pkg:xmlData><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Template>Normal</Template><TotalTime>0</TotalTime><Pages>1</Pages><Words>39</Words><Characters>250</Characters><Application>Microsoft Office Word</Application><DocSecurity>0</DocSecurity><Lines>2</Lines><Paragraphs>1</Paragraphs><ScaleCrop>false</ScaleCrop><HeadingPairs><vt:vector size="2" baseType="variant"><vt:variant><vt:lpstr>Titel</vt:lpstr></vt:variant><vt:variant><vt:i4>1</vt:i4></vt:variant></vt:vector></HeadingPairs><TitlesOfParts><vt:vector size="1" baseType="lpstr"><vt:lpstr/></vt:vector></TitlesOfParts><Company/><LinksUpToDate>false</LinksUpToDate><CharactersWithSpaces>288</CharactersWithSpaces><SharedDoc>false</SharedDoc><HyperlinksChanged>false</HyperlinksChanged><AppVersion>16.0000</AppVersion></Properties></pkg:xmlData></pkg:part><pkg:part pkg:name="/docProps/core.xml" pkg:contentType="application/vnd.openxmlformats-package.core-properties+xml" pkg:padding="256"><pkg:xmlData><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title/><dc:subject/><dc:creator>user</dc:creator><cp:keywords/><dc:description/><cp:lastModifiedBy>user</cp:lastModifiedBy><cp:revision>2</cp:revision><dcterms:created xsi:type="dcterms:W3CDTF">2017-10-26T09:10:00Z</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">2017-10-26T09:10:00Z</dcterms:modified></cp:coreProperties></pkg:xmlData></pkg:part><pkg:part pkg:name="/word/styles.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"><pkg:xmlData><w:styles xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" mc:Ignorable="w14 w15 w16se w16cid"><w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Calibri" w:eastAsia="Calibri" w:hAnsi="Calibri" w:cs="Times New Roman"/><w:lang w:val="de-DE" w:eastAsia="de-DE" w:bidi="ar-SA"/></w:rPr></w:rPrDefault><w:pPrDefault/></w:docDefaults><w:latentStyles w:defLockedState="0" w:defUIPriority="99" w:defSemiHidden="0" w:defUnhideWhenUsed="0" w:defQFormat="0" w:count="375"><w:lsdException w:name="Normal" w:uiPriority="0" w:qFormat="1"/><w:lsdException w:name="heading 1" w:uiPriority="9" w:qFormat="1"/><w:lsdException w:name="heading 2" w:semiHidden="1" w:uiPriority="9" w:unhideWhenUsed="1" w:qFormat="1"/><w:lsdException w:name="heading 3" w:semiHidden="1" w:uiPriority="9" w:unhideWhenUsed="1" w:qFormat="1"/><w:lsdException w:name="heading 4" w:semiHidden="1" w:uiPriority="9" w:unhideWhenUsed="1" w:qFormat="1"/><w:lsdException w:name="heading 5" w:semiHidden="1" w:uiPriority="9" w:unhideWhenUsed="1" w:qFormat="1"/><w:lsdException w:name="heading 6" w:semiHidden="1" w:uiPriority="9" w:unhideWhenUsed="1" w:qFormat="1"/><w:lsdException w:name="heading 7" w:semiHidden="1" w:uiPriority="9" w:unhideWhenUsed="1" w:qFormat="1"/><w:lsdException w:name="heading 8" w:semiHidden="1" w:uiPriority="9" w:unhideWhenUsed="1" w:qFormat="1"/><w:lsdException w:name="heading 9" w:semiHidden="1" w:uiPriority="9" w:unhideWhenUsed="1" w:qFormat="1"/><w:lsdException w:name="index 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="index 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="index 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="index 4" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="index 5" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="index 6" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="index 7" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="index 8" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="index 9" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="toc 1" w:semiHidden="1" w:uiPriority="39" w:unhideWhenUsed="1"/><w:lsdException w:name="toc 2" w:semiHidden="1" w:uiPriority="39" w:unhideWhenUsed="1"/><w:lsdException w:name="toc 3" w:semiHidden="1" w:uiPriority="39" w:unhideWhenUsed="1"/><w:lsdException w:name="toc 4" w:semiHidden="1" w:uiPriority="39" w:unhideWhenUsed="1"/><w:lsdException w:name="toc 5" w:semiHidden="1" w:uiPriority="39" w:unhideWhenUsed="1"/><w:lsdException w:name="toc 6" w:semiHidden="1" w:uiPriority="39" w:unhideWhenUsed="1"/><w:lsdException w:name="toc 7" w:semiHidden="1" w:uiPriority="39" w:unhideWhenUsed="1"/><w:lsdException w:name="toc 8" w:semiHidden="1" w:uiPriority="39" w:unhideWhenUsed="1"/><w:lsdException w:name="toc 9" w:semiHidden="1" w:uiPriority="39" w:unhideWhenUsed="1"/><w:lsdException w:name="Normal Indent" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="footnote text" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="annotation text" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="header" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="footer" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="index heading" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="caption" w:semiHidden="1" w:uiPriority="35" w:unhideWhenUsed="1" w:qFormat="1"/><w:lsdException w:name="table of figures" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="envelope address" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="envelope return" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="footnote reference" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="annotation reference" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="line number" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="page number" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="endnote reference" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="endnote text" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="table of authorities" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="macro" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="toa heading" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Bullet" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Number" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List 4" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List 5" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Bullet 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Bullet 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Bullet 4" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Bullet 5" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Number 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Number 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Number 4" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Number 5" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Title" w:uiPriority="10" w:qFormat="1"/><w:lsdException w:name="Closing" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Signature" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Default Paragraph Font" w:semiHidden="1" w:uiPriority="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Body Text" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Body Text Indent" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Continue" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Continue 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Continue 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Continue 4" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="List Continue 5" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Message Header" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Subtitle" w:uiPriority="11" w:qFormat="1"/><w:lsdException w:name="Salutation" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Date" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Body Text First Indent" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Body Text First Indent 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Note Heading" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Body Text 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Body Text 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Body Text Indent 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Body Text Indent 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Block Text" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Hyperlink" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="FollowedHyperlink" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Strong" w:uiPriority="22" w:qFormat="1"/><w:lsdException w:name="Emphasis" w:uiPriority="20" w:qFormat="1"/><w:lsdException w:name="Document Map" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Plain Text" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="E-mail Signature" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Top of Form" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Bottom of Form" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Normal (Web)" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Acronym" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Address" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Cite" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Code" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Definition" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Keyboard" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Preformatted" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Sample" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Typewriter" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="HTML Variable" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Normal Table" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="annotation subject" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="No List" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Outline List 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Outline List 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Outline List 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Simple 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Simple 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Simple 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Classic 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Classic 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Classic 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Classic 4" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Colorful 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Colorful 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Colorful 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Columns 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Columns 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Columns 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Columns 4" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Columns 5" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Grid 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Grid 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Grid 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Grid 4" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Grid 5" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Grid 6" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Grid 7" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Grid 8" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table List 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table List 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table List 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table List 4" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table List 5" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table List 6" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table List 7" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table List 8" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table 3D effects 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table 3D effects 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table 3D effects 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Contemporary" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Elegant" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Professional" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Subtle 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Subtle 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Web 1" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Web 2" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Web 3" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Balloon Text" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Table Grid" w:uiPriority="39"/><w:lsdException w:name="Table Theme" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Placeholder Text" w:semiHidden="1"/><w:lsdException w:name="No Spacing" w:uiPriority="1" w:qFormat="1"/><w:lsdException w:name="Light Shading" w:uiPriority="60"/><w:lsdException w:name="Light List" w:uiPriority="61"/><w:lsdException w:name="Light Grid" w:uiPriority="62"/><w:lsdException w:name="Medium Shading 1" w:uiPriority="63"/><w:lsdException w:name="Medium Shading 2" w:uiPriority="64"/><w:lsdException w:name="Medium List 1" w:uiPriority="65"/><w:lsdException w:name="Medium List 2" w:uiPriority="66"/><w:lsdException w:name="Medium Grid 1" w:uiPriority="67"/><w:lsdException w:name="Medium Grid 2" w:uiPriority="68"/><w:lsdException w:name="Medium Grid 3" w:uiPriority="69"/><w:lsdException w:name="Dark List" w:uiPriority="70"/><w:lsdException w:name="Colorful Shading" w:uiPriority="71"/><w:lsdException w:name="Colorful List" w:uiPriority="72"/><w:lsdException w:name="Colorful Grid" w:uiPriority="73"/><w:lsdException w:name="Light Shading Accent 1" w:uiPriority="60"/><w:lsdException w:name="Light List Accent 1" w:uiPriority="61"/><w:lsdException w:name="Light Grid Accent 1" w:uiPriority="62"/><w:lsdException w:name="Medium Shading 1 Accent 1" w:uiPriority="63"/><w:lsdException w:name="Medium Shading 2 Accent 1" w:uiPriority="64"/><w:lsdException w:name="Medium List 1 Accent 1" w:uiPriority="65"/><w:lsdException w:name="Revision" w:semiHidden="1"/><w:lsdException w:name="List Paragraph" w:uiPriority="34" w:qFormat="1"/><w:lsdException w:name="Quote" w:uiPriority="29" w:qFormat="1"/><w:lsdException w:name="Intense Quote" w:uiPriority="30" w:qFormat="1"/><w:lsdException w:name="Medium List 2 Accent 1" w:uiPriority="66"/><w:lsdException w:name="Medium Grid 1 Accent 1" w:uiPriority="67"/><w:lsdException w:name="Medium Grid 2 Accent 1" w:uiPriority="68"/><w:lsdException w:name="Medium Grid 3 Accent 1" w:uiPriority="69"/><w:lsdException w:name="Dark List Accent 1" w:uiPriority="70"/><w:lsdException w:name="Colorful Shading Accent 1" w:uiPriority="71"/><w:lsdException w:name="Colorful List Accent 1" w:uiPriority="72"/><w:lsdException w:name="Colorful Grid Accent 1" w:uiPriority="73"/><w:lsdException w:name="Light Shading Accent 2" w:uiPriority="60"/><w:lsdException w:name="Light List Accent 2" w:uiPriority="61"/><w:lsdException w:name="Light Grid Accent 2" w:uiPriority="62"/><w:lsdException w:name="Medium Shading 1 Accent 2" w:uiPriority="63"/><w:lsdException w:name="Medium Shading 2 Accent 2" w:uiPriority="64"/><w:lsdException w:name="Medium List 1 Accent 2" w:uiPriority="65"/><w:lsdException w:name="Medium List 2 Accent 2" w:uiPriority="66"/><w:lsdException w:name="Medium Grid 1 Accent 2" w:uiPriority="67"/><w:lsdException w:name="Medium Grid 2 Accent 2" w:uiPriority="68"/><w:lsdException w:name="Medium Grid 3 Accent 2" w:uiPriority="69"/><w:lsdException w:name="Dark List Accent 2" w:uiPriority="70"/><w:lsdException w:name="Colorful Shading Accent 2" w:uiPriority="71"/><w:lsdException w:name="Colorful List Accent 2" w:uiPriority="72"/><w:lsdException w:name="Colorful Grid Accent 2" w:uiPriority="73"/><w:lsdException w:name="Light Shading Accent 3" w:uiPriority="60"/><w:lsdException w:name="Light List Accent 3" w:uiPriority="61"/><w:lsdException w:name="Light Grid Accent 3" w:uiPriority="62"/><w:lsdException w:name="Medium Shading 1 Accent 3" w:uiPriority="63"/><w:lsdException w:name="Medium Shading 2 Accent 3" w:uiPriority="64"/><w:lsdException w:name="Medium List 1 Accent 3" w:uiPriority="65"/><w:lsdException w:name="Medium List 2 Accent 3" w:uiPriority="66"/><w:lsdException w:name="Medium Grid 1 Accent 3" w:uiPriority="67"/><w:lsdException w:name="Medium Grid 2 Accent 3" w:uiPriority="68"/><w:lsdException w:name="Medium Grid 3 Accent 3" w:uiPriority="69"/><w:lsdException w:name="Dark List Accent 3" w:uiPriority="70"/><w:lsdException w:name="Colorful Shading Accent 3" w:uiPriority="71"/><w:lsdException w:name="Colorful List Accent 3" w:uiPriority="72"/><w:lsdException w:name="Colorful Grid Accent 3" w:uiPriority="73"/><w:lsdException w:name="Light Shading Accent 4" w:uiPriority="60"/><w:lsdException w:name="Light List Accent 4" w:uiPriority="61"/><w:lsdException w:name="Light Grid Accent 4" w:uiPriority="62"/><w:lsdException w:name="Medium Shading 1 Accent 4" w:uiPriority="63"/><w:lsdException w:name="Medium Shading 2 Accent 4" w:uiPriority="64"/><w:lsdException w:name="Medium List 1 Accent 4" w:uiPriority="65"/><w:lsdException w:name="Medium List 2 Accent 4" w:uiPriority="66"/><w:lsdException w:name="Medium Grid 1 Accent 4" w:uiPriority="67"/><w:lsdException w:name="Medium Grid 2 Accent 4" w:uiPriority="68"/><w:lsdException w:name="Medium Grid 3 Accent 4" w:uiPriority="69"/><w:lsdException w:name="Dark List Accent 4" w:uiPriority="70"/><w:lsdException w:name="Colorful Shading Accent 4" w:uiPriority="71"/><w:lsdException w:name="Colorful List Accent 4" w:uiPriority="72"/><w:lsdException w:name="Colorful Grid Accent 4" w:uiPriority="73"/><w:lsdException w:name="Light Shading Accent 5" w:uiPriority="60"/><w:lsdException w:name="Light List Accent 5" w:uiPriority="61"/><w:lsdException w:name="Light Grid Accent 5" w:uiPriority="62"/><w:lsdException w:name="Medium Shading 1 Accent 5" w:uiPriority="63"/><w:lsdException w:name="Medium Shading 2 Accent 5" w:uiPriority="64"/><w:lsdException w:name="Medium List 1 Accent 5" w:uiPriority="65"/><w:lsdException w:name="Medium List 2 Accent 5" w:uiPriority="66"/><w:lsdException w:name="Medium Grid 1 Accent 5" w:uiPriority="67"/><w:lsdException w:name="Medium Grid 2 Accent 5" w:uiPriority="68"/><w:lsdException w:name="Medium Grid 3 Accent 5" w:uiPriority="69"/><w:lsdException w:name="Dark List Accent 5" w:uiPriority="70"/><w:lsdException w:name="Colorful Shading Accent 5" w:uiPriority="71"/><w:lsdException w:name="Colorful List Accent 5" w:uiPriority="72"/><w:lsdException w:name="Colorful Grid Accent 5" w:uiPriority="73"/><w:lsdException w:name="Light Shading Accent 6" w:uiPriority="60"/><w:lsdException w:name="Light List Accent 6" w:uiPriority="61"/><w:lsdException w:name="Light Grid Accent 6" w:uiPriority="62"/><w:lsdException w:name="Medium Shading 1 Accent 6" w:uiPriority="63"/><w:lsdException w:name="Medium Shading 2 Accent 6" w:uiPriority="64"/><w:lsdException w:name="Medium List 1 Accent 6" w:uiPriority="65"/><w:lsdException w:name="Medium List 2 Accent 6" w:uiPriority="66"/><w:lsdException w:name="Medium Grid 1 Accent 6" w:uiPriority="67"/><w:lsdException w:name="Medium Grid 2 Accent 6" w:uiPriority="68"/><w:lsdException w:name="Medium Grid 3 Accent 6" w:uiPriority="69"/><w:lsdException w:name="Dark List Accent 6" w:uiPriority="70"/><w:lsdException w:name="Colorful Shading Accent 6" w:uiPriority="71"/><w:lsdException w:name="Colorful List Accent 6" w:uiPriority="72"/><w:lsdException w:name="Colorful Grid Accent 6" w:uiPriority="73"/><w:lsdException w:name="Subtle Emphasis" w:uiPriority="19" w:qFormat="1"/><w:lsdException w:name="Intense Emphasis" w:uiPriority="21" w:qFormat="1"/><w:lsdException w:name="Subtle Reference" w:uiPriority="31" w:qFormat="1"/><w:lsdException w:name="Intense Reference" w:uiPriority="32" w:qFormat="1"/><w:lsdException w:name="Book Title" w:uiPriority="33" w:qFormat="1"/><w:lsdException w:name="Bibliography" w:semiHidden="1" w:uiPriority="37" w:unhideWhenUsed="1"/><w:lsdException w:name="TOC Heading" w:semiHidden="1" w:uiPriority="39" w:unhideWhenUsed="1" w:qFormat="1"/><w:lsdException w:name="Plain Table 1" w:uiPriority="41"/><w:lsdException w:name="Plain Table 2" w:uiPriority="42"/><w:lsdException w:name="Plain Table 3" w:uiPriority="43"/><w:lsdException w:name="Plain Table 4" w:uiPriority="44"/><w:lsdException w:name="Plain Table 5" w:uiPriority="45"/><w:lsdException w:name="Grid Table Light" w:uiPriority="40"/><w:lsdException w:name="Grid Table 1 Light" w:uiPriority="46"/><w:lsdException w:name="Grid Table 2" w:uiPriority="47"/><w:lsdException w:name="Grid Table 3" w:uiPriority="48"/><w:lsdException w:name="Grid Table 4" w:uiPriority="49"/><w:lsdException w:name="Grid Table 5 Dark" w:uiPriority="50"/><w:lsdException w:name="Grid Table 6 Colorful" w:uiPriority="51"/><w:lsdException w:name="Grid Table 7 Colorful" w:uiPriority="52"/><w:lsdException w:name="Grid Table 1 Light Accent 1" w:uiPriority="46"/><w:lsdException w:name="Grid Table 2 Accent 1" w:uiPriority="47"/><w:lsdException w:name="Grid Table 3 Accent 1" w:uiPriority="48"/><w:lsdException w:name="Grid Table 4 Accent 1" w:uiPriority="49"/><w:lsdException w:name="Grid Table 5 Dark Accent 1" w:uiPriority="50"/><w:lsdException w:name="Grid Table 6 Colorful Accent 1" w:uiPriority="51"/><w:lsdException w:name="Grid Table 7 Colorful Accent 1" w:uiPriority="52"/><w:lsdException w:name="Grid Table 1 Light Accent 2" w:uiPriority="46"/><w:lsdException w:name="Grid Table 2 Accent 2" w:uiPriority="47"/><w:lsdException w:name="Grid Table 3 Accent 2" w:uiPriority="48"/><w:lsdException w:name="Grid Table 4 Accent 2" w:uiPriority="49"/><w:lsdException w:name="Grid Table 5 Dark Accent 2" w:uiPriority="50"/><w:lsdException w:name="Grid Table 6 Colorful Accent 2" w:uiPriority="51"/><w:lsdException w:name="Grid Table 7 Colorful Accent 2" w:uiPriority="52"/><w:lsdException w:name="Grid Table 1 Light Accent 3" w:uiPriority="46"/><w:lsdException w:name="Grid Table 2 Accent 3" w:uiPriority="47"/><w:lsdException w:name="Grid Table 3 Accent 3" w:uiPriority="48"/><w:lsdException w:name="Grid Table 4 Accent 3" w:uiPriority="49"/><w:lsdException w:name="Grid Table 5 Dark Accent 3" w:uiPriority="50"/><w:lsdException w:name="Grid Table 6 Colorful Accent 3" w:uiPriority="51"/><w:lsdException w:name="Grid Table 7 Colorful Accent 3" w:uiPriority="52"/><w:lsdException w:name="Grid Table 1 Light Accent 4" w:uiPriority="46"/><w:lsdException w:name="Grid Table 2 Accent 4" w:uiPriority="47"/><w:lsdException w:name="Grid Table 3 Accent 4" w:uiPriority="48"/><w:lsdException w:name="Grid Table 4 Accent 4" w:uiPriority="49"/><w:lsdException w:name="Grid Table 5 Dark Accent 4" w:uiPriority="50"/><w:lsdException w:name="Grid Table 6 Colorful Accent 4" w:uiPriority="51"/><w:lsdException w:name="Grid Table 7 Colorful Accent 4" w:uiPriority="52"/><w:lsdException w:name="Grid Table 1 Light Accent 5" w:uiPriority="46"/><w:lsdException w:name="Grid Table 2 Accent 5" w:uiPriority="47"/><w:lsdException w:name="Grid Table 3 Accent 5" w:uiPriority="48"/><w:lsdException w:name="Grid Table 4 Accent 5" w:uiPriority="49"/><w:lsdException w:name="Grid Table 5 Dark Accent 5" w:uiPriority="50"/><w:lsdException w:name="Grid Table 6 Colorful Accent 5" w:uiPriority="51"/><w:lsdException w:name="Grid Table 7 Colorful Accent 5" w:uiPriority="52"/><w:lsdException w:name="Grid Table 1 Light Accent 6" w:uiPriority="46"/><w:lsdException w:name="Grid Table 2 Accent 6" w:uiPriority="47"/><w:lsdException w:name="Grid Table 3 Accent 6" w:uiPriority="48"/><w:lsdException w:name="Grid Table 4 Accent 6" w:uiPriority="49"/><w:lsdException w:name="Grid Table 5 Dark Accent 6" w:uiPriority="50"/><w:lsdException w:name="Grid Table 6 Colorful Accent 6" w:uiPriority="51"/><w:lsdException w:name="Grid Table 7 Colorful Accent 6" w:uiPriority="52"/><w:lsdException w:name="List Table 1 Light" w:uiPriority="46"/><w:lsdException w:name="List Table 2" w:uiPriority="47"/><w:lsdException w:name="List Table 3" w:uiPriority="48"/><w:lsdException w:name="List Table 4" w:uiPriority="49"/><w:lsdException w:name="List Table 5 Dark" w:uiPriority="50"/><w:lsdException w:name="List Table 6 Colorful" w:uiPriority="51"/><w:lsdException w:name="List Table 7 Colorful" w:uiPriority="52"/><w:lsdException w:name="List Table 1 Light Accent 1" w:uiPriority="46"/><w:lsdException w:name="List Table 2 Accent 1" w:uiPriority="47"/><w:lsdException w:name="List Table 3 Accent 1" w:uiPriority="48"/><w:lsdException w:name="List Table 4 Accent 1" w:uiPriority="49"/><w:lsdException w:name="List Table 5 Dark Accent 1" w:uiPriority="50"/><w:lsdException w:name="List Table 6 Colorful Accent 1" w:uiPriority="51"/><w:lsdException w:name="List Table 7 Colorful Accent 1" w:uiPriority="52"/><w:lsdException w:name="List Table 1 Light Accent 2" w:uiPriority="46"/><w:lsdException w:name="List Table 2 Accent 2" w:uiPriority="47"/><w:lsdException w:name="List Table 3 Accent 2" w:uiPriority="48"/><w:lsdException w:name="List Table 4 Accent 2" w:uiPriority="49"/><w:lsdException w:name="List Table 5 Dark Accent 2" w:uiPriority="50"/><w:lsdException w:name="List Table 6 Colorful Accent 2" w:uiPriority="51"/><w:lsdException w:name="List Table 7 Colorful Accent 2" w:uiPriority="52"/><w:lsdException w:name="List Table 1 Light Accent 3" w:uiPriority="46"/><w:lsdException w:name="List Table 2 Accent 3" w:uiPriority="47"/><w:lsdException w:name="List Table 3 Accent 3" w:uiPriority="48"/><w:lsdException w:name="List Table 4 Accent 3" w:uiPriority="49"/><w:lsdException w:name="List Table 5 Dark Accent 3" w:uiPriority="50"/><w:lsdException w:name="List Table 6 Colorful Accent 3" w:uiPriority="51"/><w:lsdException w:name="List Table 7 Colorful Accent 3" w:uiPriority="52"/><w:lsdException w:name="List Table 1 Light Accent 4" w:uiPriority="46"/><w:lsdException w:name="List Table 2 Accent 4" w:uiPriority="47"/><w:lsdException w:name="List Table 3 Accent 4" w:uiPriority="48"/><w:lsdException w:name="List Table 4 Accent 4" w:uiPriority="49"/><w:lsdException w:name="List Table 5 Dark Accent 4" w:uiPriority="50"/><w:lsdException w:name="List Table 6 Colorful Accent 4" w:uiPriority="51"/><w:lsdException w:name="List Table 7 Colorful Accent 4" w:uiPriority="52"/><w:lsdException w:name="List Table 1 Light Accent 5" w:uiPriority="46"/><w:lsdException w:name="List Table 2 Accent 5" w:uiPriority="47"/><w:lsdException w:name="List Table 3 Accent 5" w:uiPriority="48"/><w:lsdException w:name="List Table 4 Accent 5" w:uiPriority="49"/><w:lsdException w:name="List Table 5 Dark Accent 5" w:uiPriority="50"/><w:lsdException w:name="List Table 6 Colorful Accent 5" w:uiPriority="51"/><w:lsdException w:name="List Table 7 Colorful Accent 5" w:uiPriority="52"/><w:lsdException w:name="List Table 1 Light Accent 6" w:uiPriority="46"/><w:lsdException w:name="List Table 2 Accent 6" w:uiPriority="47"/><w:lsdException w:name="List Table 3 Accent 6" w:uiPriority="48"/><w:lsdException w:name="List Table 4 Accent 6" w:uiPriority="49"/><w:lsdException w:name="List Table 5 Dark Accent 6" w:uiPriority="50"/><w:lsdException w:name="List Table 6 Colorful Accent 6" w:uiPriority="51"/><w:lsdException w:name="List Table 7 Colorful Accent 6" w:uiPriority="52"/><w:lsdException w:name="Mention" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Smart Hyperlink" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Hashtag" w:semiHidden="1" w:unhideWhenUsed="1"/><w:lsdException w:name="Unresolved Mention" w:semiHidden="1" w:unhideWhenUsed="1"/></w:latentStyles><w:style w:type="paragraph" w:default="1" w:styleId="Standard"><w:name w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:after="160" w:line="259" w:lineRule="auto"/></w:pPr><w:rPr><w:sz w:val="22"/><w:szCs w:val="22"/><w:lang w:eastAsia="en-US"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="berschrift1"><w:name w:val="heading 1"/><w:basedOn w:val="Standard"/><w:next w:val="Standard"/><w:link w:val="berschrift1Zchn"/><w:uiPriority w:val="9"/><w:qFormat/><w:rsid w:val="00601692"/><w:pPr><w:keepNext/><w:keepLines/><w:spacing w:before="240" w:after="0"/><w:outlineLvl w:val="0"/></w:pPr><w:rPr><w:rFonts w:ascii="Calibri Light" w:eastAsia="Times New Roman" w:hAnsi="Calibri Light"/><w:color w:val="2F5496"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr></w:style><w:style w:type="character" w:default="1" w:styleId="Absatz-Standardschriftart"><w:name w:val="Default Paragraph Font"/><w:uiPriority w:val="1"/><w:semiHidden/><w:unhideWhenUsed/></w:style><w:style w:type="table" w:default="1" w:styleId="NormaleTabelle"><w:name w:val="Normal Table"/><w:uiPriority w:val="99"/><w:semiHidden/><w:unhideWhenUsed/><w:tblPr><w:tblInd w:w="0" w:type="dxa"/><w:tblCellMar><w:top w:w="0" w:type="dxa"/><w:left w:w="108" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="108" w:type="dxa"/></w:tblCellMar></w:tblPr></w:style><w:style w:type="numbering" w:default="1" w:styleId="KeineListe"><w:name w:val="No List"/><w:uiPriority w:val="99"/><w:semiHidden/><w:unhideWhenUsed/></w:style><w:style w:type="character" w:customStyle="1" w:styleId="berschrift1Zchn"><w:name w:val="Überschrift 1 Zchn"/><w:link w:val="berschrift1"/><w:uiPriority w:val="9"/><w:rsid w:val="00601692"/><w:rPr><w:rFonts w:ascii="Calibri Light" w:eastAsia="Times New Roman" w:hAnsi="Calibri Light" w:cs="Times New Roman"/><w:color w:val="2F5496"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr></w:style></w:styles></pkg:xmlData></pkg:part></pkg:package>
0 4 \ No newline at end of file
... ...
tests/test-data/rtfobj/issue_185.rtf 0 → 100644
  1 +{\rt{\object\objautlink\objupdate\rsltpict\objw37542\objh829\objscalex59286\objscaley86308{\*\objclass \'77}{\*\objdata 32\bin6 FF}}}
0 2 \ No newline at end of file
... ...
tests/test_utils/__init__.py 0 → 100644
  1 +from .output_capture import OutputCapture
  2 +
  3 +from os.path import dirname, join
  4 +
  5 +# Directory with test data, independent of current working directory
  6 +DATA_BASE_DIR = join(dirname(dirname(__file__)), 'test-data')
... ...
tests/test_utils/output_capture.py 0 → 100644
  1 +""" class OutputCapture to test what scripts print to stdout """
  2 +
  3 +from __future__ import print_function
  4 +import sys
  5 +
  6 +
  7 +# python 2/3 version conflict:
  8 +if sys.version_info.major <= 2:
  9 + from StringIO import StringIO
  10 +else:
  11 + from io import StringIO
  12 +
  13 +class OutputCapture:
  14 + """ context manager that captures stdout
  15 +
  16 + use as follows::
  17 +
  18 + with OutputCapture() as capturer:
  19 + run_my_script(some_args)
  20 +
  21 + # either test line-by-line ...
  22 + for line in capturer:
  23 + some_test(line)
  24 + # ...or test all output in one go
  25 + some_test(capturer.buffer.getvalue())
  26 +
  27 + """
  28 +
  29 + def __init__(self):
  30 + self.buffer = StringIO()
  31 + self.orig_stdout = None
  32 +
  33 + def __enter__(self):
  34 + # replace sys.stdout with own buffer.
  35 + self.orig_stdout = sys.stdout
  36 + sys.stdout = self.buffer
  37 + return self
  38 +
  39 + def __exit__(self, exc_type, exc_value, traceback):
  40 + sys.stdout = self.orig_stdout # re-set to original
  41 +
  42 + if exc_type: # there has been an error
  43 + print('Got error during output capture!')
  44 + print('Print captured output and re-raise:')
  45 + for line in self.buffer.getvalue().splitlines():
  46 + print(line.rstrip()) # print output before re-raising
  47 +
  48 + def __iter__(self):
  49 + for line in self.buffer.getvalue().splitlines():
  50 + yield line.rstrip() # remove newline at end of line
... ...
tests/test_utils/testdata_reader.py 0 → 100644
  1 +import os
  2 +from os.path import dirname, abspath, normpath, join
  3 +from . import DATA_BASE_DIR
  4 +
  5 +
  6 +def read(relative_path):
  7 + with open(join(DATA_BASE_DIR, relative_path), 'rb') as file_handle:
  8 + return file_handle.read()
... ...
tests/unittest_template.py 0 → 100644
  1 +""" Test my new feature
  2 +
  3 +Some more info if you want
  4 +
  5 +Should work with python2 and python3!
  6 +"""
  7 +
  8 +import unittest
  9 +
  10 +# if you need data from oletools/test-data/DIR/, uncomment these lines:
  11 +#from os.path import join, dirname, normpath
  12 +#Directory with test data, independent of current working directory
  13 +#DATA_DIR = normpath(join(dirname(__file__), '..', 'test-data', 'DIR'))
  14 +
  15 +
  16 +class TestMyFeature(unittest.TestCase):
  17 + """ Tests my cool new feature """
  18 +
  19 + def test_this(self):
  20 + """ check that this works """
  21 + pass # your code here
  22 +
  23 + def test_that(self):
  24 + """ check that that also works """
  25 + pass # your code here
  26 +
  27 + def helper_function(self, filename):
  28 + """ to be called from other test functions to avoid copy-and-paste
  29 +
  30 + this is not called by unittest directly, only from your functions """
  31 + pass # your code here
  32 + # e.g.: msodde.main(join(DATA_DIR, filename))
  33 +
  34 +
  35 +# just in case somebody calls this file as a script
  36 +if __name__ == '__main__':
  37 + unittest.main()
... ...