某易新闻App逆向

新闻搜索抓包与字段解析

新闻搜索请求如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"app": "com.netease.newsreader.activity",
"duration": "782ms",
"headers": {
"Host": "gw.m.163.com",
"user-agent": "NewsApp/116.1 Android/10 (google/Pixel XL)",
"x-nr-trace-id": "1766026470029_264453819_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM",
"x-nr-ise": "0",
"x-xr-original-host": "gw.m.163.com",
"user-c": "5pCc57Si",
"user-rc": "UjgzLZ+E4Lemnj+sMro9qwqQ3xlDp4PUECu18073DbE1QczpsTWV4NoxlU/51iL/OFnZkytSJ8NVUttJRaK4C+5QNhz9TLXVpP2VcqembRcentYAtzPoQ3SIVcMWWlm3PBbI53PX4Gd3tY1EgH3Np+rL+kbjQ1nSgTZVI2y8+C4=",
"user-d": "R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl",
"user-vd": "YajLV6eMBiMvt5IKy6j+WFR1Ef3qFXuwpUC+xyv+n+Ax1J2VeRbbazQfmJ5LP9/v",
"user-appid": "TItcOwjV9bndQ91C5VadYg==",
"user-sid": "iw0t412hC9bMZcnwa47jlPCtJAAGDbcAXKj7U7awjJM=",
"user-lc": "67NqtW9W02z/qXjaEOOHag==",
"user-n": "VnE1Iqw3/SoXRqhFJu9cFg==",
"user-cn": "8dabkxj70LEGQY+UurBODnjwStHTbMnr8pc6fFfTjog=",
"x-nr-ts": "1766026470047",
"x-nr-sign": "e042c3f8f8048430286cf510c479bb37",
"x-nr-net-lib": "okhttp",
"accept-encoding": "br,gzip"
},
"method": "GET",
"protocol": "h2",
"remoteIp": "36.99.227.75",
"remotePort": 443,
"sessionId": "b7f8b1c5-529f-4467-b7a9-fd7a124d8198",
"time": "2025-12-18 10:54:30",
"url": "https://gw.m.163.com/nc/api/v1/search/flow/comp?deviceId=R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl%2FHSMGl&version=116.1&channel=5pCc57Si&canal=QQ_news_yunying4&qId=&tabname=&ts=1766026469&lat=&lon=&sign=QsadZvZNwAvCpeHCxMyeEt64vy1W9GW9JiAd9WEuyvl48ErR02zJ6%2FKXOnxX046I&open=&openpath=&dtype=0&start=MA%3D%3D&limit=20&q=54Gr566t"
}

响应如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
"code": 0,
"message": "成功",
"data": {
"boxes": [
百度百科介绍以及用户推荐...
],
"tabList": [
{
"name": "zonghe",
"label": "综合"
},
{
"name": "zhannei",
"label": "站内"
},
{
"name": "shipin",
"label": "视频"
},
{
"name": "yonghu",
"label": "用户"
}
],
"curtabname": "zonghe",
"search_url": "https://wp.m.163.com/163/html/frontend/newsapp-search-v1/index.html",
"tab_vertical": "111",
"doc": {
"result": [
搜索结果, 根据score属性排名...
],
"total": 586,
"pos": -1,
"triggerTrustedSource": false,
"has_more": 1,
"program": "bjrec_search2v0e",
"nextCursorMark": "sqtBC1oj+fjgXaqgR9V+1rYLCzSyGq5S2mDZlM/MM7Bjvfuh1fwiCgPeC4SIA5oj",
"qId": "2539576465804824"
},
"tab_vertical_show": "1111",
"isGray": false,
"hideFeedback": true
}
}

请求体字段解析

请求头字段

字段 注释
Host gw.m.163.com 固定值
user-agent NewsApp/116.1 Android/10 (google/Pixel XL) 用户设备信息
x-nr-trace-id 1766026470029_264453819_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM 毫秒级时间戳_8位随机数_设备标识
x-nr-ise 0 固定值
x-xr-original-host gw.m.163.com 固定值
user-c 5pCc57Si 8位随机值,与用户行为相关
user-rc UjgzLZ+E4Lemnj+sMro9qwqQ3xlDp4PUECu18073DbE1QczpsTWV4NoxlU/51iL/OFn
ZkytSJ8NVUttJRaK4C+5QNhz9TLXVpP2VcqembRcentYAtzPoQ3SIVcMWWlm3PBbI53
PX4Gd3tY1EgH3Np+rL+kbjQ1nSgTZVI2y8+C4=
固定值,解码后是json
user-d R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl 固定值,由设备标识加密得到
user-vd YajLV6eMBiMvt5IKy6j+WFR1Ef3qFXuwpUC+xyv+n+Ax1J2VeRbbazQfmJ5LP9/v 固定值
user-appid TItcOwjV9bndQ91C5VadYg== 固定值,由appid加密得到
user-sid iw0t412hC9bMZcnwa47jlPCtJAAGDbcAXKj7U7awjJM= 同意会话中是固定值
user-lc 67NqtW9W02z/qXjaEOOHag== 固定值,由行政区代码加密得到
user-n VnE1Iqw3/SoXRqhFJu9cFg== 由手机网络类型加密得到
user-cn 8dabkxj70LEGQY+UurBODnjwStHTbMnr8pc6fFfTjog= 固定值,由QQ_news_yunying4加密得到
x-nr-ts 1766026470047 毫秒级时间戳
x-nr-sign e042c3f8f8048430286cf510c479bb37 当前请求的签名,由”设备标识+秒级时间戳“加密得到
x-nr-net-lib okhttp 使用的网络库(固定值)
accept-encoding br,gzip 固定值
sessionId b7f8b1c5-529f-4467-b7a9-fd7a124d8198 会话id,随机值

url参数字段

字段 注释
deviceId R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl 与user-d字段的值相同
version 116.1 app版本
channel 5pCc57Si 与user-c字段相同
canal QQ_news_yunying4 固定值
qId 第一次为空,之后为响应中的qId字段值进行base64加密
tabname 第一次为空,之后为对应分类页面(zonghe, zhannei, shipin, yonghu)
ts 1766026469 秒级时间戳
lat 纬度
lon 经度
sign QsadZvZNwAvCpeHCxMyeEt64vy1W9GW9JiAd9WEuyvl48ErR02zJ6/KXOnxX046I 请求签名
open
openpath
dtype 0 固定值
start MA== base64加密,第一次值为0,之后为响应中nextCursorMark的值
limit 20 最多获取的新闻条数
q 54Gr566t 搜索内容(base64加密)

参数生成逆向

**注意:很多参数都只生成一次,然后从缓冲中获取,因此很多参数的生成算法仅触发一次后不在触发,后续hook并不能捕获到信息。 **

请求头参数

x-nr-trace-id/X-NR-Trace-Id

jadx搜索该字段(区别大小写)

hook 该函数后发现并没有触发!

我们可以换另一种方式定位,根据该参数值的格式,可以全局搜索 System.currentTimeMillis() + “_“,再根据是三个字段和两个”_“组合的,获取可疑函数如下

hook 这两个函数后发现都触发了(GalaxyRequest.c触发多次,util.getTraceId触发一次)

1
2
3
4
5
6
7
8
9
10
11
12
GalaxyRequest.c is called: str=82335006
GalaxyRequest.c result=1766036199783_82335006_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM
GalaxyRequest.c is called: str=197139747
GalaxyRequest.c result=1766036200700_197139747_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM
GalaxyRequest.c is called: str=251744591
GalaxyRequest.c result=1766036202227_251744591_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM
util.getTraceId is called: requestIdentity=157192101
util.getTraceId result=1766036202526_157192101_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM
GalaxyRequest.c is called: str=112736705
GalaxyRequest.c result=1766036202745_112736705_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM
GalaxyRequest.c is called: str=79891345
GalaxyRequest.c result=1766036203343_79891345_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM

同时hook和抓包,发现没有找到对应的值!?

Galaxy.N()最终调用Tools.B方法

这里是获取SharedPreferences 文件中的galaxy_pre_key_device_id的值,顾名思义就是标识设备的一串字符。

然而值得注意的是util.getTraceId最后一部分与GalaxyRequest.c最后一部分是相同的,且与我们的目标值一致,该方法如下

通过AdConfig.getDevice_id()获取

可以确认我们的目标值是device_id,具体生成方式见DeviceInfo.getDevicesId()

通过SharedPreferences存储和获取”GALAXY_PRE_KEY_DEVICE_ID”键对应的值(进入PrefHelper可知),其他大部分逻辑是生成device_id。SharedPreferences 文件的名称如下

1
public static final String PREFERENCES_PATH = "ntesepaddata";

具体文件路径为/data/data/com.netease.newsreader.activity/shared_prefs/ntesepaddata

为了定该参数的生成位置,选择hook System.currentTimeMillis方法,同时进行抓包,通过请求包中改参数的毫秒级时间戳,最终打印堆栈如下

1
2
3
4
5
6
7
8
9
10
11
System.currentTimeMillis: 1766039948246
java.lang.Throwable
at java.lang.System.currentTimeMillis(Native Method)
at com.netease.newsreader.common.utils.sys.SystemUtilsWithCache.d0(SystemUtilsWithCache.java:5)
at com.netease.newsreader.common.net.BaseHttpClient$BuiltInInterceptor.intercept(BaseHttpClient.java:10)
at okhttp3.internal.http.RealInterceptorChain.c(RealInterceptorChain.kt:12)
at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:16)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:6)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)

com.netease.newsreader.common.utils.sys.SystemUtilsWithCache.d0函数如下

com.netease.newsreader.common.net.BaseHttpClient$BuiltInInterceptor.intercept函数如下

可以确认的是,x-nr-trace-idX-NR-Trace-Id对应的值都是相同的。中间值是对象的哈希值,也就是说参数x-nr-trace-id的中间字段是随机值,后者是device_id,对我们来说是固定值,前者是毫秒级时间戳。

user-c/User-C

jadx搜索user-c,定位如下

对于第一个标注处,右键查找字段f53186i的用例,定位如下

c方法如下

首先尝试获取现有的值,如果不存在,则通过URLEncoder.encode(StringUtil.e(str, "UTF-8"), "UTF-8");获取,参数str就是CommonGalaxy.o()函数的返回值。

再来看第二个标注处

同样也是通过URLEncoder.encode(StringUtil.e(o2, "UTF-8"), "UTF-8");获取,参数o2就是CommonGalaxy.o()函数的返回值。

查看StringUtil.e()方法

是一个Base64Api,尝试将结果通过base64解码,得到值”搜索“。为了二次确认,hook 该函数,然而,实际逻辑并没有触发该函数!!!

那么选择hook CommonGalaxy.o()函数,手动搜索新闻,打印的全是”搜索“二字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CommonGalaxy.o result=搜索
CommonGalaxy.o is called
java.lang.Throwable
at com.netease.newsreader.common.galaxy.CommonGalaxy.o(Native Method)
at com.netease.newsreader.newarch.base.NewarchHttpUtils.e(NewarchHttpUtils.java:13)
at com.netease.newsreader.base.net.client.NewsHttpClient$UserDataInterceptor.intercept(NewsHttpClient.java:3)
at okhttp3.internal.http.RealInterceptorChain.c(RealInterceptorChain.kt:12)
at com.netease.newsreader.common.net.sentry.SentryInterceptor.intercept(SentryInterceptor.java:9)
at okhttp3.internal.http.RealInterceptorChain.c(RealInterceptorChain.kt:12)
at com.netease.newsreader.common.net.interceptor.HostOptimizeInterceptor.intercept(HostOptimizeInterceptor.java:6)
at okhttp3.internal.http.RealInterceptorChain.c(RealInterceptorChain.kt:12)
at com.netease.newsreader.common.net.BaseHttpClient$BuiltInInterceptor.intercept(BaseHttpClient.java:16)
at okhttp3.internal.http.RealInterceptorChain.c(RealInterceptorChain.kt:12)
at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:16)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:6)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
...同上...

通过上面的堆栈函数可以知道,是执行了NewarchHttpUtils.e方法的,里面的c方法确实有StringUtil.e方法,但是c方法中调用StringUtil.e方法的前提是该值不存在!所以这就解释的通了。

另外我将https://gw.m.163.com/nc/api/v1/search/hot-word中的user-c参数也进行了base64解码

1
"user-c": "5aS05p2h"

结果是”头条“二字。

综上,已经可以肯定了user-c参数的生成是根据“访问不同资源/页面的关键字”而生成的,并且只是简单的将明文进行了base64加密。

user-n/User-N

定位同user-c一样,两种方法都可行,同样也在相同位置处。通过字段查找用例定位如下

1
a(hashMap, f53185h, g());

a方法区别于b方法如下

a方法会对值进行加密,而b方法则没有。

g方法最终调用NetUtil.i(),该方法如下

该方法主要是获取网络状态,其中networkInfo.getSubtypeName()获取的是移动网络子类型名称,常见值如下

网络 getSubtype() getSubtypeName()
2G NETWORK_TYPE_GPRS "GPRS"
2G EDGE "EDGE"
3G UMTS "UMTS"
3G HSDPA "HSDPA"
3G HSPA "HSPA"
4G LTE "LTE"
4G+ LTE_CA "LTE_CA"
5G NR "NR"

再来看看加密方法Encrypt.getEncryptedParams方法

getEncryptedParams方法中尝试获取现有值,获取不到则调用getEncryptedParamsInner方法生成,在该方法中,主要是将callEncrypt(Core.context(), str, i2)的返回值进行base64加密。具体来看看该方法

最终调用native方法,加载的库是librandom.so。IDA加载该库后,搜索Java_com_netease_nr_biz_pc_sync_Encrypt_encrypt即可定位到对应方法。重命名第一个参数(JNIEnv指针)和第二个参数(静态方法对应jclass或实例方法对应jobject对象)。

首先执行getRandomKey方法获取RandomKey,该方法如下

主要通过i2的值选择不同的随机字符串作为密钥。根据i2=0,可以确认随机字符串为

1
neteasenewsboard	(hex:6E 65 74 65 61 73 65 6E 65 77 73 62 6F 61 72 64)

然后执行doEn方法,该方法如下

加密逻辑主要是通过反射获取Java层的加密库进行AES/ECB/PKCS7Padding加密。解密如下

user-d/User-D

明文通过SystemUtilsWithCache.s()方法获取

返回值是device_id,就是x-nr-trace-id的最后一部分。

加密方法同user-n一致,解密得到的就是device_id

user-rc/User-Rc

user-xxx系列

字段 初始值 还原后的值 备注
user-d R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM device_id,进一步base64解码得到9b0d317068740eca__google_Pixel XL
user-vd YajLV6eMBiMvt5IKy6j+WFR1Ef3qFXuwpUC+xyv+n+Ax1J2VeRbbazQfmJ5LP9/v MTc2NjAyNjA0OTQ3M18xOTI0NjE1NTVfaUkybkN3bng%3D 进一步base64解码得到1766026049473_192461555_iI2nCwnx,可能与App安装、启动等相关
user-appid TItcOwjV9bndQ91C5VadYg== 2x1kfBk63z
user-sid iw0t412hC9bMZcnwa47jlPCtJAAGDbcAXKj7U7awjJM= dbhggi1766026422530(6个字符+毫秒级时间戳) sessionID加密
user-lc 67NqtW9W02z/qXjaEOOHag== 110000 行政区划代码
user-n VnE1Iqw3/SoXRqhFJu9cFg== WIFI 手机网络类型
user-cn 8dabkxj70LEGQY+UurBODnjwStHTbMnr8pc6fFfTjog= QQ_news_yunying4 channel_id

x-nr-sign

jadx全局搜索该字符串,唯一定位到声明处,右键查找用例

显然第二处才是可能赋值的地方。

l方法应该是个键值添加的地方,x-nr-sign值通过a(query, currentTimeMillis)生成,

query则是通过HttpUrl.query()方法获取,该方法如下

f83693gList<String>类型,根据函数名可以猜测是将url参数转字符串,通过hook 该方法也可以确认。

来看a方法

进一步调用StringUtils.n方法,参数为:query参数 + "gNlVGcSKf5" + 毫秒级时间戳

该部分主要是通过g方法(getBytes()方法)将参数转成字节数组,进行md5加密,然后md5结果作为参数调用a方法。

f52113af52114b都是16进制字符数组,区别在于前者是小写后者是大写,根据参数z2=false可知选择小写。这部分说是加密,其实就是将md5小写化或者大写化(根据z2决定)。

Frida hook querya方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function main(){
Java.perform(
function () {
let HttpUrl = Java.use("okhttp3.HttpUrl");
HttpUrl["query"].implementation = function () {
console.log(`HttpUrl.query is called`);
var f83693g = this["_g"].value;
if (f83693g !== null) {
console.log(`HttpUrl._g: ${Java.use("java.util.ArrayList").$new(f83693g).toString()}`);
}
let result = this["query"]();
console.log(`HttpUrl.query result: ${result}`);
return result;
};

let RequestSignInterceptor = Java.use("com.netease.newsreader.common.net.interceptor.RequestSignInterceptor");
RequestSignInterceptor["a"].implementation = function (str, j2) {
console.log(`RequestSignInterceptor.a is called: str: ${str}, j2: ${j2}`);
let result = this["a"](str, j2);
console.log(`RequestSignInterceptor.a result: ${result}`);
return result;
};
}
);
}

再结合抓包可以确认x-nr-sign参数生成部分如下

1
2
3
4
5
HttpUrl.query is called
HttpUrl._g: [deviceId, R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl, version, 116.1, channel, 5pCc57Si, canal, QQ_news_yunying4, qId, OTEzMzAzNTc3MTMwNTE5, tabname, , ts, 1766121716, lat, , lon, , sign, 4w5LCmZUAXCNfX2GsDH7Cf2tDyi2gMCymYF+Vi33mN548ErR02zJ6/KXOnxX046I, open, , openpath, , dtype, 0, start, MA==, limit, 20, q, 57ud5a+56Zu25bqm]
HttpUrl.query result: deviceId=R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl&version=116.1&channel=5pCc57Si&canal=QQ_news_yunying4&qId=OTEzMzAzNTc3MTMwNTE5&tabname=&ts=1766121716&lat=&lon=&sign=4w5LCmZUAXCNfX2GsDH7Cf2tDyi2gMCymYF+Vi33mN548ErR02zJ6/KXOnxX046I&open=&openpath=&dtype=0&start=MA==&limit=20&q=57ud5a+56Zu25bqm
RequestSignInterceptor.a is called: str: deviceId=R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl&version=116.1&channel=5pCc57Si&canal=QQ_news_yunying4&qId=OTEzMzAzNTc3MTMwNTE5&tabname=&ts=1766121716&lat=&lon=&sign=4w5LCmZUAXCNfX2GsDH7Cf2tDyi2gMCymYF+Vi33mN548ErR02zJ6/KXOnxX046I&open=&openpath=&dtype=0&start=MA==&limit=20&q=57ud5a+56Zu25bqm, j2: 1766121716635
RequestSignInterceptor.a result: 3e53cc88982ae5d775adbcc07599ec8d

最终可以确认x-nr-sign参数生成过程为:**md5(URL参数字符串 + “gNlVGcSKf5” + 毫秒级时间戳)**。

URL参数

sign

jadx全局搜索”flow/comp"

并判断是第二处,代码如下

搜索f43446c用例,定位代码如下

这里是URL组装,里面的UserReward.f42460J即为目标值sign,与deviceId值生成相比,不同的地方是获取device_id+秒级时间戳,调用StringUtils.n()进行md5加密

最后都是AES/ECB/PKCS7Padding加密再base64加密。

user-sid

jadx搜索不到,但通过抓包可以发现

1
2
3
4
5
6
//在搜索结果展示页面往下滑,触发更多请求
wKtraxGUri0o2WvFr1f1trY48uFm1UfUst8IJ9NXdFg=
wKtraxGUri0o2WvFr1f1trY48uFm1UfUst8IJ9NXdFg=
//与上述不同会话的请求
iw0t412hC9bMZcnwa47jlPCtJAAGDbcAXKj7U7awjJM=
3smlZlQvTuyjzD6RUzrQBnn+ItIQn+cpmZB7ehGoIZg=

结合名字可以确认是会话级别参数,在同一会话中保持固定值。这些值通过同user-n的解密过程得到

1
2
3
aagiga1766122722452
dbhggi1766026422530
fdbgda1766133429518

可以确定明文是由”6个随机小写字母+毫秒级时间戳”组成,然后先后通过AES/ECB/PKCS7Padding加密和base64加密得到user-sid

qId/start

同样还是在搜索结果展示页面往下滑,触发更多请求。

可以发现响应包中存在如下字段

1
2
"nextCursorMark": "EUp6sZc6vOpW8qCe5ak2sfVkg0ziiOg29UyOGG9+AfnN2x7NHdzUOXnTxju3ZcVD",
"qId": "7051912003775192"

结合之后的请求URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
https://gw.m.163.com/nc/api/v1/search/flow/comp?
deviceId=R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl%2FHSMGl
version=116.1
channel=5pCc57Si
canal=QQ_news_yunying4
qId=NzA1MTkxMjAwMzc3NTE5Mg%3D%3D
tabname=zonghe
ts=1766122748
lat=
lon=
sign=eQOXxTy7Dk8ANH5HvzniLBiX%2Fvh070Nb2LYh8lZn3H148ErR02zJ6%2FKXOnxX046I
open=
openpath=
dtype=0
start=RVVwNnNaYzZ2T3BXOHFDZTVhazJzZlZrZzB6aWlPZzI5VXlPR0c5K0Fmbk4yeDdOSGR6VU9YblR4anUzWmNWRA%3D%3D
limit=20
q=54Gr566t

将其中qId进行base64解码就是上一次响应包中的qId的值。

start进行base64解码就是上一次响应包中的nextCursorMark的值。

请求脚本

循环请求脚本如下,可持续搜索同一关键字的相关新闻。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import hashlib
import json
import random
import time

import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from base64 import b64encode, b64decode
from urllib.parse import urlencode
from string import ascii_lowercase


def AESEncrypt(plain: str, key: str, mode=AES.MODE_ECB) -> bytes:
plain_bytes = plain.encode("UTF-8")
key_bytes = key.encode("UTF-8")

ase = AES.new(key_bytes, mode)

# PKCS7Padding
padded_data = pad(plain_bytes, AES.block_size)

cipher = ase.encrypt(padded_data)

return cipher


def base64Encode(plain) -> str:
if type(plain) != bytes:
plain = plain.encode()
return b64encode(plain).decode()


def constructReqParams(qId, tabName, sign, start, queryWord):
reqParams = {
"deviceId": "R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl",
"version": 116.1,
"channel": "5pCc57Si", # b64encode("搜索".encode("UTF-8"))
"canal": "QQ_news_yunying4",
"qId": qId,
"tabname": tabName,
"ts": int(time.time()),
"lat": "",
"lon": "",
"sign": sign,
"open": "",
"openpath": "",
"dtype": 0,
"start": start,
"limit": 20,
"q": queryWord
}

return reqParams


def constructReqHeaders(xNrTraceId, xNrSign, userSid):
reqHeaders = {
"Host": "gw.m.163.com",
"user-agent": "NewsApp/116.1 Android/10 (google/Pixel XL)",
"x-nr-trace-id": xNrTraceId,
"x-nr-ise": "0",
"x-xr-original-host": "gw.m.163.com",
"user-c": "5pCc57Si", # 固定值 b64encode("搜索".encode("UTF-8"))
"user-rc": "UjgzLZ+E4Lemnj+sMro9qwqQ3xlDp4PUECu18073DbE1QczpsTWV4NoxlU/51iL/OFnZkytSJ8NVUttJRaK4C+5QNhz9TLXVpP2VcqembRcentYAtzPoQ3SIVcMWWlm3PBbI53PX4Gd3tY1EgH3Np+rL+kbjQ1nSgTZVI2y8+C4\u003d", #固定值
"user-d": "R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl", # 固定值
"user-vd": "YajLV6eMBiMvt5IKy6j+WFR1Ef3qFXuwpUC+xyv+n+Ax1J2VeRbbazQfmJ5LP9/v", # 固定值
"user-appid": "TItcOwjV9bndQ91C5VadYg\u003d\u003d", # 固定值
"user-sid": userSid, # 每次会话是固定的
"user-lc": "67NqtW9W02z/qXjaEOOHag\u003d\u003d", # 行政区代码,可固定
"user-n": "VnE1Iqw3/SoXRqhFJu9cFg\u003d\u003d", # 网络状态,可固定
"user-cn": "8dabkxj70LEGQY+UurBODnjwStHTbMnr8pc6fFfTjog\u003d", # 固定值
"x-nr-ts": str(int(time.time() * 1000)),
"x-nr-sign": xNrSign,
"x-nr-net-lib": "okhttp",
"accept-encoding": "br,gzip"
}

return reqHeaders


def hashCode():
max = 0x400000
return random.randint(0, max)


def generateXNrTraceId():
return str(int(time.time() * 1000)) + "_" + str(hashCode()) + "_" + "OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM"


def generateXNrSign(url):
plain = url + "gNlVGcSKf5" + str(int(time.time() * 1000))
XNrSign = hashlib.md5(plain.encode("UTF-8")).hexdigest()
return XNrSign


def generateSign():
plain = "OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM" + str(int(time.time()))
md5Result = hashlib.md5(plain.encode("UTF-8")).hexdigest()
sign = base64Encode(AESEncrypt(md5Result, "neteasenewsboard"))

return sign


def generateUserSid():
plain = "".join(random.choice(ascii_lowercase) for _ in range(6)) + str(int(time.time() * 1000))
userSid = base64Encode(AESEncrypt(plain,"neteasenewsboard"))
return userSid

def constructRequest(queryWord, tabeName, nextCursorMark, qId, userSid):

url = "https://gw.m.163.com/nc/api/v1/search/flow/comp"
reqParams = constructReqParams(base64Encode(qId), tabeName, generateSign(), base64Encode(nextCursorMark), base64Encode(queryWord))
reqHeaders = constructReqHeaders(generateXNrTraceId(), generateXNrSign(urlencode(reqParams)), userSid)

req = requests.Request(method="GET", url=url, headers=reqHeaders, params=reqParams)
return req


def search(keyword):

proxies = {
"http": "127.0.0.1:7890",
"https": "127.0.0.1:7890"
}

session = requests.Session()

# do while
userSid = generateUserSid()
tabeName = ""
nextCursorMark = "0"
qId = ""

print(f"[*] 正在进行关键词: {keyword} 的相关新闻搜索...")
while 1:
time.sleep(round(random.uniform(3, 5), 2))
req = constructRequest(keyword, tabeName, nextCursorMark, qId, userSid)
prepared = session.prepare_request(req)
# print(prepared.url)
# print(json.dumps(dict(prepared.headers), indent=4, ensure_ascii=False))
response = session.send(prepared, proxies=proxies)
if response.status_code == 200:
# print(json.dumps(response.json(), indent=4, ensure_ascii=False))
rspResult = response.json()
print(f"[*] 搜索结果如下:\n{rspResult}")
data = rspResult["data"]
tabeName = data["curtabname"]
# do something to store the news
news = data["doc"]["result"]
hasMore = data["doc"]["has_more"]
nextCursorMark = data["doc"]["nextCursorMark"]
qId = data["doc"]["qId"]
if hasMore != 1:
print(f"[*] 关键词: {keyword} 相关新闻已搜索完毕!请尝试更换其他关键词进行新闻搜索")
break
else:
raise Exception("网络请响应异常")



if __name__ == "__main__":
search("火箭")


某易新闻App逆向
http://example.com/2025/12/24/逆向实战/网易新闻App逆向/
作者
gla2xy
发布于
2025年12月24日
许可协议