某易新闻逆向
本文中所有内容仅供研究与学习使用,请勿用于任何商业用途和非法用途,否则后果自负!
环境
App版本:108.1(1807)豌豆荚下载
设备:Pixel XL
抓包工具:Charles + Postern
反汇编工具:jadx-gui 1.5.0、IDA Pro 7.7
hook:frida 12.8.0
抓包
Header
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完成反编译后逐个分析
Host(固定值):
gw.m.163.com
add-to-queue-millis(整数型13位时间戳):
int(time.time() * 1000)
data4-sent-millis(整数型13位时间戳):
int(time.time() * 1000)
cache-control(固定值):
no-cache
user-agent(随意给一个就好):
NewsApp/108.1 Android/8.1.0 (Android/msm8996)
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
- 共计找到了37个,着重查看其中
X-NR-ISE(固定值):
0
X-XR-Original-Host(固定值):
gw.m.163.com
User系列参数
抓包对比了过后发现
user-c
的值是会变的
在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
等等。。。
继续分析
User-U
、User-D
、User-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:
现在可以知道,整个加密流程为:
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==
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启动的时间戳
X-NR-TS(整数型13位时间戳)::
int(time.time() * 1000)
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 } })
- 对比后无误
X-NR-Net-Lib(固定值):
okhttp
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 // 固定值
根据上一张截图可以知道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 进行许可。