tools v2.1
combined kindle/mobi plugin
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
README
|
||||
|
||||
unswindle can be used to find the book specific PID but it needs to be updated for each version of Kindle4PC that Amazon releases (and therefore is also useful for Linux users who have Wine). This program “patches” the Kindle4PC executable and therefore is very release specific.
|
||||
|
||||
Unfortunately unswindle v7 the latest, has not been updated to work with the latest version of Kindle for PC. You will need to find one of the older versions of Kindle4PC and prevent later updates in order to use this tool.
|
||||
310
Kindle_Mobi_Tools/Kindle_4_PC_Unswindle/mobidedrm.py
Normal file
310
Kindle_Mobi_Tools/Kindle_4_PC_Unswindle/mobidedrm.py
Normal file
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# It can run standalone to convert files, or it can be installed as a
|
||||
# plugin for Calibre (http://calibre-ebook.com/about) so that
|
||||
# importing files with DRM 'Just Works'.
|
||||
#
|
||||
# To create a Calibre plugin, rename this file so that the filename
|
||||
# ends in '_plugin.py', put it into a ZIP file and import that Calibre
|
||||
# using its plugin configuration GUI.
|
||||
#
|
||||
# Changelog
|
||||
# 0.01 - Initial version
|
||||
# 0.02 - Huffdic compressed books were not properly decrypted
|
||||
# 0.03 - Wasn't checking MOBI header length
|
||||
# 0.04 - Wasn't sanity checking size of data record
|
||||
# 0.05 - It seems that the extra data flags take two bytes not four
|
||||
# 0.06 - And that low bit does mean something after all :-)
|
||||
# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size
|
||||
# 0.08 - ...and also not in Mobi header version < 6
|
||||
# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4!
|
||||
# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre
|
||||
# import filter it works when importing unencrypted files.
|
||||
# Also now handles encrypted files that don't need a specific PID.
|
||||
# 0.11 - use autoflushed stdout and proper return values
|
||||
# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors
|
||||
# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace
|
||||
# and extra blank lines, converted CR/LF pairs at ends of each line,
|
||||
# and other cosmetic fixes.
|
||||
# 0.14 - Working out when the extra data flags are present has been problematic
|
||||
# Versions 7 through 9 have tried to tweak the conditions, but have been
|
||||
# only partially successful. Closer examination of lots of sample
|
||||
# files reveals that a confusin has arisen because trailing data entries
|
||||
# are not encrypted, but it turns out that the multibyte entries
|
||||
# in utf8 file are encrypted. (Although neither kind gets compressed.)
|
||||
# This knowledge leads to a simplification of the test for the
|
||||
# trailing data byte flags - version 5 and higher AND header size >= 0xE4.
|
||||
# 0.15 - Now outputs 'hearbeat', and is also quicker for long files.
|
||||
# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility.
|
||||
|
||||
__version__ = '0.16'
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
print "Bad key length!"
|
||||
return None
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
|
||||
dst = ""
|
||||
for i in xrange(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in xrange(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in xrange(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
dst+=chr(curByte)
|
||||
return dst
|
||||
|
||||
def checksumPid(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
|
||||
|
||||
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||
def getSizeOfTrailingDataEntry(ptr, size):
|
||||
bitpos, result = 0, 0
|
||||
if size <= 0:
|
||||
return result
|
||||
while True:
|
||||
v = ord(ptr[size-1])
|
||||
result |= (v & 0x7F) << bitpos
|
||||
bitpos += 7
|
||||
size -= 1
|
||||
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
|
||||
return result
|
||||
num = 0
|
||||
testflags = flags >> 1
|
||||
while testflags:
|
||||
if testflags & 1:
|
||||
num += getSizeOfTrailingDataEntry(ptr, size - num)
|
||||
testflags >>= 1
|
||||
# Multibyte data, if present, is included in the encryption, so
|
||||
# we do not need to check the low bit.
|
||||
# if flags & 1:
|
||||
# num += (ord(ptr[size - num - 1]) & 0x3) + 1
|
||||
return num
|
||||
|
||||
class DrmStripper:
|
||||
def loadSection(self, section):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
return self.data_file[off:endoff]
|
||||
|
||||
def patch(self, off, new):
|
||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
|
||||
|
||||
def patchSection(self, section, new, in_off = 0):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
assert off + in_off + len(new) <= endoff
|
||||
self.patch(off + in_off, new)
|
||||
|
||||
def parseDRM(self, data, count, pid):
|
||||
pid = pid.ljust(16,'\0')
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
temp_key = PC1(keyvec1, pid, False)
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
found_key = None
|
||||
for i in xrange(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1:
|
||||
found_key = finalkey
|
||||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
temp_key = keyvec1
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
for i in xrange(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver and cksum == temp_key_sum:
|
||||
found_key = finalkey
|
||||
break
|
||||
return found_key
|
||||
|
||||
def __init__(self, data_file, pid):
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
raise DrmException("invalid PID checksum")
|
||||
pid = pid[0:-2]
|
||||
|
||||
self.data_file = data_file
|
||||
header = data_file[0:72]
|
||||
if header[0x3C:0x3C+8] != 'BOOKMOBI':
|
||||
raise DrmException("invalid file format")
|
||||
self.num_sections, = struct.unpack('>H', data_file[76:78])
|
||||
|
||||
self.sections = []
|
||||
for i in xrange(self.num_sections):
|
||||
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', data_file[78+i*8:78+i*8+8])
|
||||
flags, val = a1, a2<<16|a3<<8|a4
|
||||
self.sections.append( (offset, flags, val) )
|
||||
|
||||
sect = self.loadSection(0)
|
||||
records, = struct.unpack('>H', sect[0x8:0x8+2])
|
||||
mobi_length, = struct.unpack('>L',sect[0x14:0x18])
|
||||
mobi_version, = struct.unpack('>L',sect[0x68:0x6C])
|
||||
extra_data_flags = 0
|
||||
print "MOBI header version = %d, length = %d" %(mobi_version, mobi_length)
|
||||
if (mobi_length >= 0xE4) and (mobi_version >= 5):
|
||||
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" %extra_data_flags
|
||||
|
||||
crypto_type, = struct.unpack('>H', sect[0xC:0xC+2])
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
else:
|
||||
if crypto_type == 1:
|
||||
raise DrmException("cannot decode Mobipocket encryption type 1")
|
||||
if crypto_type != 2:
|
||||
raise DrmException("unknown encryption type: %d" % crypto_type)
|
||||
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("no PIDs found in this file")
|
||||
found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid)
|
||||
if not found_key:
|
||||
raise DrmException("no key found. maybe the PID is incorrect")
|
||||
|
||||
# kill the drm keys
|
||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||
# clear the crypto type
|
||||
self.patchSection(0, "\0" * 2, 0xC)
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
new_data = self.data_file[:self.sections[1][0]]
|
||||
for i in xrange(1, records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
new_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
if extra_size > 0:
|
||||
new_data += data[-extra_size:]
|
||||
#self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size]))
|
||||
if self.num_sections > records+1:
|
||||
new_data += self.data_file[self.sections[records+1][0]:]
|
||||
self.data_file = new_data
|
||||
print "done"
|
||||
|
||||
def getResult(self):
|
||||
return self.data_file
|
||||
|
||||
if not __name__ == "__main__":
|
||||
from calibre.customize import FileTypePlugin
|
||||
|
||||
class MobiDeDRM(FileTypePlugin):
|
||||
name = 'MobiDeDRM' # Name of the plugin
|
||||
description = 'Removes DRM from secure Mobi files'
|
||||
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
|
||||
author = 'The Dark Reverser' # The author of this plugin
|
||||
version = (0, 1, 6) # The version number of this plugin
|
||||
file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
PID = self.site_customization
|
||||
data_file = file(path_to_ebook, 'rb').read()
|
||||
ar = PID.split(',')
|
||||
for i in ar:
|
||||
try:
|
||||
unlocked_file = DrmStripper(data_file, i).getResult()
|
||||
except DrmException:
|
||||
# ignore the error
|
||||
pass
|
||||
else:
|
||||
of = self.temporary_file('.mobi')
|
||||
of.write(unlocked_file)
|
||||
of.close()
|
||||
return of.name
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "MobiDeDRM Plugin", "Couldn't decode: %s\n\nImporting encrypted version." % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
return path_to_ebook
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter PID (separate multiple PIDs with comma)'
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
||||
if len(sys.argv)<4:
|
||||
print "Removes protection from Mobipocket books"
|
||||
print "Usage:"
|
||||
print " %s <infile> <outfile> <PID>" % sys.argv[0]
|
||||
sys.exit(1)
|
||||
else:
|
||||
infile = sys.argv[1]
|
||||
outfile = sys.argv[2]
|
||||
pid = sys.argv[3]
|
||||
data_file = file(infile, 'rb').read()
|
||||
try:
|
||||
strippedFile = DrmStripper(data_file, pid)
|
||||
file(outfile, 'wb').write(strippedFile.getResult())
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
883
Kindle_Mobi_Tools/Kindle_4_PC_Unswindle/unswindle.pyw
Normal file
883
Kindle_Mobi_Tools/Kindle_4_PC_Unswindle/unswindle.pyw
Normal file
@@ -0,0 +1,883 @@
|
||||
#! /usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# unswindle.pyw, version 7
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
|
||||
# Before running this program, you must first install 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
|
||||
# 6 - Detect and just copy DRM-free books
|
||||
# 7 - Work with new (20100629) version of K4PC
|
||||
|
||||
"""
|
||||
Decrypt Kindle For PC encrypted Mobipocket books.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__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',
|
||||
},
|
||||
'd791f52dd2ecc68722212d801ad52cb79d1b6fc9': {
|
||||
0x0041724d: '_i_like_wine',
|
||||
0x004bfe3d: '_no_debugger_here',
|
||||
0x005bd9db: '_no_debugger_here',
|
||||
0x00565920: '_get_pc1_pid',
|
||||
0x0050fde9: '_get_book_path',
|
||||
},
|
||||
}
|
||||
|
||||
MOBI_EXTENSIONS = set(['.prc', '.pdb', '.mobi', '.azw', '.az1', '.azw1'])
|
||||
|
||||
@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')]
|
||||
root, ext = os.path.splitext(path)
|
||||
if ext.lower() not in self.MOBI_EXTENSIONS:
|
||||
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 MobiParser(object):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
header = data[0:72]
|
||||
if header[0x3C:0x3C+8] != 'BOOKMOBI':
|
||||
raise UnswindleError("invalid file format")
|
||||
self.nsections = nsections = struct.unpack('>H', data[76:78])[0]
|
||||
self.sections = sections = []
|
||||
for i in xrange(nsections):
|
||||
offset, a1, a2, a3, a4 = \
|
||||
struct.unpack('>LBBBB', data[78+i*8:78+i*8+8])
|
||||
flags, val = a1, ((a2 << 16) | (a3 << 8) | a4)
|
||||
sections.append((offset, flags, val))
|
||||
sect = self.load_section(0)
|
||||
self.crypto_type = struct.unpack('>H', sect[0x0c:0x0c+2])[0]
|
||||
|
||||
def load_section(self, snum):
|
||||
if (snum + 1) == self.nsections:
|
||||
endoff = len(self.data)
|
||||
else:
|
||||
endoff = self.sections[snum + 1][0]
|
||||
off = self.sections[snum][0]
|
||||
return self.data[off:endoff]
|
||||
|
||||
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 _check_topaz(self, path):
|
||||
with open(path, 'rb') as f:
|
||||
magic = f.read(4)
|
||||
if magic == 'TPZ0':
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_drm_free(self, path):
|
||||
with open(path, 'rb') as f:
|
||||
crypto = MobiParser(f.read()).crypto_type
|
||||
return (crypto == 0)
|
||||
|
||||
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._check_topaz(path):
|
||||
raise UnswindleError("cannot decrypt Topaz format book")
|
||||
return (path, pid)
|
||||
|
||||
def decrypt_book(self, inpath, outpath, pid):
|
||||
if self._check_drm_free(inpath):
|
||||
shutil.copy(inpath, outpath)
|
||||
else:
|
||||
self._mobidedrm(inpath, outpath, pid)
|
||||
return
|
||||
|
||||
def _mobidedrm(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
|
||||
if pid is None:
|
||||
raise UnswindleError("failed to determine book PID")
|
||||
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())
|
||||
Reference in New Issue
Block a user