某k歌app缓存文件解密分析

查看 82|回复 10
作者:red1y   
本文包括
  • Java层定位缓存文件加载逻辑
  • Hook加解密函数观察函数参数
  • 还原SO层解密方法
  • 解密缓存文件播放验证
  • b站视频链接: https://www.bilibili.com/video/BV1yd4y1z7Ch/?vd_source=23b9de401c27e819abddbd5551eddbda

    说明:这个缓存文件的解密很简单,本文主要是为大家提供定位关键逻辑代码的一种思路
    一、Java层定位缓存文件加载逻辑
    [ol]

  • 前置工作

  • 这个apk很大,有19个dex文件,往jeb里加载之前要把jeb的可用内存设置的大一些,我设置了8G

  • 从启动Activity大致阅览一些,发现这个发布版包含了大量log日志记录的代码,并没有剔除

  • 因此可以使用查看应用log的方式定位我们所关注的逻辑代码
    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);

  • 查看应用日志

  • 打开DDMS,在安装AndroidStudio的时候会安装在SDK的tools文件夹下,一个monitor.bat的脚本


    image-20220924173711324.png (142.04 KB, 下载次数: 0)
    下载附件
    2022-9-24 21:25 上传

  • 连接手机,运行app,查看应用进程号adb shell ps | findstr com.tencxxxt.kxxx,后面的是app的包名,linux使用grep过滤


    image-20220924174014933.png (38.96 KB, 下载次数: 1)
    下载附件
    2022-9-24 21:25 上传

  • 在DDMS的logcat窗口新建过滤器,根据进程pid过滤,此时已经可以看到app的运行日志了


    image-20220924174332129.png (127.47 KB, 下载次数: 1)
    下载附件
    2022-9-24 21:26 上传


  • 观察缓存文件加载逻辑

  • 先播放一个音乐,音乐加载完成后就会在本地生成一个缓存文件

  • 把网络断掉,清空日志

  • 从新播放刚才的音乐

  • 此时加载缓存文件的逻辑已经在日志中记录下来了


    image-20220924174901641.png (114.33 KB, 下载次数: 0)
    下载附件
    2022-9-24 21:26 上传

  • 截取的部分日志
    09-24 17:44:37.433: I/RefactorDetailInfoController(10385): [, , 0]:[UI]QueryPayTaskStatusReq error:-1  msg:网络不可用, 请检查网络设置
    09-24 17:44:37.433: I/FeedAudioOperateController(10385): [, , 0]:[UI]mFeedPlayListener notifyHideLoading
    09-24 17:44:37.433: I/FeedAudioOperateView(10385): [, , 0]:[UI]hideLyricPage
    09-24 17:44:37.433: I/FeedAudioOperateController(10385): [, , 0]:[UI]mFeedPlayListener notifyHideLoading
    09-24 17:44:37.434: I/FeedAudioOperateView(10385): [, , 0]:[UI]hideLyricPage
    09-24 17:44:37.434: I/FeedMediaController(10385): [, , 0]:[UI]notifyPlaySongListChange actionType = [1]
    09-24 17:44:37.434: I/FeedMediaController(10385): [, , 0]:[UI]setCurrentPlay null
    09-24 17:44:37.434: I/WifiDialogUtil(10385): [, , 0]:[UI]call closeNoWifiDialog function
    09-24 17:44:37.435: I/MusicPlayer(10385): [, , 0]:[UI][MusicPlayerUtils] 无网络,忽略 ugc 获取作品信息步骤
    09-24 17:44:37.435: I/PlayManager(10385): [, , 0]:[UI]online song 忽然之间
    09-24 17:44:37.435: I/PlaySongInfoDbService(10385): [, , 0]:updatePlaySongList
    09-24 17:44:37.444: I/OpusMemCache(10385): [, , 0]:[UI]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]:[UI]can PlayOffline
    09-24 17:44:37.445: I/KaraPlayerService(10385): [, , 0]:[UI]updateCurrentPlaySong 18819739_1531398846_805
    09-24 17:44:37.445: I/OpusMemCache(10385): [, , 0]:[UI]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]:[UI]online song 忽然之间
    09-24 17:44:37.446: I/OpusMemCache(10385): [, , 0]:[UI]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]:[UI]can PlayOffline
    09-24 17:44:37.446: I/KaraPlayerService(10385): [, , 0]:[UI]playSong can startPlay
    09-24 17:44:37.447: I/lib_player:PlayProxy(10385): [, , 0]:[UI]setAudioProcesser: audioProcesser = h.w.n.j.u0.v.y0.d@84c18b
    09-24 17:44:37.447: I/lib_player:PlayProxy(10385): [, , 0]:[UI]setAudioProcesser: audioProcesser = h.w.n.j.u0.v.y0.g@800f268
    09-24 17:44:37.447: I/lib_player:PlayProxy(10385): [, , 0]:[UI]setTimeOut: timeOut = 5000
    09-24 17:44:37.447: I/lib_player:ExoPlayerBuilder(10385): [, , 0]:[UI]setBufferSize: set buffer size 1000
    09-24 17:44:37.447: I/lib_player:PlayProxy(10385): [, , 0]:[UI]buildPlayer: useSpeedLimit = false
    09-24 17:44:37.447: I/lib_player:ExoPlayerBuilder(10385): [, , 0]:[UI]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 [ExoPlayerLib/2.16.1] [platina, MI 8 Lite, Xiaomi, 29]
    09-24 17:44:37.451: I/lib_player:ExoPlayerImplInternal(10385): [, , 0]:[UI]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]:[UI]setAudioStreamType: streamtype = 3
    09-24 17:44:37.455: I/lib_player(10385): [, , 0]:[UI]audioAttributes [eventTime=0.00, mediaPos=0.00, window=0, 2,0,1,1]
    09-24 17:44:37.455: I/KaraProxyPlayer:5f59b5a(10385): [, , 0]:[UI]createPlayer: playerType EXOPLAYER Player :526cd14
    09-24 17:44:37.455: I/MusicPlayer(10385): [, , 0]:[UI][MusicPlayerStateLiveData] music player state change to 2(PREPARING)
    09-24 17:44:37.455: I/KaraProxyPlayer:5f59b5a(10385): [, , 0]:[UI]initPlayer Player : 526cd14
    09-24 17:44:37.455: I/KaraProxyPlayer:5f59b5a(10385):  vid = [1021_e5e7eaee8e3718ba547e47ef80f9cdfd7c81bfac], playScene = [1], bitrateLevel = [48], hasEncrypted = [false], ugcId = [18819739_1531398846_805], sha1sum = [], ugcLoudness = [1.0], useSuperSound = [true], fmtID = [-1], cacheKey = [null], ktvDataInPlayer = [null], recyBufferSize = [0]
    09-24 17:44:37.456: I/OpusMemCache(10385): [, , 0]:[UI]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]:[UI]initPlayer: 业务没有传入验证的sha1值,不做验证,直接播放缓存
    09-24 17:44:37.456: I/lib_player:PlayProxy(10385): [, , 0]:[UI]setHasEncrypted: true
    09-24 17:44:37.456: I/lib_player:PlayProxy(10385): [, , 0]:[UI]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]:[UI]setWakeMode: context = com.tencent.karaoke.KaraokeApplication@d2a531c, mode = 1
    09-24 17:44:37.457: I/lib_player:PlayProxy(10385): [, , 0]:[UI]prepareAsync
    09-24 17:44:37.457: I/lib_player(10385): [, , 0]:[UI]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]:[UI]  period [?]
    09-24 17:44:37.457: I/lib_player(10385): [, , 0]:[UI]  window [?, seekable=false, dynamic=true]
    09-24 17:44:37.457: I/lib_player(10385): [, , 0]:[UI]]
    09-24 17:44:37.457: I/lib_player(10385): [, , 0]:[UI]mediaItem [eventTime=0.00, mediaPos=0.00, window=0, reason=PLAYLIST_CHANGED]
    09-24 17:44:37.458: I/lib_player(10385): [, , 0]:[UI]state [eventTime=0.00, mediaPos=0.00, window=0, BUFFERING]
    09-24 17:44:37.458: I/GlobalPlaySongManager(10385): [, , 0]:[UI]refreshPlaySongListAfterStartPlay -> mPlayingSongIdentif
    09-24 17:44:37.458: I/MusicPlayer(10385): [, , 0]:[UI][MusicPlayer] 正在播放: UGC作品 忽然之间(18819739_1531398846_805), 下一首: UGC作品 横冲直撞(326668413_1634048914_780)
    09-24 17:44:37.459: I/DetailDataManager(10385): [, , 0]:[UI]loadVideoSizeFromUgcInfo: stream=0-0, norma=0-0
    09-24 17:44:37.459: I/DetailDataManager(10385): [, , 0]:[UI]loadVideoSizeFromUgcInfo cancel with empty
    09-24 17:44:37.459: I/RefactorPlayController(10385): [, , 0]:[UI]adjustVideoViewLayoutOnUiThread:videoHeight=1, videoWidth=1, isEffectTemplateShow=false

  • 定位java代码

  • 从日志中找出自己认为比较关键的信息,然后在jeb里查找对应的

  • 我这里选在initPlayer Player :这一条,即初始化播放器

  • 搜索这个字符串,定位到相关代码处


    image-20220924175529966.png (80.26 KB, 下载次数: 0)
    下载附件
    2022-9-24 21:27 上传

  • 往下阅读处理流程可以看到出现了解密成功、解密失败等字样


    image-20220924175743303.png (69.35 KB, 下载次数: 0)
    下载附件
    2022-9-24 21:27 上传

  • 跟进前面的判断函数


    image-20220924175905069.png (58.59 KB, 下载次数: 1)
    下载附件
    2022-9-24 21:27 上传

  • 成功定位到关键的加密类


    image-20220924175933773.png (51.54 KB, 下载次数: 0)
    下载附件
    2022-9-24 21:28 上传


    [/ol]
    二、Hook加解密函数观察参数
    [ol]

  • 这一步的目的是查看各函数的参数,因为目的是解密缓存文件,因此这里只关注解密函数

  • 可以看到decrypt函数有三个重载,其中一个为native,另外两个参数个数不同;
    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);

  • 两个Java层的函数最终也都调用了native的函数

  • 可以自己hook一下java层的函数,看一下最终调用的是哪一个,以及参数特征

  • 我这里测试,调用的是四个参数的decrypt,之后hook一下native的函数,看一下传给native的参数,ByteBuffer不会打印kkk
    [decrypt]
    arg1:  0
    arg3:  8
    [decrypt]
    arg1:  8
    arg3:  8
    [decrypt]
    arg1:  16
    arg3:  8
    [decrypt]
    arg1:  24
    arg3:  4
    [decrypt]
    arg1:  28
    arg3:  8
    [decrypt]
    arg1:  36
    arg3:  8
    [decrypt]
    arg1:  44
    arg3:  8
    [decrypt]
    arg1:  52
    arg3:  8
    [decrypt]
    arg1:  60
    arg3:  8
    [decrypt]
    arg1:  68
    arg3:  8

  • 观察可以得出结论,这个类似一个流加解密,每次传给native层8(除个别外)个字节解密,其他的参数是一些记录偏移的
    [/ol]
    三、分析还原Native层解密算法
    [ol]

  • IDA看一下对应的函数,这个就很简单,根据算出来的偏移取密码表里的字节和原始字节异或就得到了明文
    __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[v12 - (v13 & 0xFFFFFF00)]; // 和密码表对应的字节映射
       }
       while ( size != (_DWORD)i );
    }
    return result;
    }

  • python照着写一遍,把密码表copy下来就行了

  • python实现的时候可以发现很多if条件都触发不了,因此可以自己精简一下
    [/ol]
    四、解密缓存文件测试

  • 正常播放


    image-20220924181039908.png (331.1 KB, 下载次数: 0)
    下载附件
    2022-9-24 21:28 上传


    缓存, 下载次数

  • red1y
    OP
      


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

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

    有点东西。
    alonelycute   

    也太厉害了吧
    chendipang   

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

    来看看666666
    我缘以为   

    66666666666技术佬
    htpidk   

    非常感谢楼主分享
    Pro111   

    可以可以
    焕墨如烟9817   

    高级操作!
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部