Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix object stream parsing #9

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

michaelweiser
Copy link

Commit 8cc27b6 broke object stream parsing by resetting the content
cursor PDFParser.charCounter to zero on every invocation. Unfortunately it's unclear from the change what this was fixing at the time but it certainly broke object stream parsing. Reproducer:

$ echo -e "create pdf\ncreate object_stream\nall\nsave /tmp/foo.pdf" | \
	peepdf -i

Without fix:

$ peepdf -j /tmp/foo.pdf
Error: An error has occurred while parsing an indirect object!!

With this change: JSON output as expected (same for other outputs).

$ peepdf -j /tmp/foo.pdf
{
    "peepdf_analysis": {
[...]
           "version": "0.3"
        }
    }
}

Given the object created by above call in local variable content:

<< /Length 131
/N 3
/Type /ObjStm
/Filter /FlateDecode
/First 14 >>
stream
x<9c>M<8c>[...]
endstream

at https://github.com/jbremer/peepdf/blob/11dab2ead9bf344e6aa8cca7f82a17771350f1be/peepdf/PDFCore.py#L7873 self.charCounter points to the end of the dictionary (>>) and is 65. readUntilSymbol() is supposed to advance it to the stream keyword which is three characters away. It first determines the substring to search based on the current value of 65, then resets the counter to zero, finds the symbol three characters into the string and finally sets self.charCounter to 3. The following calls to readSymbol() and colleagues fail silently but advance charCounter beyond the next newline. From there the next call to readUntilSymbol('endstream') tries to extract the actual objectstream content. Because charCounter is still in the middle of the dictionary, it extracts part of that as well and decoding of the object stream eventually fails.

Some poor man's debug prints showcase the issue:

diff --git a/peepdf/PDFCore.py b/peepdf/PDFCore.py
index a61d776..fc9df3b 100644
--- a/peepdf/PDFCore.py
+++ b/peepdf/PDFCore.py
@@ -7870,18 +7870,22 @@ class PDFParser:
                     nonDictContent = content[self.charCounter:]
                     streamFound = re.findall('[>\s]stream', nonDictContent)
                     if streamFound:
+                        print("1 charCounter: %d" % self.charCounter)
                         ret = self.readUntilSymbol(content, 'stream')
                         if ret[0] == -1:
                             return ret
+                        print("2 charCounter: %d, RET: %s" % (self.charCounter, ret[1]))
                         self.readSymbol(content, 'stream', False)
                         self.readUntilEndOfLine(content)
                         self.readSymbol(content, '\r', False)
                         self.readSymbol(content, '\n', False)
+                        print("3 charCounter: %d" % self.charCounter)
                         ret = self.readUntilSymbol(content, 'endstream')
                         if ret[0] == -1:
                             stream = content[self.charCounter:]
                         else:
                             stream = ret[1]
+                            print("4 charCounter: %d, RET: %s" % (self.charCounter, ret[1]))
                             self.readSymbol(content, 'endstream')
                         ret = self.createPDFStream(dictContent, stream)
                         if ret[0] == -1:

Output without the fix:

peepdf -j /tmp/foo.pdf
1 charCounter: 65
2 charCounter: 3, RET: >>

3 charCounter: 15
4 charCounter: 192, RET: /N 3
/Type /ObjStm
/Filter /FlateDecode
/First 14 >>
stream
x<9c>M<8c>[...]

Error: An error has occurred while parsing an indirect object!!

Output with the fix:

peepdf -j /tmp/foo.pdf
1 charCounter: 65
2 charCounter: 68, RET: >>

3 charCounter: 75
4 charCounter: 207, RET: x<9c>M<8c>[...]

@coveralls
Copy link

coveralls commented Mar 6, 2020

Coverage Status

Coverage decreased (-0.3%) to 25.447% when pulling 431e872 on michaelweiser:object-stream into 11dab2e on jbremer:master.

Commit 8cc27b6 broke object stream parsing by resetting the content
cursor PDFParser.charCounter to zero on every invocation. This broke object
stream parsing. Reproducer:

$ echo -e "create pdf\ncreate object_stream\nall\nsave /tmp/foo.pdf" | \
	peepdf -i

Without fix:

$ peepdf -j /tmp/foo.pdf
Error: An error has occurred while parsing an indirect object!!

With this change: JSON output as expected (same for other outputs).

$ peepdf -j /tmp/foo.pdf
{
    "peepdf_analysis": {
[...]
           "version": "0.3"
        }
    }
}

Signed-off-by: Michael Weiser <[email protected]>
For unclear reasons, PDFObjectStream.update() delays decoding of the
modified raw stream until all references can be resolved. It does
however then go on to always try to extract objects from the still empty
decoded stream. This produces an error from peepdf cli:

$ peepdf image.php
Error: An error has occurred while parsing an indirect object!!

The error from PDFObjectStream.update() is "Missing offsets in object
stream" because self.decodedStream is still empty at that point, making
offsetsSection and eventually the numbers list empty, causing the abort.

This is triggered by /Length being a reference and setting updateNeeded
to True. Sample: https://www.infotek.co.jp/pdflib/demo/sample/image.php.
Relevant PDF structure:

32 0 obj
<</Length 43 0 R/Filter/FlateDecode/Type/ObjStm/N 7/First 47>>
stream
[...]
endstream
endobj
43 0 obj
461
endobj

(Length in dict of object 32 R-eferences object 43 which contains 461
what presumably is the length of the stream - which does not seem to be
used or checked for consistency by peepdf atm, btw.)

This resolves the first half of jesparza#70 in that force mode is
no longer necessary to parse such files at all.

Signed-off-by: Michael Weiser <[email protected]>
With the previous change deferring reading of objects from the decoded
stream until references can be resolved, it now runs into
jesparza#70. This change provides a different approach in fixing
it to hatching#6 by syncing it with the other locations where the identical code
is in use:

1. Force the numbers extracted by re.findall to int() as before,
   avoiding the TypeError exception:

Traceback (most recent call last):
  File "peepdf-venv2/lib64/python2.7/site-packages/peepdf/main.py", line 409, in main
    ret, pdf = pdfParser.parse(fileName, options.isForceMode, options.isLooseMode, options.isManualAnalysis)
  File "peepdf-venv2/lib64/python2.7/site-packages/peepdf/PDFCore.py", line 7117, in parse
    ret = body.updateObjects()
  File "peepdf-venv2/lib64/python2.7/site-packages/peepdf/PDFCore.py", line 4291, in updateObjects
    object.resolveReferences()
  File "peepdf-venv2/lib64/python2.7/site-packages/peepdf/PDFCore.py", line 3256, in resolveReferences
    ret = PDFParser.readObject(objectsSection[offset:])
TypeError: slice indices must be integers or None or have an __index__ method

2. Instantiate a new PDFParser object by adding the missing braces,
   avoiding another TypeError because readObject is no class method:

Traceback (most recent call last):
  File "peepdf-venv2/lib64/python2.7/site-packages/peepdf/main.py", line 409, in main
    ret, pdf = pdfParser.parse(fileName, options.isForceMode, options.isLooseMode, options.isManualAnalysis)
  File "peepdf-venv2/lib64/python2.7/site-packages/peepdf/PDFCore.py", line 7118, in parse
    ret = body.updateObjects()
  File "peepdf-venv2/lib64/python2.7/site-packages/peepdf/PDFCore.py", line 4292, in updateObjects
    object.resolveReferences()
  File "peepdf-venv2/lib64/python2.7/site-packages/peepdf/PDFCore.py", line 3256, in resolveReferences
    ret = PDFParser.readObject(objectsSection[offset:])
TypeError: unbound method readObject() must be called with PDFParser instance as first argument (got str instance instead)

3. Explicitly force the id to be an int() as well and append it do the
   list of indices as at the other callsites of this code. This solves
   no issue I have run into but seems sensible to avoid other potential
   TypeErrors and keep internal bookkeeping of the object consistent.

This should conclusively resolve jesparza#70 and supersedes hatching#6.

Signed-off-by: Michael Weiser <[email protected]>
Anselm Kruis added 2 commits June 19, 2020 13:32
In PDF files the Cross-Reference Table or a Cross-Reference Stream
contain byte-offsets for the start of objects within the file or the
uncompressed stream. Such an offset does not always point the first byte
of the initial token (see ISO 32000-2008 section 7.2.2) of the referenced
object. The object may be preceded by white-space characters and comments.

Without this commit PDFParser.readSymbol() fails to read a symbol, if the
first character to be processed is a white-space character. This commit
changes PDFParser.readSymbol() to skip leading white-space characters.
(PDFParser.readSymbol() already skips any number of leading comments followed
by white-space characters.) This enables passing of PDF-files with sloppy
cross reference offsets.
An object of class PDFArray can contain JS-code, if one or more array-elements
contain JS-code. The getter method was simply missing.
@michaelweiser
Copy link
Author

Pushed some more fixes to object-stream-related problems, particularly one containing javascript or sloppy object reference offsets.

A previous commit adjusted readSymbol() to skip leading whitespace in
order to avoid errors with sloppy cross references. This did not fix
handling of literals such as numbers and booleans in readObject()
because they're not accessed using readSymbol(). Also, adjusting the
very low-level readSymbol() function might generate fallout.

So instead, this change moves the skipping of leading whitespace into
readObject() so that it affects all types of referenced objects equally
but not all symbol lookups altogether.

Signed-off-by: Michael Weiser <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants