-
-
[原创]MMKV文件解析及实现
-
发表于: 2022-12-1 13:02 23543
-
移动端在逆向登录协议时,在使用账号 密码登录成功后,接口(/appMobileLogin )一般都会返回一个 token,以后再次打开 app 时,凭借采用账号密码登录返回的 token,就可以无需使用账号密码登录了,此时登录接口地址改为(/login)。app 正常启动后,一般所有的业务接口都会携带此 token 进行访问。
这个token 在下次使用账号密码登录之前,一般都不会改变,那么这个字段必然会save 到磁盘的某个文件中。
当然这个token 的破解可能简单,也可能很困难。不论简单与困难,都需要你再次打开动态调试 app,你会进行下面一系列操作:
打开 pc(虚拟机)
adb connect
启动 frida-server
找到之前的破解日志(hook 了哪些函数)
编写 hook.js
启动 app
frida -l hook.js package_name
jadx 开发 package_name
找到算法函数(可能调用栈很多)
经过不懈努力终于找到 token,可能花费半小时甚至更久
重大喜讯:现在这个过程可以简化了
移动端一般都会采用MMKV来做序列化及持久化,这里不分析 MMKV 的优势和劣势,我们又不搞工程,分析它干嘛~
我们只是要里面的数据。如果 app 使用 mmkv,那么通常会把 token 存在 mmkv.default里,当然这个文件还会存其他关键数据,你需要知道token 的 key 是什么名称即可。
mmkv原理可以参考这篇文章: mmkv原理
这篇文章在协议图里有个问题,如图:
在总长度与 第一个key 长度之间还有一个 无用的 key 长度,这里贴上源码:
第7行这里会多读一次 key len,然后直接跳过。这段代码的含义是:如果 position为0是,直接跳过第一个 ken_len
源码并未给出原因,可能是保留字段吧(通常在设计协议的时候,都会有保留字段),有懂的大神请指正。
就是因为第一个 key_len,我老是解析不对 mmkv.default, 逼不得已开始读源码~
源码链接:mmkv 源码
下面给出两种读取 mmkv.default的方式:
官方版本源码安装: mmkv python 安装
另外就是自实现的简略版本
两个版本的对比:
官方版本需要编译,需要把 编译生成的 so 文件放到 python 的 sitepackages 里才能使用 impor mmkv
官方版本完全加载 mmkv依赖的环境,比如 mmap 映射,文件锁啥的,比如 windows需要更复杂的安装
另外在加载的时候还需要crc 校验
有兴趣的同学可以尝试编译官方版本
简略版本只需要有 python 即可,
问:为啥不需要 crc 校验?
答:app 在使用的文件肯定是校验后的呀,拿来即用即可!
当你使用简略版本时, 只需要 adb pull /data/data/com.packagename/file/mmkv/mmkv.default . 到本地即可
然后
mr = MMKVReader('./mmkv.default')
mr.get_data()
token = mr.d['token_key'] # 这里的token_key,改为你需要的即可
想象一下这个场景:
你需要大规模利用时, 你自己一个账号不足时,只需要别人把 mmkv.default发给你,你就可以用他的账号登录并做爱做的事了,岂不美哉?
其实这个文件里除了 token ,可能登录环境也有哦~,用起来更丝滑哦~
题外话:python 没有int8类型,需要使用 numpy.int8
'''see a48K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6f1k6h3&6U0k6h3&6@1i4K6u0r3e0f1#2w2g2W2)9J5c8Y4N6A6K9$3W2Q4x3V1k6H3P5i4c8Z5L8$3&6Q4y4h3k6K6k6i4c8#2M7l9`.`.build for pythoncp mmkv.cpython-38-darwin.so /PYTHONPATH/sitepackages/这样就可以使用 import mmkv'''import numpy as np
import mmkv
ed = 'little'
class MMKVReader():
def __init__(self, f_path) -> None:
self.f = open(f_path, 'rb')
self.real_size = int.from_bytes(self.f.read(4), ed)
# first, 第一个 key 长度删掉
self.get_key_len()
self.d = {}
def read_int8(self):
size = int.from_bytes(self.f.read(1), ed)
tmp = np.int8(size)
return tmp
def get_key_len(self):
tmp = self.read_int8()
if tmp >= 0:
return tmp
result = tmp & 0x7f
tmp = self.read_int8()
if tmp >= 0:
result |= tmp << 7
else:
result |= (tmp & 0x7f) << 7
tmp = self.read_int8()
if tmp >= 0:
result |= tmp << 14
else:
result |= (tmp & 0x7f) << 14
tmp = self.read_int8()
if tmp >= 0:
result |= tmp << 21
else:
result |= (tmp & 0x7f) << 21
tmp = self.read_int8()
result |= tmp << 28
if tmp < 0:
for i in range(5):
tmp = self.read_int8()
if tmp >= 0:
return result
raise ValueError('协议错误')
return result
def get_data(self):
while True:
if (self.f.tell() - 4) >= self.real_size:
break
ken_len = self.get_key_len()
key = self.f.read(ken_len)
data_len = self.get_key_len()
data = self.f.read(data_len)
self.d[key] = data
# 释放文件描述符
self.f.close()
if __name__ == "__main__":
# 实现1: 简略版本
mr = MMKVReader('./mmkv.default')
print(mr.get_data())
# 实现2 libmmkv,官方版本
mmkv.MMKV.initializeMMKV('.', mmkv.MMKVLogLevel.Debug)
kv = mmkv.MMKV.defaultMMKV()
# 查看所有的 keys
for key in kv.keys():
print(key)
# 不是所有的 key 都是string 类型
# print(kv.getString(key))
'''see aa4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6f1k6h3&6U0k6h3&6@1i4K6u0r3e0f1#2w2g2W2)9J5c8Y4N6A6K9$3W2Q4x3V1k6H3P5i4c8Z5L8$3&6Q4y4h3k6K6k6i4c8#2M7l9`.`.build for pythoncp mmkv.cpython-38-darwin.so /PYTHONPATH/sitepackages/这样就可以使用 import mmkv'''import numpy as np
import mmkv
ed = 'little'
class MMKVReader():
def __init__(self, f_path) -> None:
self.f = open(f_path, 'rb')
self.real_size = int.from_bytes(self.f.read(4), ed)
# first, 第一个 key 长度删掉
self.get_key_len()
self.d = {}
def read_int8(self):
size = int.from_bytes(self.f.read(1), ed)
tmp = np.int8(size)
return tmp
def get_key_len(self):
tmp = self.read_int8()
if tmp >= 0:
return tmp
result = tmp & 0x7f
tmp = self.read_int8()
if tmp >= 0:
[培训]传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!