red1y 发表于 2022-9-24 21:29

某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`条件都触发不了,因此可以自己精简一下

### 四、解密缓存文件测试

- 正常播放

red1y 发表于 2022-9-26 22:38

chendipang 发表于 2022-9-25 23:40
大佬问个小问题,为啥我的真机用DDMS输出的日志全在一行不会换行显示出来

这个我也不清楚,应该是一条log占一行的

taoxwl666 发表于 2022-9-25 16:29

有点东西。

alonelycute 发表于 2022-9-25 23:21

也太厉害了吧

chendipang 发表于 2022-9-25 23:40

大佬问个小问题,为啥我的真机用DDMS输出的日志全在一行不会换行显示出来

wfys66 发表于 2022-9-25 23:47

来看看666666

我缘以为 发表于 2022-9-26 08:16

66666666666技术佬

htpidk 发表于 2022-9-26 18:18

非常感谢楼主分享

Pro111 发表于 2022-9-26 20:44

可以可以

焕墨如烟9817 发表于 2022-9-26 20:52

高级操作!

hk9186 发表于 2022-9-26 21:16

点赞 局外人
看一下
页: [1] 2 3 4
查看完整版本: 某k歌app缓存文件解密分析