Monday, March 30, 2026

Calibro'dan satın aldığım kitabı okumak için verdiğim uğraş.

Geçen hafta Calibro'dan bir e-kitap satın aldım. Cynthia Stokes Brown'ın Büyük Tarih: Büyük Patlamadan Bugüne adlı kitabını. İndirdim, açmaya çalıştım — ve işte o zaman eğlence başladı.
Kitap açılmıyordu. Daha doğrusu, sadece Calibro'nun kendi uygulamasında açılıyordu. Telefona kurmayı denedim, güncel cihazlarda uygulama çalışmıyor diye hata verdi. Bilgisayarda okumayı denedim, olmadı. Satın aldığım bir şeyi okuyamıyordum.

Calibro'ya mail attım. Tweet attım. Yanıt vermediler.

O gece bir karar verdim: Bu kitabı okuyacaktım.


DRM Nedir?

DRM (Digital Rights Management), dijital içerikleri korumak için kullanılan şifreleme sistemidir. Teoride mantıklıdır: İçerik üreticilerinin haklarını korur. Pratikte ise satın aldığın bir şeyi kullanamamana neden olur.

Calibro, PocketBook'un PBDRM sistemini kullanıyor. Kitap .lndrm uzantılı bir dosya olarak geliyor. İçini hex editörle açınca şunu gördüm:

50 42 44 52 4D ...  →  "PBDRM" magic bytes

Adobe ADEPT altyapısı üzerine kurulu, RSA + AES-128-CBC şifreleme kullanıyor. Yani:

  1. Kitabın içeriği AES-128-CBC ile şifrelenmiş

  2. AES anahtarı RSA ile şifrelenmiş

  3. RSA şifrelemek için Calibro'nun sunucu tarafındaki public key kullanılmış

  4. Çözmek için Calibro'nun elindeki private key gerekiyor


İlk Hamle: Adobe Digital Editions

İlk denediğim şey Adobe Digital Editions (ADE) ve DeDRM plugin'iydi. Bu ikili çoğu e-kitap DRM'ini kaldırabiliyor. Ama ADE denemeye başlamadan önce şöyle bir hata verdi:

E_ADEPT_NO_TOKEN

Bu hata, ADE'nin Adobe'nin lisans sunucusundan (adeactivate.adobe.com) token alamaması anlamına geliyor. Çözüm denemeleri:

  • ADE'yi deauthorize edip yeniden authorize etmek

  • VPN'i kapatmak

  • Hosts dosyasında Adobe sunucularının bloklanıp bloklanmadığını kontrol etmek (/etc/hosts veya C:\Windows\System32\drivers\etc\hosts)

  • ADE'yi güncellemek

Hiçbiri işe yaramadı. Üstelik DeDRM plugin, PBDRM formatını zaten tanımıyordu — bu Adobe'nin standart ADEPT formatı değil, PocketBook'un özel bir implementasyonu.


Memu Emülatörü

Calibro Reader yalnızca Android'de çalışıyor ve güncel telefonlarda çalışmıyor. Çözüm: Memu — Windows'ta Android uygulamaları çalıştıran bir emülatör.

Memu → Android x86_64 emülatör
Calibro Reader APK → ARM binary

Uygulama kuruldu, kitap açıldı, okuyabildim. Ama bu çözüm değildi — bağımlılık yaratıyor, başka cihazda çalışmıyor.


PBDRM Header Analizi

Kitap dosyasını Python ile incelemeye başladım:

data = open('kitap.lndrm', 'rb').read()
print(data[:32].hex())
# 5042 4452 4d00 0100 a896 3191 69d0 5969...
# P B D R M

Header yapısı:

OffsetUzunlukİçerik
0x005 bytePBDRM magic
0x052 byte0x0001 type
0x072 byte0x00a8 = 168 (şifreli anahtar uzunluğu)
0x09168 byteRSA ile şifreli AES anahtarı
0x12016 byteIV (initialization vector)
0x130~AES ile şifreli içerik

168 byte ilginçti — RSA-1024 için 128 byte olması gerekir. 40 byte fazladan ne? Bunun üzerinde saatlerce çalıştım: IV mi, salt mı, başka bir şey mi?


activation.xml

Uygulamanın data dizininde kritik dosyalar vardı:

adb shell "ls /data/data/com.calibro.reader/app_resources/"
# activation.xml
# device.xml

activation.xml Adobe ADEPT formatındaydı. İçinde iki önemli şey:

  1. privateLicenseKey: RSA-1024 private key (base64 encoded)

  2. pkcs12: Şifreli PKCS12 sertifikası

import base64, xml.etree.ElementTree as ET
from Crypto.PublicKey import RSA

tree = ET.parse('activation.xml')
pk_b64 = tree.find('.//{http://ns.adobe.com/adept}privateLicenseKey').text
pk_der = base64.b64decode(pk_b64)
rsa_key = RSA.import_key(pk_der)
print(f"Key boyutu: {rsa_key.size_in_bytes()} bytes")  # 128

privateLicenseKey ile .lndrm dosyasındaki 168 byte'lık bloğu çözmeye çalıştım. 168 byte içinde 128 byte'lık RSA bloğunun hangi offset'te olduğunu bulmak için brute force:

from Crypto.Cipher import PKCS1_v1_5

full_block = data[0x09:0x09+168]

for offset in range(0, 41):
   rsa_block = full_block[offset:offset+128]
   cipher = PKCS1_v1_5.new(rsa_key)
   result = cipher.decrypt(rsa_block, b'\xff' * 32)
   if result != b'\xff' * 32:
       print(f"[+] offset={offset}: {result.hex()}")

Hiçbir offset'te başarılı decrypt elde edemedim.

Neden? Çünkü privateLicenseKey kullanıcı kimlik doğrulama anahtarıydı — kitap şifreleme anahtarı değildi. Gerçek şifre çözme anahtarı Calibro'nun sunucusundaydı.

PKCS12 sertifikasının şifresini kırmayı denedim — cihazın fingerprint'i, device ID, salt gibi değerleri şifre olarak denedim. Başarısız.


Frida ile Hook Denemesi

Eğer anahtarı statik analiz ile bulamıyorsam, uygulama çalışırken dinamik olarak yakalamayı deneyebilirdim. Frida bunun için ideal araç — çalışan bir process'e JavaScript kodu enjekte etmeyi sağlıyor.

Plan: libpb_reader.so veya libadobe_rmsdk.so içindeki AES/RSA fonksiyonlarını hook'layıp şifre çözme anında anahtarı yakalamak.

Java.perform(function() {
   var EVP_DecryptInit_ex = Module.findExportByName(
       "libpb_reader.so",
       "EVP_DecryptInit_ex"
  );
   
   Interceptor.attach(EVP_DecryptInit_ex, {
       onEnter: function(args) {
           // args[3] = key, args[4] = iv
           var key = args[3].readByteArray(16);
           console.log("[+] AES KEY: " + bytesToHex(key));
      }
  });
});

Ama Frida modülleri listelerken libpb_reader.so ve libadobe_rmsdk.so hiç görünmüyordu:

Process.enumerateModules().forEach(function(m) {
   console.log(m.name, m.path);
});
// libpb_reader.so YOK
// libadobe_rmsdk.so YOK

Neden? Houdini.

Houdini Nedir?

Memu x86_64 tabanlı bir emülatör. Calibro Reader ARM binary olarak derlenmemiş. Intel'in Houdini katmanı ARM kodunu gerçek zamanlı olarak x86'ya çevirerek çalıştırıyor. Bu işlem Frida'nın göremediği ayrı bir memory space'de gerçekleşiyor.

Frida → x86_64 process görüyor
Houdini → ARM kütüphanelerini kendi içinde çalıştırıyor
libpb_reader.so (ARM) → Frida'ya görünmüyor

Memu'yu ARM moduna geçirdim (System Settings → Preferences → ABI → ARM). Fark etmedi:

adb shell getprop ro.product.cpu.abi
# x86_64 ← hala x86_64

Android sistemi x86_64 kalmaya devam etti. Frida hala ARM kütüphanelerine ulaşamadı.


/proc/pid/mem — Memory Dump Denemesi

Eğer şifre çözme anında anahtarı yakalayamazsam, belki şifresi çözülmüş veriyi doğrudan RAM'den okuyabilirdim.

EPUB dosyaları ZIP formatındadır. ZIP magic bytes: PK\x03\x04. Uygulama kitabı render ettiğinde bu veri bir an için RAM'de olmalı.

# PID bul
frida-ps -U | grep calibro
# 5254 com.calibro.reader:viewer

# Memory map'i al
adb shell "su 0 cat /proc/5254/maps" > maps.txt

maps.txt incelenince libpb_reader.so ve libadobe_rmsdk.so ARM binary olarak yüklenmiş görünüyordu:

0b9cc000-0c07e000 r--p  /data/app/com.calibro.reader-.../lib/arm/libpb_reader.so
031cc000-039f7000 r--p /data/app/com.calibro.reader-.../lib/arm/libadobe_rmsdk.so

Heap bölgelerini dump etmeye çalıştım:

# Python ile dd komutu
cmd = f'adb shell "su 0 dd if=/proc/5254/mem bs=4096 skip={start//4096} count={size//4096}"'
result = subprocess.run(cmd, capture_output=True)
# Sonuç: boş

/proc/pid/mem okuma bu Memu kurulumunda root ile bile çalışmıyordu.


Sonuç: DRM Kırılamaz

Uzun denemeler sonunda şu neticeye vardım:

Şifre çözme anahtarı Calibro'nun sunucusunda. Cihaza hiçbir zaman tam olarak gönderilmiyor. Native ARM kod (Houdini altında) onu alıp kullanıyor, dışarıya sızdırmıyor. Frida ARM kütüphanelerine erişemiyor.

Bu noktada stratejimi değiştirdim.


Plan B: Ekran Görüntüsü Otomasyonu

Uygulama kitabı ekranda gösteriyordu. Ekranda olan her şey yakalanabilir — bu bir bilgisayar bilimi gerçeği.

İlk deneme:

adb shell screencap -p /sdcard/test.png
adb pull /sdcard/test.png

Sonuç: Tamamen siyah, boş PNG.

Neden? FLAG_SECURE. Bu Android flag'i ekran görüntüsü alınmasını engeller. DRM uygulamalarının kullandığı standart bir koruma.

Ama Memu'nun kendi screenshot mekanizması bu flag'i atlıyordu. Memu'nun sağ kenar çubuğundaki kamera ikonuna tıkladığımda içerikli ekran görüntüsü alınıyordu.

Peki bunu ADB ile tetikleyebilir miydim?

adb shell "input keyevent 120"
# 120 = KEYCODE_SYSRQ = screenshot keycode

Evet! Bu komut Memu'nun screenshot mekanizmasını tetikliyordu ve FLAG_SECURE'u atlıyordu. Dosya /sdcard/Pictures/Screenshots/ klasörüne kaydediliyordu.

Sayfa çevirmek için de klavye ok tuşları çalışıyordu — ekrana tap etmek yerine:

adb shell "input keyevent 22"
# 22 = KEYCODE_DPAD_RIGHT = sağ ok tuşu

Ekrana tap, sayfa içindeki dipnot linklerine yanlışlıkla tıklıyordu. Klavye tuşu bu sorunu çözdü.


Otomasyon Scripti

import subprocess, time, os

output_dir = r'C:\Users\hicbi\Downloads\kitap_pages'
os.makedirs(output_dir, exist_ok=True)

popup_wait = 10.0      # Screenshot popup'ı kaybolsun
page_turn_delay = 1.5  # Sayfa animasyonu tamamlansın

def adb(cmd):
   subprocess.run(f'adb shell {cmd}', shell=True, capture_output=True)

def get_latest_screenshot():
   result = subprocess.run(
       'adb shell "ls -t /sdcard/Pictures/Screenshots/"',
       shell=True, capture_output=True, text=True
  )
   files = [f.strip() for f in result.stdout.strip().split('\n') if f.strip()]
   return files[0] if files else None

def capture_page(page_num):
   local = os.path.join(output_dir, f'page_{page_num:04d}.png')
   adb('"input keyevent 120"')   # Screenshot al
   time.sleep(popup_wait)         # Popup kaybolsun
   latest = get_latest_screenshot()
   subprocess.run(
       f'adb pull "/sdcard/Pictures/Screenshots/{latest}" "{local}"',
       shell=True, capture_output=True
  )

def next_page():
   adb('"input keyevent 22"')    # Sağ ok tuşu
   time.sleep(page_turn_delay)

for i in range(1, 283):
   capture_page(i)
   if i < 282:
       next_page()

Karşılaşılan Sorunlar ve Çözümleri

1. Screenshot popup'ı Her screenshot sonrası "Screenshot saved" popup'ı çıkıyor ve kapanmıyordu. ADB ile kapatmayı denedim (input tap, keyevent 4 vs.) — olmadı. Çözüm: Popup zaten 7-10 saniye sonra otomatik kapanıyor, popup_wait = 10 yapınca sorun çözüldü.

2. Dipnot linkleri Sayfa çevirmek için input tap 1200 450 koordinatı kullanıyordum, ama bazı sayfalarda bu koordinat dipnot linkine denk geliyordu. Uygulama dipnotlar sayfasına atlıyordu. Çözüm: input keyevent 22 (sağ ok tuşu).

3. Screenshots klasörünün dolması 282 sayfa boyunca klasörde biriken PNG'ler ls -t komutunu yavaşlattı, timing bozuldu, sayfalar karıştı. Çözüm: Her 50 sayfada bir klasörü temizle:

adb shell "rm /sdcard/Pictures/Screenshots/*"

4. Windows uyku modu Uzun süre klavye/mouse aktivitesi olmayınca Windows uyku moduna girdi, Memu dondu. Çözüm: Güç ayarlarından uyku modunu "Hiçbir zaman" yap. Android tarafı için:

adb shell "settings put system screen_off_timeout 3600000"
# 3600000 ms = 60 dakika

5. EPUB sayfa numarası tutarsızlığı 282 sayfalık kitap, 1600x900 ekranda 413 ekran görüntüsüne çıktı. EPUB reader'larda sayfa numarası font boyutuna ve ekran boyutuna göre değişiyor — "sayfa" kavramı görecelidir.

6. Performans şişmesi Uzun süreli çalışmada Memu RAM'i doluyordu. Çözüm: 50'şer sayfa batch'ler halinde çalıştır, her batch sonrası Memu'yu dinlendir.


OCR: Görüntüden Metne

413 ekran görüntüsü elimdeydi. Şimdi metni çıkarmak gerekiyordu.

Tesseract OCR kurulumu:

pip install pytesseract pillow
import pytesseract
from PIL import Image
import os

pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'

folder = r'C:\Users\hicbi\Downloads\kitap_pages'
files = sorted([f for f in os.listdir(folder) if f.endswith('.png')])

with open('buyuk_tarih.txt', 'w', encoding='utf-8') as out:
   for i, filename in enumerate(files):
       img = Image.open(os.path.join(folder, filename))
       text = pytesseract.image_to_string(img, lang='tur')
       out.write(text)
       out.write(f'\n\n--- Sayfa {i+1} ---\n\n')
       print(f'Sayfa {i+1}/{len(files)}', end='\r')

Sonuç oldukça başarılıydı. Türkçe karakter tanıma (ş, ğ, ü, ö, ı, ç) neredeyse hatasız çalıştı. Tesseract, yüksek kaliteli ekran görüntülerinde gerçekten iyi iş çıkarıyor.


Sonuç Olarak

Bir kitap için saatler harcadım. Sırasıyla şunları denedim:

  1. ✗ Adobe Digital Editions + DeDRM plugin → PBDRM formatını tanımıyor

  2. ✗ PBDRM header analizi + RSA brute force → Yanlış anahtar

  3. ✗ PKCS12 şifre kırma → Şifre bulunamadı

  4. ✗ Frida hook → Houdini ARM emülasyonu engel

  5. ✗ /proc/pid/mem dump → İzin yok

  6. ✓ ADB screenshot otomasyonu → Çalıştı

DRM sistemi beni değil, içerik üreticisini korudu. Ben yasal olarak satın aldığım bir içeriği okumak için bu kadar çaba harcadım. Korsanlık yapan biri bu adımların hiçbirini atmak zorunda kalmaz — DRM kırılmış versiyonunu internetten bulur.

Belki de DRM'in asıl kurbanları dürüst kullanıcılardır.


Kullanılan Araçlar

AraçAmaç
MemuAndroid emülatör
ADB (Android Debug Bridge)Cihaz kontrolü
FridaDynamic instrumentation
Python + pycryptodomeRSA/AES analizi
Tesseract OCRGörüntüden metin
Hex editörPBDRM format analizi

Bu yazı tamamen kişisel kullanım amacıyla yapılan bir deneyin belgesidir. Hiçbir içerik üçüncü şahıslarla paylaşılmamıştır.