吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3771|回复: 30
收起左侧

[Python 原创] 【原创】记账软件随手记备份文件解密

  [复制链接]
tp522022 发表于 2023-9-13 14:50
随手记是金蝶公司出品的一款记账软件,个人比较喜欢其中的记账功能,投资模块我是一点兴趣没有,为了减少广告影响我用华为自带的手机管家ban掉了联网权限,为了支付宝和微信的账单可以正常导入到随手记中,对随手记的备份还原功能进行了一波逆向分析,成功实现了账单数据的写入,还原后可在随手记App中正常查阅。

个人使用的用于分析的Android版本号12.69.0.0.

随手记备份出来的kbf文件本质上是个zip文件,随手记备份时将账本数据mymoney.sqlite和用户信息数据backup_info打包为本质为zip的kbf文件.当然这两个文件都有一定程度的魔改backup_info这个不用管到时候原封不动打包回去就行,主要是mymoney.sqlite这个文件的解密。
使用以下Python代码可正常解密解压kbf文件得到sqlite数据库文件

[Python] 纯文本查看 复制代码
def unzip_kbf(input_files=None):
    global tp_kbf_file_name
    if input_files is None:
        if global_constant.full_update or global_constant.sdcard_root_path is None:
            input_file_abs_path = os.path.join(tp_path.parent.parent, 'f_input/')
        else:
            input_file_abs_path = os.path.join(global_constant.sdcard_root_path, '.mymoney', 'backup/')
    else:
        if global_constant.full_update or global_constant.sdcard_root_path is None:
            input_file_abs_path = os.path.join(tp_path.parent.parent, 'f_input/', input_files)
        else:
            input_file_abs_path = os.path.join(global_constant.sdcard_root_path, '.mymoney', 'backup/', input_files)
    if not os.path.exists(input_file_abs_path):
        print(f'{input_file_abs_path}不存在')
        raise FileNotFoundError(f'输入文件kbf不存在-->{input_file_abs_path}')
    print(f'输入待解压kbf文件路径:{input_file_abs_path}')
    if os.path.isdir(input_file_abs_path):
        files = os.listdir(input_file_abs_path)
        for f in files:
            if f.endswith('.kbf'):
                global tp_kbf_file_name
                tp_kbf_file_name = f
                zf = zipfile.ZipFile(os.path.join(input_file_abs_path, f))
                zf.extractall(path=os.path.join(tp_path.parent.parent, 'f_input/'))
                break
    elif os.path.isfile(input_file_abs_path):
        if input_files.lower().endswith('.kbf'):
            tp_kbf_file_name = input_files
            zf = zipfile.ZipFile(input_file_abs_path)
            zf.extractall(path=os.path.join(tp_path.parent.parent, 'f_input/'))
        pass


def ssj_kbf_sqlite_convert(input_file=None, output_file=None, convert=ConvertType.Exchanged):
    """
    convert ssj data, after kbf unzip to sqlite,convert it to normal sqlite database file
    :param convert: 0 means convert it auto, 1 kbf format to sqlite, 2 sqlite to kbf format
    :param input_file: the mymoney.sqlite file path
    :param output_file: the convert mymoney.sqlite file path
    :return:
    """
    if input_file is None:
        input_file = os.path.join(tp_path.parent.parent, 'f_input/mymoney.sqlite')
    if output_file is None:
        output_file = os.path.join(tp_path.parent.parent, 'f_output/mymoney.sqlite')
    sqlite_header = (0x53, 0x51, 0x4C, 0x69,
                     0x74, 0x65, 0x20, 0x66,
                     0x6F, 0x72, 0x6D, 0x61,
                     0x74, 0x20, 0x33, 0x0)
    kbf_header = (0x0, 0x0, 0x0, 0x0,
                  0x0, 0x0, 0x0, 0x0,
                  0x0, 0x0, 0x0, 0x0,
                  0x0, 0x46, 0xFF, 0x0)
    if global_constant.print_repeat_data_info:
        read_file_header(input_file)
    if os.path.exists(output_file):
        os.remove(output_file)
    with open(input_file, mode='rb') as f:
        with open(output_file, mode='wb') as fw:
            data_buffer = f.read()
            if data_buffer[0] == 0x53:
                kbf2sqlite = False
                print("当前为SQLite文件格式")
            if data_buffer[0] == 0x00:
                kbf2sqlite = True
                print("当前为KBF文件格式")
            write_buffer = bytearray(data_buffer)
            index = 0
            while index < len(kbf_header) and index < len(sqlite_header):
                if convert == ConvertType.Exchanged:
                    if kbf2sqlite is True:
                        write_buffer[index] = sqlite_header[index]
                    else:
                        write_buffer[index] = kbf_header[index]
                elif convert == ConvertType.SqliteToKbf and not kbf2sqlite:
                    write_buffer[index] = kbf_header[index]
                elif convert == ConvertType.KbfToSqlite and kbf2sqlite:
                    write_buffer[index] = sqlite_header[index]

                index = index + 1
            fw.write(write_buffer)
            pass
    if global_constant.print_repeat_data_info:
        read_file_header(output_file)
    pass


以上代码先将正常解压kbf文件得到mymoney.sqlite,再修改前16个字节
[Python] 纯文本查看 复制代码
    sqlite_header = (0x53, 0x51, 0x4C, 0x69,
                     0x74, 0x65, 0x20, 0x66,
                     0x6F, 0x72, 0x6D, 0x61,
                     0x74, 0x20, 0x33, 0x0)
    kbf_header = (0x0, 0x0, 0x0, 0x0,
                  0x0, 0x0, 0x0, 0x0,
                  0x0, 0x0, 0x0, 0x0,
                  0x0, 0x46, 0xFF, 0x0)


此时得到就是正常可以写入数据的sqlite数据库了。

下面再说随手记中的数据库结构
1. t_transaction 记录了所有交易信息,主要为支出,收入,转账三类
2. t_account 记录账户信息譬如银行卡,支付宝,微信支付等都属于资金流入流出的账户
3. t_category 记录的支出,收入的分类信息

以下是t_transaction的表结构


[SQL] 纯文本查看 复制代码
CREATE TABLE "t_transaction" (
	"transactionPOID"	LONG NOT NULL,
	"createdTime"	LONG NOT NULL,
	"modifiedTime"	LONG NOT NULL,
	"tradeTime"	LONG NOT NULL,
	"memo"	varchar(100),
	"type"	integer NOT NULL,
	"creatorTradingEntityPOID"	LONG,
	"modifierTradingEntityPOID"	LONG,
	"buyerAccountPOID"	LONG,
	"buyerCategoryPOID"	LONG DEFAULT 0,
	"buyerMoney"	decimal(12 , 2),
	"sellerAccountPOID"	LONG,
	"sellerCategoryPOID"	LONG DEFAULT 0,
	"sellerMoney"	decimal(12 , 2),
	"lastUpdateTime"	LONG,
	"photoName"	VARCHAR(100),
	"photoNeedUpload"	integer DEFAULT 0,
	"relation"	varchar(200) DEFAULT '',
	"relationUnitPOID"	LONG,
	"ffrom"	varchar(250) DEFAULT '',
	"clientID"	LONG DEFAULT 0,
	"FSourceKey"	varchar(100) DEFAULT NULL,
	"photos"	TEXT,
	"transaction_number"	TEXT,
	"merchant_order_number"	TEXT,
	"import_data_source"	TEXT,
	PRIMARY KEY("transactionPOID")
);


需要注意的是
支出时 buyerAccountID 为支付账号ID buyerCategory留空 sellerAccountID留空 sellerCategory为具体支出类别 type =0
收入时 buyerAccountID 留空 buyerCategory为收入类别 sellerAccountID为账户ID sellerCategory留空 type = 1
转账时 buyerAccountID为资金来源方 sellerAccountID为资金流向方 category均为空,而且会插入两条记录一个type=2一个type=3,这两条记录relation应该一致而FSourceKey不同,否则还原数据时可能导致app闪退

通过支付宝App的账单开具个人流水记录可以导出支付宝账单,同理可导出微信账单。两者都是csv文件自己解析下即可。

最后正常写入数据后,调用上面的ssj_kbf_sqlite_convert函数将sqlite数据库转化为kbf需要的格式,最后导入手机上随手记的备份目录,通过随手记的还原功能可以导入数据

免费评分

参与人数 8吾爱币 +15 热心值 +8 收起 理由
a2633063 + 1 + 1 谢谢@Thanks!
bjhjhcrc + 1 + 1 用心讨论,共获提升!
bamao666 + 1 + 1 谢谢@Thanks!
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
hrh123 + 2 + 1 用心讨论,共获提升!
11lxm + 1 + 1 谢谢@Thanks!
a731062834 + 1 + 1 用心讨论,共获提升!
helian147 + 1 + 1 热心回复!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| tp522022 发表于 2024-2-6 08:13
meteornk 发表于 2024-2-3 14:04
大佬,ConvertType.Exchanged报错。
另外,这个是在window下运行,还是要在手机安卓环境下运行?
是不是 ...

不是  ConvertType是我定义的class。
[Python] 纯文本查看 复制代码
class TradeType(int, Enum):
    Expense = 0
    Income = 1
    Transfer_Step_One = 3
    Transfer_Step_Two = 2


class ConvertType(int, Enum):
    Exchanged = 0
    KbfToSqlite = 1
    SqliteToKbf = 2

 楼主| tp522022 发表于 2023-9-15 08:16
Jun01 发表于 2023-9-14 23:17
大佬厉害。
用了好多年随手记,只喜欢用本地功能,也禁止它联网,一直找kbf文件解析密的方法,可 ...

为了导入方便,我装了QPython,直接在手机上运行脚本,贴出来的这一部分脚本就是为了兼容电脑和手机才有各种路径判断的。现在唯一的问题就是pdd订单没有商品详情 目前还在考虑怎么用油猴脚本导出pdd订单的详情信息
bdzwater 发表于 2023-9-13 15:04
感谢,随手记用了好多年了,不过我一般都是直接用网页端的接口,拉取所有数据到本地做备份,外加一些更加丰富的图表统计等
 楼主| tp522022 发表于 2023-9-13 15:12
bdzwater 发表于 2023-9-13 15:04
感谢,随手记用了好多年了,不过我一般都是直接用网页端的接口,拉取所有数据到本地做备份,外加一些更加丰富的 ...

需求不一样,我是一点网络也不想他连,而且通过随手记内置的支付宝,微信账单导入功能导入时经常出现分类不准的情况
13121039687 发表于 2023-9-13 15:14
生存手册时长
vivvon 发表于 2023-9-13 15:15
楼主厉害!
penguin520 发表于 2023-9-13 15:20
这个很实用,感谢分享
dubuqingyun 发表于 2023-9-13 15:29
厉害,感谢分享!
wan23 发表于 2023-9-13 15:31
把kbf文件当zip压缩文件处理
id3 发表于 2023-9-13 15:31
金蝶是用习惯了,从逆向来了解软件,棒
fenggod1 发表于 2023-9-13 17:32
大佬厉害
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-29 13:23

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表