大家好, 我又来破解电子书了.
话不多说, 这次搞的是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地方发现这么一个函数
这里可以看出, authenticationType分为2和6两种, case 2时尝试去取缓存到本地的授权信息, 实际测试中dmmb文件是走到这里的. dmmb文件被他们叫做capsulized file, 直译就是包了胶囊的文件.
刚才说到的windows版有一个ctcrypt_94函数会对来自dmmb文件中的内容进行解密, 然后我习惯性的hook了OSX下的CCCryptorUpdate, 在看到感兴趣的解密后的数据就设法让其崩溃.
我们写好dylib, 使用DYLD_INSERT_LIBRARIES=./DMMLogger1.dylib ./DMMbookviewer, 启动后打开一本书, 如愿以偿地崩了.
这个工作线程崩的不是地方, 因为这是一种Block调用, 是看不到发起方的, 不过崩溃也没白费,下一个线程恰好就是Block的发起方
赶得早不如赶得巧, 忽略后面写日志的调用. 往回看, 有一个函数叫做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:
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也加上日志
这两者一个处理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
可见我这里测试的文件都是Version1, 而dmmb对应的处理类是__CPIR_NCwtwzHlh6HmHQ2b, dmmr对应的是__CPIR_XdJKKTxTUCuhXpgR.
这两个类应该分别对应多文件胶囊, 和单文件胶囊. 类的名字估计是用#define的方式, 做了源码级别名称混淆. 应该不是链接后的后处理工具修改macho, 因为后面在这个库和其他库里都会看到, 方法名的每一个参数名都混淆成了很长的字符.
因为objc存在着方法类型签名, 所以反混淆必须联动修改签名. 再加上主程序或者多个库之间互相调用, 则需要多个文件一起修改.
我以前没有发现类似的重命名工具, 就自己做了一个, 能够生成修改后的文件也可以生成py脚本补丁到IDA里. 但是这种撑到很长的, 一来不方便通过长度判断本来的单词组合, 二来如果想要改短, 修改字符串表的时候可能会遇到末尾对齐的链接器优化.
比如有两个method, __CPIR_aaaa:__CPIR_bbbb:和__CPIR_bbbb:后者的SEL name可能就是指向前者的中间.
所以我也懒得改它了, 凑合着静态看完了.
当它是多文件胶囊类的时候, 文件末尾的结构如下
根据这个headeroffset我们来到文件对应的偏移, 会有一个这样的结构
当magic正确的时候, 后面会跟
因为没有遇到其他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函数解析的.
“单文件胶囊”末尾的结构如下
如果是曾经的我, 为了解决”我买了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.
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2020-12-12 12:22
被曾半仙编辑
,原因: