环境信息
| 描述 |
说明 |
| 程序平台 |
Windows x64 |
| 程序名称 |
Typora_1.0.4 |
| 使用工具 |
ida8.3 winhex Exeinfo asar Vscode Cyberchef |
| 程序架构 |
Electron |
| 系统版本 |
Windows 10 |
| 下载地址 |
https://typora.io/releases/all |
准备工作
1、软件下载:
2、软件架构分析:
- Typora采用的Electron架构进行开发的,如何看出来的?
- 下载安装好以后在软件的resources目录可以看一个app.asar,这个就是基于Electron构架开发的软件特征
3、准备node.js环境及其安装asar模块,如果环境都准备好了,可以使用如下命令查看版本信息:
node -v
asar -V
备注:这里不一定要跟作者的版本一个,能正常运行即可
静态分析
分析js文件
1、我们首先打开软件尝试进行激活,观察软件给我们反馈
- 从下面我们可以看到它是:(邮箱 + 序列号)的形式进行授权的
2、将app.asar进行解包,命令如下:
# 将app.asar 解压到source文件夹
asar e app.asar source
3、这里我们看到了有一个License.js文件,但是内容经过了Base64编码
4、尝试将它进行解码,发现都是乱码,看来文件是经过了加密
初探main.node
1、我们打开packages.json文件,查看配置信息:main字段所代表的就是程序所执行的入口文件
2、我们将它放到winhex中查看一下文件的16进制信息,发现它是4D 5A开头,是一个标准的PE模块
3、我们使用Exeinfo PE看一下main.node的基本信息,发现是64位的一个DLL文件
分析main.node
1、这里我们将main.node载入到ida中进行静态分析,分析之前设置一下文件的基址
- 依次点击:Edit->Segments->Rebase program
2、这里将基址设置成0,方便我们后续分析
3、我们之前看到js文件是经过了base64编码,那么程序在调用这些js文件的时候肯定会进行解码操作,所以我们就在IDA中搜索字符串base64相关的内容,具体如下图所示:
搜索字符串的方法:SHIFT + F12
4、然后我们在aBase64按x进行交叉引用来到此处,发现这里都调用了一个函数:napi_get_named_property来进行一些操作
- 翻译成python就是:
base64.b64decode(Buffer),是进行了一个base64解码操作
关于:napi_get_named_property 是 Node.js N-API 中的一个函数,用于从对象中获取指定名称的属性。
N-API 是 Node.js 提供的一个稳定的 API 层,用于构建本地插件。这个函数通常用于在 C/C++ 代码中与 JavaScript 对象进行交互。
分析加密算法
分析AES轮数
1、在上面我们分析js文件的时候知道,base64解码过后是乱码,文件过经过了加密,那么它在执行的时候一定会进行解密操作,接下来我们就要寻找解密算法
- 使用FindCrypt来分析程序用了哪些加密的算法,依次点击:Edit->Plugins->FindCrypt,发现了有AES加密的特征
2、这里我们双击最后一行(为什么呢?经过了反复验证最后一个是关键点)
3、同样来到这里按x交叉引用,找到调用处
4、补充:我们知道了程序使用了AES加密算法后,我们还需要知道它的一些参数:
5、首先来看我们交叉引用过来的地方,这里有一个do while循环,循环次数由变量v6控制,一共是13次
6、我们这里列一张表,记录我们得到的信息
| 属性 |
值 |
| 加密算法 |
AES |
| 加密模式 |
/ |
| 加密轮数 |
14 |
| 密钥 |
/ |
| 密钥长度 |
32 |
| IV |
/ |
分析AES模式
1、我们知道AES的轮数及密钥长度后继续往逆向分析,在函数开头继续交叉引用,看是谁调用了它
2、如下图所示:sub_EEC0函数就是刚刚上方我们分析AES轮数的地方,再看到它正文有一个异或操作
- 那么在这段代码中,一个循环里,有解密函数与异或操作的是什么加密模式呢,我们继续往下看
3、它就是AES CBC 模式:
4、记录一下我们得到的信息
| 属性 |
值 |
| 加密算法 |
AES |
| 加密模式 |
CBC |
| 加密轮数 |
14 |
| 密钥 |
/ |
| 密钥长度 |
32 |
| IV |
/ |
分析AES密钥
1、现在我们来分析AES的密钥在哪里,继续我在函数开头交叉引用找到上层调用的函数
2、继续往上
3、直到我们来到sub_3A8F调用这里,再往上翻,就是我们在最开始通过base64字符串定位到的地方,下方呢就是AES的解密操作
4、记录下这个偏移AD7C,此处调用的call sub_3A8F,为我们下面动态分析作准备
动态分析
配置环境
1、在ida中打开Typora.exe文件
2、设置为本地调试器:依次点击:Debugger->Select a debugger->Local Windows
3、设置调试选项:依次点击:Debugger -> Debugger options,勾选在载动态库时断下来,停下来的条件是:加载main.node模块时
get_event_id() == LIB_LOADED && strstr(get_event_module_name(), "main.node") != -1
分析密钥
1、接下来我们在ida中今天动态调试,F9运行程序,在加载main.node模块处时ida暂停了下来
2、定位到 AES解密函数处:7FF96C540000 + AD6A = 7FF96C54AD6A(模块基址 + 偏移 )
补充:在 Windows x64 架构中,使用的调用约定是Fastcall,其规则如下:
- 前 4 个参数通过寄存器传递(简称CD89):
- 第 1 个参数:
RCX
- 第 2 个参数:
RDX
- 第 3 个参数:
R8
- 第 4 个参数:
R9
- 其余参数通过栈传递(从右到左)。
- 调用者负责清理栈。
3、我们在CALL处下一个断点并让程序运行到此处
4、参数1:32字节的AES KEY
4E E1 B3 82 94 9A 02 4B 80 2F 52 B4 B4 FE 57 F1 BE F4 08 53 10 92 56 E2 C2 0D EC A3 DD 8D D5 6D
5、参数2:欲解密的数据,也就是base64解码后的atom.js,具体如下图所示:
疑问:
- 这里为什么是atom.js而不是其它的js呢?因为程序加载的顺序,是从小到大,这里就是从字母a开头的文件。
- 为什么解密的时候跳过了前16字节呢,前16个字节是干嘛的呢?这个问题就是接下来分析的重点
6、参数3:它就是atom.js base64解码后的字节大小:0x4030
7、现在AES KEY有了接下来就是寻找IV了。
分析IV
1、经过上访的分析我们知道它是AES CBC的解密模式,那么在还原明文最后一步的时候是将aes_decrypt的数据与IV进行异或
- 我们跟进
sub_E440函数进行查看,具体如下图所示:
2、我们在result变量处按tab来到反汇编视图,待会呢我们在这E4A0偏移处下一个断点,看看IV是什么
3、回到调试的IDA中观察rdi+rbx地址处的值是什么,会发现正是前面AES解密时跳过的那16字节,所以我们就可以知道,AES的IV就是文件前16字节
解密文件
1、我们将收集到的数据进行汇总
| 属性 |
值 |
| 加密算法 |
AES |
| 加密模式 |
CBC |
| 加密轮数 |
14 |
| 密钥 |
4E E1 B3 82 94 9A 02 4B 80 2F 52 B4 B4 FE 57 F1 BE F4 08 53 10 92 56 E2 C2 0D EC A3 DD 8D D5 6D |
| 密钥长度 |
32 |
| IV |
文件前16字节 |
2、我们在Cyberchef里面进行AES解密操作,可以看到成功的解密出正常的js代码来
编写脚本
1、编写一个pytho脚本自动化这个解密过程
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from base64 import b64decode, b64encode
from jsbeautifier import beautify
from jsmin import jsmin
from os import urandom
import argparse
import logging
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler() # 输出到控制台
]
)
# AES 密钥(32 字节,AES-256)
aes_key = bytes.fromhex('4E E1 B3 82 94 9A 02 4B 80 2F 52 B4 B4 FE 57 F1 BE F4 08 53 10 92 56 E2 C2 0D EC A3 DD 8D D5 6D')
def decrypt_script(b64: bytes) -> bytes:
"""解密脚本"""
try:
lCode = b64decode(b64)
aesIv = lCode[:16] # 前 16 字节为 IV
cipherText = lCode[16:] # 剩余部分为密文
# 使用 AES-256 CBC 模式解密
cipher = AES.new(key=aes_key, iv=aesIv, mode=AES.MODE_CBC)
decrypted = unpad(cipher.decrypt(cipherText), 16, 'pkcs7')
# 美化代码
return beautify(decrypted.decode()).encode()
except Exception as e:
logging.error(f"解密失败: {e}")
raise
def encrypt_script(code: bytes) -> bytes:
"""加密脚本"""
try:
# 压缩代码
code = jsmin(code.decode(), quote_chars="'\"`").encode()
# 生成随机的 IV
aesIv = urandom(16)
# 使用 AES-256 CBC 模式加密
cipher = AES.new(key=aes_key, iv=aesIv, mode=AES.MODE_CBC)
encrypted = aesIv + cipher.encrypt(pad(code, 16, 'pkcs7'))
# 返回 Base64 编码的结果
return b64encode(encrypted)
except Exception as e:
logging.error(f"加密失败: {e}")
raise
def process_file(input_file: str, output_file: str, mode: str):
"""处理文件(加密或解密)"""
try:
logging.info(f"开始处理文件: {input_file}")
with open(input_file, 'rb') as f:
data = f.read()
if mode == 'd':
result = decrypt_script(data)
elif mode == 'e':
result = encrypt_script(data)
else:
raise ValueError("无效的模式,请选择 'encrypt' 或 'decrypt'")
with open(output_file, 'wb') as f:
f.write(result)
logging.info(f"操作成功!结果已保存到 {output_file}")
except Exception as e:
logging.error(f"处理文件时出错: {e}")
def main():
"""主函数:解析命令行参数并执行操作"""
parser = argparse.ArgumentParser(description="AES-256 加密/解密工具")
parser.add_argument('mode', choices=['e', 'd'], help="选择模式:encrypt(加密)或 decrypt(解密)")
parser.add_argument('input_file', help="输入文件路径")
parser.add_argument('output_file', help="输出文件路径")
args = parser.parse_args()
process_file(args.input_file, args.output_file, args.mode)
if __name__ == '__main__':
main()
2、使用方法
解密:python typora.py d enc.js dec.js
加密:python typora.py e dec.js enc.js
3、解密License.js文件
python .\typora.py d .\source\License.js .\source\License_dec.js
补丁文件
<center><b>接下来我们对License.js进行Patch达到本地激活的目的</b><center>
1、第1处:搜索doActivation,修改如下:
return [!0, ""];
2、第2处:搜索unfillLicense,修改如下:
3、第3处:搜索onUnfillLicense,修改如下:
4、第4处:搜索hasLicense,修改如下:
5、修改完以后再重新加密回去
python .\typora.py e .\source\License_dec.js .\source\License.js
6、替换原app.asar文件
# 备份原来的文件
copy app.asar app.asar.old
# 将修改后的文件进行打包替换
asar p source app.asar
7、成功激活