本文仅限安全研究与学习交流,严禁将本文内容用于任何商业或非法用途,本文涉及的代码及资源版权归原权利人所有,侵删。
APK版本——9.3.8,已经是上古时代的apk了,目前app已经停止运营。
所用工具:Fiddler、jadx-gui、DDMS、雷电模拟器9.1(64位)。
首先配置模拟器代理(本机IP和Fiddler的默认端口号8888)。

在模拟器中启动App,选择“RR账号登录”,随机输入账号和密码。

点击登录按钮。Fiddler 共捕获到四个关键请求,如下图所示:

四个包依次为:
(1)公钥获取请求:客户端向 101K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3I4G2k6$3W2F1i4K6u0W2M7X3g2F1M7X3g2F1i4K6u0W2j5$3!0E0i4K6u0r3j5h3A6S2P5q4)9J5c8X3N6W2N6p5g2F1j5%4u0&6M7s2c8w2k6i4V1`. 发起 GET 请求,尝试获取 RSA 公钥。由于服务器已停止运营,该接口返回 404 Not Found。
(2)登录请求:POST 至 d90K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3q4H3K9g2)9J5k6h3#2Q4x3X3g2J5k6h3&6J5k6h3&6Q4x3X3g2U0L8$3#2Q4x3V1k6S2M7r3W2Q4x3V1k6U0L8r3W2W2L8Y4c8Q4x3V1k6D9L8$3N6A6L8R3`.`.,携带完整的登录参数(账号、密码密文、设备指纹、签名等)。
(3)错误日志上报:登录失败后,客户端自动将错误信息(error_code=10002)打包上报至 /api/apierror 接口。
(4)用户行为日志:客户端记录用户操作轨迹(如点击按钮、切换输入框等),并发往 /api/phoneclient/opLog2 用于埋点统计。
第一个包是因为APP已经停止运营,服务器已经关闭,所以返回的响应为404 not found。

这里截取一张app正常运行时抓到的正常的包。

重点分析第二个数据包,也就是登录的数据包。查看数据包,请求方法POST,下面是请求的请求体(Request Body),请求头中以 client_info= 开头,以末尾的 sig= 结束,采用 URL 百分号编码对嵌套的 JSON 对象(如 client_info)进行了转义。服务器的响应是200,下面的一串乱码是返回了gzip压缩后的{"error_code":10002,"error_msg":"账号或密码输入错误,请检查后重试"},在APP中点击登录就会弹出这样的信息。

对请求体中的 URL 编码参数进行分析,下面的参数构成了登录接口的核心协议层,划分为基础协议控制(format、v、gz)、用户凭证与校验(user、password、verifycode/isverify)、时间戳防重放(call_id)以及设备与身份标识(uniq_id、api_key、ext_info)等模块,以及包含的两个sig签名。

已知登录调用的方法中有一个方法会发送以上的参数进行请求,因此打开Jadx通过搜索参数找到该方法的实现,从而进行逆向分析,参数有很多,一般搜索不常用且最长的效率比较高,这里搜索tab_sequence只有一处调用,很容易就可以定位到登录函数中发送请求代码的部分。

点击搜索结果,调用方法的反编译原型如下:

分析a方法,首先调用A函数生成一个jsonObjectA对象:
跟进A方法,该函数是人人网客户端所有网络请求的基础参数构造入口,负责统一注入 v(版本号)、api_key(应用公钥)、call_id(本地时间戳)以及根据登录态动态添加 session_key(会话凭证),并通过调用 g() 方法以单键值对的形式,将 iR() 所采集的全部设备指纹信息以 JSON 字符串打包,统一注入 client_info 字段。

回到a方法接着分析,调用完A方法后又接着往jsonObjectA对象输入参数,这里对应了Fiddler中抓到包中的参数。

生成第一个sig:

跟进a(strArr, jhS),分析签名生成算法,算法定义了人人网登录签名的生成规则:将所有 键.值 按字典序升序排列后直接首尾相连,再追加固定密钥(jhS),最后计算 MD5 值。jsonObjectA.put("sig", a(strArr, jhS));调用了一次生成了包中的第一个sig。

查看jhS密钥,定义为一个字符串,调用了setSecretKey函数生成。


查看setSecretKey函数,没有什么有用的信息。

查看jhS的交叉引用,关注第一个引用,jhS的值是通过string赋值来的。

跟踪进赋值的代码出,可以看到string是从 String string = RenrenApplication.getContext().getResources().getString(R.string.secretkey); 中的 R.string.secretkey 中获得,这通常是从APK 内置的 res/values/strings.xml 资源文件中读取到的。

点击跟进可以看到secretkey在R.java中的资源ID值。

这里在apk中搜索在strings.xml并没有找到,这是因为通过AndResGuard 等资源混淆工具处理后的 APK 中,传统的 res/values/strings.xml 文件已被物理删除,其所有字符串常量均被压缩合并至根目录的 resources.arsc 二进制资源表中。
但是在Jadx-GUI 加载 APK 时,会自动解析 resources.arsc,并在右侧的“资源管理器”里虚拟重建出一个 res/values/strings.xml。
勾选资源,搜索secretkey,在重建后的strings.xml中找到secretkey的具体值,这里就拿到了第一次sig生成时的jhS的密钥值了:

再次回到最开始的a函数接着分析,public final void response(...) 部分是服务器响应后,App根据响应做不同的反应:
1.看是不是报错:jsonObject2.getNum("error_code")。如果是 -99 或 -97,就弹窗提示“无法连接网络”。
2.如果是密码错误(10002):调用 loginStatusListener.b(num, string2, string3),你的手机屏幕上就会弹出“账号或密码输入错误”。
3.如果登录成功:
(1)把账号密码明文存进手机本地数据库(Variables.password = str2)
(2)把用户的昵称、头像存进内存(TalkManager.INSTANCE.initUserInfo)。
(3)弹窗说“登录成功”(loginStatusListener.onLoginSuccess())。

a方法的代码最后几行:
httpRequestWrapper.setUrl(jgH + "/client/login"); // 生成发送的目标的url
httpRequestWrapper.setData(jsonObjectA); // 封装参数给客户端
httpRequestWrapper.setResponse(iNetResponse); // 获取响应
HttpProviderWrapper.getInstance().addRequest(httpRequestWrapper); // 发送请求

到此,a方法已经分析完毕,抓到的包中的除了第二个sig外都能够在方法中找到,接下来找第二个sig的方法实现。这里有两个思路,一是AS\JEB动态调试找到生成第二个sig的方法位置,二是已经找到了sig签名的核心生成函数,即jsonObjectA.put("sig", a(strArr, jhS));中的a(strArr, jhS),追踪到a方法的原型,对a方法进行交叉引用进行分析即可。
这里使用第二个方法,如下图是a方法的交叉引用结果,一共有四处出现了"sig"字符串,挨个分析,第一个调用是上面分析的a方法中用来生成第一个sig,第二个调用在h方法中定义了登录后的“获取乐视云视频Token”接口,第四个调用在toString() 方法中是人人网客户端所有网络请求的通用序列化入口。第三个调用也就是 cba() 方法中是人人网客户端实际用于网络传输的请求序列化入口,该方法对参数值进行了 URL 编码。很显然,对四处调用进行分析后,第三个很显然是第二个sig的生成,当然了这里仅是静态分析,有分析错误的可能,也可以动态调试验证一下。

查看cba函数的反编译代码,主要关注最后的sig生成调用:把排好序、截断后的参数数组(strArr)加上密钥(secretKey)搅出 MD5,贴在字符串最后。和第一处调用不同的是第一处的密钥是jhS,第二处的密钥是secretKey。

和分析jhS密钥一样,对secretKey进行交叉引用,交叉引用结果的第一处就是给secretKey进行赋值。
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!
最后于 2026-6-30 11:25
被ODcat编辑
,原因: