《某游快爆》APP sign值逆向

查看 62|回复 11
作者:sky9464   
目录
  • 目录
  • 前言
  • 一、抓包
  • 二、jadx反编译apk
  • 三、idapro反编译libtokenso
  • 四、还原token生成算法
  • 五、结语

    前言
    本篇文章主要是为了介绍《某游快爆》APP sign 值的逆向分析过程,官方网站:aHR0cHM6Ly93d3cuMzgzOS5jb20vYXBwLmh0bWw=
    最近刚入门Android逆向,看到自己日常使用的这个APP中有游戏时长统计功能(有正常的APP时长统计和本软件内部小游戏的时长统计),就想逆向一下练练手,废话不多说,直接开始~~~


    1.png (148.97 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    一、抓包
    那么好,想必大家已经能够猜测到了,这个时长统计功能肯定是通过调用系统底层API获取最近使用时长来实现的,那么作为小白的我第一步是看看有没有这个相关的文档。
    通过搜索相关文档,我们发现,APP需要一个权限:PACKAGE_USAGE_STATS(包_使用_状态)(神人翻译,忽略)


    2.png (101.26 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    那么好,这个API用jio一猜就知道肯定是和包名挂钩的(参考:https://blog.csdn.net/weixin_45951701/article/details/117486242),所以上传到服务器端的时候肯定也是按照包名上传的,我们在抓包的时候只需要在众多包里面搜索我们一个游戏的包名即可。
    我选择王者荣耀进行搜索吧,包名是:com.tencent.tmgp.sgame,然后直接一搜,果然!


    3.png (170.59 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    好了,已经知道这个API接口了,可以轻易发现,这里面有好多参数,其中长得最像签名sign值的就是这个t了:


    4.png (55.96 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    当当当当~~~主角登场!!那么好——jadx,启动!
    二、jadx反编译apk
    刚才看到一个参数c是applaunch,好多包的这个字段不一样,参数名而且都差不多,所以我们可以直接搜索这个字符串:


    5.png (31.47 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    漂亮,gogogo~出发咯~()


    6.png (82.57 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    以上是我稍微给一些方法名重命名了一下,基本上都对上了,但是没看见t参数是哪儿来的,最后一个return倒是个可去之处(这里的generateParams也是我重命名好的,我发现这里确实是会生成基本的参数)
    点开看看:


    7.png (72.38 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    可以发现,这个t参数是Token.getToken()生成的。
    这里重点来了,这个方法是将前面所有的参数的key和value分成了两个数组之中,然后传给这个方法,第一个上下文参数暂时不用管它。


    8.png (49.02 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    emmmm,好的,是native方法,废话不说,找到libtoken.so,IDA启动!
    三、IDAPro反编译libtoken.so
    打开之后,直接搜索java,都对上了都对上了~


    9.png (118.48 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    可以看到参数也是对上了,那么好,直接F5启动看伪C代码(写了点儿注释):


    10.png (99.83 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    jstring __fastcall Java_com_xmcy_hykb_data_Token_getToken(
            JNIEnv *env,
            jclass jobj,
            jobject contextObject,
            _jarray *jKeyArray,
            _jarray *jValueArray)
    {
      __int64 v5; // x9
      __int64 v7; // [xsp+0h] [xbp-1B0h] BYREF
      void *v8; // [xsp+8h] [xbp-1A8h]
      jstring v9; // [xsp+10h] [xbp-1A0h]
      const unsigned __int8 *v10; // [xsp+18h] [xbp-198h]
      jclass v11; // [xsp+20h] [xbp-190h]
      jmethodID v12; // [xsp+28h] [xbp-188h]
      _JNIEnv *v13; // [xsp+30h] [xbp-180h]
      _JNIEnv *v14; // [xsp+38h] [xbp-178h]
      std::string *v15; // [xsp+40h] [xbp-170h]
      std::string *v16; // [xsp+48h] [xbp-168h]
      _BYTE *v17; // [xsp+50h] [xbp-160h]
      _BYTE *v18; // [xsp+58h] [xbp-158h]
      _QWORD *v19; // [xsp+60h] [xbp-150h]
      unsigned __int8 *v20; // [xsp+68h] [xbp-148h]
      int *v21; // [xsp+70h] [xbp-140h]
      unsigned __int8 *v22; // [xsp+78h] [xbp-138h]
      int *v23; // [xsp+80h] [xbp-130h]
      void *v24; // [xsp+88h] [xbp-128h]
      __int64 v25; // [xsp+90h] [xbp-120h]
      __int64 v26; // [xsp+A0h] [xbp-110h]
      jobject v27; // [xsp+A8h] [xbp-108h]
      _jmethodID *StaticMethodID; // [xsp+B0h] [xbp-100h]
      jclass Class; // [xsp+B8h] [xbp-F8h]
      __int64 v30; // [xsp+C0h] [xbp-F0h]
      int j; // [xsp+CCh] [xbp-E4h]
      _QWORD *v32; // [xsp+D0h] [xbp-E0h]
      const unsigned __int8 *v33; // [xsp+D8h] [xbp-D8h]
      jstring v34; // [xsp+E0h] [xbp-D0h]
      const unsigned __int8 *StringUTFChars; // [xsp+E8h] [xbp-C8h]
      jstring ObjectArrayElement; // [xsp+F0h] [xbp-C0h]
      const unsigned __int8 **v37; // [xsp+F8h] [xbp-B8h]
      jsize i; // [xsp+104h] [xbp-ACh]
      __int64 *v39; // [xsp+108h] [xbp-A8h]
      jsize v40; // [xsp+114h] [xbp-9Ch]
      jsize v41; // [xsp+118h] [xbp-98h]
      jsize ArrayLength; // [xsp+11Ch] [xbp-94h]
      jarray v43; // [xsp+120h] [xbp-90h]
      jarray array; // [xsp+128h] [xbp-88h]
      jobject contextObjecta; // [xsp+130h] [xbp-80h]
      jclass jobja; // [xsp+138h] [xbp-78h]
      JNIEnv *enva; // [xsp+140h] [xbp-70h]
      jobject v48; // [xsp+148h] [xbp-68h]
      std::allocator v49; // [xsp+150h] [xbp-60h] BYREF
      std::string v50; // [xsp+158h] [xbp-58h] BYREF
      _BYTE v51[7]; // [xsp+160h] [xbp-50h] BYREF
      _BYTE v52[33]; // [xsp+167h] [xbp-49h] BYREF
      _DWORD v53[4]; // [xsp+188h] [xbp-28h] BYREF
      enva = env;
      jobja = jobj;
      contextObjecta = contextObject;
      array = jKeyArray;
      v43 = jValueArray;
      checkSign(env, jobj, contextObject); // 根据名字来看应该就是检查apk签名的,看看有没有被篡改,我们没改,不用管。
      ArrayLength = _JNIEnv::GetArrayLength(enva, array); // jKeyArray的长度,也就是其余参数的key组成的数组的长度(人话:其余参数的个数)
      v41 = _JNIEnv::GetArrayLength(enva, v43); // jValueArray的长度,和上面一样的
      if ( ArrayLength != v41 ) // 就是验证key和value数组的长度是不是一样,因为一会儿要进行排序
        return _JNIEnv::NewStringUTF(enva, (const unsigned __int8 *)"");
      v40 = ArrayLength + 1;  // key数组的长度+1
      v39 = &v7;
      i = 0;
      v25 = (__int64)&v7 - ((8LL * (unsigned int)(ArrayLength + 1) + 15) & 0xFFFFFFFF0LL);
      while ( i =16j0?c=a>3h=2:?`g", sizeof(v52));
      decodeStr(v20, v21);  // 按照“2、9、5、6”的顺序解密 "a8276l17g=16j0?c=a>3h=2:?`g"
      v19 = operator new(0x20uLL);
      v18 = v51;
      v17 = v52;
      memset(v19, 0, 0x20uLL);
      v32 = v19;
      *v19 = v18;
      v32[1] = v17;
      *(_QWORD *)(v25 + 8LL * (v40 - 1)) = v32;
      for ( i = 0; i ::allocator(&v49);
      std::string::string(&v50, (const unsigned __int8 *)"", &v49);
      std::allocator::~allocator(&v49);
      while ( i
    四、还原token生成算法
    整体上的反编译大致就是,它会将加密的一个key("qlftg}")和它对应的value("a8276l17g=16j0?c=a>3h=2:?`g")解密之后添加到刚才两个数组之中,然后按照key的大小排序(当然是ascii),然后用“|”拼接起来对应的value,然后MD5。所以目前就两件事:
    ①解出来新加的键值对
    ②复现排序算法,拼接好后进行MD5
    关键在于decodeStr函数,我们得跳过去看看:


    11.png (15.93 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    void __fastcall decodeStr(const char *pstr, int *pkey)
    {
      int i; // [xsp+8h] [xbp-18h]
      int v3; // [xsp+Ch] [xbp-14h]
      v3 = strlen(pstr);
      for ( i = 0; i
    其中的pkey就是数组2、5、9、6的地址,pstr是 key的数组/value的数组 。
    这个很简单就是便利执行decodeChar函数,我们过去瞅瞅:


    12.png (14.43 KB, 下载次数: 0)
    下载附件
    2025-5-14 19:45 上传

    unsigned __int8 __cdecl decodeChar(unsigned __int8 c, int key)
    {
      return c ^ key;
    }
    emmmm,太简单了,就是个相乘。
    好了游戏结束,就是将所有的计算过程自己(AI (纠正) )写一遍就行了,没什么难度。
    但是我是让AI写的,稍微简化了一下:(其实还挺简单的)
    import hashlib
    def getToken(keys, values):
        secret_key = 'secret'  # "qlftg}"  的明文
        secret_value = 'c1714e41e5a907874c59a4d81a8486ea' # "a8276l17g=16j0?c=a>3h=2:?`g" 的明文
        keys_copy = keys.copy()
        values_copy = values.copy()
        keys_copy.append(secret_key)  
        values_copy.append(secret_value)
        sorted_pairs = sorted(zip(keys_copy, values_copy), key=lambda x: x[0])
        # 反转拼接顺序
        reversed_concatenated = '|'.join([pair[1] for pair in sorted_pairs][::-1])
        # 使用反转拼接的结果计算MD5
        md5 = hashlib.md5()
        md5.update(reversed_concatenated.encode('utf-8'))
        digest = md5.digest()
        # 十六进制转换
        hex_str = ''.join(f"{b & 0xff:02x}" for b in digest)
        return hex_str
    五、结语
    第一次逆一个正儿八经的APP(bushi),写教程也有不足之道,刚入门逆向,还请各位大佬发现不足之处能够不吝指导!!![抱拳]

    下载次数, 数组

  • sky9464
    OP
      


    bfqn123 发表于 2025-5-18 17:36
    我看这个帖子里用了c++,和java,
    就想问这两种语言都不会,可以学逆向吗?
    还是必须得把以上两种语言学的 ...

    可以学,但是还是建议先学学正向开发,这样一些基本点都知道是啥(当然,如果你能坚持做到看到一个不会的点就去搜去问AI来得到解决的话,我觉得也可以从逆向开始学)。我是学了快三年了正向开发,最近才开始主要学逆向(之前是两个同时学,但还是正向开发为主)。到目前为止,我的C++、Java还是大一水平(自我感觉),所以这两门语言的要求倒并不是很高,但是基本语法啥的一定要懂!
    a13389809   


    bfqn123 发表于 2025-5-18 17:36
    我看这个帖子里用了c++,和java,
    就想问这两种语言都不会,可以学逆向吗?
    还是必须得把以上两种语言学的 ...

    当然也可以不会,你可以直接学汇编和smali,只不过这两者是c和java编译后的指令码,如果不会的话,这两个学起来肯定吃力不少。  我是做了3年的正向开发才学的逆向,我认为不会正向的逆向的成本要远远高出两者加起来。
    我爱胡萝卜   

    安卓逆向入门有推荐的吗
    amwquhwqas128   

    不错的文章, 要慢慢看才能跟得上
    a62626218   

    感谢分享
    Yonghang   

    这是小白?我的天哪,太强了吧
    linweite   

    我的天哪
    szba1120   

    感觉逆向没有弄到SO 就不是逆向。
    sky9464
    OP
      


    szba1120 发表于 2025-5-16 11:35
    感觉逆向没有弄到SO 就不是逆向。

    确实。不过有时候Java层的混淆就够吃一壶了,这个APP算是我入门看到的最简单的吧,里面的WebView调试js全部没混淆,写得清清楚楚,甚至还有注释
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部