SVG/Font Glyph Analysis & Web DRM Deobfuscation (Raster Hashing + SSIM)
Reading time: 11 minutes
tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: 
HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure: 
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
 - Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
 - Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
 
Esta página documenta técnicas práticas para recuperar texto de web readers que entregam positioned glyph runs além de definições vetoriais de glyph por requisição (SVG paths), e que randomizam glyph IDs por requisição para impedir scraping. A ideia central é ignorar os glyph IDs numéricos com escopo de requisição e fingerprintar as formas visuais via raster hashing, então mapear formas para caracteres usando SSIM contra um atlas de fontes de referência. O fluxo de trabalho generaliza além do Kindle Cloud Reader para qualquer viewer com proteções semelhantes.
Aviso: Use estas técnicas apenas para fazer backup de conteúdo que você possui legitimamente e em conformidade com leis e termos aplicáveis.
Aquisição (exemplo: Kindle Cloud Reader)
Endpoint observado:
- https://read.amazon.com/renderer/render
 
Materiais necessários por sessão:
- Cookies de sessão do navegador (login normal da Amazon)
 - Rendering token de uma startReading API call
 - Token de sessão ADP adicional usado pelo renderer
 
Comportamento:
- Cada requisição, quando enviada com headers e cookies equivalentes aos do navegador, retorna um arquivo TAR limitado a 5 páginas.
 - Para um livro longo você precisará de muitos batches; cada batch usa um mapeamento randomizado diferente de glyph IDs.
 
Conteúdo típico do TAR:
- page_data_0_4.json — positioned text runs as sequences of glyph IDs (not Unicode)
 - glyphs.json — per-request SVG path definitions for each glyph and fontFamily
 - toc.json — índice
 - metadata.json — metadados do livro
 - location_map.json — mapeamentos de posição lógico→visual
 
Exemplo de estrutura de page run:
{
"type": "TextRun",
"glyphs": [24, 25, 74, 123, 91],
"rect": {"left": 100, "top": 200, "right": 850, "bottom": 220},
"fontStyle": "italic",
"fontWeight": 700,
"fontSize": 12.5
}
Exemplo de entrada glyphs.json:
{
"24": {"path": "M 450 1480 L 820 1480 L 820 0 L 1050 0 L 1050 1480 ...", "fontFamily": "bookerly_normal"}
}
Notas sobre truques com paths anti-scraping:
- Paths podem incluir micro movimentos relativos (e.g., 
m3,1 m1,6 m-4,-7) que confundem muitos parsers de vetores e amostragem ingênua de path. - Sempre renderize paths completos preenchidos com um engine SVG robusto (e.g., CairoSVG) em vez de fazer diferenciação por comando/coordenação.
 
Por que decodificação ingênua falha
- Substituição de glifos randomizada por solicitação: o mapeamento glyph ID→character muda a cada lote; IDs não têm significado globalmente.
 - Comparação direta de coordenadas SVG é frágil: formas idênticas podem diferir nas coordenadas numéricas ou na codificação de comandos por solicitação.
 - OCR em glifos isolados tem desempenho ruim (≈50%), confunde pontuação e glifos parecidos, e ignora ligaturas.
 
Pipeline de trabalho: normalização e mapeamento de glifos independente da solicitação
- Rasterizar glifos SVG por solicitação
 
- Monte um documento SVG mínimo por glifo com o 
pathfornecido e renderize em uma tela fixa (e.g., 512×512) usando CairoSVG ou um engine equivalente que lide com sequências de path complicadas. - Renderize preenchido em preto sobre branco; evite strokes para eliminar artefatos dependentes do renderer e de AA.
 
- Hash perceptual para identidade entre solicitações
 
- Calcule um hash perceptual (e.g., pHash via 
imagehash.phash) de cada imagem de glifo. - Trate o hash como um ID estável: a mesma forma visual entre solicitações colapsa para o mesmo hash perceptual, derrotando IDs randomizados.
 
- Geração de atlas de fontes de referência
 
- Baixe as fontes TTF/OTF alvo (e.g., Bookerly normal/italic/bold/bold-italic).
 - Renderize candidatos para A–Z, a–z, 0–9, pontuação, marcas especiais (em/en dashes, quotes), e ligaturas explícitas: 
ff,fi,fl,ffi,ffl. - Mantenha atlas separados por variante de fonte (normal/italic/bold/bold-italic).
 - Use um text shaper adequado (HarfBuzz) se quiser fidelidade ao nível de glifo para ligaturas; rasterização simples via Pillow ImageFont pode ser suficiente se você renderizar as strings de ligatura diretamente e o engine de shaping as resolver.
 
- Casamento por similaridade visual com SSIM
 
- Para cada imagem de glifo desconhecida, calcule SSIM (Structural Similarity Index) contra todas as imagens candidatas em todos os atlas de variantes de fonte.
 - Atribua a string de caractere da melhor correspondência. SSIM absorve pequenas diferenças de antialiasing, escala e coordenadas melhor que comparações exatas por pixel.
 
- Tratamento de bordas e reconstrução
 
- Quando um glifo mapeia para uma ligatura (multi-char), expanda-a durante a decodificação.
 - Use run rectangles (top/left/right/bottom) para inferir quebras de parágrafo (deltas Y), alinhamento (padrões X), estilo e tamanhos.
 - Serialize para HTML/EPUB preservando 
fontStyle,fontWeight,fontSize, e links internos. 
Dicas de implementação
- Normalize todas as imagens para o mesmo tamanho e escala de cinza antes do hashing e do SSIM.
 - Cacheie por hash perceptual para evitar recomputar SSIM para glifos repetidos entre lotes.
 - Use um tamanho de raster de alta qualidade (e.g., 256–512 px) para melhor discriminação; reduza a escala conforme necessário antes do SSIM para acelerar.
 - Se usar Pillow para renderizar candidatos TTF, defina o mesmo tamanho de canvas e centralize o glifo; adicione padding para evitar recorte de ascendentes/descendentes.
 
Python: normalização e correspondência de glifos de ponta a ponta (raster hash + SSIM)
# pip install cairosvg pillow imagehash scikit-image uharfbuzz freetype-py
import io, json, tarfile, base64, math
from PIL import Image, ImageOps, ImageDraw, ImageFont
import imagehash
from skimage.metrics import structural_similarity as ssim
import cairosvg
CANVAS = (512, 512)
BGCOLOR = 255  # white
FGCOLOR = 0    # black
# --- SVG -> raster ---
def rasterize_svg_path(path_d: str, canvas=CANVAS) -> Image.Image:
# Build a minimal SVG document; rely on CAIRO for correct path handling
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{canvas[0]}" height="{canvas[1]}" viewBox="0 0 2048 2048">
<rect width="100%" height="100%" fill="white"/>
<path d="{path_d}" fill="black" fill-rule="nonzero"/>
</svg>'''
png_bytes = cairosvg.svg2png(bytestring=svg.encode('utf-8'))
img = Image.open(io.BytesIO(png_bytes)).convert('L')
return img
# --- Perceptual hash ---
def phash_img(img: Image.Image) -> str:
# Normalize to grayscale and fixed size
img = ImageOps.grayscale(img).resize((128, 128), Image.LANCZOS)
return str(imagehash.phash(img))
# --- Reference atlas from TTF ---
def render_char(candidate: str, ttf_path: str, canvas=CANVAS, size=420) -> Image.Image:
# Render centered text on same canvas to approximate glyph shapes
font = ImageFont.truetype(ttf_path, size=size)
img = Image.new('L', canvas, color=BGCOLOR)
draw = ImageDraw.Draw(img)
w, h = draw.textbbox((0,0), candidate, font=font)[2:]
dx = (canvas[0]-w)//2
dy = (canvas[1]-h)//2
draw.text((dx, dy), candidate, fill=FGCOLOR, font=font)
return img
# --- Build atlases for variants ---
FONT_VARIANTS = {
'normal':   '/path/to/Bookerly-Regular.ttf',
'italic':   '/path/to/Bookerly-Italic.ttf',
'bold':     '/path/to/Bookerly-Bold.ttf',
'bolditalic':'/path/to/Bookerly-BoldItalic.ttf',
}
CANDIDATES = [
*[chr(c) for c in range(0x20, 0x7F)],  # basic ASCII
'–', '—', '“', '”', '‘', '’', '•',      # common punctuation
'ff','fi','fl','ffi','ffl'              # ligatures
]
def build_atlases():
atlases = {}  # variant -> list[(char, img)]
for variant, ttf in FONT_VARIANTS.items():
out = []
for ch in CANDIDATES:
img = render_char(ch, ttf)
out.append((ch, img))
atlases[variant] = out
return atlases
# --- SSIM match ---
def best_match(img: Image.Image, atlases) -> tuple[str, float, str]:
# Returns (char, score, variant)
img_n = ImageOps.grayscale(img).resize((128,128), Image.LANCZOS)
img_n = ImageOps.autocontrast(img_n)
best = ('', -1.0, '')
import numpy as np
candA = np.array(img_n)
for variant, entries in atlases.items():
for ch, ref in entries:
ref_n = ImageOps.grayscale(ref).resize((128,128), Image.LANCZOS)
ref_n = ImageOps.autocontrast(ref_n)
candB = np.array(ref_n)
score = ssim(candA, candB)
if score > best[1]:
best = (ch, score, variant)
return best
# --- Putting it together for one TAR batch ---
def process_tar(tar_path: str, cache: dict, atlases) -> list[dict]:
# cache: perceptual-hash -> mapping {char, score, variant}
out_runs = []
with tarfile.open(tar_path, 'r:*') as tf:
glyphs = json.load(tf.extractfile('glyphs.json'))
# page_data_0_4.json may differ in name; list members to find it
pd_name = next(m.name for m in tf.getmembers() if m.name.startswith('page_data_'))
page_data = json.load(tf.extractfile(pd_name))
# 1. Rasterize + hash all glyphs for this batch
id2hash = {}
for gid, meta in glyphs.items():
img = rasterize_svg_path(meta['path'])
h = phash_img(img)
id2hash[int(gid)] = (h, img)
# 2. Ensure all hashes are resolved to characters in cache
for h, img in {v[0]: v[1] for v in id2hash.values()}.items():
if h not in cache:
ch, score, variant = best_match(img, atlases)
cache[h] = { 'char': ch, 'score': float(score), 'variant': variant }
# 3. Decode text runs
for run in page_data:
if run.get('type') != 'TextRun':
continue
decoded = []
for gid in run['glyphs']:
h, _ = id2hash[gid]
decoded.append(cache[h]['char'])
run_out = {
'text': ''.join(decoded),
'rect': run.get('rect'),
'fontStyle': run.get('fontStyle'),
'fontWeight': run.get('fontWeight'),
'fontSize': run.get('fontSize'),
}
out_runs.append(run_out)
return out_runs
# Usage sketch:
# atlases = build_atlases()
# cache = {}
# for tar in sorted(glob('batches/*.tar')):
#     runs = process_tar(tar, cache, atlases)
#     # accumulate runs for layout reconstruction → EPUB/HTML
Heurísticas de reconstrução de layout/EPUB
- Quebras de parágrafo: Se o top Y do run seguinte exceder a linha de base da linha anterior por um limiar (relativo ao tamanho da fonte), inicie um novo parágrafo.
 - Alinhamento: Agrupe por valores similares de left X para parágrafos alinhados à esquerda; detecte linhas centralizadas por margens simétricas; detecte parágrafos alinhados à direita pelas bordas à direita.
 - Estilização: Preserve italic/bold via 
fontStyle/fontWeight; varie classes CSS por faixas defontSizepara aproximar títulos do corpo. - Links: Se runs incluírem metadados de link (ex.: 
positionId), emita anchors e internal hrefs. 
Mitigando truques anti-scraping de path em SVG
- Use caminhos preenchidos com 
fill-rule: nonzeroe um renderizador adequado (CairoSVG, resvg). Não dependa da normalização dos tokens do path. - Evite renderização de stroke; concentre-se em sólidos preenchidos para contornar artefatos hairline causados por micro-movimentos relativos.
 - Mantenha um viewBox estável por render para que formas idênticas rasterizem consistentemente entre lotes.
 
Notas de desempenho
- Na prática, livros convergem para algumas centenas de glifos únicos (por exemplo, ~361 incluindo ligaduras). Cacheie resultados de SSIM por perceptual hash.
 - Após a descoberta inicial, lotes futuros predominantemente reutilizam hashes conhecidos; a decodificação torna-se limitada por I/O.
 - SSIM médio ≈0.95 é um sinal forte; considere sinalizar correspondências com baixa pontuação para revisão manual.
 
Generalização para outros viewers
Qualquer sistema que:
- Retorne positioned glyph runs com IDs numéricos com escopo por requisição
 - Forneça glifos vetoriais por requisição (SVG paths ou subset fonts)
 - Limite páginas por requisição para prevenir exportação em massa
 
…pode ser tratado com a mesma normalização:
- Rasterize formas por requisição → perceptual hash → shape ID
 - Atlas de glifos/ligaduras candidatas por variante de fonte
 - SSIM (ou métrica perceptual similar) para atribuir caracteres
 - Reconstruir layout a partir dos retângulos/estilos dos runs
 
Exemplo mínimo de aquisição (esboço)
Use as DevTools do seu navegador para capturar os headers, cookies e tokens exatos usados pelo leitor ao solicitar /renderer/render. Em seguida, replique-os a partir de um script ou curl. Esboço de exemplo:
curl 'https://read.amazon.com/renderer/render' \
-H 'Cookie: session-id=...; at-main=...; sess-at-main=...' \
-H 'x-adp-session: <ADP_SESSION_TOKEN>' \
-H 'authorization: Bearer <RENDERING_TOKEN_FROM_startReading>' \
-H 'User-Agent: <copy from browser>' \
-H 'Accept: application/x-tar' \
--compressed --output batch_000.tar
Ajuste a parametrização (book ASIN, page window, viewport) para corresponder às solicitações do leitor. Espere um limite de 5 páginas por solicitação.
Resultados alcançáveis
- Colapsar 100+ alfabetos randomizados em um único espaço de glyphs via perceptual hashing
 - Mapeamento 100% dos glyphs únicos com SSIM médio ~0.95 quando os atlases incluem ligatures e variantes
 - EPUB/HTML reconstruído visualmente indistinguível do original
 
Referências
- Kindle Web DRM: Breaking Randomized SVG Glyph Obfuscation with Raster Hashing + SSIM (Pixelmelt blog)
 - CairoSVG – SVG to PNG renderer
 - imagehash – Perceptual image hashing (pHash)
 - scikit-image – Structural Similarity Index (SSIM)
 
tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: 
HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure: 
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
 - Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
 - Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
 
HackTricks