evilfonttool 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evilfonttool-2.0.0/.github/workflows/python-publish.yml +70 -0
- evilfonttool-2.0.0/.gitignore +6 -0
- evilfonttool-2.0.0/LICENSE +21 -0
- evilfonttool-2.0.0/PKG-INFO +149 -0
- evilfonttool-2.0.0/README.md +137 -0
- evilfonttool-2.0.0/pyproject.toml +22 -0
- evilfonttool-2.0.0/src/evilfonttool/__init__.py +8 -0
- evilfonttool-2.0.0/src/evilfonttool/__main__.py +3 -0
- evilfonttool-2.0.0/src/evilfonttool/_core.py +363 -0
- evilfonttool-2.0.0/src/evilfonttool/cli.py +107 -0
- evilfonttool-2.0.0/src/evilfonttool/data/template.html +12 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# This workflow will upload a Python Package to PyPI when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
release-build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.x"
|
|
28
|
+
|
|
29
|
+
- name: Build release distributions
|
|
30
|
+
run: |
|
|
31
|
+
# NOTE: put your own distribution build steps here.
|
|
32
|
+
python -m pip install build
|
|
33
|
+
python -m build
|
|
34
|
+
|
|
35
|
+
- name: Upload distributions
|
|
36
|
+
uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: release-dists
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
pypi-publish:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
needs:
|
|
44
|
+
- release-build
|
|
45
|
+
permissions:
|
|
46
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
47
|
+
id-token: write
|
|
48
|
+
|
|
49
|
+
# Dedicated environments with protections for publishing are strongly recommended.
|
|
50
|
+
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
|
|
51
|
+
environment:
|
|
52
|
+
name: pypi
|
|
53
|
+
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
|
|
54
|
+
# url: https://pypi.org/p/YOURPROJECT
|
|
55
|
+
#
|
|
56
|
+
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
|
|
57
|
+
# ALTERNATIVE: exactly, uncomment the following line instead:
|
|
58
|
+
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
|
|
59
|
+
|
|
60
|
+
steps:
|
|
61
|
+
- name: Retrieve release distributions
|
|
62
|
+
uses: actions/download-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: release-dists
|
|
65
|
+
path: dist/
|
|
66
|
+
|
|
67
|
+
- name: Publish release distributions to PyPI
|
|
68
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
69
|
+
with:
|
|
70
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Griess
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: evilfonttool
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Evil Font steganography tool for security research
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: brotli
|
|
9
|
+
Requires-Dist: fonttools
|
|
10
|
+
Requires-Dist: python-docx
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# EvilFontTool
|
|
14
|
+
|
|
15
|
+
> **A font-based deception tool for red teaming, security research, and whatever else.**
|
|
16
|
+
|
|
17
|
+
EvilFontTool hides machine-readable text inside a document that displays completely different text to a human reader. It does this using **Evil Fonts** — fonts that intentionally deceive the viewer by rendering a different letter than understood by a computer. By remapping font glyphs, the document's visible characters show humans one thing while terminals, AI systems, and clipboard copy paste see another.
|
|
18
|
+
|
|
19
|
+
## Evil Font Demo !!DON'T MISS THIS!!
|
|
20
|
+
|
|
21
|
+
**[View the Demo → Here](https://doctoreww.github.io/EvilFontTool/)** *(hosted on GitHub Pages)*
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Table of Contents
|
|
26
|
+
|
|
27
|
+
- [Installation](#installation)
|
|
28
|
+
- [Usage](#usage)
|
|
29
|
+
- [Ethical Use & Disclaimer](#ethical-use--disclaimer)
|
|
30
|
+
- [Contributing](#contributing)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## [How It Works — Blog Post Coming Soon](#)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/DoctorEww/EvilFontTool.git
|
|
42
|
+
cd EvilFontTool
|
|
43
|
+
pip install .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For development (editable install):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Dependencies
|
|
53
|
+
- `fonttools` — font parsing and manipulation
|
|
54
|
+
- `python-docx` — DOCX generation
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
## Where can I find fonts to use?
|
|
58
|
+
|
|
59
|
+
* Ubuntu: `/usr/share/fonts`
|
|
60
|
+
* Windows: `C:\Windows\Fonts`
|
|
61
|
+
* https://fonts.google.com/
|
|
62
|
+
* The internet??
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
All functionality is exposed via a single CLI with three subcommands.
|
|
71
|
+
|
|
72
|
+
### `create` — Generate the font family for use in HTML or DOC files
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
evilfonttool create <reference_font> <output_dir> <font_name>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Argument | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `reference_font` | Path to a `.ttf` or `.woff` source font |
|
|
81
|
+
| `output_dir` | Directory to write fonts and CSS into |
|
|
82
|
+
| `font_name` | Internal name prefix for the generated font family |
|
|
83
|
+
|
|
84
|
+
**Example:**
|
|
85
|
+
```bash
|
|
86
|
+
evilfonttool create fonts/Arial.ttf output/ 'Arial'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Outputs:
|
|
90
|
+
- `output/fonts/*.woff` — web fonts, one per character
|
|
91
|
+
- `output/ttffonts/*.ttf` — TTF fonts for document embedding
|
|
92
|
+
- `output/fonts.css` — `@font-face` declarations for web use
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### `web` — Generate an evil font HTML file
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
evilfonttool web <human_file> <computer_file> <output_file>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
> Requires `fonts.css` and the generated fonts to be in the output directory so the HTML file can use it (or change the path in the HTML file).
|
|
103
|
+
|
|
104
|
+
**Example:**
|
|
105
|
+
```bash
|
|
106
|
+
evilfonttool web human.txt computer.txt output/index.html
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `doc` — Generate a evil font DOCX file
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
evilfonttool doc <human_file> <computer_file> <output_file> <font_name>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
> The `font_name` must match the name used in the `create` step. The TTF fonts must be installed on the system or embedded in the document. When saving the doc file you can choose in the file -> options -> save to embed the fonts in the file so its portable.
|
|
118
|
+
|
|
119
|
+
**Example:**
|
|
120
|
+
```bash
|
|
121
|
+
evilfonttool doc human.txt computer.txt output/secret.docx MyFont
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### Input File Format
|
|
127
|
+
|
|
128
|
+
- Plain `.txt` files, one sentence or phrase per line
|
|
129
|
+
- Each line in `computer_file` must be **equal to or longer** than the corresponding line in `human_file`
|
|
130
|
+
- Lines are matched positionally (line 1 to line 1, etc.)
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Ethical Use & Disclaimer
|
|
135
|
+
|
|
136
|
+
**You are responsible for how you use this tool.** Deploying this technique against systems or individuals without explicit authorization is unethical and may be illegal. The authors provide this tool to help defenders understand and test for this class of vulnerability — not to enable attacks.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Contributing
|
|
141
|
+
|
|
142
|
+
Contributions are welcome. If you've found a new attack surface, an improvement to the font generation pipeline, or a defense technique worth documenting, please open an issue or PR.
|
|
143
|
+
|
|
144
|
+
1. Fork the repo
|
|
145
|
+
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
|
146
|
+
3. Commit your changes
|
|
147
|
+
4. Open a pull request with a clear description of what changed and why
|
|
148
|
+
|
|
149
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# EvilFontTool
|
|
2
|
+
|
|
3
|
+
> **A font-based deception tool for red teaming, security research, and whatever else.**
|
|
4
|
+
|
|
5
|
+
EvilFontTool hides machine-readable text inside a document that displays completely different text to a human reader. It does this using **Evil Fonts** — fonts that intentionally deceive the viewer by rendering a different letter than understood by a computer. By remapping font glyphs, the document's visible characters show humans one thing while terminals, AI systems, and clipboard copy paste see another.
|
|
6
|
+
|
|
7
|
+
## Evil Font Demo !!DON'T MISS THIS!!
|
|
8
|
+
|
|
9
|
+
**[View the Demo → Here](https://doctoreww.github.io/EvilFontTool/)** *(hosted on GitHub Pages)*
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Usage](#usage)
|
|
17
|
+
- [Ethical Use & Disclaimer](#ethical-use--disclaimer)
|
|
18
|
+
- [Contributing](#contributing)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [How It Works — Blog Post Coming Soon](#)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/DoctorEww/EvilFontTool.git
|
|
30
|
+
cd EvilFontTool
|
|
31
|
+
pip install .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For development (editable install):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Dependencies
|
|
41
|
+
- `fonttools` — font parsing and manipulation
|
|
42
|
+
- `python-docx` — DOCX generation
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
## Where can I find fonts to use?
|
|
46
|
+
|
|
47
|
+
* Ubuntu: `/usr/share/fonts`
|
|
48
|
+
* Windows: `C:\Windows\Fonts`
|
|
49
|
+
* https://fonts.google.com/
|
|
50
|
+
* The internet??
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
All functionality is exposed via a single CLI with three subcommands.
|
|
59
|
+
|
|
60
|
+
### `create` — Generate the font family for use in HTML or DOC files
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
evilfonttool create <reference_font> <output_dir> <font_name>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Argument | Description |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `reference_font` | Path to a `.ttf` or `.woff` source font |
|
|
69
|
+
| `output_dir` | Directory to write fonts and CSS into |
|
|
70
|
+
| `font_name` | Internal name prefix for the generated font family |
|
|
71
|
+
|
|
72
|
+
**Example:**
|
|
73
|
+
```bash
|
|
74
|
+
evilfonttool create fonts/Arial.ttf output/ 'Arial'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Outputs:
|
|
78
|
+
- `output/fonts/*.woff` — web fonts, one per character
|
|
79
|
+
- `output/ttffonts/*.ttf` — TTF fonts for document embedding
|
|
80
|
+
- `output/fonts.css` — `@font-face` declarations for web use
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### `web` — Generate an evil font HTML file
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
evilfonttool web <human_file> <computer_file> <output_file>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> Requires `fonts.css` and the generated fonts to be in the output directory so the HTML file can use it (or change the path in the HTML file).
|
|
91
|
+
|
|
92
|
+
**Example:**
|
|
93
|
+
```bash
|
|
94
|
+
evilfonttool web human.txt computer.txt output/index.html
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### `doc` — Generate a evil font DOCX file
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
evilfonttool doc <human_file> <computer_file> <output_file> <font_name>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
> The `font_name` must match the name used in the `create` step. The TTF fonts must be installed on the system or embedded in the document. When saving the doc file you can choose in the file -> options -> save to embed the fonts in the file so its portable.
|
|
106
|
+
|
|
107
|
+
**Example:**
|
|
108
|
+
```bash
|
|
109
|
+
evilfonttool doc human.txt computer.txt output/secret.docx MyFont
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### Input File Format
|
|
115
|
+
|
|
116
|
+
- Plain `.txt` files, one sentence or phrase per line
|
|
117
|
+
- Each line in `computer_file` must be **equal to or longer** than the corresponding line in `human_file`
|
|
118
|
+
- Lines are matched positionally (line 1 to line 1, etc.)
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Ethical Use & Disclaimer
|
|
123
|
+
|
|
124
|
+
**You are responsible for how you use this tool.** Deploying this technique against systems or individuals without explicit authorization is unethical and may be illegal. The authors provide this tool to help defenders understand and test for this class of vulnerability — not to enable attacks.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Contributing
|
|
129
|
+
|
|
130
|
+
Contributions are welcome. If you've found a new attack surface, an improvement to the font generation pipeline, or a defense technique worth documenting, please open an issue or PR.
|
|
131
|
+
|
|
132
|
+
1. Fork the repo
|
|
133
|
+
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
|
134
|
+
3. Commit your changes
|
|
135
|
+
4. Open a pull request with a clear description of what changed and why
|
|
136
|
+
|
|
137
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "evilfonttool"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Evil Font steganography tool for security research"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"fonttools",
|
|
14
|
+
"python-docx",
|
|
15
|
+
"brotli",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
evilfonttool = "evilfonttool.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/evilfonttool"]
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
import pathlib
|
|
4
|
+
import string
|
|
5
|
+
|
|
6
|
+
from fontTools.ttLib import TTFont
|
|
7
|
+
from docx import Document
|
|
8
|
+
from docx.oxml.ns import qn
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Configuration
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
# The set of characters to include in the font family.
|
|
18
|
+
# Modify this to limit which characters are processed.
|
|
19
|
+
LETTERS = (
|
|
20
|
+
string.ascii_lowercase
|
|
21
|
+
+ " "
|
|
22
|
+
+ string.punctuation
|
|
23
|
+
+ string.ascii_uppercase
|
|
24
|
+
+ string.digits
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Advance width for invisible (stealth) characters.
|
|
28
|
+
# 0 works for most renderers, but some applications may behave unexpectedly.
|
|
29
|
+
WIDTH = 0
|
|
30
|
+
|
|
31
|
+
# Path to the bundled HTML template.
|
|
32
|
+
_TEMPLATE = pathlib.Path(__file__).parent / "data" / "template.html"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Font helpers
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def _get_unicode_cmap(font):
|
|
40
|
+
"""Return the first suitable Windows Unicode cmap subtable (format 4 or 12).
|
|
41
|
+
|
|
42
|
+
Raises ValueError if none is found.
|
|
43
|
+
"""
|
|
44
|
+
for table in font['cmap'].tables:
|
|
45
|
+
if (table.format in (4, 12)
|
|
46
|
+
and table.platformID == 3
|
|
47
|
+
and table.platEncID in (1, 10)):
|
|
48
|
+
return table
|
|
49
|
+
raise ValueError("No suitable Unicode cmap subtable found in the font.")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _remove_layout_tables(font):
|
|
53
|
+
"""Remove OpenType tables that control ligatures, kerning, and substitution.
|
|
54
|
+
|
|
55
|
+
These tables operate on glyph names rather than cmap entries, so they can
|
|
56
|
+
override our remapping in unpredictable ways depending on surrounding characters.
|
|
57
|
+
"""
|
|
58
|
+
for tag in ('GSUB', 'GPOS', 'kern', 'GDEF'):
|
|
59
|
+
if tag in font:
|
|
60
|
+
del font[tag]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _rename_font(font, new_name):
|
|
64
|
+
"""Overwrite the family name records in the font's name table."""
|
|
65
|
+
for record in font['name'].names:
|
|
66
|
+
if record.nameID in (1, 4, 6):
|
|
67
|
+
record.string = new_name.encode("utf-16-be")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Core font generation
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def createstealthfont(reference_font, output_dir, font_name):
|
|
75
|
+
"""Generate a stealth font where every character renders as an invisible space.
|
|
76
|
+
|
|
77
|
+
The stealth font is used to hide the extra computer-file characters that
|
|
78
|
+
have no corresponding human-file character to disguise themselves as.
|
|
79
|
+
It is saved as both WOFF (web) and TTF (document/desktop).
|
|
80
|
+
"""
|
|
81
|
+
font = TTFont(reference_font)
|
|
82
|
+
unicode_cmap = _get_unicode_cmap(font)
|
|
83
|
+
|
|
84
|
+
# Use the space glyph as the target — all other characters will map to it
|
|
85
|
+
space_glyph_name = unicode_cmap.cmap.get(ord(" "))
|
|
86
|
+
|
|
87
|
+
# Remap every character in LETTERS to the space glyph and zero its advance width
|
|
88
|
+
for table in font['cmap'].tables:
|
|
89
|
+
for char in LETTERS:
|
|
90
|
+
if ord(char) in table.cmap:
|
|
91
|
+
table.cmap[ord(char)] = space_glyph_name
|
|
92
|
+
font['hmtx'].metrics[space_glyph_name] = (
|
|
93
|
+
WIDTH,
|
|
94
|
+
font['hmtx'].metrics[space_glyph_name][1],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
_remove_layout_tables(font)
|
|
98
|
+
|
|
99
|
+
# Internal font name for the stealth variant uses "0" as a sentinel
|
|
100
|
+
new_name = f'{font_name} 0'
|
|
101
|
+
_rename_font(font, new_name)
|
|
102
|
+
logger.debug(f" Stealth font internal name: {new_name}")
|
|
103
|
+
|
|
104
|
+
# Append the @font-face CSS rule for the stealth font
|
|
105
|
+
with open(f'{output_dir}/fonts.css', "a") as css_file:
|
|
106
|
+
css_file.write(
|
|
107
|
+
"@font-face {"
|
|
108
|
+
'font-family: "0";'
|
|
109
|
+
'src: url("fonts/0.woff") format(\'woff\');'
|
|
110
|
+
"}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Save WOFF (flavor must be set explicitly) then TTF
|
|
114
|
+
font.flavor = 'woff'
|
|
115
|
+
font.save(f'{output_dir}/fonts/0.woff')
|
|
116
|
+
font.flavor = None
|
|
117
|
+
font.save(f'{output_dir}/ttffonts/0.ttf')
|
|
118
|
+
|
|
119
|
+
logger.debug(f"[DONE] stealth font -> {output_dir}/fonts/0.woff + ttffonts/0.ttf")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def createfonts(reference_font, output_dir, font_name):
|
|
123
|
+
"""Generate one Evil Font variant per character in LETTERS.
|
|
124
|
+
|
|
125
|
+
In each variant, every character's glyph is replaced with the glyph for
|
|
126
|
+
`currentletter`. This means that no matter what Unicode byte is stored in
|
|
127
|
+
the document, the renderer will draw `currentletter` — the core Evil Font trick.
|
|
128
|
+
|
|
129
|
+
Also writes all @font-face rules to fonts.css (overwrites any existing file).
|
|
130
|
+
"""
|
|
131
|
+
logger.debug(f"Source font: {reference_font}")
|
|
132
|
+
logger.debug(f"Output dir: {output_dir}")
|
|
133
|
+
logger.debug(f"Characters: {len(LETTERS)} variants to generate")
|
|
134
|
+
|
|
135
|
+
with open(f'{output_dir}/fonts.css', "w") as css_file:
|
|
136
|
+
|
|
137
|
+
for currentletter in LETTERS:
|
|
138
|
+
# Load a fresh copy of the font for each variant to avoid cross-contamination
|
|
139
|
+
font = TTFont(reference_font)
|
|
140
|
+
font.recalcBBoxes = False
|
|
141
|
+
|
|
142
|
+
unicode_cmap = _get_unicode_cmap(font)
|
|
143
|
+
|
|
144
|
+
# Get the source glyph and its advance width for this letter
|
|
145
|
+
source_glyph_name = unicode_cmap.cmap.get(ord(currentletter))
|
|
146
|
+
source_width = font['hmtx'].metrics[source_glyph_name][0]
|
|
147
|
+
|
|
148
|
+
# Take a deep copy of the source glyph to use as a stamp
|
|
149
|
+
source_glyph = copy.deepcopy(font['glyf'][source_glyph_name])
|
|
150
|
+
|
|
151
|
+
# Remap every other character in LETTERS to look like currentletter
|
|
152
|
+
for table in font['cmap'].tables:
|
|
153
|
+
for char in LETTERS:
|
|
154
|
+
if char == currentletter:
|
|
155
|
+
continue
|
|
156
|
+
if ord(char) not in table.cmap:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
target_glyph_name = table.cmap[ord(char)]
|
|
160
|
+
target_original = font['glyf'][target_glyph_name]
|
|
161
|
+
|
|
162
|
+
# Preserve the target's original bounding box so that
|
|
163
|
+
# spacing and baseline positioning remain correct
|
|
164
|
+
orig_xMin = getattr(target_original, 'xMin', 0)
|
|
165
|
+
orig_yMax = getattr(target_original, 'yMax', 0)
|
|
166
|
+
orig_yMin = getattr(target_original, 'yMin', 0)
|
|
167
|
+
|
|
168
|
+
# Stamp a copy of the source glyph into the target slot
|
|
169
|
+
font['glyf'][target_glyph_name] = copy.deepcopy(source_glyph)
|
|
170
|
+
|
|
171
|
+
# Restore the vertical bounds (keeps line height consistent)
|
|
172
|
+
font['glyf'][target_glyph_name].yMax = orig_yMax
|
|
173
|
+
font['glyf'][target_glyph_name].yMin = orig_yMin
|
|
174
|
+
|
|
175
|
+
# Match advance width to source; preserve original LSB for positioning
|
|
176
|
+
font['hmtx'].metrics[target_glyph_name] = (source_width, orig_xMin)
|
|
177
|
+
|
|
178
|
+
_remove_layout_tables(font)
|
|
179
|
+
|
|
180
|
+
# Each font variant is named using the hex encoding of the letter
|
|
181
|
+
# so the name is unique and safely usable as a filename
|
|
182
|
+
letter_hex = currentletter.encode().hex()
|
|
183
|
+
new_name = f'{font_name} {letter_hex}'
|
|
184
|
+
_rename_font(font, new_name)
|
|
185
|
+
|
|
186
|
+
# Save as WOFF for web use and TTF for document/desktop use
|
|
187
|
+
font.flavor = 'woff'
|
|
188
|
+
font.save(f'{output_dir}/fonts/{letter_hex}.woff')
|
|
189
|
+
font.flavor = None
|
|
190
|
+
font.save(f'{output_dir}/ttffonts/{letter_hex}.ttf')
|
|
191
|
+
|
|
192
|
+
# Write the @font-face rule for this variant
|
|
193
|
+
css_file.write(
|
|
194
|
+
f'@font-face {{'
|
|
195
|
+
f'font-family: "{letter_hex}";'
|
|
196
|
+
f'src: url("fonts/{letter_hex}.woff") format(\'woff\');'
|
|
197
|
+
f'}}'
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
logger.debug(f" [{letter_hex}] '{currentletter}' -> {output_dir}/fonts/{letter_hex}.woff + ttffonts/{letter_hex}.ttf")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# HTML output
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
def _write_html(output_file, content):
|
|
208
|
+
"""Inject `content` into the bundled HTML template and write to `output_file`."""
|
|
209
|
+
html = _TEMPLATE.read_text()
|
|
210
|
+
html = html.replace("<!-- #STUFF HERE -->", content)
|
|
211
|
+
with open(output_file, "w") as f:
|
|
212
|
+
f.write(html)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def createhtml(input_human_file, input_computer_file, output_file):
|
|
216
|
+
"""Build a steganographic HTML file from human and computer text files.
|
|
217
|
+
|
|
218
|
+
Each character from the computer file is wrapped in a <span> that applies
|
|
219
|
+
an Evil Font chosen by the corresponding human file character. To a human
|
|
220
|
+
reading the rendered page, the text looks like the human file. A machine
|
|
221
|
+
parsing the raw HTML sees the computer file.
|
|
222
|
+
|
|
223
|
+
Lines where the computer text is shorter than the human text are skipped.
|
|
224
|
+
Extra computer characters (beyond the human line length) are hidden using
|
|
225
|
+
the stealth font (font-family: '0').
|
|
226
|
+
"""
|
|
227
|
+
logger.debug(f"Human file: {input_human_file}")
|
|
228
|
+
logger.debug(f"Computer file: {input_computer_file}")
|
|
229
|
+
logger.debug(f"Output file: {output_file}")
|
|
230
|
+
|
|
231
|
+
spans = []
|
|
232
|
+
lines_processed = 0
|
|
233
|
+
total_hidden = 0
|
|
234
|
+
line_number = 0
|
|
235
|
+
|
|
236
|
+
with open(input_human_file, "r") as human_file, \
|
|
237
|
+
open(input_computer_file, "r") as computer_file:
|
|
238
|
+
|
|
239
|
+
for human_line, computer_line in zip(
|
|
240
|
+
(l.rstrip('\n') for l in human_file),
|
|
241
|
+
(l.rstrip('\n') for l in computer_file),
|
|
242
|
+
):
|
|
243
|
+
h_len = len(human_line)
|
|
244
|
+
c_len = len(computer_line)
|
|
245
|
+
|
|
246
|
+
line_number += 1
|
|
247
|
+
logger.debug(f"Human ({h_len} chars): {human_line}")
|
|
248
|
+
logger.debug(f"Computer({c_len} chars): {computer_line}")
|
|
249
|
+
|
|
250
|
+
if c_len < h_len:
|
|
251
|
+
logger.warning(f" Skipping line {line_number}: computer line must be >= human line length.")
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Extra computer characters are inserted at the midpoint of the human text
|
|
255
|
+
diff = c_len - h_len
|
|
256
|
+
mid = h_len // 2
|
|
257
|
+
|
|
258
|
+
logger.debug(f" diff={diff}, hidden chars inserted at mid={mid}")
|
|
259
|
+
|
|
260
|
+
line_spans = []
|
|
261
|
+
h_index = 0
|
|
262
|
+
|
|
263
|
+
for i, computer_char in enumerate(computer_line):
|
|
264
|
+
if mid <= i < mid + diff:
|
|
265
|
+
# Hidden character — rendered invisibly using the stealth font
|
|
266
|
+
line_spans.append(
|
|
267
|
+
f"<span style=\"font-family: '0';\">{computer_char}</span>"
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
# Visible character — disguised as the corresponding human character
|
|
271
|
+
human_char = human_line[h_index]
|
|
272
|
+
letter_hex = human_char.encode().hex()
|
|
273
|
+
line_spans.append(
|
|
274
|
+
f"<span style=\"font-family: '{letter_hex}';\">{computer_char}</span>"
|
|
275
|
+
)
|
|
276
|
+
h_index += 1
|
|
277
|
+
|
|
278
|
+
spans.append("".join(line_spans))
|
|
279
|
+
lines_processed += 1
|
|
280
|
+
total_hidden += diff
|
|
281
|
+
|
|
282
|
+
_write_html(output_file, "\n<br>\n".join(spans))
|
|
283
|
+
logger.debug(f"[DONE] HTML written -> {output_file} ({lines_processed} lines, {total_hidden} hidden chars)")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
# DOCX output
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
def create_doc(input_human_file, input_computer_file, output_file, font_name_in):
|
|
291
|
+
"""Build a steganographic DOCX file from human and computer text files.
|
|
292
|
+
|
|
293
|
+
Works identically to createhtml() but outputs a Word document. Each run is
|
|
294
|
+
assigned an Evil Font by setting all four font slots (ascii, hAnsi, eastAsia,
|
|
295
|
+
cs) to prevent Word from falling back to a system font.
|
|
296
|
+
|
|
297
|
+
The TTF variants of the Evil Fonts must be installed on the system (or
|
|
298
|
+
embedded in the document) for the deception to render correctly in Word.
|
|
299
|
+
|
|
300
|
+
Lines where the computer text is shorter than the human text are skipped.
|
|
301
|
+
"""
|
|
302
|
+
logger.debug(f"Human file: {input_human_file}")
|
|
303
|
+
logger.debug(f"Computer file: {input_computer_file}")
|
|
304
|
+
logger.debug(f"Output file: {output_file}")
|
|
305
|
+
logger.debug(f"Font family: {font_name_in}")
|
|
306
|
+
|
|
307
|
+
doc = Document()
|
|
308
|
+
lines_processed = 0
|
|
309
|
+
total_hidden = 0
|
|
310
|
+
line_number = 0
|
|
311
|
+
|
|
312
|
+
with open(input_human_file, "r") as human_file, \
|
|
313
|
+
open(input_computer_file, "r") as computer_file:
|
|
314
|
+
|
|
315
|
+
for human_line, computer_line in zip(
|
|
316
|
+
(l.rstrip('\n') for l in human_file),
|
|
317
|
+
(l.rstrip('\n') for l in computer_file),
|
|
318
|
+
):
|
|
319
|
+
h_len = len(human_line)
|
|
320
|
+
c_len = len(computer_line)
|
|
321
|
+
|
|
322
|
+
line_number += 1
|
|
323
|
+
logger.debug(f"Human ({h_len} chars): {human_line}")
|
|
324
|
+
logger.debug(f"Computer({c_len} chars): {computer_line}")
|
|
325
|
+
|
|
326
|
+
if c_len < h_len:
|
|
327
|
+
logger.warning(f" Skipping line {line_number}: computer line must be >= human line length.")
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
diff = c_len - h_len
|
|
331
|
+
mid = h_len // 2
|
|
332
|
+
|
|
333
|
+
logger.debug(f" diff={diff}, hidden chars inserted at mid={mid}")
|
|
334
|
+
|
|
335
|
+
p = doc.add_paragraph()
|
|
336
|
+
h_index = 0
|
|
337
|
+
|
|
338
|
+
for i, computer_char in enumerate(computer_line):
|
|
339
|
+
if mid <= i < mid + diff:
|
|
340
|
+
# Hidden character — use the stealth (zero-width) font
|
|
341
|
+
font_name = f'{font_name_in} 0'
|
|
342
|
+
else:
|
|
343
|
+
# Visible character — disguised as the corresponding human character
|
|
344
|
+
human_char = human_line[h_index]
|
|
345
|
+
font_name = f'{font_name_in} {human_char.encode().hex()}'
|
|
346
|
+
h_index += 1
|
|
347
|
+
|
|
348
|
+
# Add a run and explicitly set all four font slots.
|
|
349
|
+
# Word will fall back to a system font if any slot is unset,
|
|
350
|
+
# which would break the illusion.
|
|
351
|
+
run = p.add_run(computer_char)
|
|
352
|
+
run.font.name = font_name
|
|
353
|
+
rFonts = run._element.rPr.rFonts
|
|
354
|
+
rFonts.set(qn("w:ascii"), font_name)
|
|
355
|
+
rFonts.set(qn("w:hAnsi"), font_name)
|
|
356
|
+
rFonts.set(qn("w:eastAsia"), font_name)
|
|
357
|
+
rFonts.set(qn("w:cs"), font_name)
|
|
358
|
+
|
|
359
|
+
lines_processed += 1
|
|
360
|
+
total_hidden += diff
|
|
361
|
+
|
|
362
|
+
doc.save(output_file)
|
|
363
|
+
logger.debug(f"[DONE] DOCX written -> {output_file} ({lines_processed} lines, {total_hidden} hidden chars)")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from evilfonttool._core import createfonts, createstealthfont, createhtml, create_doc
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_parser():
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
description="EvilFontTool — Evil Font steganography tool for security research.",
|
|
14
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
'--log',
|
|
18
|
+
default='WARNING',
|
|
19
|
+
metavar='LEVEL',
|
|
20
|
+
help='Log level: DEBUG, INFO, WARNING, ERROR (default: WARNING).',
|
|
21
|
+
)
|
|
22
|
+
subparsers = parser.add_subparsers(dest='command', required=True)
|
|
23
|
+
|
|
24
|
+
# --- create ---
|
|
25
|
+
create_parser = subparsers.add_parser(
|
|
26
|
+
'create',
|
|
27
|
+
help='Generate an Evil Font family from a reference font.',
|
|
28
|
+
description=(
|
|
29
|
+
'Produces one font variant per printable character plus a stealth font, '
|
|
30
|
+
'along with a fonts.css file for web use.'
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
create_parser.add_argument('reference_font', help='Path to the source .ttf font file.')
|
|
34
|
+
create_parser.add_argument('output_dir', help='Directory to write fonts and CSS into.')
|
|
35
|
+
create_parser.add_argument('font_name', help='Internal name prefix for the font family.')
|
|
36
|
+
|
|
37
|
+
# --- web ---
|
|
38
|
+
web_parser = subparsers.add_parser(
|
|
39
|
+
'web',
|
|
40
|
+
help='Generate a steganographic HTML file.',
|
|
41
|
+
description=(
|
|
42
|
+
'Wraps computer-file characters in Evil Font spans so the page displays '
|
|
43
|
+
'human-file text visually while the raw HTML contains the computer text. '
|
|
44
|
+
'fonts.css must be present in the output directory.'
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
web_parser.add_argument('input_human_file', help='Text visible to human readers.')
|
|
48
|
+
web_parser.add_argument('input_computer_file', help='Text visible to machines / AI.')
|
|
49
|
+
web_parser.add_argument('output_file', help='Path for the generated HTML file.')
|
|
50
|
+
|
|
51
|
+
# --- doc ---
|
|
52
|
+
doc_parser = subparsers.add_parser(
|
|
53
|
+
'doc',
|
|
54
|
+
help='Generate a steganographic DOCX file.',
|
|
55
|
+
description=(
|
|
56
|
+
'Produces a Word document using Evil Fonts so the displayed text differs '
|
|
57
|
+
'from the underlying Unicode. font_name must match the value used in create.'
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
doc_parser.add_argument('input_human_file', help='Text visible to human readers.')
|
|
61
|
+
doc_parser.add_argument('input_computer_file', help='Text visible to machines / AI.')
|
|
62
|
+
doc_parser.add_argument('output_file', help='Path for the generated DOCX file.')
|
|
63
|
+
doc_parser.add_argument('font_name', help='Font family name (must match create step).')
|
|
64
|
+
|
|
65
|
+
return parser
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
parser = setup_parser()
|
|
70
|
+
args = parser.parse_args()
|
|
71
|
+
|
|
72
|
+
level = getattr(logging, args.log.upper(), None)
|
|
73
|
+
if not isinstance(level, int):
|
|
74
|
+
print(f"Error: invalid log level '{args.log}'. Choose from DEBUG, INFO, WARNING, ERROR.", file=sys.stderr)
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
|
|
77
|
+
logging.basicConfig(level=level, format='%(message)s', stream=sys.stderr)
|
|
78
|
+
|
|
79
|
+
if args.command == 'create':
|
|
80
|
+
if not os.path.isfile(args.reference_font):
|
|
81
|
+
logger.error("'%s' does not exist.", args.reference_font)
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
for subdir in ('', '/fonts', '/ttffonts'):
|
|
85
|
+
os.makedirs(args.output_dir + subdir, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
createfonts(args.reference_font, args.output_dir, args.font_name)
|
|
88
|
+
createstealthfont(args.reference_font, args.output_dir, args.font_name)
|
|
89
|
+
|
|
90
|
+
elif args.command == 'web':
|
|
91
|
+
for path in (args.input_human_file, args.input_computer_file):
|
|
92
|
+
if not os.path.isfile(path):
|
|
93
|
+
logger.error("'%s' does not exist.", path)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
createhtml(args.input_human_file, args.input_computer_file, args.output_file)
|
|
96
|
+
|
|
97
|
+
elif args.command == 'doc':
|
|
98
|
+
for path in (args.input_human_file, args.input_computer_file):
|
|
99
|
+
if not os.path.isfile(path):
|
|
100
|
+
logger.error("'%s' does not exist.", path)
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
create_doc(
|
|
103
|
+
args.input_human_file,
|
|
104
|
+
args.input_computer_file,
|
|
105
|
+
args.output_file,
|
|
106
|
+
args.font_name,
|
|
107
|
+
)
|