[python] 파이썬 pdf 포렌식

글쓴이 Engineer Myoa 날짜

조금 지난 이야기지만

2012년경 어나니머스에서 pdf를 통해 자신들의 Operation을 발표했었는데

pdf를 조사해보니 생성시 자동으로 metadata가 포함되어 저자가 밝혀졌다고…

이를 통해 늦은감이 있지만 python을 통한 포렌식에도 관심이 생겼다.

제일 먼저 접하게된 pdf에 대해서 파이썬 코드를 짜보도록 한다.

 

참고한 글은 다음과 같다.

http://cpuu.postype.com/post/76874/

 

 

 

개인적으로 원본이 지저분해지는것을 좋아하지 않기때문에

git에서 branch를 만들듯 interpreter를 virtualenv로 가상화한다.

 

아래 명령은 cmd에서 작업한다.

virtualenv venv

 

venv생성을 완료했다면 가상화된 venv를 선택한다.

./venv/Scripts/activate

 

python에서 pdf를 핸들링할 수 있는 pypdf 패키지를 설치한다.

pip install pypdf

 

ANONOPS_The_Press_Release.pdf

어나니머스에서 배포했던 pdf파일을 받아 .py파일과 동일한 경로에 위치하도록 한다.

(테스트를 위함)

 

기본준비는 완료됐다.

 

이제 편집기에서 pyPdf 모듈을 이용하여 pdf파일을 연다.

기본코드로 다음과 같이 하드코딩해본다.

# -*- coding: utf-8 -*-

from pyPdf import PdfFileReader

def printMetadata(fileName):
    pdfFile = PdfFileReader(open(fileName, 'rb'))
    docInfo = pdfFile.getDocumentInfo()
    print(" {0} : ↓↓↓".format(fileName) )
    #print(docInfo)
    for k, v in docInfo.items():
        print("{0} : {1}".format(k, v))


filename = "ANONOPS_The_Press_Release.pdf"
printMetadata(filename)

결과는 다음과 같이 나온다.

파일명에 대한 pdf파일을 기반으로 저자, 프로그램, 제작자, 제작날짜 key가 존재하여 해당 key에 상응하는 value들을 출력해준다.

 

그러면 이를 활용하여 포렌식 툴을 만들어 본다.

여기서는 flask를 활용한 웹 pdf 포렌식 어플리케이션을 제작해보도록 한다.

(소규모에 간단하기 때문)

먼저 flask를 설치한다.

pip install flask

 

조금전에 작성한 pdf포렌식 py파일을 아래와 같이 수정한다.

# -*- coding: utf-8 -*-

from flask import Flask, request, render_template, jsonify


from pyPdf import PdfFileReader
import os

UPLOAD_FOLDER = "uploads"
ALLOW_EXTENSIONS = set(["pdf"]) # Anti XSS Attack

app= Flask(__name__)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024 # 10MB로 제한

def printMetadata(fileName):
    pdfFile = PdfFileReader(open("uploads/" + fileName, 'rb'))
    docInfo = pdfFile.getDocumentInfo()
    print(" {0} : ↓↓↓".format(fileName) )
    #print(docInfo)
    for k, v in docInfo.items():
        print("{0} : {1}".format(k, v))


filename = "ANONOPS_The_Press_Release.pdf"
printMetadata(filename)


def allow_file(filename):
    return '.' in filename and filename.split('.',1)[1] in ALLOW_EXTENSIONS

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/uploads", methods=["POST"])
def post_upload():
    try:
        file = request.files["file"]
        if file and allow_file(file.filename):

            filename = file.filename
            file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename))
            print(file)
            returnData = {"msg": "success : " + file.filename}
            return jsonify(returnData)
        returnData = {"msg": "PDF파일만 업로드 가능합니다."}
        return jsonify(returnData)
    except Exception as e:
        print(e)
        returnData = {"msg" : str(e)}
        return jsonify(returnData)


if __name__ == "__main__":
    app.run(host="localhost", port="1010")

 

업로드된 파일이 저장될 uploads폴더를 생성해준다.

그리고 templates폴더를 생성해 index.html 파일을 만들어준다.

<!doctype html>
<html>
    <title>Upload pdf File</title>
    <h1>Upload pdf File</h1>
    <form action="uploads" method=post enctype=multipart/form-data id="upload_form">
      <p><input type=file name=file>
         <input type=submit value=Upload>
    </form>
</html>

 

최종적으로 작업을 진행하는 working directory의 구조

이제 파이썬코드를 run하면 index.html을 jinja에서 rendering해줄 것이다.

 

 

테스트할 겸 upload를 해본다.

returnData에 파일명을 담아서 return하도록 했기때문에

별다른 작업없이 메시지를 json화 해서 넘겨준다.

서버내에 해당 업로드한  pdf파일이 저장된 모습이다.

 

아직 해당 파일을 이용해서 아무런 작업을 하지않고 file명만 return해준다.

따라서 pdf파일을 분석하고, 분석된 내용을 dict객체에 담아 json을 return해줄 필요가 있다.

이제는 콘솔에 출력하는 것이 아니라 dict객체에 담아 return할 수 있도록 getMetadata 함수로 이름과 코드를 수정한다.

# -*- coding: utf-8 -*-

from flask import Flask, request, render_template, jsonify


from pyPdf import PdfFileReader
import os

UPLOAD_FOLDER = "uploads"
ALLOW_EXTENSIONS = set(["pdf"]) # Anti XSS Attack

app= Flask(__name__)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024 # 10MB로 제한

def getMetadata(fileName):
    pdfFile = PdfFileReader(open("uploads/" +&nbsp;fileName, 'rb'))
    docInfo = pdfFile.getDocumentInfo()

    returnData = {}

    print(" {0} : ↓↓↓".format(fileName) )
    for k, v in docInfo.items():
        #print("{0} : {1}".format(k, v))
        returnData.update( {k:v} )

    return returnData # 콘솔에 print 하는것이아니라 객체에 담아서 return

def allow_file(filename):
    return '.' in filename and filename.split('.',1)[1] in ALLOW_EXTENSIONS

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/uploads", methods=["POST"])
def post_upload():
    try:
        file = request.files["file"]
        if file and allow_file(file.filename):

            filename = file.filename
            file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename))
            returnData = getMetadata(filename)

            returnData.update( {"msg": "success : " + filename} )

            return jsonify(returnData)
        returnData = {"msg": "PDF파일만 업로드 가능합니다."}
        return jsonify(returnData)
    except Exception as e:
        print(e)
        returnData = {"msg" : str(e)}
        return jsonify(returnData)


if __name__ == "__main__":
    app.run(host="localhost", port="1010")

 

.py를 다시 실행하여 업로드 해본다.

정상적으로 원하는 json이 return되었다.

 

이제 남은 문제는

1. 확장자만 바꾼 .pdf파일인지에 대해

2. 분석이 끝난 pdf파일은 삭제 또는 장기 보관함

3. 필요하다면 DB에 stacking

 

여기서는 1,2번만 진행하도록 하겠다.

 

당장 1번은 pypdf모듈을 사용하기 때문에 아래와 같은 오류 메시지를 return해준다.

(굉장히 영리한 모습을 볼 수 있다.)

하지만 우리는 이에대해 수정할 필요가있다.

 

post_upload함수를 아래와 같이,

@app.route("/uploads", methods=["POST"])
def post_upload():
    try:
        file = request.files["file"]
        if file and allow_file(file.filename):

            filename = file.filename
            file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename))
            returnData = getMetadata(filename)

            if returnData == None:
                returnData = {"msg" : "failed : " + "올바른 PDF파일을 업로드하세요"}
                return jsonify(returnData)

            returnData.update( {"msg": "success : " + filename} )

            return jsonify(returnData)
        returnData = {"msg": "PDF파일만 업로드 가능합니다."}
        return jsonify(returnData)
    except Exception as e:
        print(e)
        returnData = {"msg" : str(e)}
        return jsonify(returnData)

 

getMetadata함수를 아래와 같이 수정하도록 한다.

def getMetadata(fileName):
    try:
        pdfFile = PdfFileReader(open("uploads/" + fileName, 'rb'))
        docInfo = pdfFile.getDocumentInfo()

        returnData = {}

        print(" {0} : ↓↓↓".format(fileName) )
        for k, v in docInfo.items():
            #print("{0} : {1}".format(k, v))
            returnData.update( {k:v} )

        return returnData # 콘솔에 print 하는것이아니라 객체에 담아서 return
    except Exception as e:
        print(e)
        return None

 

결과적으로 다시 run하여 post를 하면 아래와 같이 메시지가 출력된다.

 

2번은 매우 간단하다. getMetadata함수에서 return전에 파일 핸들을 해준면 되는데, 나의 경우는 삭제할 수 있도록 했다.

os.remove(fileName), os.remove(filename)을 각각 한줄 추가하여 나오는 최종코드는,

# -*- coding: utf-8 -*-

from flask import Flask, request, render_template, jsonify


from pyPdf import PdfFileReader
import os

UPLOAD_FOLDER = "uploads"
ALLOW_EXTENSIONS = set(["pdf"]) # Anti XSS Attack

app= Flask(__name__)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024 # 10MB로 제한

def getMetadata(fileName):
    try:
        returnData = {}

        with open("uploads/" + fileName, 'rb') as f:
            pdfFile = PdfFileReader(f)
            docInfo = pdfFile.getDocumentInfo()

            print(" {0} : ↓↓↓".format(fileName) )
            for k, v in docInfo.items():
                returnData.update( {k:v} )
        os.remove("uploads/" + fileName)

        return returnData # 콘솔에 print 하는것이아니라 객체에 담아서 return
    except Exception as e:
        print(e)
        return None

def allow_file(filename):
    return '.' in filename and filename.split('.',1)[1] in ALLOW_EXTENSIONS

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/uploads", methods=["POST"])
def post_upload():
    try:
        file = request.files["file"]
        if file and allow_file(file.filename):

            filename = file.filename
            file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename))
            returnData = getMetadata(filename)

            if returnData == None:
                os.remove("uploads/" + filename)
                returnData = {"msg" : "failed : " + "올바른 PDF파일을 업로드하세요"}
                return jsonify(returnData)

            returnData.update( {"msg": "success : " + filename} )

            return jsonify(returnData)

        returnData = {"msg": "PDF파일만 업로드 가능합니다."}
        return jsonify(returnData)

    except Exception as e:
        print(e)
        returnData = { "msg" : str(e) }
        return jsonify(returnData)


if __name__ == "__main__":
    app.run(host="localhost", port="1010")

 

지금은 급한대로 index.html에 file폼을 때려박은 추잡한 방식이지만

Front end개발자와 Back end 개발자로 구분되는 현 시장에서는 백엔드단에서는

위와같이 api서버만 만들어서 제공해주면 된다. (index.html없이, 루트 라우팅 없이)

 

카테고리: 강의

1,513개의 댓글

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다