某k歌app缓存文件解密分析
### 本文包括- **Java**层定位缓存文件加载逻辑
- **Hook**加解密函数观察函数参数
- 还原**SO**层解密方法
- 解密缓存文件播放验证
- b站视频链接: https://www.bilibili.com/video/BV1yd4y1z7Ch/?vd_source=23b9de401c27e819abddbd5551eddbda
说明:这个缓存文件的解密很简单,本文主要是为大家提供定位关键逻辑代码的一种思路
### 一、Java层定位缓存文件加载逻辑
1. 前置工作
- 这个apk很大,有19个dex文件,往jeb里加载之前要把jeb的可用内存设置的大一些,我设置了8G
- 从启动Activity大致阅览一些,发现这个发布版包含了大量log日志记录的代码,并没有剔除
- 因此可以使用查看应用log的方式定位我们所关注的逻辑代码
```java
LogUtil.i("SplashBaseActivity", "isDexActivityInRoot: id=" + v4 + ",numOfActivityes=" + v5 + ",topActivity=" + v6 + ",baseActivity=" + v1_2);
LogUtil.i("SplashBaseActivity", "isDexActivityInRoot: rootActivity is special activity");
LogUtil.e("SplashBaseActivity", "unexpected intent.");
LogUtil.i("SplashBaseActivity", "relogin tag = " + v2_1);
```
2. 查看应用日志
- 打开`DDMS`,在安装`AndroidStudio `的时候会安装在`SDK`的`tools`文件夹下,一个`monitor.bat`的脚本
- 连接手机,运行app,查看应用进程号`adb shell ps | findstr com.tencxxxt.kxxx`,后面的是app的包名,`linux`使用`grep`过滤
- 在`DDMS`的`logcat`窗口新建过滤器,根据进程`pid`过滤,此时已经可以看到app的运行日志了
3. 观察缓存文件加载逻辑
- 先播放一个音乐,音乐加载完成后就会在本地生成一个缓存文件
- 把网络断掉,清空日志
- 从新播放刚才的音乐
- 此时加载缓存文件的逻辑已经在日志中记录下来了
- 截取的部分日志
```shell
09-24 17:44:37.433: I/RefactorDetailInfoController(10385): [, , 0]:QueryPayTaskStatusReq error:-1msg:网络不可用, 请检查网络设置
09-24 17:44:37.433: I/FeedAudioOperateController(10385): [, , 0]:mFeedPlayListener notifyHideLoading
09-24 17:44:37.433: I/FeedAudioOperateView(10385): [, , 0]:hideLyricPage
09-24 17:44:37.433: I/FeedAudioOperateController(10385): [, , 0]:mFeedPlayListener notifyHideLoading
09-24 17:44:37.434: I/FeedAudioOperateView(10385): [, , 0]:hideLyricPage
09-24 17:44:37.434: I/FeedMediaController(10385): [, , 0]:notifyPlaySongListChange actionType =
09-24 17:44:37.434: I/FeedMediaController(10385): [, , 0]:setCurrentPlay null
09-24 17:44:37.434: I/WifiDialogUtil(10385): [, , 0]:call closeNoWifiDialog function
09-24 17:44:37.435: I/MusicPlayer(10385): [, , 0]: 无网络,忽略 ugc 获取作品信息步骤
09-24 17:44:37.435: I/PlayManager(10385): [, , 0]:online song 忽然之间
09-24 17:44:37.435: I/PlaySongInfoDbService(10385): [, , 0]:updatePlaySongList
09-24 17:44:37.444: I/OpusMemCache(10385): [, , 0]:addMemCache, info: OpusCacheInfo{path='/storage/emulated/0/Android/data/com.tencent.karaoke/files/opus/-1991663251', bitrateLevel=48, vid='1021_e5e7eaee8e3718ba547e47ef80f9cdfd7c81bfac', cacheKey='1021_e5e7eaee8e3718ba547e47ef80f9cdfd7c81bfac_0'}
09-24 17:44:37.444: I/PlayManager(10385): [, , 0]:can PlayOffline
09-24 17:44:37.445: I/KaraPlayerService(10385): [, , 0]:updateCurrentPlaySong 18819739_1531398846_805
09-24 17:44:37.445: I/OpusMemCache(10385): [, , 0]:addMemCache, info: OpusCacheInfo{path='/storage/emulated/0/Android/data/com.tencent.karaoke/files/opus/-1991663251', bitrateLevel=48, vid='1021_e5e7eaee8e3718ba547e47ef80f9cdfd7c81bfac', cacheKey='1021_e5e7eaee8e3718ba547e47ef80f9cdfd7c81bfac_0'}
09-24 17:44:37.445: I/PlayManager(10385): [, , 0]:online song 忽然之间
09-24 17:44:37.446: I/OpusMemCache(10385): [, , 0]:addMemCache, info: OpusCacheInfo{path='/storage/emulated/0/Android/data/com.tencent.karaoke/files/opus/-1991663251', bitrateLevel=48, vid='1021_e5e7eaee8e3718ba547e47ef80f9cdfd7c81bfac', cacheKey='1021_e5e7eaee8e3718ba547e47ef80f9cdfd7c81bfac_0'}
09-24 17:44:37.446: I/PlayManager(10385): [, , 0]:can PlayOffline
09-24 17:44:37.446: I/KaraPlayerService(10385): [, , 0]:playSong can startPlay
09-24 17:44:37.447: I/lib_player:PlayProxy(10385): [, , 0]:setAudioProcesser: audioProcesser = h.w.n.j.u0.v.y0.d@84c18b
09-24 17:44:37.447: I/lib_player:PlayProxy(10385): [, , 0]:setAudioProcesser: audioProcesser = h.w.n.j.u0.v.y0.g@800f268
09-24 17:44:37.447: I/lib_player:PlayProxy(10385): [, , 0]:setTimeOut: timeOut = 5000
09-24 17:44:37.447: I/lib_player:ExoPlayerBuilder(10385): [, , 0]:setBufferSize: set buffer size 1000
09-24 17:44:37.447: I/lib_player:PlayProxy(10385): [, , 0]:buildPlayer: useSpeedLimit = false
09-24 17:44:37.447: I/lib_player:ExoPlayerBuilder(10385): [, , 0]:buildPlayer: fromTag is 12
09-24 17:44:37.448: I/lib_player:DefaultRenderersFactory(10385): Loaded Libgav1VideoRenderer.
09-24 17:44:37.451: I/lib_player:ExoPlayerImpl(10385): Init 6b31681
09-24 17:44:37.451: I/lib_player:ExoPlayerImplInternal(10385): [, , 0]:init h.j.a.a.u1@3e21326 : create player PB 12
09-24 17:44:37.452: D/AudioManager(10385): getStreamVolume isRestricted mode = 0
09-24 17:44:37.455: I/lib_player:PlayProxy(10385): [, , 0]:setAudioStreamType: streamtype = 3
09-24 17:44:37.455: I/lib_player(10385): [, , 0]:audioAttributes
09-24 17:44:37.455: I/KaraProxyPlayer:5f59b5a(10385): [, , 0]:createPlayer: playerType EXOPLAYER Player :526cd14
09-24 17:44:37.455: I/MusicPlayer(10385): [, , 0]: music player state change to 2(PREPARING)
09-24 17:44:37.455: I/KaraProxyPlayer:5f59b5a(10385): [, , 0]:initPlayer Player : 526cd14
09-24 17:44:37.455: I/KaraProxyPlayer:5f59b5a(10385):vid = , playScene = , bitrateLevel = , hasEncrypted = , ugcId = , sha1sum = [], ugcLoudness = , useSuperSound = , fmtID = [-1], cacheKey = , ktvDataInPlayer = , recyBufferSize =
09-24 17:44:37.456: I/OpusMemCache(10385): [, , 0]:addMemCache, info: OpusCacheInfo{path='/storage/emulated/0/Android/data/com.tencent.karaoke/files/opus/-1991663251', bitrateLevel=48, vid='1021_e5e7eaee8e3718ba547e47ef80f9cdfd7c81bfac', cacheKey='1021_e5e7eaee8e3718ba547e47ef80f9cdfd7c81bfac_0'}
09-24 17:44:37.456: I/KaraProxyPlayer:5f59b5a(10385): [, , 0]:initPlayer: 业务没有传入验证的sha1值,不做验证,直接播放缓存
09-24 17:44:37.456: I/lib_player:PlayProxy(10385): [, , 0]:setHasEncrypted: true
09-24 17:44:37.456: I/lib_player:PlayProxy(10385): [, , 0]:setDataSource: filePath = /storage/emulated/0/Android/data/com.tencent.karaoke/files/opus/-1991663251
09-24 17:44:37.456: I/lib_player:PlayProxy(10385): [, , 0]:setWakeMode: context = com.tencent.karaoke.KaraokeApplication@d2a531c, mode = 1
09-24 17:44:37.457: I/lib_player:PlayProxy(10385): [, , 0]:prepareAsync
09-24 17:44:37.457: I/lib_player(10385): [, , 0]:timeline [eventTime=0.00, mediaPos=0.00, window=0, periodCount=1, windowCount=1, reason=PLAYLIST_CHANGED
09-24 17:44:37.457: I/lib_player(10385): [, , 0]:period [?]
09-24 17:44:37.457: I/lib_player(10385): [, , 0]:window [?, seekable=false, dynamic=true]
09-24 17:44:37.457: I/lib_player(10385): [, , 0]:]
09-24 17:44:37.457: I/lib_player(10385): [, , 0]:mediaItem
09-24 17:44:37.458: I/lib_player(10385): [, , 0]:state
09-24 17:44:37.458: I/GlobalPlaySongManager(10385): [, , 0]:refreshPlaySongListAfterStartPlay -> mPlayingSongIdentif
09-24 17:44:37.458: I/MusicPlayer(10385): [, , 0]: 正在播放: UGC作品 忽然之间(18819739_1531398846_805), 下一首: UGC作品 横冲直撞(326668413_1634048914_780)
09-24 17:44:37.459: I/DetailDataManager(10385): [, , 0]:loadVideoSizeFromUgcInfo: stream=0-0, norma=0-0
09-24 17:44:37.459: I/DetailDataManager(10385): [, , 0]:loadVideoSizeFromUgcInfo cancel with empty
09-24 17:44:37.459: I/RefactorPlayController(10385): [, , 0]:adjustVideoViewLayoutOnUiThread:videoHeight=1, videoWidth=1, isEffectTemplateShow=false
```
4. 定位java代码
- 从日志中找出自己认为比较关键的信息,然后在jeb里查找对应的
- 我这里选在`initPlayer Player :`这一条,即初始化播放器
- 搜索这个字符串,定位到相关代码处
- 往下阅读处理流程可以看到出现了`解密成功`、`解密失败等字样`
- 跟进前面的判断函数
- 成功定位到关键的加密类
### 二、Hook加解密函数观察参数
1. 这一步的目的是查看各函数的参数,因为目的是解密缓存文件,因此这里只关注解密函数
2. 可以看到`decrypt`函数有三个重载,其中一个为`native`,另外两个参数个数不同;
```java
private native int decrypt(int arg1, ByteBuffer arg2, int arg3);
public int decrypt(int arg6, byte[] arg7, int arg8);
public int decrypt(int arg6, byte[] arg7, int arg8, int arg9);
```
3. 两个`Java`层的函数最终也都调用了`native`的函数
4. 可以自己`hook`一下`java`层的函数,看一下最终调用的是哪一个,以及参数特征
5. 我这里测试,调用的是四个参数的`decrypt`,之后`hook`一下`native`的函数,看一下传给`native`的参数,`ByteBuffer`不会打印kkk
```shell
arg1:0
arg3:8
arg1:8
arg3:8
arg1:16
arg3:8
arg1:24
arg3:4
arg1:28
arg3:8
arg1:36
arg3:8
arg1:44
arg3:8
arg1:52
arg3:8
arg1:60
arg3:8
arg1:68
arg3:8
```
6. 观察可以得出结论,这个类似一个流加解密,每次传给`native`层`8`(除个别外)个字节解密,其他的参数是一些记录偏移的
### 三、分析还原Native层解密算法
1. `IDA`看一下对应的函数,这个就很简单,根据算出来的偏移取密码表里的字节和原始字节异或就得到了明文
```c
__int64 __fastcall Java_com_tencent_karaoke_audiobasesdk_KaraMediaCrypto_decrypt(__int64 a1, __int64 a2, int start, __int64 a4, int size)
{
__int64 data; // x1
__int64 result; // x0
__int64 i; // x9
int offset; // w16
int v11; // w16
int v12; // w18
int v13; // w16
data = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)a1 + 1840LL))(a1, a4);
result = (unsigned int)size;
if ( (start & 0x80000000) != 0 )
return 4294967294LL;
if ( size > 0 )
{
i = 0LL;
do
{
if ( ((start + i) & 0x8000000000000000LL) != 0 )
{
offset = 0;
}
else
{
offset = start + i;
if ( start + i >= 0x8000 )
offset %= 0x7FFF;
}
v11 = offset * offset;
v12 = v11 + 80923;
v13 = v11 + 81178;
if ( v12 >= 0 )
v13 = v12;
*(_BYTE *)(data + i++) ^= byte_7191359B20; // 和密码表对应的字节映射
}
while ( size != (_DWORD)i );
}
return result;
}
```
2. `python`照着写一遍,把密码表`copy`下来就行了
3. `python`实现的时候可以发现很多`if`条件都触发不了,因此可以自己精简一下
### 四、解密缓存文件测试
- 正常播放
chendipang 发表于 2022-9-25 23:40
大佬问个小问题,为啥我的真机用DDMS输出的日志全在一行不会换行显示出来
这个我也不清楚,应该是一条log占一行的 有点东西。 也太厉害了吧 大佬问个小问题,为啥我的真机用DDMS输出的日志全在一行不会换行显示出来 来看看666666 66666666666技术佬 非常感谢楼主分享 可以可以 高级操作! 点赞 局外人
看一下