一直比较喜欢Typora的简洁与美观(尝试过用 vscode 搭配插件编辑 markdown 文件,体验还是要差一些的),突然发现自己windows机器上很久前安装的typora不让用了,提示:
1.png (14.97 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
幸好原始安装文件还在,对比一下版本,原始安装文件是 0.9.93 版本,而不让用的是 0.10.11。
在我的 ubuntu22.04 机器上安装有 1.7.4 版本,win10上安装了 1.7.6版本。貌似最新的版本开始收费了,还分国内/国外两个版本。(typora.io 这个官网貌似被墙了,真不知道为了个啥。)
本着一个cracker无知者无畏的折腾到底的精神,开始瞎整!
二、摸底、知识和工具准备
逆向分析的第一步就是要了解目标软件是用啥开发的,架构是啥。
一看 typora.exe 好家伙,100多个M,这么大的可执行程序还分析个啥?IDA分析一下都不知道要多久。
由于目前主用 ubuntu22.04 系统,想着顺便从 0 开始学习使用 ghidra 进行 Linux 平台的逆向分析工作。于是从一个小目标开始,需要瞎搞的知识点有了第一次扩充。
悲催的是用 ghidra 静态分析 1.7.4 版本的 typora 有160多M,分析了10个小时也没结束。这个弯路绕太远了,毫无意义。直接放弃。
正确的打开方式:直接网上搜索
有大神直接用 IDA 分析 typora.exe,提示了一些有用的东东。结合两位成功破解的大神的文章,可以确定 typora 是使用 electron + nodejs 框架,用 javascript 开发的跨平台的桌面程序。(和vscode类似)
所以,啥是 electron/nodejs/javascript?0 基础的暴走,奇怪的知识点需要进行第二次扩充。
这几个每一个都是大部头,越底层的难度越大。被V8引擎绕进去花了不少时间(虽然了解一下好处很多,带着VM的思路去了解,能有不少收获),但就typora的破解来说,只需要涉猎以下内容:
[ol]
这个是需要重点把握的,熟练查阅 nods.js 中 N-API 的文档。
N-API 实际上是 V8 接口的封装,进行了抽象,但对逆向增加了困难。因为其参数都是一系列 void** 之类的指针,具体在 V8 层进行实际的数据类型转换。所以逆向时,对于托管对象的指针,就别想着查看其数据了。重点应该放在 C++ 的本地数据结构上。
[/ol]
三、聚焦 main.node
node.js Addon 可以在 js 层直接 require 一个 dll/so。electron 入口可能也是 require 引入 package.json 及里面定义的入口 js 文件的。
0.10.11 版本:PEid 查看算法常数,发现 sbox/rsbox,基本可以确定使用了AES算法。只是windows版本去符号,要从 rbox 逆推找到 AES KEY 和取定其 mode 难度还是比较大的。网上也有人分析到这一步放弃的。当然大神可以直接找到 mode CBC iv 和 AES KEY。
1.7.4 linux 版本:神奇的是没有去除符号,但与0.10.11版本不同,KEY 和 mode 的地方进行了混淆处理,不知道把 CBC 的 iv 怎么藏起来了。懒得去反混淆分析了(主要是能力不够)
有大神直接给出了 main.node 保护electron app的概念验证项目:https://github.com/toyobayashi/electron-asar-encrypt-demo。
有了第二部分提到的知识准备后,就可以尝试看一下这个项目。当然像我一样 0 基础的,也可以边看边学,借助 N-API 文档和chatGPT的帮助。
简单总结一下这个项目的思路:
[ol]
[/ol]
结合V8执行js的基本逻辑来理解上面的思路:
// 定义一个 JavaScript 代码字符串
V8::Local source_code =
V8::String::NewFromUtf8(isolate, "'Hello, ' + 'World!'").ToLocalChecked();
// 编译和运行 JavaScript 代码
V8::Local script = Script::Compile(context, source_code).ToLocalChecked();
V8::Local result = script->Run(context).ToLocalChecked();
V8执行js代码有两个过程,Compile 和 Run。Compile 只接受 V8 的托管字符串(js代码),而 C++ 字符串需要在 isolate(V8的一个独立的运行时虚拟机容器)内创建一个托管的 V8::String,用 N-API 库的话,相应函数是 napi_create_string_utf8(...)。
所以,既然把入口 js 的代码放到 C++ 层加解密,那么要执行它,比然经过上面的三步。重载 _compile 函数就是为了在这一步,给传入的 C++ string(js code)进行解密并创建对应的托管String,以便传给原来的 compile 函数。
理解了这个过程,那么最快、最容易的获取解密后的入口 js code,就在这个过程中。有两个思路,后面介绍。
四、分析 main.node
1、找入口:
2.png (5.78 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
3.png (1.22 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
4.png (8.85 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
5.png (10.45 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
6.png (23.51 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
addon 开发最后一个宏 NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init) 实现的导出绑定,实际上就是调用 napi_module_register 注册了一个module,这个module结构包含一些源代码开发环境的一些信息,和一个__napi_main,在__napi_main中传递了 init 函数的地址。
7.png (13.1 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
实际上找入口这一步是多余的,有了源代码只要结合关键字符串 _compile 和相应的函数调用,就能很快定位到 init 函数。只是作为一个cracker对流程跟踪的执念,瞎整。
2、找_compile的重载函数:跟着源代码中的关键字符串找,很容易定位
module_prototype.DefineProperty(
Napi::PropertyDescriptor::Function(env,
Napi::Object::New(env),
"_compile",
ModulePrototypeCompile, // 就是这个函数
napi_enumerable, // 实际这个传递参数的地方做了一个wrapper。
addon_data));
3、作者的 trick 一。
疑似的ModulePrototypeCompile这个函数往里面看的时候,和源代码完全不一致:
8.png (15.39 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
当时也懵了,因为自己是在windows系统上用IDA完全对照源码静态分析。使用静态分析工具查看 refrence to 和 refrence from 都连不上。无奈之下启用 x64DBG 调试。(找到真实的ModulePrototypeCompile函数还是很容易的,因为根据源代码,这个函数里面有非常明显的字符串"app.asar",而且是一个对称的if ... else ...结构。只是想弄清楚开发者到底怎么魔改的。)最后发现真实的ModulePrototypeCompile地址被打包在了上面替换函数的参数里面。调用变成了 call qword ptr [r8],从而避免了静态分析时候被一撸到底。实际的trick是这样的:
9.png (15.84 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
直到分析 1.7.4 linux版本的时候,就看的很清楚了。
10.png (20.96 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
两者结合可以清楚看到多了一层 Wrapper 把真实函数地址隐藏在 Wrapper的参数里面。
4、AES解密
后面的部分对于 0.10.11 windows版本来说,和源代码几乎一模一样,就是AES解密的过程,没有源代码那么多函数层级,中间的函数调用估计被 inline 了。但基本结构是一样的。CBC 模式的 iv 被藏在了实际chunck的最前面16个字节。
对于 1.7.4 Linux 版本整体架构不变,但 getKey 和 getIV 做了混淆,IV 怎么藏的也没看明白。尝试用函数的返回值,参照老版本的格式进行解密,发现是错误的。对混淆没什么能力,还是绕路吧。毕竟最终目标是去 main.node 运行。
五、获取源代码/解密app.asar
1. 0.10.11 windows版本:
11.png (9.55 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
可以直接解密 app.asar(因为打包了几乎所有重要 js 文件,所以一个个调试获取源码也麻烦。)
其AES KEY就编码在指令中,无论是静态获取还是调试 getKey 处获取都可以。
IV 是放在chunck前的16个字节。根据这两个条件,可以直接编写代码对 app.asar 进行解密。
2. 1.7.4 linux 和 1.7.6 win10版本:
因为看不懂 key 和 iv 的混淆,只能骚操作——直接调试获取 atom.js 的源代码。(只加密了这一个文件,估计是要保证启动速度)
在第三部分的最后,卖了一个关子,说到有两种方式可以获取解密后的 atom.js 入口,这里介绍一下:
(1)其实这个main.node加密框架本身就提供了获取方法,既然能重载_compile,在调用原始_compile之前对js code 进行解密,那么这个框架本身,也可以被cracker用来再次重载_compile,等着后续的main.node将解密好的 js code 传过来,然后 console.log 就可以了。
我不是搞开发的,看这个框架要配置编译环境貌似挺复杂的,留给有electron开发经验的朋友吧。这个方法的好处是:一次投入,终身享受。只要还使用这个框架,点下鼠标就能获得源代码。(当然typora作者也是有备而来,后面会讲到)
(2)napi_create_string_utf8,这个函数接受 C++ 字符串,可以直接从 GDB 或者 x64DBG 中把参数的值 dump 出来。不过因为调用这个函数的地方很多,需要手动定位 ModulePrototypeCompile 函数解密分支里面的那个,下断点。Decrypt函数直接返回了托管字符串,所以还需要深入进去找到正确的返回前最后一个调用 napi_create_string_utf8 的地方。
iVar4 = napi_create_string_utf8(pnVar15,&DAT_00145b42,0,ppvVar14);
当然大神使用 frida 动态挂钩 napi_create_string_utf8 这种骚操作,咱小白也不懂。
六、去 main.node 运行
优点是可以保持最快的加载速度,相当于完全开源了。
1、0.10.11版本
根据0.9.93版本的目录架构,将解密后的文件丢到 typora/resources/app/ 目录下,其他保持不变,就可以直接运行了。
2、1.7.4 linux 和 1.7.6 win10版本
使用解密后的 atom.js 按照 0.10.11 版本那样直接运行 win10 会弹窗提示 scheme 变量没有定义;linux 下有 typora 进程,不提示错误,需要用 GDB 调试运行才能知道,错误是一样的。这又是闹哪一出?
一开始不能确定导致变量没定义的原因,可能在 js 层,也可能在 main.node 层。js 层搜索了一圈没啥发现,感觉暗桩及有可能还是在 main.node 里面。想个办法验证一下:
其实完成这一步,也可以实现破解了,后面注册逻辑就都是 js 层面的事情了。不过对于一个喜欢瞎折腾的 cracker,做到这个程度,不是很满意,于是继续瞎整。。。
在 js 层搜索一下 scheme 没有特殊的用法,对照 0.10.11版本,scheme 是定义一个自定义协议解析的字段,其值为 typora,可以使用 typora://xxx 这样的协议进行处理。
在 main.node 里面全局搜索 scheme 啥也没发现,一时陷入僵局,作者显然是埋了一个深坑。因为是在 win10 下分析的去符号的版本,所以一时没有头绪。后来决定换个思路,atom.js 最后启动窗口肯定是要加载一个 html 的,这就是 typora 程序的入口。按照这个逻辑 发现了 “entry” 字符串/变量,其值应该是 window.html。现在有了两个键值对 scheme --> typora 和 entry --> 包含window.html。用这4个字符串去IDA搜索交叉引用,可以很快找到一个函数 sub_180012290。分析这个函数,实际一共埋了5个暗桩:addonPath,entry,scheme,shost,shostc。
这5个暗桩,有的是隐藏了键名,有的是隐藏了键值。
12.png (21.37 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
这是键值对 addonPath :app.asar。键名 addonPath 的ascii码有些字符做了 + 0x0a 处理。对键值 app.asar 没有处理,就是字符串引用。
13.png (51.73 KB, 下载次数: 1)
下载附件
2023-11-6 15:44 上传
这是键值对 entry :typora://app/typemark/window.html。键名没有加密,键值的处理方式很直观。当然 shost 的值藏的更复杂一些,主要是将单字节的 ascii 变成了4直接的 int。不一一列出来了。
安装函数linux版本名字是 _initGlobal。当然无论哪个版本,这个函数一开始的 napi_get_global 函数说明了一切。都加到 js 层的全局对象 global 里面了。由于我们要去 main.node 运行,5个暗桩实际上只有后面4个有用。
napi_get_global(env, global);
global['addonPath'] = 'app.asar'
global['entry'] = 'typora://app/typemark/window.html'
global['scheme']='typora'
global['shost'] = 'https://store.typora.io'
global['shostc'] = 'https://dian.typora.com.cn'
1.7 版本的 js 使用了 webpack 打包,main.node init 最后的入口调用有两步:
const a = require('./atom.js'); // require js 貌似一定要带路径,不然找不到模块,js 也不懂,唉
a(); // 从 webpack 打包的 atom.js 看,这是一个空函数。反正main.node 调用了,那就照葫芦画瓢吧。
有了上面这些信息,单独写一个 main.js 把4个全局变量加进去,按步骤调用入口就能实现去 main.node 运行了。
七、注册思路
这些属于 js 层面的东东,还是经过 webpack 打包和混淆过的。对于 javascript 小白来说,看这种代码和天书差不多。找了个debundle工具,能还原未加密的 dist/static/js/ 里面的文件,但还原 atom.js 报错。最后找了个 webcrack 工具,效果是比较好的格式化了 atom.js,比vscode js fommatter 之类的强。果断祭出大杀器——chatGPT。一开始心太黑,直接把整个 atom.js 复制给 chatGPT,想让它给格式化代码,并给出解释。结果 chatGPT 直接给了一个长长的菜谱,真的是用心良苦。
1、0.10.11版本
0.x版本应该都是作者发布的开发测试版本。而0.10版本很可能是作者第一个使用 main.node 进行加密的验证版本,因为 js 里面连个注册页面都没有,只有简单的验证逻辑和时间校验,超时不让用。
让 0.10.11版本重新跑起来也很间,直接将 License.js 里面表述注册与否的变量的初始值从 null 改成 true 就可以了。
hasLicense = true, // null,
2、1.7.4 linux 和 1.7.6 win10版本。
这两个版本在 main.node 里面对 AES KEY 和 IV,以及暗桩的处理上,有一些差异,win10版本的更复杂和强化了一些。js 层上,关于注册的 445 类,对win32系统有着更加强化的校验。1.7.4linux版本,似乎是可以无限试用的,超时验证逻辑存在bug。
js 分析后,确定 445 类是处理所有注册/校验相关的。联网校验优先,涉及数据结构:
SLicense 的初步结构:base64(RSA.encrypt(json2str({license:xxx, email:xxx, type:xxx...})))#failCounts#lastRetryDate
注册信息会用服务端私钥加密,本地用公钥解密。再进行 email 和 license 的校验。本地注册也是这个解密验证过程。网络验证不成功,会清空 SLicense,回到未注册版本。
由于公钥解密的介入,想要完美本地注册已经没有意义。想持续使用的思路大致两个(没仔细研究,可能还有坑,看个大概齐吧):
(1)无限试用:
手动修改 IDate(install date) 时间。
(2)patch js code:
八、加固建议
由于采用 main.node 这种框架把入口的解密放到 C++ 层,那么不可避免的,框架本身可以用来直接获取解密后的 js code,或者是绕不开字符串从 C++ 到 V8容器托管的转换过程。这是 electron 项目保护最大的难点。
作者也进行了很多努力:AES 关键的 key 和 iv 进行了隐藏和混淆;埋了一些暗桩并加密。但用前面的各种骚操作都可以绕过。
个人建议既然埋暗桩,与其花经历在几个全局变量的名字和值上进行各种花式操作,还不如把 445 注册校验类整体/部分放到 main.node 中。至少尽可能让不能脱离 main.node 运行。底层还能使用各种成熟的二进制保护技术,甚至可以引入虚拟机保护,增加逆向成本。
九、后记
typora风格的markdown编辑器,好像github上有开源的了,有道笔记的 markdown笔记似乎也改成typora这种所见即所得风格了(不过上图要付费,或者自己注册一个免费图床)。选择面变多了。不过typora依然是桌面最优雅的。
整个逆向分析过程学到了不少东西,毕竟几乎整个是从 0 开始学起的。
在 ubuntu 平台使用 ghidra 进行静态分析和动态调试;
对面向对象有了更深层次的理解;
托管与非托管的相互调用,从C#、V8 js、python,有点理解python作为胶水语言在这种场景下为什么强大;发现 js 后面的 AST 是一条不归路啊。
对框架的理解,在逆向中的作用越来越重要了。现在的程序似乎都依赖于某个框架。
参考文章:
《向Typora学习electron安全攻防》
《Typora 授权解密与剖析》
《Typora解密之跳动的二进制》
《Electron程序逆向(asar归档解包)》
《electron开发、打包与逆向分析》