Commit d64c474309c8d6cef972da51053ce3c63301f5dc
Committed by
GitHub
Merge branch 'master' into master
Showing
65 changed files
with
2463 additions
and
677 deletions
.travis.yml
0 → 100644
README.md
| 1 | 1 | python-oletools |
| 2 | 2 | =============== |
| 3 | +[](https://pypi.python.org/pypi/oletools) | |
| 4 | +[](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, +, &, 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 "Single File Web Page" (.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 "<strong>pip install oletools</strong>" or "<strong>easy_install oletools</strong>" to download and install in one go. Otherwise you may download/extract the zip archive and run "<strong>setup.py install</strong>".</p> | |
| 68 | -<p><strong>Important: to update oletools</strong> if it is already installed, you must run <strong>"pip install -U oletools"</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
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
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)</code></pre> |
| 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">'Indicator id=</span><span class="ot">%s</span><span class="st"> name="</span><span class="ot">%s</span><span class="st">" type=</span><span class="ot">%s</span><span class="st"> value=</span><span class="ot">%s</span><span class="st">'</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">'description:'</span>, i.description | |
| 97 | - <span class="dt">print</span> <span class="st">''</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">'Indicator id=</span><span class="sc">%s</span><span class="st"> name="</span><span class="sc">%s</span><span class="st">" type=</span><span class="sc">%s</span><span class="st"> value=</span><span class="sc">%s</span><span class="st">'</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">'description:'</span>, i.description | |
| 115 | + <span class="bu">print</span> <span class="st">''</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
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</code></pre> |
| 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">'my_file_with_macros.doc'</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">'my_file_with_macros.doc'</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">'my_file_with_macros.doc'</span> | |
| 228 | -filedata = <span class="dt">open</span>(myfile, <span class="st">'rb'</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">'my_file_with_macros.doc'</span> | |
| 246 | +filedata <span class="op">=</span> <span class="bu">open</span>(myfile, <span class="st">'rb'</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">'VBA Macros found'</span> | |
| 236 | -<span class="kw">else</span>: | |
| 237 | - <span class="dt">print</span> <span class="st">'No VBA Macros found'</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">'VBA Macros found'</span> | |
| 254 | +<span class="cf">else</span>: | |
| 255 | + <span class="bu">print</span> <span class="st">'No VBA Macros found'</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)</code></pre> |
| 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">'-'</span>*<span class="dv">79</span> | |
| 251 | - <span class="dt">print</span> <span class="st">'Filename :'</span>, filename | |
| 252 | - <span class="dt">print</span> <span class="st">'OLE stream :'</span>, stream_path | |
| 253 | - <span class="dt">print</span> <span class="st">'VBA filename:'</span>, vba_filename | |
| 254 | - <span class="dt">print</span> <span class="st">'- '</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">'-'</span><span class="op">*</span><span class="dv">79</span> | |
| 269 | + <span class="bu">print</span> <span class="st">'Filename :'</span>, filename | |
| 270 | + <span class="bu">print</span> <span class="st">'OLE stream :'</span>, stream_path | |
| 271 | + <span class="bu">print</span> <span class="st">'VBA filename:'</span>, vba_filename | |
| 272 | + <span class="bu">print</span> <span class="st">'- '</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)</code></pre> |
| 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">'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">'</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">'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">'</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">'AutoExec keywords: </span><span class="ot">%d</span><span class="st">'</span> % vbaparser.nb_autoexec | |
| 273 | -<span class="dt">print</span> <span class="st">'Suspicious keywords: </span><span class="ot">%d</span><span class="st">'</span> % vbaparser.nb_suspicious | |
| 274 | -<span class="dt">print</span> <span class="st">'IOCs: </span><span class="ot">%d</span><span class="st">'</span> % vbaparser.nb_iocs | |
| 275 | -<span class="dt">print</span> <span class="st">'Hex obfuscated strings: </span><span class="ot">%d</span><span class="st">'</span> % vbaparser.nb_hexstrings | |
| 276 | -<span class="dt">print</span> <span class="st">'Base64 obfuscated strings: </span><span class="ot">%d</span><span class="st">'</span> % vbaparser.nb_base64strings | |
| 277 | -<span class="dt">print</span> <span class="st">'Dridex obfuscated strings: </span><span class="ot">%d</span><span class="st">'</span> % vbaparser.nb_dridexstrings | |
| 278 | -<span class="dt">print</span> <span class="st">'VBA obfuscated strings: </span><span class="ot">%d</span><span class="st">'</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">'AutoExec keywords: </span><span class="sc">%d</span><span class="st">'</span> <span class="op">%</span> vbaparser.nb_autoexec | |
| 291 | +<span class="bu">print</span> <span class="st">'Suspicious keywords: </span><span class="sc">%d</span><span class="st">'</span> <span class="op">%</span> vbaparser.nb_suspicious | |
| 292 | +<span class="bu">print</span> <span class="st">'IOCs: </span><span class="sc">%d</span><span class="st">'</span> <span class="op">%</span> vbaparser.nb_iocs | |
| 293 | +<span class="bu">print</span> <span class="st">'Hex obfuscated strings: </span><span class="sc">%d</span><span class="st">'</span> <span class="op">%</span> vbaparser.nb_hexstrings | |
| 294 | +<span class="bu">print</span> <span class="st">'Base64 obfuscated strings: </span><span class="sc">%d</span><span class="st">'</span> <span class="op">%</span> vbaparser.nb_base64strings | |
| 295 | +<span class="bu">print</span> <span class="st">'Dridex obfuscated strings: </span><span class="sc">%d</span><span class="st">'</span> <span class="op">%</span> vbaparser.nb_dridexstrings | |
| 296 | +<span class="bu">print</span> <span class="st">'VBA obfuscated strings: </span><span class="sc">%d</span><span class="st">'</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)</code></pre> |
| 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">'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">'</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">'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">'</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">'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">'</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">'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">'</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">'Auto-executable macro keywords found:'</span> | |
| 318 | - <span class="kw">for</span> keyword, description in autoexec_keywords: | |
| 319 | - <span class="dt">print</span> <span class="st">'</span><span class="ot">%s</span><span class="st">: </span><span class="ot">%s</span><span class="st">'</span> % (keyword, description) | |
| 320 | -<span class="kw">else</span>: | |
| 321 | - <span class="dt">print</span> <span class="st">'Auto-executable macro keywords: None found'</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">'Auto-executable macro keywords found:'</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">'</span><span class="sc">%s</span><span class="st">: </span><span class="sc">%s</span><span class="st">'</span> <span class="op">%</span> (keyword, description) | |
| 338 | +<span class="cf">else</span>: | |
| 339 | + <span class="bu">print</span> <span class="st">'Auto-executable macro keywords: None found'</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">'Suspicious VBA keywords found:'</span> | |
| 331 | - <span class="kw">for</span> keyword, description in suspicious_keywords: | |
| 332 | - <span class="dt">print</span> <span class="st">'</span><span class="ot">%s</span><span class="st">: </span><span class="ot">%s</span><span class="st">'</span> % (keyword, description) | |
| 333 | -<span class="kw">else</span>: | |
| 334 | - <span class="dt">print</span> <span class="st">'Suspicious VBA keywords: None found'</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">'Suspicious VBA keywords found:'</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">'</span><span class="sc">%s</span><span class="st">: </span><span class="sc">%s</span><span class="st">'</span> <span class="op">%</span> (keyword, description) | |
| 351 | +<span class="cf">else</span>: | |
| 352 | + <span class="bu">print</span> <span class="st">'Suspicious VBA keywords: None found'</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">'Patterns found:'</span> | |
| 344 | - <span class="kw">for</span> pattern_type, value in patterns: | |
| 345 | - <span class="dt">print</span> <span class="st">'</span><span class="ot">%s</span><span class="st">: </span><span class="ot">%s</span><span class="st">'</span> % (pattern_type, value) | |
| 346 | -<span class="kw">else</span>: | |
| 347 | - <span class="dt">print</span> <span class="st">'Patterns: None found'</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">'Patterns found:'</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">'</span><span class="sc">%s</span><span class="st">: </span><span class="sc">%s</span><span class="st">'</span> <span class="op">%</span> (pattern_type, value) | |
| 364 | +<span class="cf">else</span>: | |
| 365 | + <span class="bu">print</span> <span class="st">'Patterns: None found'</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">"myfile.rtf"</span>): | |
| 72 | - <span class="dt">print</span>(<span class="st">'found object size </span><span class="ot">%d</span><span class="st"> at index </span><span class="ot">%08X</span><span class="st">'</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">"myfile.rtf"</span>): | |
| 91 | + <span class="bu">print</span>(<span class="st">'found object size </span><span class="sc">%d</span><span class="st"> at index </span><span class="sc">%08X</span><span class="st">'</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__ = '0.51' |
| 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__ = '0.50py3' |
| 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 = 'Please report this issue on %s' % 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__ = '0.02' |
| 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__ = '0.50' |
| 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__ = '0.51' |
| 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__ = '0.51' |
| 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__ = '0.51a' |
| 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='utf8', errors='replace'): |
| 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__ = '0.51a' |
| 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='utf8', errors='replace'): |
| 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__ = '0.50' |
| 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__ = '0.51' |
| 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'([a-zA-Z]{1,250})' |
| 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__ = '0.44' |
| 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__ = '0.44' |
| 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__ = '0.44' |
| 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__ = '0.44' |
| 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__ = '0.44' |
| 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__ = '0.44' |
| 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__ == "__main__": |
| 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
tests/test_utils/__init__.py
0 → 100644
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
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() | ... | ... |