吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 6185|回复: 46
收起左侧

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

  [复制链接]
tp522022 发表于 2023-9-13 14:50
本帖最后由 tp522022 于 2024-9-23 16:49 编辑

随手记是金蝶公司出品的一款记账软件,个人比较喜欢其中的记账功能和综合展示的图标,其他模块啥兴趣没有还一堆的广告,为此我用华为自带的手机管家ban掉了联网权限,为了支付宝和微信的账单可以正常导入到随手记中,对随手记的备份还原功能进行了一波逆向分析,成功实现了账单数据的写入,还原后可在随手记App中正常查阅。

个人使用的用于分析的Android版本号12.69.0.0.
先说结论随手记备份出来的kbf文件其实就是zip文件,解压后可以得到两个文件mymoney.sqlite和backup_info,其中backup_info包含账户的头像,昵称等信息,mymoney.sqlite这个文件才是账单信息的所在,不过这个文件是被魔改过的,前16个字节被替换过,因此无法直接用数据库管理软件直接打开,只要将前16个字节还原成回去即可正常打开。
文件头的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)


随手记备份出来的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需要的格式,最后导入手机上随手记的备份目录,通过随手记的还原功能可以导入数据

免费评分

参与人数 11吾爱币 +18 热心值 +11 收起 理由
xiaoyu2032 + 2 + 1 感谢,换手机后原来的随后记新手机安装不了,数据也导不出来,这个真不错!
lovecrt + 1 + 1 谢谢@Thanks!
bjjette + 1 谢谢@Thanks!
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 热心回复!

查看全部评分

本帖被以下淘专辑推荐:

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

雪山凌狐 发表于 2024-5-18 14:49

来,咱们重新补充一下,全部转为python代码后的代码:

解压解密代码:

import zipfile
import os
def unzip_kbf(input_files=None):
    zf = zipfile.ZipFile(input_files)
    zf.extractall(path='./')

def ssj_kbf_sqlite_convert(input_file, output_file):
    """
    convert ssj data, after kbf unzip to sqlite,convert it to normal sqlite database file
    :param input_file: the mymoney.sqlite file path
    :param output_file: the convert mymoney.sqlite file path
    :return:
    """
    sqlite_header = (0x53, 0x51, 0x4C, 0x69,
                     0x74, 0x65, 0x20, 0x66,
                     0x6F, 0x72, 0x6D, 0x61,
                     0x74, 0x20, 0x33, 0x0)
    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()
            write_buffer = bytearray(data_buffer)
            index = 0
            while index < len(sqlite_header):
                write_buffer[index] = sqlite_header[index]
                index = index + 1
            fw.write(write_buffer)
    print("convert done")

# 执行kbf文件解密
unzip_kbf("record.kbf")
ssj_kbf_sqlite_convert("mymoney.sqlite", "record_decrypt.sqlite")

转为一木记账可用的导入的模板的代码:

需要安装三方库:

pip install pandas xlwt

代码:

import sqlite3
import pandas as pd
import xlwt

# 连接到SQLite数据库,如果数据库文件不存在,会自动在当前目录创建一个
# 数据库文件名为example.db
conn = sqlite3.connect('record_decrypt.sqlite')

# 创建一个游标对象,用于执行SQL语句
cursor = conn.cursor()

sql = '''SELECT strftime('%Y-%m-%d %H:%M', a.tradeTime / 1000 + 8 * 3600, 'unixepoch') as 日期,
 case
   when a.type = 1 then '收入'
     when a.type = 0 then '支出'
 end as 收支类型,
 case
   when a.type = 1 then (select case 
                             when (select b.currencyType from t_account b where b.accountPOID = a.sellerAccountPOID) = 'CNY' then a.buyerMoney
                                                     else (select round(a.buyerMoney * d.rate, 2) from 
                                                                    (select b.currencyType from t_account b where b.accountPOID = a.sellerAccountPOID) c, 
                                                                    t_exchange d where c.currencyType = d.sell)
                                                     end)
   when a.type = 0 then (select case 
                             when (select b.currencyType from t_account b where b.accountPOID = a.buyerAccountPOID) = 'CNY' then a.buyerMoney
                                                     else (select round(a.buyerMoney * d.rate, 2) from 
                                                                    (select b.currencyType from t_account b where b.accountPOID = a.buyerAccountPOID) c, 
                                                                    t_exchange d where c.currencyType = d.sell)
                                                     end)
 end as 金额,
 case 
   when a.type = 1 then (select d.name from (select b.parentCategoryPOID from t_category b 
                              where b.categoryPOID = a.buyerCategoryPOID) c, t_category d
                                                            where c.parentCategoryPOID = d.categoryPOID)
     when a.type = 0 then (select d.name from (select b.parentCategoryPOID from t_category b 
                              where b.categoryPOID = a.sellerCategoryPOID) c, t_category d
                                                            where c.parentCategoryPOID = d.categoryPOID)
 end as 类别,
 case 
   when a.type = 1 then (select b.name from t_category b where b.categoryPOID = a.buyerCategoryPOID)
     when a.type = 0 then (select b.name from t_category b where b.categoryPOID = a.sellerCategoryPOID)
 end as 子类,
 '日常账本' as 所属账本,
 case 
   when a.type = 1 then (select b.name from t_account b where b.accountPOID = a.sellerAccountPOID)
     when a.type = 0 then (select b.name from t_account b where b.accountPOID = a.buyerAccountPOID)
 end as 收支账户,
 a.memo as 备注,
 (select c.name 
     from t_transaction_projectcategory_map b, t_tag c 
     where b.transactionPOID = a.transactionPOID 
     and b.projectCategoryPOID = c.tagPOID
     and b.type = 2) as 标签,
 '' as 地址
 FROM t_transaction a
 order by a.tradetime desc;'''

# 运行一个查询语句
select_sql = sql

# 使用游标执行查询
cursor.execute(select_sql)

# 获取所有查询结果
results = cursor.fetchall()

# 打印结果
for row in results:
    print(row)

# 将结果转换为DataFrame
columns = [column[0] for column in cursor.description]  # 获取列名
df = pd.DataFrame(results, columns=columns)

# 关闭游标和连接
cursor.close()
conn.close()

# 使用xlwt库将DataFrame写入Excel文件
excel_filename = '一木记账导入.xls'

# 创建一个Excel工作簿
wb = xlwt.Workbook()
# 添加一个工作表
ws = wb.add_sheet('Sheet 1')

# 将DataFrame数据写入Excel工作表
for col_num, col_data in enumerate(df.columns):
    ws.write(0, col_num, col_data)  # 写入列名

for row_num, row_data in enumerate(df.values):
    for col_num, col_data in enumerate(row_data):
        ws.write(row_num + 1, col_num, col_data)  # 写入单元格数据

# 保存Excel文件
wb.save(excel_filename)

print(f'数据已成功写入到 {excel_filename}')

效果如下:




Screenshot_2024-05-18_14-45-32.png

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
lovecrt + 1 + 1 谢谢@Thanks!

查看全部评分

雪山凌狐 发表于 2024-5-15 23:38
雪山凌狐 发表于 2024-5-15 21:37
经过仔细测试楼主的代码缺少了很多信息,没有办法直接执行成功,而用到这个的肯定是解密而不是加密,这让我 ...

我主要的目的是导出随手记的账目,换一木记账,全网搜索了很久都没有找到合适的手段,感觉只能充值随手记的会员实现,一个月要14块啊,太贵了不划算。

一木记账的导入模板如下:
image.png

于是按照模板写了对应的sqlite取数语句,大家可以在相关工具如navicat执行导出即可,目前测试可用。
SELECT strftime('%Y-%m-%d', a.tradeTime / 1000, 'unixepoch') as 日期,
 case
   when a.type = 1 then '收入'
     when a.type = 0 then '支出'
 end as 收支类型,
 case
   when a.type = 1 then (select case 
                             when (select b.currencyType from t_account b where b.accountPOID = a.sellerAccountPOID) = 'CNY' then a.buyerMoney
                                                     else (select round(a.buyerMoney * d.rate, 2) from 
                                                                    (select b.currencyType from t_account b where b.accountPOID = a.sellerAccountPOID) c, 
                                                                    t_exchange d where c.currencyType = d.sell)
                                                     end)
   when a.type = 0 then (select case 
                             when (select b.currencyType from t_account b where b.accountPOID = a.buyerAccountPOID) = 'CNY' then a.buyerMoney
                                                     else (select round(a.buyerMoney * d.rate, 2) from 
                                                                    (select b.currencyType from t_account b where b.accountPOID = a.buyerAccountPOID) c, 
                                                                    t_exchange d where c.currencyType = d.sell)
                                                     end)
 end as 金额,
 case 
   when a.type = 1 then (select d.name from (select b.parentCategoryPOID from t_category b 
                              where b.categoryPOID = a.buyerCategoryPOID) c, t_category d
                                                            where c.parentCategoryPOID = d.categoryPOID)
     when a.type = 0 then (select d.name from (select b.parentCategoryPOID from t_category b 
                              where b.categoryPOID = a.sellerCategoryPOID) c, t_category d
                                                            where c.parentCategoryPOID = d.categoryPOID)
 end as 类别,
 case 
   when a.type = 1 then (select b.name from t_category b where b.categoryPOID = a.buyerCategoryPOID)
     when a.type = 0 then (select b.name from t_category b where b.categoryPOID = a.sellerCategoryPOID)
 end as 子类,
 '日常账本' as 所属账本,
 case 
   when a.type = 1 then (select b.name from t_account b where b.accountPOID = a.sellerAccountPOID)
     when a.type = 0 then (select b.name from t_account b where b.accountPOID = a.buyerAccountPOID)
 end as 收支账户,
 a.memo as 备注,
 (select c.name 
     from t_transaction_projectcategory_map b, t_tag c 
     where b.transactionPOID = a.transactionPOID 
     and b.projectCategoryPOID = c.tagPOID
     and b.type = 1) as 标签,
 '' as 地址
 FROM t_transaction a
 order by a.tradetime desc;


同时我还想知道我当时随手记的这一笔是谁花费的,是我还是我爱人还是我爹娘,就是增加一个成员字段的语句为:(给自己看着用)

-- 增加一个成员字段取出,可以看到这比消费是谁消费的
SELECT strftime('%Y-%m-%d', a.tradeTime / 1000, 'unixepoch') as 日期,
 case
   when a.type = 1 then '收入'
     when a.type = 0 then '支出'
 end as 收支类型,
 case
   when a.type = 1 then (select case 
                             when (select b.currencyType from t_account b where b.accountPOID = a.sellerAccountPOID) = 'CNY' then a.buyerMoney
                                                     else (select round(a.buyerMoney * d.rate, 2) from 
                                                                    (select b.currencyType from t_account b where b.accountPOID = a.sellerAccountPOID) c, 
                                                                    t_exchange d where c.currencyType = d.sell)
                                                     end)
   when a.type = 0 then (select case 
                             when (select b.currencyType from t_account b where b.accountPOID = a.buyerAccountPOID) = 'CNY' then a.buyerMoney
                                                     else (select round(a.buyerMoney * d.rate, 2) from 
                                                                    (select b.currencyType from t_account b where b.accountPOID = a.buyerAccountPOID) c, 
                                                                    t_exchange d where c.currencyType = d.sell)
                                                     end)
 end as 金额,
 case 
   when a.type = 1 then (select d.name from (select b.parentCategoryPOID from t_category b 
                              where b.categoryPOID = a.buyerCategoryPOID) c, t_category d
                                                            where c.parentCategoryPOID = d.categoryPOID)
     when a.type = 0 then (select d.name from (select b.parentCategoryPOID from t_category b 
                              where b.categoryPOID = a.sellerCategoryPOID) c, t_category d
                                                            where c.parentCategoryPOID = d.categoryPOID)
 end as 类别,
 case 
   when a.type = 1 then (select b.name from t_category b where b.categoryPOID = a.buyerCategoryPOID)
     when a.type = 0 then (select b.name from t_category b where b.categoryPOID = a.sellerCategoryPOID)
 end as 子类,
 '日常账本' as 所属账本,
 case 
   when a.type = 1 then (select b.name from t_account b where b.accountPOID = a.sellerAccountPOID)
     when a.type = 0 then (select b.name from t_account b where b.accountPOID = a.buyerAccountPOID)
 end as 收支账户,
 a.memo as 备注,
 (select c.name 
     from t_transaction_projectcategory_map b, t_tag c 
     where b.transactionPOID = a.transactionPOID 
     and b.projectCategoryPOID = c.tagPOID
     and b.type = 1) as 标签,
 '' as 地址,
 (select c.name 
     from t_transaction_projectcategory_map b, t_tag c 
     where b.transactionPOID = a.transactionPOID 
     and b.projectCategoryPOID = c.tagPOID
     and b.type = 2) as 成员
 FROM t_transaction a
 order by a.tradetime desc;



有空我再整理个详细教程了,太晚了,睡觉!
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-11-24 11:56

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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