首页
论坛
课程
招聘
[原创]DMM的DRM解密
2020-12-12 10:45 21061

[原创]DMM的DRM解密

2020-12-12 10:45
21061

大家好, 我又来破解电子书了.

话不多说, 这次搞的是DMM和DLsite的DRM保护. 有认识半仙的观众们可能要说了, 你咋老是盯着电子书电纸书一类的薅, 咋不去搞点硬货? 那没办法, 老了牙口不好只会吃软柿子嘛.

本来上次ida时候打算发一发爆破的优化的, 不过写着写着热度就过去了. 毕竟嘛, 10分钟优化到1秒, 别人看你的目光就是你花了几天搞这个还不如等九分钟这样的.

好了这次DMM的电子书, DRM保护系统使用的是CypherTec的CypherGuard, 包括了授权, 反截图, 文件解密三个模块, 分别是ctlicis.dll, cguard.dll, ctcrypt.dll. 这些dll有Themdia壳, exe无壳.

主程序架构是Qt+cocos2d, 应该是为了多平台支持的缘故. 果然它Mac版, 同样是Qt+cocos2d的.

众所周知, DRM一定存在的弱点就是, 本地在某个阶段存在解密数据. 对于图片构成的电子书讲, 可能是中间数据, 也可能是解码后的RGB像素等.

当然了, 这些做DRM的公司也不是吃干饭的, 一定会想办法比如返回后销毁原数据, 对抗apihook, 使用内嵌memcpy等等, 不过嘛再好的SDK也得写exe的程序员用, 你到了Qt和cocos2d这个大坑里, 哪怕exe程序员小心翼翼地使用抗hook的思维, 他们里面那些默认的对象/结构拷贝一类的还是memcpy/memmove, 让我们来看看有没有敏感信息.

简单的写了个Logger的dll, 取名DMMLogger1, 勾了memcpy, memcpy_s, 然后发现尺寸较大时候就hexdump到日志文件, 果不其然, 里面发现了JPEG的文件头, 一块一块的, 每块4k.

结合静态分析和尝试log一些ctcrypt的函数, 我发现4634D4这里的

    if ( !ctcrypt_94(*(_DWORD *)(this + 4148), workmem, size, this + 52, &procsize, rev1048, isscrap) )

这个栈上的workmem就是memcpy的内容来源, 也是4k一块, 第一块有JFIF文件头, 然后dump下执行ctcrypt_94之前的workmem, 再在加密的电子书文件dmmb里面搜索, 可以发现就是dmmb里面的内容.

这时候观众老爷可能就要想了, 啊, 这个破解笔记就到头了, 把dump下来的解密数据组装一下不就jpeg图出来了吗? 比起截图大法已经是原汁原味了.

对于强迫症患者来说, 算法呢, 没有算法, 睡不着觉啊.

刚才在dmmb里面搜索解密前数据时候我也没闲着, 用010Editor打开进程内存, 果不其然搜索解密后的JFIF头, 也能找到完整的jpeg解密后的内容, 其后跟随着解码后的RGB像素. 这应该是cocos2d或者是Qt包装后的对象, 这里也是一个入手点, 最早的时候我也是想过hook下QImage, 从那里抓; 不过现在已经不重要了.

我找到了更好的软柿子. 那就是Mac版的DMMbookViewer.app, 让我们来看看里面有什么敏感信息.

经过一番探索, 在主程序100082150地方发现这么一个函数

id __fastcall func_applylicense_by_authType(__int64 a1)
{
    unsigned int authtype; // eax
    id result; // rax
    id mgr; // rax
    id locallic; // rax
    _QWORD *v5; // rax
    id err[3]; // [rsp+8h] [rbp-18h] BYREF
 
    authtype = (unsigned int)objc_msgSend(*(id *)(*(_QWORD *)(a1 + 16) + 8LL), "authenticationType");
    if ( authtype == 2 )
    {
        func_load_service_to_capsulizedfile(a1);
        mgr = objc_msgSend(&OBJC_CLASS___CPSecurityLibraryManager, "sharedManager");
        // contentskey restore from ???
        locallic = objc_msgSend(mgr, "lookupLocalLicenseWithCapsulizedFile:error:", *(_QWORD *)(*(_QWORD *)(a1 + 16) + 8LL), err);
        if ( locallic )
        {
            // char __cdecl -[__CPIR_5V6QgdU9ocuNGcL3 applyLicense:error:](__CPIR_5V6QgdU9ocuNGcL3 *self, SEL a2, id a3, id *a4)
            result = objc_msgSend(*(id *)(*(_QWORD *)(a1 + 16) + 8LL), "applyLicense:error:", locallic, err);
            if ( !(_BYTE)result )
                func_throw_nserror((NSError *)err[0]);
        }
        else
        {
            // lookup server lic?
            result = (id)sub_100082020(a1, a1);
        }
        *(_BYTE *)(a1 + 24) = 1;
    }
    else
    {
        if ( authtype != 6 )
        {
            v5 = __cxa_allocate_exception(8uLL);
            *v5 = &`vtable for'drm::cypherguard::auth_type_not_supported + 2;
            __cxa_throw(v5, (struct type_info *)&`typeinfo for'drm::cypherguard::auth_type_not_supported, (void (__cdecl *)(void *))sub_10001BE70);
        }
        result = objc_msgSend(*(id *)(*(_QWORD *)(a1 + 16) + 8LL), "applyOfflineParameterByNoneWithError:", err);
        if ( !(_BYTE)result )
            func_throw_nserror((NSError *)err[0]);
    }
    return result;
}

 

这里可以看出, authenticationType分为2和6两种, case 2时尝试去取缓存到本地的授权信息, 实际测试中dmmb文件是走到这里的. dmmb文件被他们叫做capsulized file, 直译就是包了胶囊的文件.

刚才说到的windows版有一个ctcrypt_94函数会对来自dmmb文件中的内容进行解密, 然后我习惯性的hook了OSX下的CCCryptorUpdate, 在看到感兴趣的解密后的数据就设法让其崩溃.

我们写好dylib, 使用DYLD_INSERT_LIBRARIES=./DMMLogger1.dylib ./DMMbookviewer, 启动后打开一本书, 如愿以偿地崩了.

Thread 33 Crashed:
0   libDMMLogger1.dylib             0x000000010609ae3c my_CCCryptorUpdate(_CCCryptor*, void const*, unsigned long, void*, unsigned long, unsigned long*) + 188 (DMMLogger1.mm:140)
1   libcommonCrypto.dylib           0x00007fff83a70c46 CCCrypt + 126
2   jp.co.cyphertec.CypherGuardCore 0x00000001060ad243 0x1060a9000 + 16963
3   libdispatch.dylib               0x00007fff93c5093d _dispatch_call_block_and_release + 12
4   libdispatch.dylib               0x00007fff93c4540b _dispatch_client_callout + 8
5   libdispatch.dylib               0x00007fff93c4a03b _dispatch_queue_drain + 754
6   libdispatch.dylib               0x00007fff93c50707 _dispatch_queue_invoke + 549
7   libdispatch.dylib               0x00007fff93c48d53 _dispatch_root_queue_drain + 538
8   libdispatch.dylib               0x00007fff93c48b00 _dispatch_worker_thread3 + 91
9   libsystem_pthread.dylib         0x00007fff87e064de _pthread_wqthread + 1129
10  libsystem_pthread.dylib         0x00007fff87e04341 start_wqthread + 13

这个工作线程崩的不是地方, 因为这是一种Block调用, 是看不到发起方的, 不过崩溃也没白费,下一个线程恰好就是Block的发起方

Thread 34:
0   libsystem_kernel.dylib          0x00007fff87348612 __write_nocancel + 10
1   libsystem_c.dylib               0x00007fff8bcf61fa _swrite + 87
2   libsystem_c.dylib               0x00007fff8bcee725 __sflush + 87
3   libsystem_c.dylib               0x00007fff8bcf1039 __sfvwrite + 823
4   libsystem_c.dylib               0x00007fff8bcfb9bd __vfprintf + 17858
5   libsystem_c.dylib               0x00007fff8bd216c1 __v2printf + 669
6   libsystem_c.dylib               0x00007fff8bcf7382 vfprintf_l + 54
7   libDMMLogger1.dylib             0x0000000106099e93 applog + 611 (shelllogger.cpp:37)
8   libDMMLogger1.dylib             0x000000010609adbd my_CCCryptorUpdate(_CCCryptor*, void const*, unsigned long, void*, unsigned long, unsigned long*) + 61 (DMMLogger1.mm:134)
9   jp.co.cyphertec.CypherGuardCore 0x00000001060ad0e0 CypherGuardCore::AES_PCTS_Decrypt_Body(unsigned char vector[16]*, unsigned char vector[16]*, unsigned long, unsigned char vector[16] const*, unsigned long, unsigned char vector[16] const*, unsigned long long, CypherGuardCore::PCTS_IVCache*, unsigned char vector[16]*) + 248
10  Capsule                         0x000000011f13add4 0x11f12f000 + 48596
11  Capsule                         0x000000011f13aef0 0x11f12f000 + 48880
12  jp.co.cyphertec.CypherGuardCore 0x00000001060af5e9 0x1060a9000 + 26089
13  com.dmm.DMMbookviewer           0x0000000105ae1471 0x105a4d000 + 607345
14  com.dmm.DMMbookviewer           0x0000000105b63f1a 0x105a4d000 + 1142554
15  com.dmm.DMMbookviewer           0x0000000105c5b824 0x105a4d000 + 2156580
16  libsystem_pthread.dylib         0x00007fff87e0699d _pthread_body + 131
17  libsystem_pthread.dylib         0x00007fff87e0691a _pthread_start + 168
18  libsystem_pthread.dylib         0x00007fff87e04351 thread_start + 13

赶得早不如赶得巧, 忽略后面写日志的调用. 往回看, 有一个函数叫做CypherGuardCore::AES_PCTS_Decrypt_Body, 对应的库在DMMbookviewer.app\Contents\Frameworks\CypherGuardCore.framework, 没有加壳.

而调用方Capsule是加壳了的, 在DMMbookviewer.app\Contents\PlugIns\Capsule.bundle\Contents\MacOS中. 看来这个CypherGuardCore做加解密操作, 而Capsule做文件解析操作.

继续分析主程序可见, 在100214256处, 有如下代码加载Capsule.bundle:

    if ( !g_capsulebundle_loaded )
    {
        func_init_capsule_key();
        pluginpath = [[NSBundle mainBundle] builtInPlugInsPath];
        bundlepath = [pluginpath stringByAppendingPathComponent:@"Capsule.bundle"];
        newpath = func_decypt_bundle(bundlepath);
        bundle = [[NSBundle alloc] initWithPath:newpath];
        if ( ![bundle loadAndReturnError: &err] )
        {
            [bundle release];
            func_deletefile(newpath);
            [[NSException exceptionWithName:CPGlueException reason:@"Capsule Component did fail to load." userInfo:Nil] raise];
        }
        [bundle release];
        func_deletefile(newpath);
        g_capsulebundle_loaded = 1;
    }

func_decypt_bundle会在/Users/Shared/.Cypher License Information新建包含相对路径的临时文件, 并把原来目录中的capsule解密写入. 然后因为osx允许载入macho后删除, 运行起来后实际是找不到文件的. 它应该是对NSBundle加载的hook.

我们可以选择hook NSBundle的loadAndReturnError方法, 把解密后的文件复制出来, 不过我比较懒, 我hook的是dlopen, 然后检查路径是否在此目录下, 并且复制出来.

我们先来看CypherGuardCore::AES_PCTS_Decrypt_Body的参数, 好来hook它.

void CypherGuardCore::AES_PCTS_Decrypt_Body(char *dst, const char *src, uint64_t srcsize, const void *key, size_t keylen, char *iv, uint64_t offset, void* ivcache, char *finiv)

这里unsigned char vector[16]*都给我简化为了char*, 实际源码应该有类似typedef char aesblock[16]的定义, 然后参数写成aesblock*, 据说是这样数组指针相对于char*更方便编译器对齐和寻址优化, 当然对现代编译器没有意义.

我们顺便把CypherGuardCore::AES_PCTS_Decrypt_CFB_scrap/ AES_PCTS_Decrypt_CBC_scrap/AES_PCTS_Decrypt_CTS_scrap也加上日志

4s.9767.0088 CypherGuardCore::AES_PCTS_Decrypt_Body++
4s.9767.0106 dst: 0x12878a000, src: 0x12878a000, srcsize: 52000, key: 0x7fdf1dba6ee0, keylen: 10
00000000: EF BD 5E 7E 3C 44 63 01  91 86 D0 B2 3A C6 70 C1 | ..^~<Dc.....:.p.
4s.9767.0122 iv: 0x700006ab6c50, offset: 0, ivcache: 0x7fdf1dba6ee0, finiv: 0x700006ab6c70
00000000: 32 05 0B 83 87 6D 7A D0  12 5B 74 54 94 77 EB FC | 2....mz..[tT.w..
……
4s.9771.0558 CypherGuardCore::AES_PCTS_Decrypt_Body--
4s.9771.0566 CCCryptorUpdate(0x7fdf1b703a00, 0x700006ab6c70, 10, ...)
4s.9771.0571 CypherGuardCore::AES_PCTS_Decrypt_CTS_scrap++
4s.9771.0574 data: 0x1287dc000, datalen: F57, key: 0x7fdf1dba6ee0, keylen: 10, iv: 0x700006ab6c70
4s.9771.0579 CCCryptorUpdate(0x7fdf1b704a00, 0x1287dc000, F40, ...)
4s.9771.0784 CCCryptorUpdate(0x7fdf1b704a00, 0x1287dcf40, 10, ...)
4s.9771.0790 CCCryptorUpdate(0x7fdf1b704a00, 0x700006ab6b00, 10, ...)
4s.9771.0794 CypherGuardCore::AES_PCTS_Decrypt_CTS_scrap—

这两者一个处理52000一个处理F57是整个一个子文件的大小52F57, 分为body和scrap分别处理, scrap估计他们取得是边角料的意思, 也就是不能按照4k对齐的部分.

然后是下一个body和scrap.

Body的数据拆分为4k的大块进行解密, 让我们把大块单位设为cube, 和每组16字节的block区分开来.

首先分为n个大块, 然后每块的key是EFBD5E7E3C4463019186D0B23AC670C1, 每块的iv是初始iv使用key循环加密的结果, 第0块经过1轮加密, 依次类推. 最后经过n+1次加密的iv复制给finiv, 后继还有用.

然后CTS_scrap的解密不是太复杂, 也是以16字节为一组的粒度, 分为前面n-2行和后面两行, 前面连续解密后面分两次解密.

连续解密使用的iv从日志中可以看出, 是跟Body输出的finiv同一个地址. 实际确实是解密body最后一次的iv.

后面两块的key也是输入的key, iv则要看是否存在倒数第三行.

如果存在倒数第三行, 使用倒数第三行的密文作为iv, 否则用输入的iv做iv.

最后处理这最后两行, 倒数第二行一定是16字节字节, 直接aes_ecb解密, 输出记为decfullrow,

然后最后一行左对齐复制到16字节的本地缓存tailrow, 与decfullrow进行xor后存入localrow1.

如果最后一行是不完整的, 将localrow1的右侧和tailrow的左侧组合成localrow0.

然后localrow0再进行aes_ecb解密, 输出记为localrowdec. localrowdec 与iv进行xor后存入倒数第二行.

localrow1则存入最后一行.

 

根据以上信息, 我写了个dmmbdec小程序, 能够解出dmmb文件中的子文件的原文. 如果再加上dmmb的文件解析, 就能够解出整个”胶囊包”了.

胶囊包的格式是从哪整理出来的呢?

在+[CPCapsulizedFile capsulizedFileWithContentsOfFile:error:]函数里面, 有一个读取footersize的函数, 然后根据读取出来的footersize选择不同的类初始化.

footersize是输入文件的末尾的4字节内容. 看了下两个dmmb都是3C, 又买了本小说<<龙王的工作>> 网页写是epub, 后缀dmmr, footersize是90. 后面又买了一些限时免费的epub, 后缀dmme的, 也是90.

4个footersize对应4种文件类型, 分别是3C->3 90->1 60->4 F6->2

    footertype = CypherGuardCore::test_footer_size(fd);
    switch ( footertype ) {
        case 1:
            return [[__CPIR_XdJKKTxTUCuhXpgR initWithContentsOfFileVersion1:aFile error:error] autorelease];
        case 2:
            return [[__CPIR_XdJKKTxTUCuhXpgR initWithContentsOfFileVersion2:aFile error:error] autorelease];
        case 3:
            return [[__CPIR_NCwtwzHlh6HmHQ2b initWithContentsOfFileVersion1:aFile error:error] autorelease];
        case 4:
            return [[__CPIR_NCwtwzHlh6HmHQ2b initWithContentsOfFileVersion2:aFile error:error] autorelease];
        default:
            [__ErrorRepresantation assignError: error domain:CPSecurityLibraryErrorDomain code:1];
            return 0;
    }

可见我这里测试的文件都是Version1, 而dmmb对应的处理类是__CPIR_NCwtwzHlh6HmHQ2b, dmmr对应的是__CPIR_XdJKKTxTUCuhXpgR.

这两个类应该分别对应多文件胶囊, 和单文件胶囊. 类的名字估计是用#define的方式, 做了源码级别名称混淆. 应该不是链接后的后处理工具修改macho, 因为后面在这个库和其他库里都会看到, 方法名的每一个参数名都混淆成了很长的字符.

  

因为objc存在着方法类型签名, 所以反混淆必须联动修改签名. 再加上主程序或者多个库之间互相调用, 则需要多个文件一起修改.

我以前没有发现类似的重命名工具, 就自己做了一个, 能够生成修改后的文件也可以生成py脚本补丁到IDA里. 但是这种撑到很长的, 一来不方便通过长度判断本来的单词组合, 二来如果想要改短, 修改字符串表的时候可能会遇到末尾对齐的链接器优化.

比如有两个method, __CPIR_aaaa:__CPIR_bbbb:和__CPIR_bbbb:后者的SEL name可能就是指向前者的中间.

所以我也懒得改它了, 凑合着静态看完了.

当它是多文件胶囊类的时候, 文件末尾的结构如下

struct _cp_package_footer1_s
{
    uint32_t magic; // DNBF
    uint32_t titleoffset;
    uint32_t titlelen;
    uint32_t headeroffset;
    uint32_t headercount;
    uint32_t rev14;
    uint32_t rev18;
    uint32_t rev1C;
    uint32_t rev20;
    uint32_t rev24;
    uint32_t rev28;
    uint32_t rev2C;
    uint32_t rev30;
    uint32_t rev34;
    uint32_t footersize;
};

根据这个headeroffset我们来到文件对应的偏移, 会有一个这样的结构

struct _cp_package_file_header0_s
{
    uint32_t magic; // DNBA
    uint16_t feature;
    uint16_t rectype;
};

当magic正确的时候, 后面会跟

struct _cp_package_file_header1_s
{
    uint32_t flag;
    uint32_t attributes;
    uint64_t filetime;
    uint32_t filesize;
    uint32_t crc32;
    uint16_t namelen;
    uint16_t padlen;
};

因为没有遇到其他magic, 因此不知道会不会有其他格式的header1.

这个结构末尾紧跟着的是Unicode的文件名, 长度是包含末尾0字符的. 然后跳过padlen后是附带的文件内容, padlen我这里看到都是0.

flag里面会有bit提示文件是不是加密, attributes里面有bit表明他是不是目录. 比如我这里dmmb里面会有一个不加密的index.bin文件, 里面包含了guid信息. 阅读器会用这些信息向CypherTech的服务器请求授权.

这里面还有我们之前提到的iv的前体, 我叫他preiv, 为了便于区分, 之前说的解密文件的叫做filekey/fileiv, 而preiv就是两者的来源.

服务器返回的是证书信息, 存储在cyphertech自己的X:\Cypher License Information\Services目录, 里面会包含servicekey, 用它来解index.bin里面的fileinfo, 在偏移50的地方得到prekey.

描述大概如下

fileiv=aes_ecb_enc(footer1->preiv, keykey)

fileinfo9c=readfile(footer->fileinfodelta, footer->size)   // index.bin for dmmb or entire file

prekey=aes_cts_dec(fileinfo9c, servicekey)->offset50

filekey=aes_ecb_dec(contentskey, prekey)

 

当时主要静态看的, 还发现一种生成key的方式, 估计是测试用途, 实际没有抓到执行.

ivkey=aes_ecb_dec(sha256(phase), keysource)

keykey= hi128(ivkey) ^ lo128(ivkey)

这里ivkey/keykey是servicekey/contentskey昵称, 容易记一些.

我企图比较一下不同的加密文件, 是不是这两个servicekey/contentskey是相同的, 这样我们可以一次抓取, 然后自己生成filekey/fileiv, 不用一本一本书的点开, 并查看日志.

很遗憾根据hook出来的日志, 并非如此.

顺便一提, dmmb里面的index.bin的格式其实和dmmr/dmme格式一样, 只不过不包含文件内容. 它在windows版hook时候是跟dmmr/dmme走同样的VFT函数解析的.

“单文件胶囊”末尾的结构如下

struct _cp_footer1_s
{
    uint16_t var0;
    uint16_t type1;
    uint32_t flag2;
    uint32_t var3;
    uint32_t fileinfodelta;
    uint32_t fileinfosize;
    uint32_t contentoffset;
    uint32_t contentsize;
    uint8_t encmd5[16];
    uint8_t preIV[16];
    uint8_t zero10[32];
    uint32_t servinfooffset;
    uint32_t servinfosize;
    uint32_t var13;
    uint16_t var14;
    uint8_t var15;
    uint8_t var16;
    GUID guid;
    uint8_t servdll[16];
    uint32_t footersize;
};

如果是曾经的我, 为了解决”我买了5本书, 还要分5次抓key/iv”的烦恼, 我会花很大功夫, 重写个客户端, 自己搞定验证和返回servicekey/contentskey的流程, 然后就随便怎么解密了.

而现在的我会思考这一切有什么意义, 我起初的初衷只不过是为了博取小朋友的夸奖啊?

抛开这一切, 半仙强迫症复发, 还是决定要做个windows下的工具, 为此我要把mac版的hook位置, 在windows版定位出来.

首先再次用010Editor打开处于阅读状态的阅读器, 然后搜索iv内容, 可以找到iv, 也可以找到key, iv前后的内容还包含有像是证书的字眼.

当然了, mac和windows版虽然界面都是Qt+QML的, 但就是解密架构不一样,  半仙重新拉起来ctcrypt_43开始分析, 因为在之前的分析和hook中, 打开一个dmmb多文件胶囊, ctcrypt_94调用很多次, 而ctcrypt_43只调用一次.

推测的几个导入函数作用为

ctcrypt_43是给CEncryptOperator设置keycontainer.

ctcrypt_23 CSymmetricKeyContainer析构

ctcrypt_42 CEncryptBufferOperator析构.

ctcrypt_41 CEncryptBufferOperator构造

ctcrypt_22 CSymmetricKeyContainer构造

ctcrypt_94是解密块/末尾

ctcrypt_1是初始化?异常.

调用ctcrypt_43的地方是哪里呢? 就是drm::cypherguard::decryptor的VFT第二个函数.

我大概整理了一下

int __thiscall decryptor_vf1(drm::cypherguard::decryptor *this, std::wstring *footer1path)
{
    const wchar_t *wfilename_cstr; // eax
    int initret; // eax
 
    wfilename_cstr = (const wchar_t *)footer1path;
    if ( footer1path->_Myres >= 8 )
        wfilename_cstr = footer1path->_Bx._Ptr;
    // 解析footer
    if ( !ctlicis_34(this->licmgr8, wfilename_cstr, 8, 1) )
        func_throw_cguard_sdkerror_ctlicis((void *)this->licmgr8);
    func_decryptor_pre1(this);
    // 应该是从footer+service+contntskey初始化container
    if ( !ctlicis_106(this->licmgr8, this->containerC) )
        func_throw_cguard_sdkerror_ctlicis((void *)this->licmgr8);
    if ( !ctlicis_36(this->licmgr8) )
        func_throw_cguard_sdkerror_ctlicis((void *)this->licmgr8);
    // 送key或者取key
    // +C = CKeyContainer
    // +10 = CEncryptOperator
    initret = ctcrypt_43(this->operartor10, 0, this->containerC);
    if ( !initret )
        func_throw_cguard_sdkerror_ctcrypt((void *)this->operartor10);
    return initret;
}

为了从实例中取数据, 要知道对象的布局, 首先我们来找对象的大小, 好建立一个对应的结构(非必需但先确定大小可以省事).

这里我用drm::cypherguard::decryptor举例子, 它是有typeinfo的. 首先我们通过decryptor_vf1参考跳转到vft的位置

const drm::cypherguard::decryptor::`vftable'

然后往上看, 点击RTTI complete Object Locator, 然后点击reference to type description.

可以看到识别出来的继承关系

.data:0085DE2C                      ; public class drm::cypherguard::decryptor /* mdisp:0 */ :
.data:0085DE2C                      ;   public class drm::abstract_decryptor /* mdisp:0 */ :
.data:0085DE2C                      ;     invisible private protected class boost::noncopyable_::noncopyable /* mdisp:4 */

然后我们返回到vtable处, 在vtable查看引用, 会发现有两处使用此处地址的.

进去查看会发现第一个是初始化decryptor第二个是销毁decryptor. 初始化的时候是从基类往下不断修改vtable, 销毁时候是相反.

继续往上回溯, 到第三层会发现

    mem1078 = (drm::cypherguard::decryptor *)operator new(0x1078u);
    v13 = 1;
    if ( mem1078 )
        decryptor = decryptor_init_para2(mem1078, a1 + 36, a3, (int)v12);
    else

好了, 大小确定, 1078.

同时在寻找vtable的引用时候, 初始化函数中, 刚好有一个子函数是初始化licmgr/container/operator成员.

 

PS: 以前IDA的结构继承会把”父结构”直接叠在自身前面, 对于有VFT的结构, 会造成子结构在反编译时候vft都是父结构的类型, 而子结构的vft里函数可是可能比父结构多的.

为此我是用把结构劈掉vft出来个Body的做法, 这样如果继承就是排列vft, body, 然后自身字段, 如果多重继承则, 第二个开始包含整个上级结构, 就带上了它的VFT.

后来IDA出了__cppobj的支持, 直接针对VFT成员做了特殊处理, 相当于把上级的vft类型改为了自己的VFT类型, 正好省了我们的工夫了.

用了它这c++支持后引用上级的成员时候, 中间就不会加上结构名了(以前不支持__cppobj时候, 结构的继承是会在中间加上上级的结构名称的). 连vft的调用也省掉了vft成员名, 确实是方便可居然不大习惯.

最后我找到key和iv缓冲到了CSymmetricKeyContainer*的CEncryptSymmetricKey*的CSymmetricKeyInfo里面, 但这个在dll里面用过就会抛弃, 不美!

因此我是从CEncryptOperator的CPCTSEncryptor*的CCryptInfo*里的CPCTSEngine*中取的. 也就是调用ctcrypt_43后的CEncryptOperator* this里面.

具体是在这个成员函数里面做的:

int __thiscall CPCTSEngine_vf6(CPCTSEngine *this, const void *src, size_t size)
{
    void *newkey; // eax
 
    if ( src && size )
    {
        if ( this->key4C )
        {
            func_free2(this->key4C);
            this->key4C = 0;
            this->keysize2C = 0;
        }
        newkey = func_alloc1(size);
        this->key4C = newkey;
        memset(newkey, 0, size);
        memmove(this->key4C, src, size);
        this->keysize2C = size;
        return 1;
    }
    else
    {
        this->SetError(this, 302, 0, 0);
        return 0;
    }
}

这里的memset跟memove, 前者毫无必要, 这也是程序员认为的”安全措施”吧, 值得表扬的是他这些dll都是静态链接VC运行库, 因此memset和memove是内嵌的, 不能通过IAT hook.

 

好了, 有了这个点, 我们能做什么样的操作呢, 嗯, 可以写个程序, 用dmmb路径做参数启动阅读器, 然后注入dll, 拿到key/iv后, 解压并退出.

这个流程比较简易, 问题也少, 可就是这阅读器每次启动有点慢, 在老衲的E5v4上也要两三秒呢.

我心生一计, 不如我们利用他本身的书架, 拦截打开图书的路径, 结合刚才ctcrypt_43的拦截, 可以做到点击一本书就解压这本书, 不用反复启动.

“打开电子书”的hook点好像是通过ApiMonitor记录CreateFile系列api的callstack看出来的, 然后分两个路径, 一个是在书架点击, 另一个是通过文件关联打开(包括已有实例运行时候的双击).

然后我程序员毛病又犯了, 这样会不会不稳定, 阅读器崩溃了多丢脸啊, 如果内存泄漏了咋办, 要想办法把解密的部分跟宿主进程隔离开! 也就是注入的dll在点开一本书后, 通知解密程序, 解密程序上面有选项, 可以选择自动解压到书架所在目录, 或者手动选择目标目录/文件, 再开始解密.

这个DMMBDecGUI编写还算顺利, 只不过还是遇到一个小坑, 那就是在Debug下, 函数地址实际取到的是一个跳转到真正函数的垫片, 悲催的是IDE调试时候想看看这个地址, 它会自动显示成真正函数地址.

当然看了一下编译选项这个是增量链接导致的, 总不能在说明和注释里面写吧, 毕竟真正的程序员从不看说明的. 我又吭哧吭哧动态判断一下要注入到exe的代码是不是个垫片跳转.

流程就是我启动exe, 给exe用RemoteThread加进去我的dll, 然后再加进去一段代码, 在dll里面GetProcAddr出个变量给他赋值, 目的就是让阅读器通过WM_COPYDATA给我们的GUI发消息.

写好以后, 又打磨打磨, 换wdk编译, 链接msvcrt.dll. (本来因为DMMbookviewer用的是vc2013我也可以用, 但强迫症吗, 老想着通用性)

然后呢, 我又一想, 如果先启动好应用, 然后拦截文件打开的调用, 然后再在资源管理器里面双击下载好的dmmb/dmme, 我们不是也能拦截到吗, 可是, 尴尬的就是这DMMBookviewer它可以不要安装, 解出来就能用.

那我们能不能启动好应用, 然后我自己模拟是阅读器叫他打开我们选择的文件呢? 这里其实后来我发现我犯傻了, 我直接给原本的exe带个参数让它给现有的exe发消息也一样呀.

不过最后我还是偷懒, 只制作了书架点击版本的配套GUI.

好了, 凭着回忆写, 省略了很多, 其实在搞DMM的同时, 我查了下, DLSite跟它也是同一家公司, 阅读器也用了CypherGuard, 说不定也能轻松搞定哦.

DLsiteViewer的程序目录明显比DMMbookviewer清爽很多, 就光秃秃的exe和几个dll, 不过很遗憾, 他需要安装驱动才让启动. 推测它是早期版本的接口, 不象DMMbookviewer, 虽然安装包会装驱动但是不要也能正常运行.

DLsiteViewer.exe是WTL架构的. 脱壳后, 要手动在IDA里面重建段, 不能保持默认可写可执行. 不然一些使用相对偏移的跳转, ida不会识别和引用.

一开始我写了一个脚本, 寻找这类的跳转, 并试图补丁. 不过后来发现改好段至少在F5里面就能识别到目标了. 但最后还是需要脚本来做一下对那些偏移指向的目标的处理, 他们能被IDA识别出来是延迟导入表但没引用; 因此没转换为函数.

我本来想是把mov eax/add eax去掉, 把省下的空间用于重新排列之间的opcode, 然后下面的call eax直接改为e8call目标, 因为8D不是5字节? 但后来发现B8/BB好像是5字节哦 因为第一字节编码到了opcode里面.

.text:00455240                      func_init_CAntiCapture proc near        ; CODE XREF: sub_457F00+51C↓p
.text:00455240 A1 C0 7B 72 00                       mov     eax, dword_727BC0
.text:00455245 56                                   push    esi
.text:00455246 8B F1                                mov     esi, ecx
.text:00455248 05 A5 89 60 BC                       add     eax, 0BC6089A5h
.text:0045524D C7 06 18 80 72 00                    mov     dword ptr [esi], offset ??_7CAntiCapture@@6B@ ; const CAntiCapture::`vftable'
.text:00455253 C7 46 04 00 00 00 00                 mov     dword ptr [esi+4], 0
.text:0045525A FF D0                                call    eax
.text:0045525C 89 46 04                             mov     [esi+4], eax
.text:0045525F 8B C6                                mov     eax, esi
.text:00455261 5E                                   pop     esi
.text:00455262 C3                                   retn
.text:00455262                      func_init_CAntiCapture endp

 

.text:00455240                      func_init_CAntiCapture proc near        ; CODE XREF: sub_457F00+51C↓p
.text:00455240 A1 C0 7B 72 00                       mov     eax, dword_727BC0
.text:00455245 56                                   push    esi
.text:00455246 8B F1                                mov     esi, ecx
.text:00455248 C7 06 18 80 72 00                    mov     dword ptr [esi], offset ??_7CAntiCapture@@6B@ ; const CAntiCapture::`vftable'
.text:0045524E C7 46 04 00 00 00 00                 mov     dword ptr [esi+4], 0
.text:00455255 E8 D9 38 27 00                       call    cguard_18
.text:0045525A 90                                   nop
.text:0045525B 90                                   nop
.text:0045525C 89 46 04                             mov     [esi+4], eax
.text:0045525F 8B C6                                mov     eax, esi
.text:00455261 5E                                   pop     esi
.text:00455262 C3                                   retn
.text:00455262                      func_init_CAntiCapture endp

因为ida得到的反汇编如出现offset这样就无法再用汇编指令处理, 用了危险的方法, 直接用二进制搬运, 还好这中间不会有相对跳转, x86也没有ADD PC, 程序员的老毛病, 折腾了一天发现又写了些用了一次性的东西, 还优化来优化去.

 

所以又发奋偷懒. 为了能够重复利用之前的解包程序GUI, 我打算在DLsiteviewer里面也拦截打开文件和接受key.

好了, 继续套用之前的Logger, 看看ctcrypt_43里面有没有key. 之前对比了一下两者的ctcrypt.dll一模一样, 应该是可以的.

因为是带壳操作, 需要等待壳代码解压好对应的代码才能补丁.

这是原本的带壳exe区段, 第一个无名的段我们不如叫他RWX段, 之前我们IDA里面重建的段就是从它划分出来的. 这里.idata是壳的段, 这个段的名称和大小会在壳解压途中被填充破坏, 而不是解压后破坏. 因此不能检测这个来卡合适的搜索点.

我用的是检测代码段末尾的方法.

    DWORD codeend = hmodule + ntheader->OptionalHeader.BaseOfCode + ntheader->OptionalHeader.SizeOfCode;
    quickdump(codeend - 0x200, (LPBYTE)codeend - 0x200, 0x200);

加壳后没有改变这些exe头信息, 可以通过检查原本是00的代码段末尾被还原的时机.

原本应该是想办法拦到跳OEP的代码, 然后和往常一样搜索补丁点的, 搜早了代码没解完, 搜不到, 搜晚了…一些要在启动前nop掉的代码就执行完了. (这里埋个伏笔)

因为偷懒我暂时直接硬编码要补的地址然后检测解析目标是否是call, 然后这里要注意由于themdia壳会改造对API的调用, 把6字节的CALL DWORD PTR[_imp_func]改为call jmp_imp_func, 是5字节的e8call, 因此会随机选择前面或者后面插NOP补齐.

006E023D: 90 E8 12 04 24 04
006E023D: E8 2E 03 20 04 90

大概就是这模样.

.text:006E023D FF 15 C4 64 72 00                    call    CreateFileW

这个就是我选的拦截的打开文件的点, 但实际它会可能变成上面那两种.

.text:0046F5BF E8 64 31 24 00                       call    ctcrypt_43

这个是我选的拦截接受key的点.

有兴趣的小朋友可以试试用这两个点来做一个Dlsite版本的解密GUI?


看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~

最后于 2020-12-12 12:22 被曾半仙编辑 ,原因:
上传的附件:
收藏
点赞6
打赏
分享
最新回复 (9)
雪    币: 614
活跃值: 活跃值 (635)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_foyotena 活跃值 2020-12-12 10:56
2
0
支持大仙
雪    币: 3504
活跃值: 活跃值 (2450)
能力值: ( LV15,RANK:490 )
在线值:
发帖
回帖
粉丝
曾半仙 活跃值 12 2020-12-12 11:03
3
0
哈哈哈, 不是dmm的dcv解密, 不过dcv我还真的花了时间解了个, 它跟m3u8的ts分段, 压制参数完全相同不说, 帧的数据除了中间一点儿NAL或者时间码一类的几字节格式不同, 其他二进制相等.
再加上electron我不是很熟悉, 也就没写这部分
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
moenikiller 活跃值 2021-2-9 20:25
4
0
大佬方便传一份可用的安装包吗?DMM更新后就再也找不到旧版了
此外这个软件能否破解DLSITE?用法也是相同吗?
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_康康_570 活跃值 2021-2-15 14:13
5
0
谢谢大仙
雪    币: 174
活跃值: 活跃值 (68)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_kkgqhizg 活跃值 2021-9-1 16:03
6
0
问一下3.1.13还能用吗,在3.1.13下好像打开书也抓不到文件?提示Select book first!
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_xdwxzlwr 活跃值 2021-12-9 21:47
7
0
问问现在还能用吗
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_lpzrormd 活跃值 2022-4-13 15:38
8
0
能联系到你吗
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_lpzrormd 活跃值 2022-4-13 15:39
9
0
可以私信下我吗
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
745ujx 活跃值 2022-8-5 21:22
10
0
mb_lpzrormd 可以私信下我吗
fanza的视频(2D,1080p)和电子写真。近两天我自己买了视频和写真,从零碎的帖子里找到了办法,成功提取了。你现在如果仍有需要我可以帮你。
游客
登录 | 注册 方可回帖
返回