diff --git a/auto-analysis/check_version.py b/auto-analysis/check_version.py new file mode 100644 index 0000000..c0c3375 --- /dev/null +++ b/auto-analysis/check_version.py @@ -0,0 +1,12 @@ +from utils import get_version, download_file, extract_file, log +import os + +if __name__ == '__main__': + DOWNLOAD_URL = "https://typora.io/windows/typora-setup-x64.exe" + BASE_DIR = os.path.join(os.path.dirname(__file__), "win/x64") + + download_path = os.path.join(BASE_DIR, os.path.basename(DOWNLOAD_URL)) + download_file(DOWNLOAD_URL, download_path) + extract_file(download_path, BASE_DIR) + version = get_version(BASE_DIR) + print(version+"") diff --git a/auto-analysis/linux/arm/LATEST_VERSION b/auto-analysis/linux/arm/LATEST_VERSION new file mode 100644 index 0000000..e69de29 diff --git a/auto-analysis/linux/x64/LATEST_VERSION b/auto-analysis/linux/x64/LATEST_VERSION new file mode 100644 index 0000000..e69de29 diff --git a/auto-analysis/patch.py b/auto-analysis/patch.py new file mode 100644 index 0000000..6ac3a26 --- /dev/null +++ b/auto-analysis/patch.py @@ -0,0 +1,12 @@ +# -*- coding:utf-8 -*- +""" +@Author: Mas0n +@File: patch.py +@Time: 2022/4/3 18:36 +@Desc: It's all about getting better. +""" +import utils + +if __name__ == '__main__': + utils.win_x64_run() + diff --git a/auto-analysis/utils.py b/auto-analysis/utils.py new file mode 100644 index 0000000..5007f6b --- /dev/null +++ b/auto-analysis/utils.py @@ -0,0 +1,94 @@ +# -*- coding:utf-8 -*- +""" +@Author: Mas0n +@File: utils.py +@Time: 2022/4/3 18:36 +@Desc: It's all about getting better. +""" +from loguru import logger as log +import subprocess +import json +import os + +# Usage: +# innoextract +# +BASE_DIR = os.path.dirname(__file__) + +DOWNLOAD_LINK = { + "win": { + "x86": "https://typora.io/windows/typora-setup-ia32.exe", + "x64": "https://typora.io/windows/typora-setup-x64.exe", + "arm": "https://typora.io/windows/typora-setup-arm64.exe", + }, + "linux": { + "x64": "https://download.typora.io/linux/Typora-linux-x64.tar.gz", + "arm": "https://download.typora.io/linux/Typora-linux-arm64.tar.gz", + }, +} + + +def get_version(to_path): + package_file_path = os.path.join(to_path, "app/resources/package.json") + package_info = open(package_file_path, "r").read() + package_obj = json.loads(package_info) + return package_obj["version"] + + +def download_file(from_link, to_path): + subprocess.check_call(["wget", from_link, "-O", to_path]) + + +def extract_file(from_path, to_path): + subprocess.check_call(["innoextract", from_path, "-d", to_path]) + + +def patch_file(_key, _iv, to_dir): + exports_file_path = os.path.join(BASE_DIR, "../exports.tar.gz") + save_dir = os.path.join(to_dir, "build") + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + subprocess.check_call(["tar", "-zxvf", exports_file_path, "-C", save_dir]) + patch_file_path = os.path.join(save_dir, "typora.py") + content = open(patch_file_path, "r").read() + content = content.replace("{AES_KEY}", f"b''.fromhex('{_key}')") + content = content.replace("{AES_IV}", f"b''.fromhex('{_iv}')") + open(patch_file_path, "w").write(content) + + +def win_x64_run(): + from win.x64 import analysis + basedir = os.path.join(BASE_DIR, "win/x64") + link = DOWNLOAD_LINK["win"]["x64"] + + download_path = os.path.join(basedir, os.path.basename(link)) + log.info(f"downloading from {link}") + download_file(link, download_path) + log.info("ready extract package") + + extract_file(download_path, basedir) + log.info("preparation stage completed") + main_node_path = os.path.join(basedir, "app/resources/app.asar.unpacked/main.node") + log.info("auto analysis start") + key, iv = analysis.get_aes_key_and_iv(main_node_path) + log.success("analysis done") + + patch_file(key.hex(), iv.hex(), basedir) + log.success("patch done") + + +if __name__ == '__main__': + win_x64_run() + + # hashString = open("LATEST_VERSION", "r").read() + # if hashString == "": + # log.info("not history for typora version") + # exit() + + # basedir = os.path.dirname(__file__) + # for h1 in DOWNLOAD_LINK.keys(): + # h1dir = os.path.join(basedir, h1) + # for h2 in DOWNLOAD_LINK.get(h1).keys(): + # h2dir = os.path.join(h1dir, h2) + # print(h2dir) diff --git a/auto-analysis/win/arm/LATEST_VERSION b/auto-analysis/win/arm/LATEST_VERSION new file mode 100644 index 0000000..e69de29 diff --git a/auto-analysis/win/x64/LATEST_VERSION b/auto-analysis/win/x64/LATEST_VERSION new file mode 100644 index 0000000..e69de29 diff --git a/auto-analysis/win/x64/analysis.py b/auto-analysis/win/x64/analysis.py new file mode 100644 index 0000000..2390071 --- /dev/null +++ b/auto-analysis/win/x64/analysis.py @@ -0,0 +1,37 @@ +# -*- coding:utf-8 -*- +""" +@Author: Mas0n +@Name: typora_win_x64_analysis +@Time: 2022/4/3 18:26 +@Desc: It's all about getting better. +""" +import struct +import r2pipe + + +def get_aes_key_and_iv(file_path): + r = r2pipe.open(file_path) + # auto analysis + r.cmd("aaa") + # string "base64" x-cross reference + regex = r.cmdj("axtj @@ str.base64") + assert len(regex) == 1 + + func = regex[0]["fcn_name"] + # disasm func + r.cmd(f"s {func}") + asm = r.cmdj("pdfj")['ops'] + assert len(asm) != 0 + + asm_regex = [] + for body in asm: + if "=[4]" in body["esil"] and body['type'] == 'mov': + opcode, value = body["disasm"].split(", ") + asm_regex.append({"opcode": opcode, "value": value}) + + assert len(asm_regex) == 12 + + iv = struct.pack("<4L", *[int(asm_regex[i]['value'], 16) for i in range(4)]) + key = struct.pack("<8L", *[int(asm_regex[i]['value'], 16) for i in range(4, 12)]) + # print(key, iv) + return key, iv \ No newline at end of file diff --git a/auto-analysis/win/x86/LATEST_VERSION b/auto-analysis/win/x86/LATEST_VERSION new file mode 100644 index 0000000..e69de29 diff --git a/exports.tar.gz b/exports.tar.gz new file mode 100644 index 0000000..3ac50b7 Binary files /dev/null and b/exports.tar.gz differ diff --git a/masar.py b/masar.py deleted file mode 100644 index 65f81d5..0000000 --- a/masar.py +++ /dev/null @@ -1,176 +0,0 @@ -# -*- coding:utf-8 -*- -""" -@Author: Mas0n -@File: masar.py -@Time: 2021-11-29 22:34 -@Desc: It's all about getting better. -""" -import os -import errno -import io -import struct -import shutil -import fileinput -import json - - -def round_up(i, m): - return (i + m - 1) & ~(m - 1) - - -class Asar: - def __init__(self, path, fp, header, base_offset): - self.path = path - self.fp = fp - self.header = header - self.base_offset = base_offset - - @classmethod - def open(cls, path): - fp = open(path, 'rb') - data_size, header_size, header_object_size, header_string_size = struct.unpack('<4I', fp.read(16)) - header_json = fp.read(header_string_size).decode('utf-8') - return cls( - path=path, - fp=fp, - header=json.loads(header_json), - base_offset=round_up(16 + header_string_size, 4) - ) - - @classmethod - def compress(cls, path): - offset = 0 - paths = [] - - def _path_to_dict(path): - nonlocal offset, paths - result = {'files': {}} - for f in os.scandir(path): - if os.path.isdir(f.path): - result['files'][f.name] = _path_to_dict(f.path) - elif f.is_symlink(): - result['files'][f.name] = { - 'link': os.path.realpath(f.name) - } - # modify - elif f.name == "main.node": - size = f.stat().st_size - result['files'][f.name] = { - 'size': size, - "unpacked": True - } - else: - paths.append(f.path) - size = f.stat().st_size - result['files'][f.name] = { - 'size': size, - 'offset': str(offset) - } - offset += size - return result - - def _paths_to_bytes(paths): - _bytes = io.BytesIO() - with fileinput.FileInput(files=paths, mode="rb") as f: - for i in f: - _bytes.write(i) - return _bytes.getvalue() - - header = _path_to_dict(path) - header_json = json.dumps(header, sort_keys=True, separators=(',', ':')).encode('utf-8') - header_string_size = len(header_json) - data_size = 4 - aligned_size = round_up(header_string_size, data_size) - header_size = aligned_size + 8 - header_object_size = aligned_size + data_size - diff = aligned_size - header_string_size - header_json = header_json + b'\0' * diff if diff else header_json - fp = io.BytesIO() - fp.write(struct.pack('<4I', data_size, header_size, header_object_size, header_string_size)) - fp.write(header_json) - fp.write(_paths_to_bytes(paths)) - - return cls( - path=path, - fp=fp, - header=header, - base_offset=round_up(16 + header_string_size, 4)) - - def _copy_unpacked_file(self, source, destination): - unpacked_dir = self.path + '.unpacked' - if not os.path.isdir(unpacked_dir): - print("Couldn't copy file {}, no extracted directory".format(source)) - return - - src = os.path.join(unpacked_dir, source) - if not os.path.exists(src): - print("Couldn't copy file {}, doesn't exist".format(src)) - return - - dest = os.path.join(destination, source) - shutil.copyfile(src, dest) - - def _extract_file(self, source, info, destination): - if 'offset' not in info: - self._copy_unpacked_file(source, destination) - return - - self.fp.seek(self.base_offset + int(info['offset'])) - r = self.fp.read(int(info['size'])) - - dest = os.path.join(destination, source) - with open(dest, 'wb') as f: - f.write(r) - - def _extract_link(self, source, link, destination): - dest_filename = os.path.normpath(os.path.join(destination, source)) - link_src_path = os.path.dirname(os.path.join(destination, link)) - link_to = os.path.join(link_src_path, os.path.basename(link)) - - try: - os.symlink(link_to, dest_filename) - except OSError as e: - if e.errno == errno.EXIST: - os.unlink(dest_filename) - os.symlink(link_to, dest_filename) - else: - raise e - - def _extract_directory(self, source, files, destination): - dest = os.path.normpath(os.path.join(destination, source)) - - if not os.path.exists(dest): - os.makedirs(dest) - - for name, info in files.items(): - item_path = os.path.join(source, name) - - if 'files' in info: - self._extract_directory(item_path, info['files'], destination) - elif 'link' in info: - self._extract_link(item_path, info['link'], destination) - else: - self._extract_file(item_path, info, destination) - - def extract(self, path): - if not os.path.isdir(path): - raise NotADirectoryError() - self._extract_directory('.', self.header['files'], path) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.fp.close() - - -def pack_asar(source, dest): - with Asar.compress(source) as a: - with open(dest, 'wb') as fp: - a.fp.seek(0) - fp.write(a.fp.read()) - - -def extract_asar(source, dest): - with Asar.open(source) as a: - a.extract(dest) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 68ad2b6..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -jsbeautifier==1.14.0 -jsmin==3.0.0 -loguru==0.5.3 -pycryptodome==3.11.0 diff --git a/typora.py b/typora.py deleted file mode 100644 index b9f22de..0000000 --- a/typora.py +++ /dev/null @@ -1,182 +0,0 @@ -# -*- coding:utf-8 -*- -""" -@Author: Mas0n -@File: typora.py -@Time: 2021-11-29 21:24 -@Desc: It's all about getting better. -""" -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 listdir, urandom, makedirs -from os.path import isfile, isdir, join as pjoin, split as psplit, exists, abspath -from loguru import logger as log -from masar import extract_asar, pack_asar -from shutil import rmtree -from argparse import ArgumentParser -import struct -import sys - -# DEBUG -DEBUG = False - -log.remove() -if DEBUG: - log.add(sys.stderr, level="DEBUG") -else: - log.add(sys.stderr, level="INFO") - -AES_KEY = struct.pack("<4Q", *[0x4B029A9482B3E14E, 0xF157FEB4B4522F80, 0xE25692105308F4BE, 0x6DD58DDDA3EC0DC2]) - - -def _mkDir(_path): - if not exists(_path): - makedirs(_path) - else: - if _path == psplit(__file__)[0]: - log.warning("plz try not to use the root dir.") - else: - log.warning(f"May FolderExists: {_path}") - - -def decScript(b64: bytes, prettify: bool): - lCode = b64decode(b64) - # iv: the first 16 bytes of the file - aesIv = lCode[0:16] - # cipher text - cipherText = lCode[16:] - # AES 256 CBC - ins = AES.new(key=AES_KEY, iv=aesIv, mode=AES.MODE_CBC) - code = unpad(ins.decrypt(cipherText), 16, 'pkcs7') - if prettify: - code = beautify(code.decode()).encode() - return code - - -def extractWdec(asarPath, path, prettify): - """ - :param prettify: bool - :param asarPath: asar out dir - :param path: out dir - :return: None - """ - # try to create empty dir to save extract files - path = pjoin(path, "typoraCrackerTemp") - - if exists(path): - rmtree(path) - _mkDir(path) - - log.info(f"extract asar file: {asarPath}") - # extract app.asar to {path}/* - extract_asar(asarPath, path) - log.success(f"extract ended.") - - log.info(f"read Directory: {path}") - # construct the save directory {pathRoot}/dec_app - outPath = pjoin(psplit(path)[0], "dec_app") - # try to create empty dir to save decryption files - if exists(outPath): - rmtree(outPath) - _mkDir(outPath) - - log.info(f"set Directory: {outPath}") - # enumerate extract files - fileArr = listdir(path) - for name in fileArr: - # read files content - fpath = pjoin(path, name) - scode = open(fpath, "rb").read() - log.info(f"open file: {name}") - # if file suffix is *.js then decryption file - if isfile(fpath) and name.endswith(".js"): - scode = decScript(scode, prettify) - else: - log.debug(f"skip file: {name}") - # save content {outPath}/{name} - open(pjoin(outPath, name), "wb").write(scode) - log.success(f"decrypt and save file: {name}") - - rmtree(path) - log.debug("remove temp dir") - - -def encScript(_code: bytes, compress): - if compress: - _code = jsmin(_code.decode(), quote_chars="'\"`").encode() - aesIv = urandom(16) - cipherText = _code - ins = AES.new(key=AES_KEY, iv=aesIv, mode=AES.MODE_CBC) - enc = aesIv + ins.encrypt(pad(cipherText, 16, 'pkcs7')) - lCode = b64encode(enc) - return lCode - - -def packWenc(path, outPath, compress): - """ - :param path: out dir - :param outPath: pack path app.asar - :param compress: Bool - :return: None - """ - # check out path - if isfile(outPath): - log.error("plz input Directory for app.asar") - raise NotADirectoryError - - _mkDir(outPath) - - encFilePath = pjoin(psplit(outPath)[0], "typoraCrackerTemp") - if exists(encFilePath): - rmtree(encFilePath) - _mkDir(encFilePath) - - outFilePath = pjoin(outPath, "app.asar") - log.info(f"set outFilePath: {outFilePath}") - fileArr = listdir(path) - - for name in fileArr: - fpath = pjoin(path, name) - if isdir(fpath): - log.error("TODO: found folder") - raise IsADirectoryError - - scode = open(fpath, "rb").read() - log.info(f"open file: {name}") - if isfile(fpath) and name.endswith(".js"): - scode = encScript(scode, compress) - - open(pjoin(encFilePath, name), "wb").write(scode) - log.success(f"encrypt and save file: {name}") - - log.info("ready to pack") - pack_asar(encFilePath, outFilePath) - log.success("pack done") - - rmtree(encFilePath) - log.debug("remove temp dir") - - -def main(): - argParser = ArgumentParser( - description="[extract and decryption / pack and encryption] app.asar file from [Typora].", - epilog="If you have any questions, please contact [ MasonShi@88.com ]") - argParser.add_argument("asarPath", type=str, help="app.asar file path/dir [input/ouput]") - argParser.add_argument("dirPath", type=str, help="as tmp and out directory.") - - argParser.add_argument('-u', dest='mode', action='store_const', - const=packWenc, default=extractWdec, - help='pack & encryption (default: extract & decryption)') - argParser.add_argument('-f', dest='format', action='store_const', - const=True, default=False, - help='enabled prettify/compress (default: disabled)') - args = argParser.parse_args() - - args.mode(args.asarPath, args.dirPath, args.format) - log.success("Done!") - - -if __name__ == '__main__': - main()