某易新闻逆向

xiaoeryu Lv5

本文中所有内容仅供研究与学习使用,请勿用于任何商业用途和非法用途,否则后果自负!

环境

App版本:108.1(1807)豌豆荚下载

设备:Pixel XL

抓包工具:Charles + Postern

反汇编工具:jadx-gui 1.5.0、IDA Pro 7.7

hook:frida 12.8.0

抓包

Add-To-Queue-Millis	1716155605001
data4-Sent-Millis	1716155605002
Cache-Control	no-cache
User-Agent	NewsApp/108.1 Android/8.1.0 (Android/msm8996)
X-NR-Trace-Id	1716155605008_36948439_ZDBkNDQ3NjFhYjZjN2ZiNl9fQW5kcm9pZF9tc204OTk2
X-NR-ISE	0
X-XR-Original-Host	gw.m.163.com
User-C	5pCc57Si
User-RC	UjgzLZ+E4Lemnj+sMro9qwqQ3xlDp4PUECu18073DbE2Sp1cMm3KWoG4EVq0Iff0
User-D	IMi3SNGXpwgZp82uZ0+RgpAF6y/2KeNwsdMOCNyDzAlKc35nEIdpbwHZVg+KaduA
User-VD	xsTHQE9xf3vYWy8XeWECeuLJx3sxZ0eDNrxxIJT5gl6bR7URJ6L1Q7A4TGIYxCEc
User-appid	TItcOwjV9bndQ91C5VadYg==
User-sid	RCeQjCDjXXK9Yh6VD8kD5xMguO+V0brlBu6A5b4whQE=
User-LC	67NqtW9W02z/qXjaEOOHag==
User-N	yEWDFuJGE3Gmj2a0IPdYcA==
X-NR-TS	1716155605026
X-NR-SIGN	f4655b581e7701ef1416dcc22dd94bd9
X-NR-Net-Lib	okhttp
Accept-Encoding	br,gzip
Host	gw.m.163.com
Connection	Keep-Alive
params

url解码后

GET /nc/api/v1/search/flow/comp?
start=Kg==&
limit=20&
q=5Lul6Imy5YiX5pS/5bqc6YOo6Zeo5Y+R5biW56ew5ZOI5bC85Lqa6KKr5bmy5o6J&
deviceId=IMi3SNGXpwgZp82uZ0+RgpAF6y/2KeNwsdMOCNyDzAlKc35nEIdpbwHZVg+KaduA&
version=newsclient.108.1.android&channel=c2VhcmNo&canal=UVFfbmV3c195dW55aW5nNA==&
dtype=0&tabname=zonghe&
position=5Lit6Ze06aG154Ot5qac&
ts=1716104794&
sign=nZwy0Lv5hnVYZmQrZWwseXcZ28nunsOjaYWlAW/IxwF48ErR02zJ6/KXOnxX046I&
spever=FALSE HTTP/1.1

逆向分析

分析抓包抓到的参数,看哪些是固定的哪些是变动的

Header

多次抓包对比参数,看哪些参数是固定的,哪些参数需要逆向获取。用jadx分析app完成反编译后逐个分析

  1. Host(固定值):

    gw.m.163.com

  2. add-to-queue-millis(整数型13位时间戳):

    int(time.time() * 1000)

  3. data4-sent-millis(整数型13位时间戳):

    int(time.time() * 1000)

  4. cache-control(固定值):

    no-cache

  5. user-agent(随意给一个就好):

    NewsApp/108.1 Android/8.1.0 (Android/msm8996)

  6. x-nr-trace-id:

    整数型13位时间戳 + “_” + ??? + “_” + ZDBkNDQ3NjFhYjZjN2ZiNl9fQW5kcm9pZF9tc204OTk2

    分析其中的未知参数:???

    • 共计找到了37个,着重查看其中Request相关的

    GalaxyResponse.a函数

    • x-nr-trace-id的值来自于this.f12885b
    @Override // com.netease.galaxy.net.IRequest
    public GalaxyResponse a() throws Throwable {
        OkHttpClient a2;
        method.header("X-NR-Trace-Id", this.f12885b);
    
    ......
    
    public GalaxyRequest(RequestData requestData) {
        this.f12885b = "";
        this.f12885b = c(String.valueOf(hashCode()));	// "???"是一个hashCode
    }
    

    跟进c(String str)函数中

        private String c(String str) {
            return System.currentTimeMillis() + "_" + str + "_" + Galaxy.N(Galaxy.M());
        }
    
    • 定位到了this.f12885b的来源,写frida脚本获取str的值

    hook c函数

    Java.perform(function(){
        var GalaxyRequest = Java.use("com.netease.galaxy.net.GalaxyRequest")
        GalaxyRequest["c"].implementation = function(str){
            console.log('c is called' + ',' + 'str: ' + str)
            var ret = this.c(str)
            console.log('c ret value is: ' + ret)
            return ret
        }
    })
    

    根据返回值可以发现:“ZDBkNDQ3NjFhYjZjN2ZiNl9fQW5kcm9pZF9tc204OTk2”,字符串一直是固定的,那么总结可得

    整数型13位时间戳 + “_” + “hashCode()” + “_” + ZDBkNDQ3NjFhYjZjN2ZiNl9fQW5kcm9pZF9tc204OTk2

  7. X-NR-ISE(固定值):

    0

  8. X-XR-Original-Host(固定值):

    gw.m.163.com

  9. User系列参数

  • 抓包对比了过后发现user-c的值是会变的

    1. 在jadx中查找user-c

      User-C的值来自于,对StringUtil.e(o2, "UTF-8")返回的字符串进行URL编码,使用UTF-8字符集,hookStringUtil.e()查看编码前的值

      Java.perform(function(){
          var StringUtil = Java.use("com.netease.newsreader.support.utils.string.StringUtil")
          StringUtil["e"].implementation = function(str, str2){
              console.log('e is called, ' + 'str: ' + str + 'str2: ' + str2 + "\n")
              var ret = this.e(str, str2)
              console.log('e ret value is: ' + ret + "\n")
              return ret
          }
      })
      

      执行脚本

      ┌──(root㉿r0env)-[~/Documents/FridaHook]
      └─# frida -U -f com.netease.newsreader.activity -l wangyiNews.js 
           ____
          / _  |   Frida 12.8.0 - A world-class dynamic instrumentation toolkit
         | (_| |
          > _  |   Commands:
         /_/ |_|       help      -> Displays the help system
         . . . .       object?   -> Display information about 'object'
         . . . .       exit/quit -> Exit
         . . . .
         . . . .   More info at https://www.frida.re/docs/home/
      Spawned `com.netease.newsreader.activity`. Use %resume to let the main thread start executing!    
      [Google msm8996::com.netease.newsreader.activity]-> %resume 
      [Google msm8996::com.netease.newsreader.activity]-> e is called, str: com.netease.newsreader.common.serverconfig.ServerConfigData$Porxystr2: utf-8
               
      e ret value is: Y29tLm5ldGVhc2UubmV3c3JlYWRlci5jb21tb24uc2VydmVyY29uZmlnLlNlcnZlckNvbmZpZ0RhdGEkUG9yeHk=
      e is called, str: 头条str2: UTF-8
      e ret value is: 5aS05p2h                                                     
      e is called, str: com.netease.newsreader.common.serverconfig.ServerConfigData$Porxystr2: utf-8
               
      e ret value is: Y29tLm5ldGVhc2UubmV3c3JlYWRlci5jb21tb24uc2VydmVyY29uZmlnLlNlcnZlckNvbmZpZ0RhdGEkUG9yeHk=
               
      e is called, str: *str2: utf-8
               
      e ret value is: Kg==
               
      e is called, str: 巴黎奥运天天惹韩国人生气str2: utf-8
               
      e ret value is: 5be06buO5aWl6L+Q5aSp5aSp5oO56Z+p5Zu95Lq655Sf5rCU
               
      e is called, str: searchstr2: utf-8
               
      e ret value is: c2VhcmNo
               
      。。。。。。
      

      访问不同的栏目User-C的值会有变动:

      6KeG6aKR // base64 -> 视频

      6ZmE6L%2BR // base64 -> 附近

      5aS05p2h // base64 -> 头条

      5aWl6L%2BQ // base64 -> 奥运

      c2VhcmNo // base64 -> search

      等等。。。

  1. 继续分析User-UUser-DUser-N。。。等参数,跟进getEncryptedParams()方法

    调用getEncryptedParamsInner(str, i2)方法,跟进查看

    调用callEncrypt方法,跟进查看

    调用encrypt方法,跟进查看

    private static native synchronized byte[] encrypt(Context context, String str, int i2);

    接下来到了so层,查看此处加载的so文件名

        static {
            try {
                System.loadLibrary("random");
            } catch (Error unused) {
            }
        }
    

    用IDA打开librandom.so文件,查看Java_com_netease_nr_biz_pc_sync_Encrypt_encrypt方法的实现,在分析之前先hook确认下查找的点没错:

    Java.perform(function () {
        var ByteString = Java.use("com.android.okhttp.okio.ByteString")
        var Encrypt = Java.use("com.netease.nr.biz.pc.sync.Encrypt")
        Encrypt["encrypt"].implementation = function (context, str, i2) {
            console.log('encrypt is called' + ', ' + 'context: ' + context + ', ' + 'str: ' + ', ' + 'i2: ' + i2)
            var ret = this.encrypt(context, str, i2)
            console.log('encrypt ret value is ' + JSON.stringify(ret))
            console.log('\n\ncallEncrypt ret str_hex: ' + ByteString.of(ret).hex())
            return ret
        }
    })
    

    执行脚本:

    ┌──(root㉿r0env)-[~/Documents/FridaHook]
    └─# frida -U -f com.netease.newsreader.activity -l wangyiNews.js
         ____
        / _  |   Frida 12.8.0 - A world-class dynamic instrumentation toolkit
       | (_| |
        > _  |   Commands:
       /_/ |_|       help      -> Displays the help system
       . . . .       object?   -> Display information about 'object'
       . . . .       exit/quit -> Exit
       . . . .
       . . . .   More info at https://www.frida.re/docs/home/
    Spawned `com.netease.newsreader.activity`. Use %resume to let the main thread start executing!
    [Google msm8996::com.netease.newsreader.activity]-> %resume  
    encrypt is called, context: com.netease.nr.base.activity.BaseApplication@99736ce, str: , i2: 0
    encrypt ret value is [104,3,-109,49,-115,-1,47,52,-120,18,25,-25,-39,33,-33,-59,-84,-106,-115,-88,-1,-57,71,43,-74,-46,-92,66,68,-3,46,-98,120,-16,74,-47,-45,108,-55,-21,-14,-105,58,124,87,-45,-114,-120]
    
    callEncrypt ret str_hex: 680393318dff2f34881219e7d921dfc5ac968da8ffc7472bb6d2a44244fd2e9e78f04ad1d36cc9ebf2973a7c57d38e88
    encrypt is called, context: com.netease.nr.base.activity.BaseApplication@99736ce, str: , i2: 0
    encrypt ret value is [-78,7,-46,-63,-97,29,-40,76,-94,-112,101,-75,74,72,80,-10]
    
    callEncrypt ret str_hex: b207d2c19f1dd84ca29065b54a4850f6
    encrypt is called, context: com.netease.nr.base.activity.BaseApplication@99736ce, str: , i2: 0
    encrypt ret value is [-116,34,61,0,-117,27,-24,43,-68,-32,54,-58,-34,31,64,10,-19,76,-96,93,97,79,120,-55,-29,-104,18,-76,-13,-52,-40,14,120,-16,74,-47,-45,108,-55,-21,-14,-105,58,124,87,-45,-114,-120]
    
    callEncrypt ret str_hex: 8c223d008b1be82bbce036c6de1f400aed4ca05d614f78c9e39812b4f3ccd80e78f04ad1d36cc9ebf2973a7c57d38e88
    

    确认没找错点,跟进so中进行分析(环境:ida64_7.7)

    • 分析后加密函数在doEn
    __int64 __fastcall doEn(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5)
    {
      __int64 v8; // x22
      __int64 v9; // x23
      __int64 v10; // x22
      __int64 v11; // x24
      __int64 v12; // x20
      char *v13; // x0
      char *v14; // x21
      __int64 v15; // x22
      __int64 v16; // x21
      __int64 v17; // x0
      __int64 v18; // x21
      char *v19; // x22
      __int64 v20; // x23
      char *v21; // x0
      char *v22; // x22
      __int64 v23; // x24
      __int64 v24; // x22
      __int64 v25; // x0
      __int64 v26; // x23
      __int64 v27; // x0
      __int64 v28; // x2
    
      v8 = (*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 48LL))(a1, "java/lang/String");
      v9 = (*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 1336LL))(a1, "utf-8");
      v10 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 264LL))(
              a1,
              v8,
              "getBytes",
              "(Ljava/lang/String;)[B");
      v11 = (*(__int64 (__fastcall **)(__int64, __int64, __int64, __int64))(*(_QWORD *)a1 + 272LL))(a1, a5, v10, v9);
      v12 = (*(__int64 (__fastcall **)(__int64, __int64, __int64, __int64))(*(_QWORD *)a1 + 272LL))(a1, a4, v10, v9);
      v13 = (char *)malloc(0x15u);
      strcpy(v13, "AES/ECB/PKCS7Padding");
      v14 = v13;
      v15 = (*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 1336LL))(a1, "D@V");
      free(v14);
      v16 = (*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 48LL))(a1, "javax/crypto/spec/SecretKeySpec");
      v17 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 264LL))(
              a1,
              v16,
              "<init>",
              "([BLjava/lang/String;)V");
      v18 = (*(__int64 (__fastcall **)(__int64, __int64, __int64, __int64, __int64))(*(_QWORD *)a1 + 224LL))(
              a1,
              v16,
              v17,
              v11,
              v15);
      v19 = (char *)malloc(0x15u);
      strcpy(v19, "AES/ECB/PKCS7Padding");
      v20 = (*(__int64 (__fastcall **)(__int64, char *))(*(_QWORD *)a1 + 1336LL))(a1, v19);
      free(v19);
      v21 = (char *)malloc(3u);
      strcpy(v21, "BC");
      v22 = v21;
      v23 = (*(__int64 (__fastcall **)(__int64, char *))(*(_QWORD *)a1 + 1336LL))(a1, v21);
      free(v22);
      v24 = (*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 48LL))(a1, "javax/crypto/Cipher");
      v25 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 904LL))(
              a1,
              v24,
              "getInstance",
              "(Ljava/lang/String;Ljava/lang/String;)Ljavax/crypto/Cipher;");
      v26 = (*(__int64 (__fastcall **)(__int64, __int64, __int64, __int64, __int64))(*(_QWORD *)a1 + 912LL))(
              a1,
              v24,
              v25,
              v20,
              v23);
      v27 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 264LL))(
              a1,
              v24,
              "init",
              "(ILjava/security/Key;)V");
      (*(void (__fastcall **)(__int64, __int64, __int64, __int64, __int64))(*(_QWORD *)a1 + 488LL))(a1, v26, v27, 1LL, v18);
      v28 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 264LL))(
              a1,
              v24,
              "doFinal",
              "([B)[B");
      return (*(__int64 (__fastcall **)(__int64, __int64, __int64, __int64))(*(_QWORD *)a1 + 272LL))(a1, v26, v28, v12);
    }
    
    • 这个函数的主要功能是使用 AES 加密算法对输入的数据进行加密。它使用 ECB 模式和 PKCS7Padding 填充方式,并且使用 Bouncy Castle 安全提供者来创建和初始化 Cipher 对象。最终返回加密后的字节数组

    使用自吐脚本进行hook:

    脚本1

    脚本2

    现在可以知道,整个加密流程为:

    str -> AES/ECB/PKCS7Padding ->base64

    例如:

    • 使用CyberChef解密成功

    对照抓包及hook到的参数,可以确定user-rc、user-d、user-vd、user-appid、user-lc、user-n参数的生成都是如此,其原始值大多为某一个值经过base64编码后在经过url编码得到的:

    user-rc: UjgzLZ+E4Lemnj+sMro9qwqQ3xlDp4PUECu18073DbE2Sp1cMm3KWoG4EVq0Iff0
    user-d: IMi3SNGXpwgZp82uZ0+RgpAF6y/2KeNwsdMOCNyDzAlKc35nEIdpbwHZVg+KaduA
    user-vd: xsTHQE9xf3vYWy8XeWECeuLJx3sxZ0eDNrxxIJT5gl6bR7URJ6L1Q7A4TGIYxCEc
    user-appid: TItcOwjV9bndQ91C5VadYg==
    user-lc: 67NqtW9W02z/qXjaEOOHag==
    user-n: yEWDFuJGE3Gmj2a0IPdYcA==
    
  2. user-sid

    在jadx中查找并跟踪

    • 获取一个实现了IGalaxyApi接口的对象,并调用getSessionId()方法来获取当前会话的ID,然后将该ID作为字符串返回。对其进行hook:
    Java.perform(function () {
        var NRGalaxy = Java.use('com.netease.newsreader.newarch.galaxy.NRGalaxy');
        NRGalaxy.G.implementation = function () {
            var originalResult = this.G();
            console.log('G()方法返回值: ' + originalResult);
            return originalResult;
        };
        
        // 为了触发 hook,我们需要获取 NRGalaxy 的实例
        var instance = NRGalaxy.F();
        instance.G(); // 调用 G() 方法
    });
    

    执行脚本:

    加密返回值,将其与user-sid对比

    User-sid qlNtWweNuThGiTKXJrZSnzEzjaqi2osTzDRV6IYJhvU=

    • 构成:字符串 + 时间戳

      • 每次重启App后字符串和时间戳会改变,字符串应该是随机生成的,时间戳为App启动的时间戳
  1. X-NR-TS(整数型13位时间戳)::

    int(time.time() * 1000)

  2. X-NR-SIGN:

    在jadx中跟踪定位

    String n2 = StringUtils.n(((Object) queryString) + HttpUtils.f29796s + ts);

    将 queryString、HttpUtils.f29796s、ts 进行拼接

    • HttpUtils.f29796s是一个静态变量:gNlVGcSKf5

    拼接后使用StringUtils.n()对拼接后的字符串进行MD5摘要计算

        public static String n(String str) {
            if (TextUtils.isEmpty(str)) {
                return str;
            }
            try {
                return a(MessageDigest.getInstance("MD5").digest(g(str, Charset.forName("UTF-8"))), false);
            } catch (NoSuchAlgorithmException e2) {
                throw new AssertionError(e2);
            }
        }
    

    对这个方法进行hook

    Java.perform(function(){
        var StringUtils = Java.use("com.netease.newsreader.framework.util.string.StringUtils")
        StringUtils.n.implementation = function(str){
            console.log("n is called str -> " + str + "\n")
            var ret = this.n(str)
            console.log("n ret value is -> " + ret + "\n")
            return ret
        }
    })
    
    • 对比后无误
  3. X-NR-Net-Lib(固定值):

    okhttp

  4. Accept-Encoding (固定值):

    br,gzip

params

搜索关键字定位

  • 可以看到相关参数都是在这里生成的

对数据包进行对比分析后

start=Kg==&		// 固定值
limit=20&		// 固定值
q=5Lul6Imy5YiX5pS/5bqc6YOo6Zeo5Y+R5biW56ew5ZOI5bC85Lqa6KKr5bmy5o6J&		// 搜索词的base64编码
deviceId=IMi3SNGXpwgZp82uZ0+RgpAF6y/2KeNwsdMOCNyDzAlKc35nEIdpbwHZVg+KaduA&
version=newsclient.108.1.android&	// 固定值
channel=c2VhcmNo&	// 栏目的base64编码
canal=UVFfbmV3c195dW55aW5nNA==&		// 固定值:QQ_news_yunying4的base64编码
dtype=0&	// 固定值
tabname=zonghe&		// 固定值
position=5Lit6Ze06aG154Ot5qac&		// 搜索内容的base64编码
ts=1716104794&		// 时间戳
sign=nZwy0Lv5hnVYZmQrZWwseXcZ28nunsOjaYWlAW/IxwF48ErR02zJ6/KXOnxX046I&		// 未知
spever=FALSE HTTP/1.1		// 固定值
  1. sign

根据上一张截图可以知道sign的值来自于str7

截取代码片段
String str7 = s2 + String.valueOf(currentTimeMillis);
if (!TextUtils.isEmpty(str7)) {
    str7 = StringUtil.c(Encrypt.getEncryptedParams(StringUtils.n(str7)));
arrayList.add(new FormPair("sign", str7));
  • StringUtils.n(str7)上面分析过是一个计算MD5摘要方法:先对str7进行了MD5操作

  • Encrypt.getEncryptedParams()上面也分析过最终是调用了so中的AES/ECB/PKCS7Padding加密

  • StringUtil.c()是对输入字符串进行URLEncoder处理然后返回

        public static String c(String str) {
            if (TextUtils.isEmpty(str)) {
                return "";
            }
            try {
                return URLEncoder.encode(str, "UTF-8");
            } catch (Exception unused) {
                return "";
            }
        }
    
    验证:

    先对StringUtil.n()进行hook拿到str7的MD5摘要值

    再把MD5进行AES加密并转为Base64编码

    然后hook StringUtil.c()跟它的参数进行比较

    • 结果相同

所有参数分析完毕

模拟请求

上述将所有相关参数都分析完毕,接下来写代码对会变动的参数进行还原后重新组包进行模拟请求

请求头中只使用gzip解码,以免处理不了br编码

  • 标题: 某易新闻逆向
  • 作者: xiaoeryu
  • 创建于 : 2024-08-01 10:55:05
  • 更新于 : 2024-08-09 11:31:18
  • 链接: https://github.com/xiaoeryu/2024/08/01/某易新闻逆向/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
某易新闻逆向