打印调用堆栈

原文地址:http://blog.csdn.net/chief1985/article/details/4618492

java里面可以使用Throwable类来获取堆栈,示例代码如下:


package name.xu;
public class CallStack {
	public static void printCallStatck() {
		Throwable ex = new Throwable();
		StackTraceElement[] stackElements = ex.getStackTrace();
		if (stackElements != null) {
			for (int i = 0; i < stackElements.length; i++) {
				System.out.print(stackElements[i].getClassName()+"/t");
				System.out.print(stackElements[i].getFileName()+"/t");
				System.out.print(stackElements[i].getLineNumber()+"/t");
				System.out.println(stackElements[i].getMethodName());
				System.out.println("-----------------------------------");
			}
		}
	}
	
}

 

C#里面使用与java类似的方法,示例代码如下:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
namespace TestProjectCSharp
{
    class CallStack
    {
        public static void printCallStack()
        {
            StackTrace ss = new StackTrace(true);
            String flName = ss.GetFrame(1).GetFileName();// GetMethod().DeclaringType; 
            int lineNo = ss.GetFrame(1).GetFileLineNumber();
            String methodName = ss.GetFrame(1).GetMethod().Name;
            Console.WriteLine(flName+"---"+lineNo+"---"+methodName);
        }
    }
}

 

c里面获取堆栈跟系统有关(主要是获取函数名称不一致,获取地址是一致的),
linux下使用backtrace和backtrace_symbols函数,示例代码如下:(编译方法:gcc -o funstack -rdynamic -ldl funstack.c)


//funstack.c
#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#if defined(REG_RIP)
# define SIGSEGV_STACK_IA64
# define REGFORMAT "%016lx"
#elif defined(REG_EIP)
# define SIGSEGV_STACK_X86
# define REGFORMAT "%08x"
#else
# define SIGSEGV_STACK_GENERIC
# define REGFORMAT "%x"
#endif
static void signal_segv(int signum, siginfo_t* info, void*ptr) {
        static const char *si_codes[3] = {"", "SEGV_MAPERR", "SEGV_ACCERR"};
        size_t i;
        ucontext_t *ucontext = (ucontext_t*)ptr;
#if defined(SIGSEGV_STACK_X86) || defined(SIGSEGV_STACK_IA64)
        int f = 0;
        Dl_info dlinfo;
        void **bp = 0;
        void *ip = 0;
#else
        void *bt[20];
        char **strings;
        size_t sz;
#endif
#if defined(SIGSEGV_STACK_X86) || defined(SIGSEGV_STACK_IA64)
# if defined(SIGSEGV_STACK_IA64)
        ip = (void*)ucontext->uc_mcontext.gregs[REG_RIP];
        bp = (void**)ucontext->uc_mcontext.gregs[REG_RBP];
# elif defined(SIGSEGV_STACK_X86)
        ip = (void*)ucontext->uc_mcontext.gregs[REG_EIP];
        bp = (void**)ucontext->uc_mcontext.gregs[REG_EBP];
# endif
        fprintf(stderr, "Stack trace:/n");
        while(bp && ip) {
                if(!dladdr(ip, &dlinfo))
                        break;
                const char *symname = dlinfo.dli_sname;
                fprintf(stderr, "% 2d: %p %s+%u (%s)/n",
                                ++f,
                                ip,
                                symname,
                                (unsigned)(ip - dlinfo.dli_saddr),
                                dlinfo.dli_fname);
                if(dlinfo.dli_sname && !strcmp(dlinfo.dli_sname, "main"))
                        break;
                ip = bp[1];
                bp = (void**)bp[0];
        }
#else
        fprintf(stderr, "Stack trace (non-dedicated):/n");
        sz = backtrace(bt, 20);
        strings = backtrace_symbols(bt, sz);
        for(i = 0; i < sz; ++i)
                fprintf(stderr, "%s/n", strings[i]);
#endif
        fprintf(stderr, "End of stack trace/n");
        return;
}
int setup_sigsegv() {
        struct sigaction action;
        memset(&action, 0, sizeof(action));
        action.sa_sigaction = signal_segv;
        action.sa_flags = SA_SIGINFO;
        if(sigaction(SIGUSR1, &action, NULL) < 0) {
                perror("sigaction");
                return 0;
        }
        return 1;
}

void func1()
{
        raise(SIGUSR1);
        return ;
}
void func2()
{
        raise(SIGUSR1);
        return ;
}
void entry()
{
        func1();
        func2();
        return;
}
int main()
{
        setup_sigsegv();
        entry();
}

 

windows下使用GetThreadContext ,StackWalk,SymGetOptions ,SymFunctionTableAccess,示例代码如下:


#include "SimpleSymbolEngine.h"
#include 
#include 
#include 
#include 
#include 
#include 
#pragma comment( lib, "dbghelp" )
static char const szRCSID[] = "$Id: SimpleSymbolEngine.cpp,v 1.4 2005/05/04 21:52:05 Eleanor Exp $";
//////////////////////////////////////////////////////////////////////////////////////
// Singleton for the engine (SymInitialize doesn't support multiple calls)
SimpleSymbolEngine& SimpleSymbolEngine::instance()
{
static SimpleSymbolEngine theEngine;
    return theEngine;
}
/////////////////////////////////////////////////////////////////////////////////////
SimpleSymbolEngine::SimpleSymbolEngine()
{
    hProcess = GetCurrentProcess();
    DWORD dwOpts = SymGetOptions();
    dwOpts |= SYMOPT_LOAD_LINES | SYMOPT_DEFERRED_LOADS;
    SymSetOptions ( dwOpts );
    ::SymInitialize( hProcess, 0, true );
}
/////////////////////////////////////////////////////////////////////////////////////
SimpleSymbolEngine::~SimpleSymbolEngine()
{
    ::SymCleanup( hProcess );
}
/////////////////////////////////////////////////////////////////////////////////////
std::string SimpleSymbolEngine::addressToString( PVOID address )
{
    std::ostringstream oss;
    // First the raw address
    oss << "0x" <
MaxNameLength = sizeof( SymInfo ) – offsetof( tagSymInfo, symInfo.Name ); DWORD dwDisplacement; if ( SymGetSymFromAddr( hProcess, (DWORD)address, &dwDisplacement, pSym) ) { oss << ” ” <Name; if ( dwDisplacement != 0 ) oss << “+0x” << std::hex << dwDisplacement << std::dec; } // Finally any file/line number IMAGEHLP_LINE lineInfo = { sizeof( IMAGEHLP_LINE ) }; if ( SymGetLineFromAddr( hProcess, (DWORD)address, &dwDisplacement, &lineInfo ) ) { char const *pDelim = strrchr( lineInfo.FileName, ‘//’ ); oss << ” at ” << ( pDelim ? pDelim + 1 : lineInfo.FileName ) << “(” << lineInfo.LineNumber << “)”; } return oss.str(); } ///////////////////////////////////////////////////////////////////////////////////// // StackTrace: try to trace the stack to the given output void SimpleSymbolEngine::StackTrace( PCONTEXT pContext, std::ostream & os ) { os <Eip; stackFrame.AddrPC.Mode = AddrModeFlat; stackFrame.AddrFrame.Offset = pContext->Ebp; stackFrame.AddrFrame.Mode = AddrModeFlat; stackFrame.AddrStack.Offset = pContext->Esp; stackFrame.AddrStack.Mode = AddrModeFlat; while ( ::StackWalk( IMAGE_FILE_MACHINE_I386, hProcess, GetCurrentThread(), // this value doesn’t matter much if previous one is a real handle &stackFrame, pContext, NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL ) ) { os << ” 0x” << (PVOID) stackFrame.AddrFrame.Offset << ” ” << addressToString( (PVOID)stackFrame.AddrPC.Offset ) << “/n”; } os.flush(); }

 

完整的代码到http://www.howzatt.demon.co.uk/articles/SimpleSymbolEngine.zip下载。http://www.codeproject.com/KB/threads/StackWalker.aspx里面也有例子。

参考:

http://www.diybl.com/course/3_program/java/javashl/2008119/96739.html

http://topic.csdn.net/u/20090618/11/7c19832a-975e-4be6-987b-e61d789b31b5.html

http://bbs3.chinaunix.net/viewthread.php?tid=950357&extra=&page=2

http://www.codeproject.com/KB/threads/StackWalker.aspx

http://topic.csdn.net/u/20070515/21/fc3ebc11-b871-4761-90ae-3c6ddc7b4248.html

http://www.diybl.com/course/3_program/c++/cppjs/20090403/163752_2.html

http://accu.org/index.php/journals/276

http://blog.thepimp.net/archives/how-to-generate-backtraces-on-windows-without-compiler.html

———-淡定的分割线———-

codeproject的例子可以编译通过,但是运行报错。又找到第二个更简洁的可行例子。

原文地址:http://blog.csdn.net/jfyy/article/details/6759243


BOOL WINAPI StackWalk64(
    __in      DWORD MachineType,
    __in      HANDLE hProcess,
    __in      HANDLE hThread,
    __inout   LPSTACKFRAME64 StackFrame,
    __inout   PVOID ContextRecord,
    __in_opt  PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine,
    __in_opt  PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine,
    __in_opt  PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine,
    __in_opt  PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress
    );

而后将原文中的的方法简单做了修改和封装。

调用方法: vector CallStack::GetCallStack();

#include 
#include 
#include 
#include 

#define MAX_ADDRESS_LENGTH 32
#define MAX_NAME_LENGTH 1024
using namespace std;

struct CrashInfo
{
    char ErrorCode[MAX_ADDRESS_LENGTH];
    char Address[MAX_ADDRESS_LENGTH];
    char Flags[MAX_ADDRESS_LENGTH];
};

// CallStack信息
//
struct CallStackInfo
{
    char ModuleName[MAX_NAME_LENGTH];
    char MethodName[MAX_NAME_LENGTH];
    char FileName[MAX_NAME_LENGTH];
    char LineNumber[MAX_NAME_LENGTH];
};


class CallStack
{
public:
    CallStack(void);
    ~CallStack(void);
    static vector GetCallStack();

protected:
    static void SafeStrCpy(char* szDest, size_t nMaxDestSize, const char* szSrc);
    static vector GetCallStack(const CONTEXT *pContext);
    static CrashInfo GetCrashInfo(const EXCEPTION_RECORD *pRecord);
};


#include "StdAfx.h"
#include "CallStackInfo.h"


// 添加对dbghelp.lib的编译依赖   
//   
#pragma comment(lib, "dbghelp.lib")
#pragma once


CallStack::CallStack(void)
{
}

CallStack::~CallStack(void)
{
}

// 安全拷贝字符串函数
//   
void CallStack::SafeStrCpy(char* szDest, size_t nMaxDestSize, const char* szSrc)
{
    if (nMaxDestSize <= 0)
        return;
    if (strlen(szSrc) ExceptionAddress);
    sprintf_s(crashinfo.ErrorCode, "%08X", pRecord->ExceptionCode);
    sprintf_s(crashinfo.Flags, "%08X", pRecord->ExceptionFlags);

    return crashinfo;
}


vector CallStack::GetCallStack()
{
    CONTEXT context;
    HANDLE hThread = GetCurrentThread();

    // get context
    context.ContextFlags = (CONTEXT_FULL);
    if (GetThreadContext(hThread, &context))
    {
        return GetCallStack(&context);
    }
    else{
        vector arrCallStackInfo;
        return arrCallStackInfo;
    }

}

// 得到CallStack信息
//   
vector CallStack::GetCallStack(const CONTEXT *pContext)
{
    HANDLE hProcess = GetCurrentProcess();

    SymInitialize(hProcess, NULL, TRUE);

    vector arrCallStackInfo;

    CONTEXT c = *pContext;

    STACKFRAME64 sf;
    memset(&sf, 0, sizeof(STACKFRAME64));
    DWORD dwImageType = IMAGE_FILE_MACHINE_I386;

    // 不同的CPU类型,具体信息可查询MSDN
    //
#ifdef _M_IX86   
    sf.AddrPC.Offset = c.Eip;
    sf.AddrPC.Mode = AddrModeFlat;
    sf.AddrStack.Offset = c.Esp;
    sf.AddrStack.Mode = AddrModeFlat;
    sf.AddrFrame.Offset = c.Ebp;
    sf.AddrFrame.Mode = AddrModeFlat;
#elif _M_X64   
    dwImageType = IMAGE_FILE_MACHINE_AMD64;
    sf.AddrPC.Offset = c.Rip;
    sf.AddrPC.Mode = AddrModeFlat;
    sf.AddrFrame.Offset = c.Rsp;
    sf.AddrFrame.Mode = AddrModeFlat;
    sf.AddrStack.Offset = c.Rsp;
    sf.AddrStack.Mode = AddrModeFlat;
#elif _M_IA64   
    dwImageType = IMAGE_FILE_MACHINE_IA64;
    sf.AddrPC.Offset = c.StIIP;
    sf.AddrPC.Mode = AddrModeFlat;
    sf.AddrFrame.Offset = c.IntSp;
    sf.AddrFrame.Mode = AddrModeFlat;
    sf.AddrBStore.Offset = c.RsBSP;
    sf.AddrBStore.Mode = AddrModeFlat;
    sf.AddrStack.Offset = c.IntSp;
    sf.AddrStack.Mode = AddrModeFlat;
#else   
#error "Platform not supported!"
#endif   

    HANDLE hThread = GetCurrentThread();

    while (true)
    {
        // 该函数是实现这个功能的最重要的一个函数
        // 函数的用法以及参数和返回值的具体解释可以查询MSDN
        //   
        if (!StackWalk64(dwImageType, hProcess, hThread, &sf, &c, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL))
        {
            break;
        }

        if (sf.AddrFrame.Offset == 0)
        {
            break;
        }

        CallStackInfo callstackinfo;
        SafeStrCpy(callstackinfo.MethodName, MAX_NAME_LENGTH, "N/A");
        SafeStrCpy(callstackinfo.FileName, MAX_NAME_LENGTH, "N/A");
        SafeStrCpy(callstackinfo.ModuleName, MAX_NAME_LENGTH, "N/A");
        SafeStrCpy(callstackinfo.LineNumber, MAX_NAME_LENGTH, "N/A");

        BYTE symbolBuffer[sizeof(IMAGEHLP_SYMBOL64)+MAX_NAME_LENGTH];
        IMAGEHLP_SYMBOL64 *pSymbol = (IMAGEHLP_SYMBOL64*)symbolBuffer;
        memset(pSymbol, 0, sizeof(IMAGEHLP_SYMBOL64)+MAX_NAME_LENGTH);

        pSymbol->SizeOfStruct = sizeof(symbolBuffer);
        pSymbol->MaxNameLength = MAX_NAME_LENGTH;

        DWORD symDisplacement = 0;

        BOOL bGetFileName = FALSE;
        // 得到函数名
        //   
        if (SymGetSymFromAddr64(hProcess, sf.AddrPC.Offset, NULL, pSymbol))
        {
            SafeStrCpy(callstackinfo.MethodName, MAX_NAME_LENGTH, pSymbol->Name);
        }

        IMAGEHLP_LINE64 lineInfo;
        memset(&lineInfo, 0, sizeof(IMAGEHLP_LINE64));

        lineInfo.SizeOfStruct = sizeof(IMAGEHLP_LINE64);

        DWORD dwLineDisplacement;

        // 得到文件名和所在的代码行
        //
        if (SymGetLineFromAddr64(hProcess, sf.AddrPC.Offset, &dwLineDisplacement, &lineInfo))
        {
            //该类本身不需要记载
            if (strcmp(lineInfo.FileName, __FILE__))
                bGetFileName = TRUE;

            SafeStrCpy(callstackinfo.FileName, MAX_NAME_LENGTH, lineInfo.FileName);
            sprintf_s(callstackinfo.LineNumber, "%d", lineInfo.LineNumber);//后文中有改动——亦忧
        }

        IMAGEHLP_MODULE64 moduleInfo;
        memset(&moduleInfo, 0, sizeof(IMAGEHLP_MODULE64));

        moduleInfo.SizeOfStruct = sizeof(IMAGEHLP_MODULE64);

        // 得到模块名
        //
        if (SymGetModuleInfo64(hProcess, sf.AddrPC.Offset, &moduleInfo))
        {
            SafeStrCpy(callstackinfo.ModuleName, MAX_NAME_LENGTH, moduleInfo.ModuleName);
        }

        if (bGetFileName)
            arrCallStackInfo.push_back(callstackinfo);
    }

    SymCleanup(hProcess);

    return arrCallStackInfo;
}

 

———-淡定的分割线———-

运行发现除了调用堆栈的行号除了第一句外,其他都不准确。http://www.codeproject.com/KB/threads/StackWalker.aspx的例子讨论中有解决方法。


This is solvable as follows:
 
Add this to the StackWalkerInternal

//pSGLFA != NULL )
    { // yes, we have SymGetLineFromAddr64()
        if (this->pSGLFA(hProcess, dwAddr, pdwDisplacement, pLine) != FALSE)
        {
            csEntry.lineNumber = pLine->LineNumber;
            // TODO: Mache dies sicher...!
            strcpy_s(csEntry.lineFileName, sizeof(csEntry.lineFileName), pLine->FileName);
            success = TRUE;
        }
        else
        {
            this->m_parent->OnDbgHelpErr("SymGetLineFromAddr64", GetLastError(), dwAddr);
        }
    } // yes, we have SymGetLineFromAddr64()
    return success;
}
//pSGLFA != NULL )
    { // yes, we have SymGetLineFromAddr64()
        if (this->pSGLFA(hProcess, dwAddr, pdwDisplacement, pLine) != FALSE)
        {
            csEntry.lineNumber = pLine->LineNumber;
            // TODO: Mache dies sicher...!
            strcpy_s(csEntry.lineFileName, sizeof(csEntry.lineFileName), pLine->FileName);
            success = TRUE;
        }
        else
        {
            this->m_parent->OnDbgHelpErr("SymGetLineFromAddr64", GetLastError(), dwAddr);
        }
    } // yes, we have SymGetLineFromAddr64()

NEW:

    if (this->m_sw->FindLine(this->m_hProcess, s.AddrPC.Offset, &(csEntry.offsetFromLine), &Line, csEntry))
    {
        this->m_sw->FindPreviousLine(this->m_hProcess, s.AddrPC.Offset, &(csEntry.offsetFromLine), &Line, csEntry);	// ignore return status
    }

 

再次测试发现,应该除去第一次的情况。即调用堆栈第一句不适合FindPreviousLine。结合几家所言,终于完美了。

第二个例子获得文件名和行号的部分改为以下:


        if (SymGetLineFromAddr64(hProcess, sf.AddrPC.Offset, &dwLineDisplacement, &lineInfo))
        {
            SafeStrCpy(callstackinfo.FileName, MAX_NAME_LENGTH, lineInfo.FileName);

            // Get correct line number including return status
            if (arrCallStackInfo.size() > 0)
            {
                DWORD64 qwAddr = sf.AddrPC.Offset;
                DWORD pdwDisplacement = dwLineDisplacement;
                IMAGEHLP_LINE64 Line64 = lineInfo;
                DWORD nextLine = lineInfo.LineNumber;
                BOOL bOk = FALSE;
                do
                {
                    --qwAddr;
                    bOk = SymGetLineFromAddr64(hProcess, qwAddr, &pdwDisplacement, &lineInfo);
                } while (bOk && (nextLine == lineInfo.LineNumber));
                if (!bOk)
                {
                    dwLineDisplacement = pdwDisplacement;
                    lineInfo = Line64;
                }
            }

            sprintf_s(callstackinfo.LineNumber, "%d", lineInfo.LineNumber);
        }

 

Advertisements

Android逆向工程初探——解密当当的epub电子书(2013年底Android客户端3.1.1版)

曾几何时,当当读书电子版全场活动免费下载。当时我也参合了一把,下了几百本电子书。虽然很多重要的图书(如技术类)被当当临时下架,但一下有这么多免费正版图书看,感觉还是不错的。

在手机上或平板上阅读当当,是有5个名额的限制的,即一个账户最多链接5个手机客户端,包括iOS和Android。电脑上看的是书是flash格式的,手机上看的书是epub格式的,都加了密。想要复制粘贴后直接打开是看不了的。

出于有很多原因(比如可用名额不够用、换个手机需要重新一本本下载、官方客户端阅读体验不佳、升级出问题闪退等等),我始终希望有达人解密这些电子书。终于,在2013年年底,当我的可用名额只剩一个时,我又一次在网上搜索,发现有达人已经给出了破解思路,并且测试通过了,只是没有傻瓜式的破解程序放出。原文地址在看雪学院:http://bbs.pediy.com/showthread.php?t=147903

令我汗颜的是,虽然我知道达人做了什么,但是其中所罗列的技术,我基本都不熟悉。Android程序,是Java编写的。而我对于Java的认识,还停步在十多年前,那时J2xE才刚刚兴起……不过,Google在手,天下我有。自力更生,丰衣足食。于是我亦步亦趋,追随达人。

PC端破解出来的是Flash文件,价值不大。鉴于手机端的epub格式通用性更好,直接从手机端破解开始。以下,“达人法”专指达人使用的破解方法,现已不适用,仅做参考。

第一步,到https://code.google.com/p/dex2jar/,‎取工具dex2jar。此乃神器,它能从apk文件中解出jar文件。

根据dex2jar官网的例子:(xxx为文件名。在Windows上用.bat代替.sh,运行命令行。)
# convert classes.dex in test_apk-debug.apk to test_apk-debug_dex2jar.jar
d2j-dex2jar.sh -f -o xxx.jar xxx.apk
# verify jar
d2j-asm-verify.sh xxx.jar
# convert to jasmin format
d2j-jar2jasmin.sh -f -o test_apk_jasmin xxx.jar

什么,命令行提示java not found?好吧,先下载安装Oracle Java JDK。

第二步,到http://www.neshkov.com/,取工具DJ Java Decompiler,此乃Java反编译神器之一。

什么,最新DJ试用版只能用免费10次?好吧,我就用一次。另外,官网提示:DJ老版本3.7.7.81还是免费的。

反编译jar文件后,得到一堆Java的.jad源码。什么,源码已经做了代码混淆?来晚了……

能搞就搞,不能搞想着办法搞呗。新建一个Eclipse工程,将源码目录导入Eclipse。嫌麻烦,就用Visual Studio。什么,你说Visual Studio更麻烦?好吧……用支持文件查找的任何文本编辑器搜索源码目录即可。

第三步,根据达人法,在源码中查找”drm”、”decrypt”关键字。但是,源码代码混淆后,大多数类名函数名变量名都是字母!怎么办?找不能混淆的关键字呗。

搜一下bouncycastle是什么?一个Java的加密解密算法库。达人说了,“当当的兄弟痴情于bouncycastle”。那就搜一下“new BouncyCastleProvider()”看看,Aes类出现,main函数出现,decrypt函数出现!他们改了名字,穿了件马甲,但是底裤还是没换。由此可知书页的加密算法是AES/CBC/PKCS7Padding没变。(reader\c\b.jad)

第四步、算法有了,接着找密钥book_key。根据达人法,将sd卡上/data/data/dangdang/目录下的书拷贝出来。

什么,你没书?去当当下载一本免费的电子书到手机吧,否则破解的目标都没有了。

书是epub格式的,章节文本、CSS和图片都加了密。达人法里,书的目录下有一个叫book_key的文件,这个文件里面有个key,“是128位的AES密钥,自身又用了512位的RSA加密”(这句话我在达人法里反复看了不下十遍,再对照最新版的源码才弄明白)。

什么,目录下没有book_key这个文件?!

第五步,接着找book_key。达人法里中有:
key = CertificateManager.fetchDecryptKey((new StringBuilder(String.valueOf((new File(bookDir)).getParent()))).append(File.separator).append(“book_key”).toString(), mConfigManager.getPrivateKey());

那就在源码中搜索“.append(“book_key”)”,唯一结果出现!在reader\service\g.jad里的private static byte[] c(String s, String s1)函数中,该函数中调用了abyte0 = com.dangdang.reader.a.a.e(file);跟下去,发现是打开book_key文件,然后又调用:byte abyte0[] = com.dangdang.reader.a.f.a(fileinputstream);再跟下去,发现是做的是将一个文件流读到内存的字节流,然后又清空了内存流。

那book_key文件曾经确实存在,但实际目录中却没有。为什么?肯定被删了。问谁删了book_key。继续搜索“g.c(”,看谁调用了private static byte[] c(String s, String s1)函数。唯一结果出现!在epubreader\a\b.jad里protected final volatile void onPostExecute(Object obj)调用了c函数产生的book_key文件被立即删除。看来这是当当对旧版客户端下载的电子书里的book_key文件进行了清洗。book_key文件没有了。

达人法里,加解密book_key的RSA密钥放在手机的/data/data/com.dangdang.reader/shared_prefs/dang_reader_config.xml里面,目录是由Android系统的getSharedPreferences()定义的。里面有base64过的private_key,public_key。

什么,dang_reader_config.xml里没有private_key和public_key?private_key和public_key文件也没有了。

达人领我到这里,后面要靠自己摸索了!

在reader\service\g.jad文件中继续搜索“book_key”,提示有数据库操作,表名shelfbook、字段名book_key等,对应z类(这个类后文还会提到)。看来book_key从文件转移到了数据库里!

将/data/data/com.dangdang.reader从手机的系统盘里拷贝出来。

什么,你的手机没root权限?Google、百度,搜索Root大师,傻瓜型操作无难度。Root后装一个RE管理器,就可以挂载/data分区,然后将/data/data/com.dangdang.reader复制到SD卡。再从SD卡复制到电脑。

看一下com.dangdang.reader目录,有些有趣的东西。有貌似加了密的文件和数据库!用二进制编辑器打开随便一个数据库,发现是SQLite格式。使用Firefox的SQLite Manager插件或者其他数据库工具,打开数据库shelfbook.db,发现shelfbook表里有blob型字段book_key!哦,原来的book_key文件躲在这呢!

第六步、既然当当还是要加密解密,那就搜索“Cipher ”,这个是Java里加密解密要用到的类。搜索结果中RSA加密用于支付宝,可以忽略。AES加密即在第三步的main函数,是用于书页的,留待最后。剩下的两个是DES加密解密(reader\a\m.jad)。跟下去,发现DES加密密钥”dangreader”以明文写在源码里了,算法如下:

DES加密:调用cipher.init(1, secretkey, securerandom);加密,然后在密文首部加10个0x01字节。
DES解密:去掉密文首部10个(0x01)字节,然后调用cipher.init(2, secretkey, securerandom);解密。

那么看看谁调用了DES加密。搜索“a.m.a(”。reader\a\c.jad提示:”public”文件出现!”priave”文件出现!在com.dangdang.reader\files目录下找到。严重怀疑是躲藏起来的public_key和private_key文件。

reader\a\o.jad提示:reader.domain.z类(即第五步中说的z类)用到DES加密。如果第五步中没有注意,这里的提示就更明显了。打开reader\domain\z.jad,发现z类继承自h类。跟进h类,看似是一个面向对象的类。继续搜索“z z1;”。service\g.jad等文件提示:z类即描述书的类。SQLite数据库出现!

小结一下:目前已知,书页是AES加密,密钥在数据库里的book_key字段里。book_key字段、public文件和priave文件都用了DES加密,并在开头添加了10个字节0x01。DES密钥和算法都有了。用十六进制编辑器查看book_key字段、public文件和priave文件,可发现三者头部都有10个0x01字节。去掉这10个字节,总字节可以被8整除。正反对照,符合DES加密特征!

第七步:看看谁调用了DES解密。搜索“a.m.b(”。reader\a\c.jad提示:”public”文件出现!”priave”文件出现!book_key呢?继续搜索“m.b(”,多了reader\c\e.jad提示:public static String a(byte abyte0[], String s)函数这是在干嘛?用”e.a(“搜索调用者,找到调用者在epubreader\EpubReaderActivity.jad里。调用者放了两个参数如下:
aC = com.dangdang.reader.c.e.a(V.n(), m.j());

第一参数V.n()是什么?看上文,V = U.a(n);提示V来自U。U又是什么?U = com.dangdang.reader.service.g.a(this);。g.a函数又是什么?reader\service\g.jad提示public static g a(Context context)函数返回一个g类对象,而g类继承自u类。打开reader\service\u.jad,发现是与数据库相关的类。回到V = U.a(n);,参数n是”productid”字段,U.a(n)调用的是reader\service\g.jad里的public final z a(String s)函数,传进去的参数即是SQL里的book_id,返回的是z类,又见z类!上文说过z类即对应数据库里描述书的类。所以,V就是数据库里一条shelfbook记录,V.n()就是z类描述的book_key字段!

第二参数 m.j()是什么?m = new c(getApplicationContext());提示m是c类的实例。c是哪个c?看下头文件import com.dangdang.reader.a.c;。打开reader\a\c.jad里的public final String j()函数,发现是将priave文件DES解密后转的utf-8字符串!

搞清楚了两个参数,那回到aC = com.dangdang.reader.c.e.a(V.n(), m.j());,即reader\c\e.jad里的public static String a(byte abyte0[], String s)函数,发现程序做了以下操作:

1、通过调用m.b函数,将book_key做DES解密为字节数组。

2、将book_key字节数组转成utf-8字符串。依据”_”拆为三份字符串。

2.1、第一份字符串为版本号:比如1.0或1.1。测试中发现我的是1.1版本。这个很重要,不同的版本,代码调用了不同的函数。由于对DJ反编译的不完整代码段MISSING_BLOCK_LABEL_不理解怎么处理,错跟了函数c.c.a,导致RSA解密出错或异常,卡了很久!其实应该跟函数c.b。

DJ反编译后代码如下:
if(!s2.equalsIgnoreCase(“1.0”))
break MISSING_BLOCK_LABEL_124;
s5 = com.dangdang.reader.c.c.a(s, s3);
return s5;
String s4 = c.b(s, s3);
return s4;

还原后的代码如下:
if(s2.equalsIgnoreCase(“1.0”))
return com.dangdang.reader.c.c.a(s, s3);
else
return c.b(s, s3);

2.2、第二份字符串似乎是一个Base64编码的加密串。这段程序并没有使用到。

2.3、第三份字符串为一个XML文档,里面似乎有Base64编码的Key:

3、调用XPath,读取第三份字符串即XML文档里的key结点内容。

4、如果2.1里的版本号是1.0,调用c.c.a函数。否则调用c.b(即c.c.b)函数。两个函数都需要两个参数。第一参数为本函数第二参数字符串s,即DES解密后的priave文件转的utf-8字符串;第二参数为XML文档的/rights/agreement/asset/key结点的内容,即一个貌似Base64编码过的字符串。

5、由于我的版本号是1.1,打开reader\c\c.jad,跟进public static String b(String s, String s1)函数,发现三行:

public static String b(String s, String s1)
{
Key key = b(s);
b.init(2, key);
return com.dangdang.reader.c.a.a(b.doFinal(com.dangdang.reader.c.a.b(s1)));
}

5.1、第一行,Key key = b(s);提示:创建了一个RSA解密的私钥。
b(s)即private static Key b(String s)函数,调用new PKCS8EncodedKeySpec(com.dangdang.reader.c.a.a(s));函数,返回的是一个KeyFactory产生的RSA私钥。

5.1.1、跟进reader\c\a.jad的public static byte[] a(String s)函数,发现,它调用了同文件中的c(s)函数。跟进private static byte[] c(String s)函数,发现它做了以下工作:

5.1.1.1、将参数字符串s(s是将priave文件DES解密后转的utf-8字符串)通过getBytes(“US-ASCII”)转成字节数组。(字节数组到字符串,又到字节数组,折腾啊。)

5.1.1.2、将字节数组通过调用同文件中的private static byte[] b(byte abyte0[], int i)函数做Base64解码。

这里我发现DJ反编译的函数有问题,或者我自己去掉反编译的goto语句时把函数恢复错了。尝试了几种反编译工具,自己也试着恢复源码,都不理想。不是Base64解码时抛异常,就是解出来的编码在RSA解密时抛异常。于是,我根据异常关键字用Google搜索一下,找到原版的Base64类http://code.google.com/p/gdata-java-client/source/browse/branches/2/gdata/src/com/google/api/client/util/Base64.java?r=441。就用Google里面的decode函数代替了此b函数。这是破解成功的关键之一。

5.1.1.3、通过一个复杂的判断条件:if(abyte1 == null || abyte1.length < 4 || 35615 != (0xff & abyte1[0] | 0xff00 & abyte1[1] << 8))
如是,返回Base64解码后的字节数组。测试时,程序始终走的是这里。
如否,将Base64解码后的字节数组转为一个ByteArrayInputStream,再转为一个GZIPInputStream,然后分段逐位读取到一个字节数组,然后分段写到一个ByteArrayOutputStream,最后返回ByteArrayOutputStream转成的字节数组。似乎在做GZip解码。

5.1.1.4、private static byte[] c(String s)函数小结:其实就是做了一件事,Base64解码。

5.1.2、privatekey = KeyFactory.getInstance(“RSA”, “BC”).generatePrivate(pkcs8encodedkeyspec);提示产生了一个私钥。

5.1.2.1、什么,有异常java.security.NoSuchProviderException: no such provider: BC?

Google搜索一下,提示:BouncyCastle没有安装。

5.1.2.2、好吧,安装BouncyCastle。去http://www.bouncycastle.org/latest_releases.html下载 bcprov-jdk15on-150.jar和bcprov-ext-jdk15on-150.jar,保存到\jre\lib\ext\目录下。编辑jre\lib\security\java.security,增加一行security.provider.=org.bouncycastle.jce.provider.BouncyCastleProvider。BouncyCastle配置完毕, no such provider: BC异常没有了吧。

5.2、第二行,b.init(2, key);中b是什么?b = Cipher.getInstance(“RSA/ECB/PKCS1Padding”);提示b是一个Cipher类的实例。所以,这句源码是用5.1步骤里的key为RSA解密做准备。

5.3、第三行,com.dangdang.reader.c.a.a(b.doFinal(com.dangdang.reader.c.a.b(s1)));)。看清楚,虽然都是c.a.a或c.a.b(1.0版本更容易看错,因为两个函数调用都是c.a.a。)函数调用,参数类型不同,实际上走的是不同的函数。一步步来。

5.3.1、…com.dangdang.reader.c.a.b(s1)));
这里调用了c.a.b函数将第二参数字符串s1,即XML文档的/rights/agreement/asset/key结点内容转成字节数组。反编译的c.a.b函数没看明白在干什么,只能依样画葫芦去掉goto语句恢复成可编译的源码。

5.3.2、…b.doFinal(…));
就是做RSA解密,将book_key解码成明文了。这是破解成功的关键之二。我在写破解程序时,发现RSA解密抛出各种异常,如Data不对、Padding不对、Encoding不对等。Google搜索了半天给出的答案五花八门。其实那些都没用,真相只有一个,RSA解密失败。要么是Java反编译失败(如5.1.1.2里说的b函数),要么是RSA的私钥不对,要么是RSA加密的密文不对。这步一旦成功,破解就完成了九成!

5.3.3.com.dangdang.reader.c.a.a(…);
c.a.a函数也在reader\c\a.jad里。它将字节数组转成了字符串,从测试结果看似乎是在做Base64编码。这步对于破解程序并不重要,只要5.3.2通过,破解程序所有的技术难点都已攻克。

第八步:有了解密了的Key,有了算法AES(见第三步里提到的reader\c\b.jad),后面就是AES解密书页了。

关于当当3.1.1版的安全改进小结:

达人法将当当旧版弱智的DRM保护机制狠狠奚落了一番后,新版当当卷土重来。它做了以下补救:

1、将书目录下保存的book_key文件删除。将版本号、book_key和其他一些字符串以”_”联合,然后做DES加密,再通过在头部添加10个0x01字节破坏密文,保存到数据库。
2、将RSA算法的公钥和私钥从配置文件中删除。将公钥和私钥做DES加密,再通过在头部添加10个0x01字节破坏DES密文,保存成单独的文件。
3、依据版本号不同,在RSA解密前对密文做了其他一些转换(即破坏RSA密文)。
4、源码混淆。

关于当当3.1.1版的解密算法总结:

1、书页是AES加密的,密钥Key自身又是RSA/ECB/PKCS1Padding加密的。

2、RSA的解密密钥即私钥由以下步骤产生:读取”priave”文件到内存(474字节),去掉头部10个0x01字节(464字节),以密钥”dangreader”做DES解密(460字节),做Base64解码(345字节)。

3、RSA的密文即加密的Key由以下步骤产生:从Sqlite数据库shelfbook.db中的表shelfbook读取目标书的记录,得到book_key字段(402字节),去掉头部10个0x01字节(392字节),以密钥”dangreader”做DES解密(字节数每本书都不同),得到明文字符串,以”_”分成三段字符串,如果第一段不是1.0,则取第三段中XML结点的内容,调用reader\c\a.jad里的private static byte[] b(String s) 函数将字符串转字节数组(64字节)。

4、综合2和3,用RSA私钥解密64字节的Key密文,得到16字节的Key明文。

5、用Key明文给每篇加密书页做AES解密。

6、当单页书页解密成功时,破解程序就来了:将epub文件做unzip解压缩,根据展开的META-INF目录下的container.xml得知该书加密的所有书页;用明文key对所有加密的书页做AES解密;将解密后的目录做zip压缩,改后缀名为epub。大功告成!

经测试发现,解密后的epub比加密的epub文件小好多,只有原先的43%左右。不知道是不是所有的书都小那么多。密文吃硬盘啊。

以上,可以看出,当当同时用了AES、RSA、DES加密,并对密文进行了破坏,一定程度上干扰了解密者。源码混淆增加了反编译的难度。我不知道当当用的是哪个混淆工具,但是目前看来,拦不住有Java基础知识的破解者。DES加密密钥以明文形式写在源码里,等于废了。DES对密文的破坏太简单。这两条相加等于DES白做。RSA其实就是最后一道防线,其性能决定,无法对所有书页做RSA加密,所以只能对key加密。RSA对密文的破坏稍显复杂,但是反编译还原成功后,这道防线也等于破了。至于最后的AES……依然没有改进啊。

写给破解者的话:
反编译后以无法混淆的类名做搜索起点。虽然混淆后都是字母,但是依然有规律可循:包名.类名.函数名。Java更有类名和文件名一致的弱点。处理好反编译的goto语句和MISSING_BLOCK_LABEL_相当于成功了一半。

写给防卫者的话:
尽量少用第三方库,那些不属于自己的函数调用,混淆不了反而暴露了自己的底裤。尽可能自己写所有代码,即使偷懒也要去掉所有明文形式写在源码里的敏感信息,否则方便自己的同时,也是方便了破解者。

技术从来不是瓶颈,就算是曾经的瓶颈,终将被打破。所以,破解还是要看付出的代价和获得的回报啊。

最后,本人不对任何因为此破解所带来的问题和责任而负责!本文为独家原创,转载请注明!