THE PDFALYZER
A PDF analysis tool for visualizing the inner tree-like data structure1 of a PDF in spectacularly large and colorful diagrams as well as scanning the binary streams embedded in the PDF for hidden potentially malicious content. The Pdfalyzer makes heavy use of YARA (via The Yaralyzer) for matching/extracting byte patterns.
PyPi Users: This document renders a lot better on GitHub. Pictures, footnotes, etc.
Quick Start
pipx install pdfalyzer
pdfalyze "martin_heidegger-being_illmatic.pdf"
What It Do
- Generate in depth visualizations of PDF tree structures1. Shows every property of every PDF object at a glance. See the Example Output section below for details.
- Scan for mad sus content with a bunch of PDF specific YARA rules.
- Forcibly decode suspect bytes. The Yaralyzer does the heavy lifting.
- Display detailed information about embedded fonts. With character maps.
- Usable as a library for your own PDF related code.
If you're looking for one of these things this may be the tool for you.
What It Don't Do
This tool is mostly for examining/working with a PDF's data and logical structure. As such it doesn't have much to offer as far as extracting text, rendering2, writing, etc. etc.
Installation
Installation with pipx3 is preferred though pip3
/ pip
should also work.
pipx install pdfalyzer
See PyPDF2 installation notes about PyCryptodome
if you plan to pdfalyze
any files that use AES encryption.
If you are on macOS someone out there was kind enough to make The Pdfalyzer available via homebrew so brew install pdfalyzer
should work.
Troubleshooting
- If you used
pip3
instead of pipx
and have an issue you should try to install with pipx
. - If you run into an issue about missing YARA try to install yara-python.
- If you encounter an error building the python
cryptography
package check your pip
version (pip --version
). If it's less than 22.0, upgrade pip
with pip install --upgrade pip
. - If you get a YARA internal error number you can look up what it actually means here.
- If you can't get the
pdfalyze
command to work try python -m pdfalyzer
. It's an equivalent but more portable version of the same command that does not rely on your python script paths being set up in a sane way. - While The Pdfalyzer has been tested on quite a few large and very complicated PDFs there are no doubt a bunch of edge cases that will trip up the code. Sifting through the various interconnected internal PDF objects and building the correct tree representation is much, much harder than it should be and requires multiple scans and a little bit of educated guessing. If a PDF fails to parse and you hit an error please open a GitHub issue with the compressed (
.zip
, .gz
, whatever) PDF that is causing the problem attached (if possible) and I'll take a look when I can. I will not take a look at any uncompressed PDFs due to the security risks so make sure you zip it before you ship it. - On Linux if you encounter an error building
wheel
or cffi
you may need to install some packages:
sudo apt-get install build-essential libssl-dev libffi-dev rustc
Usage
Run pdfalyze --help
to see usage instructions. As of right now these are the options:
Runtime Options
If you provide none of the flags in the ANALYSIS SELECTION
section of the --help
then all of the analyses will be done except the --streams
. In other words, these two commands are equivalent:
pdfalyze lacan_buys_the_dip.pdf
pdfalyze lacan_buys_the_dip.pdf -d -t -r -f -y -c
The --streams
output is the one used to hunt for patterns in the embedded bytes and can be extremely verbose depending on the --quote-char
options chosen (or not chosen) and contents of the PDF. The Yaralyzer handles this task; if you want to hunt for patterns in the bytes other than bytes surrounded by backticks/frontslashes/brackets/quotes/etc. you may want to use The Yaralyzer directly. As The Yaralyzer is a prequisite for The Pdfalyzer you may already have the yaralyze
command installed and available.
Setting Command Line Options Permanently With A .pdfalyzer
File
When you run pdfalyze
on some PDF the tool will check for a file called .pdfalyzer
first in the current directory and then in the home directory. If it finds a file in either such place it will load configuration options from it. Documentation on the options that can be configured with these files lives in .pdfalyzer.example
which doubles as an example file you can copy into place and edit to your needs. Handy if you find yourself typing the same command line options over and over again.
Environment Variables
Even if you don't configure your own .pdfalyzer
file you may still glean some insight from reading the descriptions of the various variables in .pdfalyzer.example
; there's a little more exposition there than in the output of pdfalyze -h
.
Colors And Themes
Run pdfalyzer_show_color_theme
to see the color theme employed.
Guarantees
Warnings will be printed if any PDF object ID between 1 and the /Size
reported by the PDF itself could not be successfully placed in the tree. If you do not get any warnings then all4 of the inner PDF objects should be seen in the output.
Example Usage
BUFFERZONE Team posted an excellent example of how one might use The Pdfalyzer in tandem with Didier Stevens' PDF tools to investigate a potentially malicious PDF (archived in the doc/
dir in this repo if the link rots).
Use As A Code Library
For info about setting up a dev environment see Contributing below.
At its core The Pdfalyzer is taking PDF internal objects gathered by PyPDF2 and wrapping them in AnyTree's NodeMixin
class. Given that things like searching the tree or accessing internal PDF properties will be done through those packages' code it may be helpful to review their documentation.
As far as The Pdfalyzer's unique functionality goes, Pdfalyzer
is the class at the heart of the operation. It holds the PDF's logical tree as well as a few other data structures. Chief among these are the FontInfo
class which pulls together various properties of a font strewn across 3 or 4 different PDF objects and the BinaryScanner
class which lets you dig through the embedded streams' bytes looking for suspicious patterns.
Here's a short intro to how to access these objects:
from pdfalyzer.pdfalyzer import Pdfalyzer
pdfalyzer = Pdfalyzer("/path/to/the/evil_or_non_evil.pdf")
actual_pdf_tree: PdfTreeNode = pdfalyzer.pdf_tree
from pdfalyzer.output.pdfalyzer_presenter import PdfalyzerPresenter
PdfalyzerPresenter(pdfalyzer).print_everything()
for node in pdfalyzer.node_iterator():
do_stuff(node)
for font in pdfalyzer.font_infos:
do_stuff(font)
for node in pdfalyzer.stream_nodes():
do_stuff(node.stream_data)
node = pdfalyzer.find_node_by_idnum(44)
pdf_object: PdfObject = node.obj
from anytree.search import findall_by_attr
page_nodes = findall_by_attr(pdfalyzer.pdf_tree, name='type', value='/Page')
font_info: FontInfo = pdfalyzer.font_infos[0]
for backtick_quoted_string in font.binary_scanner.extract_backtick_quoted_bytes():
do_stuff(backtick_quoted_string)
Example Output
The Pdfalyzer can export visualizations to HTML, ANSI colored text, and SVG images using the file export functionality that comes with Rich. SVGs can be turned into png
format images with a tool like Inkscape or cairosvg
(Inkscape works a lot better in our experience). See pdfalyze --help
for the specifics.
Basic Tree View
As you can see the suspicious /OpenAction
relationship is highlighted bright red, as would be a couple of other sus PDF instructions like /JavaScript
or /AcroForm
if they exist in the PDF being pdfalyzed.
The dimmer (as in "harder to see") nodes5 marked with Non Child Reference
give you a way to visualize the relationships between PDF objects that exist outside of the tree structure's parent/child relationships.
That's a pretty basic document. Here's the basic tree for a more complicated PDF containing an NMAP cheat sheet.
Rich Tree View
This image shows a more in-depth view of of the PDF tree for the same document shown above. This tree (AKA the "rich" tree) has almost everything. Shows all PDF object properties, all relationships between objects, and sizable previews of any binary data streams embedded or encrypted in the document. Note that in addition to /OpenAction
, the Adobe Type1 font binary is also red (Google's project zero regards any Adobe Type1 font as "mad sus").
And here's the rich tree for the same more complicated NMAP cheat sheet PDF linked instead of shown directly in the previous section.
Fonts
View the properties of the fonts in the PDF. Comes with a preview of the beginning and end of the font's raw binary data stream (if it's that kind of font).
Binary Analysis (And Lots Of It)
YARA Scan: Check PDF for malicious content.
This repo contains all the PDF specific YARA rules I could dig up that identify byte patterns indicating a PDF may have been modified for evil. While the real dangers exist in PDF modifications no one has seen yet (and thus we have no YARA rules for), there are still a bunch of 'sus' indicators (e.g. has a Type1 font, is incorrectly terminated, etc.) that will be tripped when you scan a PDF with The Pdfalyzer.
Search Internal Binary Data for Sus Content No Malware Scanner Will Catch6:
Things like, say, a hidden binary /F
(PDF instruction meaning "URL") followed by a JS
(I'll let you guess what "JS" stands for) and then a binary »
character (AKA "the character the PDF specification uses to close a section of the PDF's logical structure"). Put all that together and it says that you're looking at a secret JavaScript instruction embedded in the encrypted part of a font binary. A secret instruction that causes the PDF renderer to pop out of its frame prematurely as it renders the font.
Extract And Decode Binary Patterns: Like, say, bytes between common regular expression markers that you might want to force a decode of in a lot of different encodings.
See stats: When all is said and done you can see some stats that may help you figure out what the character encoding may or may not be for the bytes matched by those patterns:
Fancy Table To Tell You What The chardet
Library Would Rank As The Most Likely Encoding For A Chunk Of Binary Data
PDF Resources
Included PDF Tools
The Pdfalyzer ships with a command line tool combine_pdfs
that combines multiple PDFs into a single PDF. Run combine_pdfs --help
to see the options.
3rd Party PDF Tools
Installing Didier Stevens's PDF Analysis Tools
Stevens's tools provide comprehensive info about the contents of a PDF, are guaranteed not to trigger the rendering of any malicious content (especially pdfid.py
), and have been battle tested for well over a decade. It would probably be a good idea to analyze your PDF with his tools before you start working with this one.
If you're lazy and don't want to retrieve his tools yourself there's a simple bash script to download them from his github repo and place them in a tools/
subdirectory off the project root. Just run this:
scripts/install_didier_stevens_pdf_tools.sh
If there is a discrepancy between the output of betweeen his tools and this one you should assume his tool is correct and The Pdfalyzer is wrong until you conclusively prove otherwise.
Installing The t1utils
Font Suite
t1utils
is a suite of old but battle tested apps for manipulating old Adobe font formats. You don't need it unless you're dealing with an older Type 1 or Type 2 font binary but given that those have been very popular exploit vectors in the past few years it can be extremely helpful. One of the tools in the suite, t1disasm
, is particularly useful because it decrypts and decompiles Adobe Type 1 font binaries into a more human readable string representation.
There's a script to help you install the suite if you need it:
scripts/install_t1utils.sh
External Documentation
Official Adobe Documentation
- Official Adobe PDF 1.7 Specification - Indispensable map when navigating a PDF forest.
- Adobe Type 1 Font Format Specification - Official spec for Adobe's original font description language and file format. Useful if you have suspicions about malicious fonts. Type1 seems to be the attack vector of choice recently which isn't so surprising when you consider that it's a 30 year old technology and the code that renders these fonts probably hasn't been extensively tested in decades because almost no one uses them anymore outside of people who want to use them as attack vectors.
- Adobe CMap and CIDFont Files Specification - Official spec for the character mappings used by Type1 fonts / basically part of the overall Type1 font specification.
- Adobe Type 2 Charstring Format - Describes the newer Type 2 font operators which are also used in some multiple-master Type 1 fonts.
Other Stuff
Did The World Really Need Another PDF Tool?
This tool was built to fill a gap in the PDF assessment landscape following my own recent experience trying to find malicious content in a PDF file. Didier Stevens's pdfid.py and pdf-parser.py are still the best game in town when it comes to PDF analysis tools but they lack in the visualization department and also don't give you much to work with as far as giving you a data model you can write your own code around. Peepdf seemed promising but turned out to be in a buggy, out of date, and more or less unfixable state. And neither of them offered much in the way of tooling for embedded binary analysis.
Thus I felt the world might be slightly improved if I strung together a couple of more stable/well known/actively maintained open source projects (AnyTree, PyPDF2, Rich, and YARA via The Yaralyzer) into this tool.
Contributing
One easy way of contributing is to run the script to test against all the PDFs in your ~/Documents
folder and report any issues.
Beyond that see CONTRIBUTING.md.
Code Glossary
These are the naming conventions at play in The Pdfalyzer code base:
Term | Meaning |
---|
PDF Object | Instance of a PyPDF2 class that represents the information stored in the PDF binary between open and close guillemet quotes (« and ») |
reference_key | String found in a PDF object that names a property (e.g. /BaseFont or /Subtype ) |
reference | Link from a PDF object to another node. Outward facing relationships, basically. |
address | reference_key plus a hash key or numerical array index if that's how the reference works. e.g. if node A has a reference key /Resources pointing to a dict {'/Font2': [IndirectObject(55), IndirectObject(2)]} the address of IndirectObject(55) from node A would be /Resources[/Font2][0] |
tree_address | Like the address but starting at the root of the tree. Describes the series of objects you would step through if you walked the tree from the root to this node. |
relationship | Any link between nodes created by addresses/reference keys |
non_tree_relationship | any link between nodes that is not considered a parent/child tree relationship |
indeterminate_node | any node whose place in the tree cannot be decided until every node has been seen |
link_node | nodes like /Dest that just contain a pointer to another node |
Reference
TODO