--- /dev/null
+#! /usr/bin/python
+
+# unswindle.pyw, version 5
+
+# To run this program install a 32-bit version of Python 2.6 from
+# <http://www.python.org/download/>. Save this script file as unswindle.pyw.
+# Find and save in the same directory a copy of mobidedrm.py. Double-click on
+# unswindle.pyw. It will run Kindle For PC. Open the book you want to
+# decrypt. Close Kindle For PC. A dialog will open allowing you to select the
+# output file. And you're done!
+
+# Revision history:
+# 1 - Initial release
+# 2 - Fixes to work properly on Windows versions >XP
+# 3 - Fix minor bug in path extraction
+# 4 - Fix error opening threads; detect Topaz books;
+# detect unsupported versions of K4PC
+# 5 - Work with new (20091222) version of K4PC
+
+"""
+Decrypt Kindle For PC encrypted Mobipocket books.
+"""
+
+__license__ = 'GPL v3'
+
+import sys
+import os
+import re
+import tempfile
+import shutil
+import subprocess
+import struct
+import hashlib
+import ctypes
+from ctypes import *
+from ctypes.wintypes import *
+import binascii
+import _winreg as winreg
+import Tkinter
+import Tkconstants
+import tkMessageBox
+import tkFileDialog
+import traceback
+
+#
+# _extrawintypes.py
+
+UBYTE = c_ubyte
+ULONG_PTR = POINTER(ULONG)
+PULONG = ULONG_PTR
+PVOID = LPVOID
+LPCTSTR = LPTSTR = c_wchar_p
+LPBYTE = c_char_p
+SIZE_T = c_uint
+SIZE_T_p = POINTER(SIZE_T)
+
+#
+# _ntdll.py
+
+NTSTATUS = DWORD
+
+ntdll = windll.ntdll
+
+class PROCESS_BASIC_INFORMATION(Structure):
+ _fields_ = [('Reserved1', PVOID),
+ ('PebBaseAddress', PVOID),
+ ('Reserved2', PVOID * 2),
+ ('UniqueProcessId', ULONG_PTR),
+ ('Reserved3', PVOID)]
+
+# NTSTATUS WINAPI NtQueryInformationProcess(
+# __in HANDLE ProcessHandle,
+# __in PROCESSINFOCLASS ProcessInformationClass,
+# __out PVOID ProcessInformation,
+# __in ULONG ProcessInformationLength,
+# __out_opt PULONG ReturnLength
+# );
+NtQueryInformationProcess = ntdll.NtQueryInformationProcess
+NtQueryInformationProcess.argtypes = [HANDLE, DWORD, PVOID, ULONG, PULONG]
+NtQueryInformationProcess.restype = NTSTATUS
+
+#
+# _kernel32.py
+
+INFINITE = 0xffffffff
+
+CREATE_UNICODE_ENVIRONMENT = 0x00000400
+DEBUG_ONLY_THIS_PROCESS = 0x00000002
+DEBUG_PROCESS = 0x00000001
+
+THREAD_GET_CONTEXT = 0x0008
+THREAD_QUERY_INFORMATION = 0x0040
+THREAD_SET_CONTEXT = 0x0010
+THREAD_SET_INFORMATION = 0x0020
+
+EXCEPTION_BREAKPOINT = 0x80000003
+EXCEPTION_SINGLE_STEP = 0x80000004
+EXCEPTION_ACCESS_VIOLATION = 0xC0000005
+
+DBG_CONTINUE = 0x00010002L
+DBG_EXCEPTION_NOT_HANDLED = 0x80010001L
+
+EXCEPTION_DEBUG_EVENT = 1
+CREATE_THREAD_DEBUG_EVENT = 2
+CREATE_PROCESS_DEBUG_EVENT = 3
+EXIT_THREAD_DEBUG_EVENT = 4
+EXIT_PROCESS_DEBUG_EVENT = 5
+LOAD_DLL_DEBUG_EVENT = 6
+UNLOAD_DLL_DEBUG_EVENT = 7
+OUTPUT_DEBUG_STRING_EVENT = 8
+RIP_EVENT = 9
+
+class DataBlob(Structure):
+ _fields_ = [('cbData', c_uint),
+ ('pbData', c_void_p)]
+DataBlob_p = POINTER(DataBlob)
+
+class SECURITY_ATTRIBUTES(Structure):
+ _fields_ = [('nLength', DWORD),
+ ('lpSecurityDescriptor', LPVOID),
+ ('bInheritHandle', BOOL)]
+LPSECURITY_ATTRIBUTES = POINTER(SECURITY_ATTRIBUTES)
+
+class STARTUPINFO(Structure):
+ _fields_ = [('cb', DWORD),
+ ('lpReserved', LPTSTR),
+ ('lpDesktop', LPTSTR),
+ ('lpTitle', LPTSTR),
+ ('dwX', DWORD),
+ ('dwY', DWORD),
+ ('dwXSize', DWORD),
+ ('dwYSize', DWORD),
+ ('dwXCountChars', DWORD),
+ ('dwYCountChars', DWORD),
+ ('dwFillAttribute', DWORD),
+ ('dwFlags', DWORD),
+ ('wShowWindow', WORD),
+ ('cbReserved2', WORD),
+ ('lpReserved2', LPBYTE),
+ ('hStdInput', HANDLE),
+ ('hStdOutput', HANDLE),
+ ('hStdError', HANDLE)]
+LPSTARTUPINFO = POINTER(STARTUPINFO)
+
+class PROCESS_INFORMATION(Structure):
+ _fields_ = [('hProcess', HANDLE),
+ ('hThread', HANDLE),
+ ('dwProcessId', DWORD),
+ ('dwThreadId', DWORD)]
+LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION)
+
+EXCEPTION_MAXIMUM_PARAMETERS = 15
+class EXCEPTION_RECORD(Structure):
+ pass
+EXCEPTION_RECORD._fields_ = [
+ ('ExceptionCode', DWORD),
+ ('ExceptionFlags', DWORD),
+ ('ExceptionRecord', POINTER(EXCEPTION_RECORD)),
+ ('ExceptionAddress', LPVOID),
+ ('NumberParameters', DWORD),
+ ('ExceptionInformation', ULONG_PTR * EXCEPTION_MAXIMUM_PARAMETERS)]
+
+class EXCEPTION_DEBUG_INFO(Structure):
+ _fields_ = [('ExceptionRecord', EXCEPTION_RECORD),
+ ('dwFirstChance', DWORD)]
+
+class CREATE_THREAD_DEBUG_INFO(Structure):
+ _fields_ = [('hThread', HANDLE),
+ ('lpThreadLocalBase', LPVOID),
+ ('lpStartAddress', LPVOID)]
+
+class CREATE_PROCESS_DEBUG_INFO(Structure):
+ _fields_ = [('hFile', HANDLE),
+ ('hProcess', HANDLE),
+ ('hThread', HANDLE),
+ ('dwDebugInfoFileOffset', DWORD),
+ ('nDebugInfoSize', DWORD),
+ ('lpThreadLocalBase', LPVOID),
+ ('lpStartAddress', LPVOID),
+ ('lpImageName', LPVOID),
+ ('fUnicode', WORD)]
+
+class EXIT_THREAD_DEBUG_INFO(Structure):
+ _fields_ = [('dwExitCode', DWORD)]
+
+class EXIT_PROCESS_DEBUG_INFO(Structure):
+ _fields_ = [('dwExitCode', DWORD)]
+
+class LOAD_DLL_DEBUG_INFO(Structure):
+ _fields_ = [('hFile', HANDLE),
+ ('lpBaseOfDll', LPVOID),
+ ('dwDebugInfoFileOffset', DWORD),
+ ('nDebugInfoSize', DWORD),
+ ('lpImageName', LPVOID),
+ ('fUnicode', WORD)]
+
+class UNLOAD_DLL_DEBUG_INFO(Structure):
+ _fields_ = [('lpBaseOfDll', LPVOID)]
+
+class OUTPUT_DEBUG_STRING_INFO(Structure):
+ _fields_ = [('lpDebugStringData', LPSTR),
+ ('fUnicode', WORD),
+ ('nDebugStringLength', WORD)]
+
+class RIP_INFO(Structure):
+ _fields_ = [('dwError', DWORD),
+ ('dwType', DWORD)]
+
+class _U(Union):
+ _fields_ = [('Exception', EXCEPTION_DEBUG_INFO),
+ ('CreateThread', CREATE_THREAD_DEBUG_INFO),
+ ('CreateProcessInfo', CREATE_PROCESS_DEBUG_INFO),
+ ('ExitThread', EXIT_THREAD_DEBUG_INFO),
+ ('ExitProcess', EXIT_PROCESS_DEBUG_INFO),
+ ('LoadDll', LOAD_DLL_DEBUG_INFO),
+ ('UnloadDll', UNLOAD_DLL_DEBUG_INFO),
+ ('DebugString', OUTPUT_DEBUG_STRING_INFO),
+ ('RipInfo', RIP_INFO)]
+
+class DEBUG_EVENT(Structure):
+ _anonymous_ = ('u',)
+ _fields_ = [('dwDebugEventCode', DWORD),
+ ('dwProcessId', DWORD),
+ ('dwThreadId', DWORD),
+ ('u', _U)]
+LPDEBUG_EVENT = POINTER(DEBUG_EVENT)
+
+CONTEXT_X86 = 0x00010000
+CONTEXT_i386 = CONTEXT_X86
+CONTEXT_i486 = CONTEXT_X86
+
+CONTEXT_CONTROL = (CONTEXT_i386 | 0x0001) # SS:SP, CS:IP, FLAGS, BP
+CONTEXT_INTEGER = (CONTEXT_i386 | 0x0002) # AX, BX, CX, DX, SI, DI
+CONTEXT_SEGMENTS = (CONTEXT_i386 | 0x0004) # DS, ES, FS, GS
+CONTEXT_FLOATING_POINT = (CONTEXT_i386 | 0x0008L) # 387 state
+CONTEXT_DEBUG_REGISTERS = (CONTEXT_i386 | 0x0010L) # DB 0-3,6,7
+CONTEXT_EXTENDED_REGISTERS = (CONTEXT_i386 | 0x0020L)
+CONTEXT_FULL = (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS)
+CONTEXT_ALL = (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS |
+ CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS |
+ CONTEXT_EXTENDED_REGISTERS)
+
+SIZE_OF_80387_REGISTERS = 80
+class FLOATING_SAVE_AREA(Structure):
+ _fields_ = [('ControlWord', DWORD),
+ ('StatusWord', DWORD),
+ ('TagWord', DWORD),
+ ('ErrorOffset', DWORD),
+ ('ErrorSelector', DWORD),
+ ('DataOffset', DWORD),
+ ('DataSelector', DWORD),
+ ('RegisterArea', BYTE * SIZE_OF_80387_REGISTERS),
+ ('Cr0NpxState', DWORD)]
+
+MAXIMUM_SUPPORTED_EXTENSION = 512
+class CONTEXT(Structure):
+ _fields_ = [('ContextFlags', DWORD),
+ ('Dr0', DWORD),
+ ('Dr1', DWORD),
+ ('Dr2', DWORD),
+ ('Dr3', DWORD),
+ ('Dr6', DWORD),
+ ('Dr7', DWORD),
+ ('FloatSave', FLOATING_SAVE_AREA),
+ ('SegGs', DWORD),
+ ('SegFs', DWORD),
+ ('SegEs', DWORD),
+ ('SegDs', DWORD),
+ ('Edi', DWORD),
+ ('Esi', DWORD),
+ ('Ebx', DWORD),
+ ('Edx', DWORD),
+ ('Ecx', DWORD),
+ ('Eax', DWORD),
+ ('Ebp', DWORD),
+ ('Eip', DWORD),
+ ('SegCs', DWORD),
+ ('EFlags', DWORD),
+ ('Esp', DWORD),
+ ('SegSs', DWORD),
+ ('ExtendedRegisters', BYTE * MAXIMUM_SUPPORTED_EXTENSION)]
+LPCONTEXT = POINTER(CONTEXT)
+
+class LDT_ENTRY(Structure):
+ _fields_ = [('LimitLow', WORD),
+ ('BaseLow', WORD),
+ ('BaseMid', UBYTE),
+ ('Flags1', UBYTE),
+ ('Flags2', UBYTE),
+ ('BaseHi', UBYTE)]
+LPLDT_ENTRY = POINTER(LDT_ENTRY)
+
+kernel32 = windll.kernel32
+
+# BOOL WINAPI CloseHandle(
+# __in HANDLE hObject
+# );
+CloseHandle = kernel32.CloseHandle
+CloseHandle.argtypes = [HANDLE]
+CloseHandle.restype = BOOL
+
+# BOOL WINAPI CreateProcess(
+# __in_opt LPCTSTR lpApplicationName,
+# __inout_opt LPTSTR lpCommandLine,
+# __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
+# __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
+# __in BOOL bInheritHandles,
+# __in DWORD dwCreationFlags,
+# __in_opt LPVOID lpEnvironment,
+# __in_opt LPCTSTR lpCurrentDirectory,
+# __in LPSTARTUPINFO lpStartupInfo,
+# __out LPPROCESS_INFORMATION lpProcessInformation
+# );
+CreateProcess = kernel32.CreateProcessW
+CreateProcess.argtypes = [LPCTSTR, LPTSTR, LPSECURITY_ATTRIBUTES,
+ LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCTSTR,
+ LPSTARTUPINFO, LPPROCESS_INFORMATION]
+CreateProcess.restype = BOOL
+
+# HANDLE WINAPI OpenThread(
+# __in DWORD dwDesiredAccess,
+# __in BOOL bInheritHandle,
+# __in DWORD dwThreadId
+# );
+OpenThread = kernel32.OpenThread
+OpenThread.argtypes = [DWORD, BOOL, DWORD]
+OpenThread.restype = HANDLE
+
+# BOOL WINAPI ContinueDebugEvent(
+# __in DWORD dwProcessId,
+# __in DWORD dwThreadId,
+# __in DWORD dwContinueStatus
+# );
+ContinueDebugEvent = kernel32.ContinueDebugEvent
+ContinueDebugEvent.argtypes = [DWORD, DWORD, DWORD]
+ContinueDebugEvent.restype = BOOL
+
+# BOOL WINAPI DebugActiveProcess(
+# __in DWORD dwProcessId
+# );
+DebugActiveProcess = kernel32.DebugActiveProcess
+DebugActiveProcess.argtypes = [DWORD]
+DebugActiveProcess.restype = BOOL
+
+# BOOL WINAPI GetThreadContext(
+# __in HANDLE hThread,
+# __inout LPCONTEXT lpContext
+# );
+GetThreadContext = kernel32.GetThreadContext
+GetThreadContext.argtypes = [HANDLE, LPCONTEXT]
+GetThreadContext.restype = BOOL
+
+# BOOL WINAPI GetThreadSelectorEntry(
+# __in HANDLE hThread,
+# __in DWORD dwSelector,
+# __out LPLDT_ENTRY lpSelectorEntry
+# );
+GetThreadSelectorEntry = kernel32.GetThreadSelectorEntry
+GetThreadSelectorEntry.argtypes = [HANDLE, DWORD, LPLDT_ENTRY]
+GetThreadSelectorEntry.restype = BOOL
+
+# BOOL WINAPI ReadProcessMemory(
+# __in HANDLE hProcess,
+# __in LPCVOID lpBaseAddress,
+# __out LPVOID lpBuffer,
+# __in SIZE_T nSize,
+# __out SIZE_T *lpNumberOfBytesRead
+# );
+ReadProcessMemory = kernel32.ReadProcessMemory
+ReadProcessMemory.argtypes = [HANDLE, LPCVOID, LPVOID, SIZE_T, SIZE_T_p]
+ReadProcessMemory.restype = BOOL
+
+# BOOL WINAPI SetThreadContext(
+# __in HANDLE hThread,
+# __in const CONTEXT *lpContext
+# );
+SetThreadContext = kernel32.SetThreadContext
+SetThreadContext.argtypes = [HANDLE, LPCONTEXT]
+SetThreadContext.restype = BOOL
+
+# BOOL WINAPI WaitForDebugEvent(
+# __out LPDEBUG_EVENT lpDebugEvent,
+# __in DWORD dwMilliseconds
+# );
+WaitForDebugEvent = kernel32.WaitForDebugEvent
+WaitForDebugEvent.argtypes = [LPDEBUG_EVENT, DWORD]
+WaitForDebugEvent.restype = BOOL
+
+# BOOL WINAPI WriteProcessMemory(
+# __in HANDLE hProcess,
+# __in LPVOID lpBaseAddress,
+# __in LPCVOID lpBuffer,
+# __in SIZE_T nSize,
+# __out SIZE_T *lpNumberOfBytesWritten
+# );
+WriteProcessMemory = kernel32.WriteProcessMemory
+WriteProcessMemory.argtypes = [HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T_p]
+WriteProcessMemory.restype = BOOL
+
+# BOOL WINAPI FlushInstructionCache(
+# __in HANDLE hProcess,
+# __in LPCVOID lpBaseAddress,
+# __in SIZE_T dwSize
+# );
+FlushInstructionCache = kernel32.FlushInstructionCache
+FlushInstructionCache.argtypes = [HANDLE, LPCVOID, SIZE_T]
+FlushInstructionCache.restype = BOOL
+
+
+#
+# debugger.py
+
+FLAG_TRACE_BIT = 0x100
+
+class DebuggerError(Exception):
+ pass
+
+class Debugger(object):
+ def __init__(self, process_info):
+ self.process_info = process_info
+ self.pid = process_info.dwProcessId
+ self.tid = process_info.dwThreadId
+ self.hprocess = process_info.hProcess
+ self.hthread = process_info.hThread
+ self._threads = {self.tid: self.hthread}
+ self._processes = {self.pid: self.hprocess}
+ self._bps = {}
+ self._inactive = {}
+
+ def read_process_memory(self, addr, size=None, type=str):
+ if issubclass(type, basestring):
+ buf = ctypes.create_string_buffer(size)
+ ref = buf
+ else:
+ size = ctypes.sizeof(type)
+ buf = type()
+ ref = byref(buf)
+ copied = SIZE_T(0)
+ rv = ReadProcessMemory(self.hprocess, addr, ref, size, byref(copied))
+ if not rv:
+ addr = getattr(addr, 'value', addr)
+ raise DebuggerError("could not read memory @ 0x%08x" % (addr,))
+ if copied.value != size:
+ raise DebuggerError("insufficient memory read")
+ if issubclass(type, basestring):
+ return buf.raw
+ return buf
+
+ def set_bp(self, addr, callback, bytev=None):
+ hprocess = self.hprocess
+ if bytev is None:
+ byte = self.read_process_memory(addr, type=ctypes.c_byte)
+ bytev = byte.value
+ else:
+ byte = ctypes.c_byte(0)
+ self._bps[addr] = (bytev, callback)
+ byte.value = 0xcc
+ copied = SIZE_T(0)
+ rv = WriteProcessMemory(hprocess, addr, byref(byte), 1, byref(copied))
+ if not rv:
+ addr = getattr(addr, 'value', addr)
+ raise DebuggerError("could not write memory @ 0x%08x" % (addr,))
+ if copied.value != 1:
+ raise DebuggerError("insufficient memory written")
+ rv = FlushInstructionCache(hprocess, None, 0)
+ if not rv:
+ raise DebuggerError("could not flush instruction cache")
+ return
+
+ def _restore_bps(self):
+ for addr, (bytev, callback) in self._inactive.items():
+ self.set_bp(addr, callback, bytev=bytev)
+ self._inactive.clear()
+
+ def _handle_bp(self, addr):
+ hprocess = self.hprocess
+ hthread = self.hthread
+ bytev, callback = self._inactive[addr] = self._bps.pop(addr)
+ byte = ctypes.c_byte(bytev)
+ copied = SIZE_T(0)
+ rv = WriteProcessMemory(hprocess, addr, byref(byte), 1, byref(copied))
+ if not rv:
+ raise DebuggerError("could not write memory")
+ if copied.value != 1:
+ raise DebuggerError("insufficient memory written")
+ rv = FlushInstructionCache(hprocess, None, 0)
+ if not rv:
+ raise DebuggerError("could not flush instruction cache")
+ context = CONTEXT(ContextFlags=CONTEXT_FULL)
+ rv = GetThreadContext(hthread, byref(context))
+ if not rv:
+ raise DebuggerError("could not get thread context")
+ context.Eip = addr
+ callback(self, context)
+ context.EFlags |= FLAG_TRACE_BIT
+ rv = SetThreadContext(hthread, byref(context))
+ if not rv:
+ raise DebuggerError("could not set thread context")
+ return
+
+ def _get_peb_address(self):
+ hthread = self.hthread
+ hprocess = self.hprocess
+ try:
+ pbi = PROCESS_BASIC_INFORMATION()
+ rv = NtQueryInformationProcess(hprocess, 0, byref(pbi),
+ sizeof(pbi), None)
+ if rv != 0:
+ raise DebuggerError("could not query process information")
+ return pbi.PebBaseAddress
+ except DebuggerError:
+ pass
+ try:
+ context = CONTEXT(ContextFlags=CONTEXT_FULL)
+ rv = GetThreadContext(hthread, byref(context))
+ if not rv:
+ raise DebuggerError("could not get thread context")
+ entry = LDT_ENTRY()
+ rv = GetThreadSelectorEntry(hthread, context.SegFs, byref(entry))
+ if not rv:
+ raise DebuggerError("could not get selector entry")
+ low, mid, high = entry.BaseLow, entry.BaseMid, entry.BaseHi
+ fsbase = low | (mid << 16) | (high << 24)
+ pebaddr = self.read_process_memory(fsbase + 0x30, type=c_voidp)
+ return pebaddr.value
+ except DebuggerError:
+ pass
+ return 0x7ffdf000
+
+ def get_base_address(self):
+ addr = self._get_peb_address() + (2 * 4)
+ baseaddr = self.read_process_memory(addr, type=c_voidp)
+ return baseaddr.value
+
+ def main_loop(self):
+ event = DEBUG_EVENT()
+ finished = False
+ while not finished:
+ rv = WaitForDebugEvent(byref(event), INFINITE)
+ if not rv:
+ raise DebuggerError("could not get debug event")
+ self.pid = pid = event.dwProcessId
+ self.tid = tid = event.dwThreadId
+ self.hprocess = self._processes.get(pid, None)
+ self.hthread = self._threads.get(tid, None)
+ status = DBG_CONTINUE
+ evid = event.dwDebugEventCode
+ if evid == EXCEPTION_DEBUG_EVENT:
+ first = event.Exception.dwFirstChance
+ record = event.Exception.ExceptionRecord
+ exid = record.ExceptionCode
+ flags = record.ExceptionFlags
+ addr = record.ExceptionAddress
+ if exid == EXCEPTION_BREAKPOINT:
+ if addr in self._bps:
+ self._handle_bp(addr)
+ elif exid == EXCEPTION_SINGLE_STEP:
+ self._restore_bps()
+ else:
+ status = DBG_EXCEPTION_NOT_HANDLED
+ elif evid == LOAD_DLL_DEBUG_EVENT:
+ hfile = event.LoadDll.hFile
+ if hfile is not None:
+ rv = CloseHandle(hfile)
+ if not rv:
+ raise DebuggerError("error closing file handle")
+ elif evid == CREATE_THREAD_DEBUG_EVENT:
+ info = event.CreateThread
+ self.hthread = info.hThread
+ self._threads[tid] = self.hthread
+ elif evid == EXIT_THREAD_DEBUG_EVENT:
+ hthread = self._threads.pop(tid, None)
+ if hthread is not None:
+ rv = CloseHandle(hthread)
+ if not rv:
+ raise DebuggerError("error closing thread handle")
+ elif evid == CREATE_PROCESS_DEBUG_EVENT:
+ info = event.CreateProcessInfo
+ self.hprocess = info.hProcess
+ self._processes[pid] = self.hprocess
+ elif evid == EXIT_PROCESS_DEBUG_EVENT:
+ hprocess = self._processes.pop(pid, None)
+ if hprocess is not None:
+ rv = CloseHandle(hprocess)
+ if not rv:
+ raise DebuggerError("error closing process handle")
+ if pid == self.process_info.dwProcessId:
+ finished = True
+ rv = ContinueDebugEvent(pid, tid, status)
+ if not rv:
+ raise DebuggerError("could not continue debug")
+ return True
+
+
+#
+# unswindle.py
+
+KINDLE_REG_KEY = \
+ r'Software\Classes\Amazon.KindleForPC.content\shell\open\command'
+
+class UnswindleError(Exception):
+ pass
+
+class PC1KeyGrabber(object):
+ HOOKS = {
+ 'b9f7e422094b8c8966a0e881e6358116e03e5b7b': {
+ 0x004a719d: '_no_debugger_here',
+ 0x005a795b: '_no_debugger_here',
+ 0x0054f7e0: '_get_pc1_pid',
+ 0x004f9c79: '_get_book_path',
+ },
+ 'd5124ee20dab10e44b41a039363f6143725a5417': {
+ 0x0041150d: '_i_like_wine',
+ 0x004a681d: '_no_debugger_here',
+ 0x005a438b: '_no_debugger_here',
+ 0x0054c9e0: '_get_pc1_pid',
+ 0x004f8ac9: '_get_book_path',
+ },
+ }
+
+ @classmethod
+ def supported_version(cls, hexdigest):
+ return (hexdigest in cls.HOOKS)
+
+ def _taddr(self, addr):
+ return (addr - 0x00400000) + self.baseaddr
+
+ def __init__(self, debugger, hexdigest):
+ self.book_path = None
+ self.book_pid = None
+ self.baseaddr = debugger.get_base_address()
+ hooks = self.HOOKS[hexdigest]
+ for addr, mname in hooks.items():
+ debugger.set_bp(self._taddr(addr), getattr(self, mname))
+
+ def _i_like_wine(self, debugger, context):
+ context.Eax = 1
+ return
+
+ def _no_debugger_here(self, debugger, context):
+ context.Eip += 2
+ context.Eax = 0
+ return
+
+ def _get_book_path(self, debugger, context):
+ addr = debugger.read_process_memory(context.Esp, type=ctypes.c_voidp)
+ try:
+ path = debugger.read_process_memory(addr, 4096)
+ except DebuggerError:
+ pgrest = 0x1000 - (addr.value & 0xfff)
+ path = debugger.read_process_memory(addr, pgrest)
+ path = path.decode('utf-16', 'ignore')
+ if u'\0' in path:
+ path = path[:path.index(u'\0')]
+ if path[-4:].lower() not in ('.prc', '.pdb', '.mobi'):
+ return
+ self.book_path = path
+
+ def _get_pc1_pid(self, debugger, context):
+ addr = context.Esp + ctypes.sizeof(ctypes.c_voidp)
+ addr = debugger.read_process_memory(addr, type=ctypes.c_char_p)
+ pid = debugger.read_process_memory(addr, 8)
+ pid = self._checksum_pid(pid)
+ self.book_pid = pid
+
+ def _checksum_pid(self, s):
+ letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
+ crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
+ crc = crc ^ (crc >> 16)
+ res = s
+ l = len(letters)
+ for i in (0,1):
+ b = crc & 0xff
+ pos = (b // l) ^ (b % l)
+ res += letters[pos%l]
+ crc >>= 8
+ return res
+
+class Unswindler(object):
+ def __init__(self):
+ self._exepath = self._get_exe_path()
+ self._hexdigest = self._get_hexdigest()
+ self._exedir = os.path.dirname(self._exepath)
+ self._mobidedrmpath = self._get_mobidedrm_path()
+
+ def _get_mobidedrm_path(self):
+ basedir = sys.modules[self.__module__].__file__
+ basedir = os.path.dirname(basedir)
+ for basename in ('mobidedrm', 'mobidedrm.py', 'mobidedrm.pyw'):
+ path = os.path.join(basedir, basename)
+ if os.path.isfile(path):
+ return path
+ raise UnswindleError("could not locate MobiDeDRM script")
+
+ def _get_exe_path(self):
+ path = None
+ for root in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
+ try:
+ regkey = winreg.OpenKey(root, KINDLE_REG_KEY)
+ path = winreg.QueryValue(regkey, None)
+ break
+ except WindowsError:
+ pass
+ else:
+ raise UnswindleError("Kindle For PC installation not found")
+ if '"' in path:
+ path = re.search(r'"(.*?)"', path).group(1)
+ return path
+
+ def _get_hexdigest(self):
+ path = self._exepath
+ sha1 = hashlib.sha1()
+ with open(path, 'rb') as f:
+ data = f.read(4096)
+ while data:
+ sha1.update(data)
+ data = f.read(4096)
+ hexdigest = sha1.hexdigest()
+ if not PC1KeyGrabber.supported_version(hexdigest):
+ raise UnswindleError("Unsupported version of Kindle For PC")
+ return hexdigest
+
+ def _is_topaz(self, path):
+ with open(path, 'rb') as f:
+ magic = f.read(4)
+ if magic == 'TPZ0':
+ return True
+ return False
+
+ def get_book(self):
+ creation_flags = (CREATE_UNICODE_ENVIRONMENT |
+ DEBUG_PROCESS |
+ DEBUG_ONLY_THIS_PROCESS)
+ startup_info = STARTUPINFO()
+ process_info = PROCESS_INFORMATION()
+ path = pid = None
+ try:
+ rv = CreateProcess(self._exepath, None, None, None, False,
+ creation_flags, None, self._exedir,
+ byref(startup_info), byref(process_info))
+ if not rv:
+ raise UnswindleError("failed to launch Kindle For PC")
+ debugger = Debugger(process_info)
+ grabber = PC1KeyGrabber(debugger, self._hexdigest)
+ debugger.main_loop()
+ path = grabber.book_path
+ pid = grabber.book_pid
+ finally:
+ if process_info.hThread is not None:
+ CloseHandle(process_info.hThread)
+ if process_info.hProcess is not None:
+ CloseHandle(process_info.hProcess)
+ if path is None:
+ raise UnswindleError("failed to determine book path")
+ if self._is_topaz(path):
+ raise UnswindleError("cannot decrypt Topaz format book")
+ if pid is None:
+ raise UnswindleError("failed to determine book PID")
+ return (path, pid)
+
+ def decrypt_book(self, inpath, outpath, pid):
+ # darkreverser didn't protect mobidedrm's script execution to allow
+ # importing, so we have to just run it in a subprocess
+ with tempfile.NamedTemporaryFile(delete=False) as tmpf:
+ tmppath = tmpf.name
+ args = [sys.executable, self._mobidedrmpath, inpath, tmppath, pid]
+ mobidedrm = subprocess.Popen(args, stderr=subprocess.STDOUT,
+ stdout=subprocess.PIPE,
+ universal_newlines=True)
+ output = mobidedrm.communicate()[0]
+ if not output.endswith("done\n"):
+ try:
+ os.remove(tmppath)
+ except OSError:
+ pass
+ raise UnswindleError("problem running MobiDeDRM:\n" + output)
+ shutil.move(tmppath, outpath)
+ return
+
+class ExceptionDialog(Tkinter.Frame):
+ def __init__(self, root, text):
+ Tkinter.Frame.__init__(self, root, border=5)
+ label = Tkinter.Label(self, text="Unexpected error:",
+ anchor=Tkconstants.W, justify=Tkconstants.LEFT)
+ label.pack(fill=Tkconstants.X, expand=0)
+ self.text = Tkinter.Text(self)
+ self.text.pack(fill=Tkconstants.BOTH, expand=1)
+ self.text.insert(Tkconstants.END, text)
+
+def gui_main(argv=sys.argv):
+ root = Tkinter.Tk()
+ root.withdraw()
+ progname = os.path.basename(argv[0])
+ try:
+ unswindler = Unswindler()
+ inpath, pid = unswindler.get_book()
+ outpath = tkFileDialog.asksaveasfilename(
+ parent=None, title='Select unencrypted Mobipocket file to produce',
+ defaultextension='.mobi', filetypes=[('MOBI files', '.mobi'),
+ ('All files', '.*')])
+ if not outpath:
+ return 0
+ unswindler.decrypt_book(inpath, outpath, pid)
+ except UnswindleError, e:
+ tkMessageBox.showerror("Unswindle For PC", "Error: " + str(e))
+ return 1
+ except Exception:
+ root.wm_state('normal')
+ root.title('Unswindle For PC')
+ text = traceback.format_exc()
+ ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
+ root.mainloop()
+ return 1
+
+def cli_main(argv=sys.argv):
+ progname = os.path.basename(argv[0])
+ args = argv[1:]
+ if len(args) != 1:
+ sys.stderr.write("usage: %s OUTFILE\n" % (progname,))
+ return 1
+ outpath = args[0]
+ unswindler = Unswindler()
+ inpath, pid = unswindler.get_book()
+ unswindler.decrypt_book(inpath, outpath, pid)
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(gui_main())