吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 849|回复: 1
收起左侧

[Android 原创] RR视频.apk客户端登录协议分析

  [复制链接]
xvguu 发表于 2026-6-23 19:34
本文仅限安全研究与学习交流,严禁将本文内容用于任何商业或非法用途,本文涉及的代码及资源版权归原权利人所有,侵删。

APK版本——9.3.8,已经是上古时代的apk了,目前app已经停止运营。


所用工具:Fiddler、jadx-gui、DDMS、雷电模拟器9.1(64位)


首先配置模拟器代理(本机IP和Fiddler的默认端口号888)
5e8cafd4af38723487439c6522dbd11d.png
在模拟器中启动App,选择“RR账号登录”,随机输入账号和密码。

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

四个包依次为:
公钥获取请求
客户端向 http://login.renren.com/ajax/getEncryptKey 发起 GET 请求,尝试获取 RSA 公钥。由于服务器已停止运营,该接口返回 404 Not Found。

登录请求
POST 至 http://api.m.renren.com/api/client/login,携带完整的登录参数(账号、密码密文、设备指纹、签名等)。


错误日志上报
登录失败后,客户端自动将错误信息(error_code=10002)打包上报至 /api/apierror 接口。


用户行为日志
客户端记录用户操作轨迹(如点击按钮、切换输入框等),并发往 /api/phoneclient/opLog2 用于埋点统计。


重点分析登录请求的数据包:登录请求体采用 application/x-www-form-urlencoded 格式,包含两个核心部分:client_info:经 URL 编码的 JSON 对象,内含设备指纹(IMEI、IMSI、型号、系统版本、屏幕分辨率等)。顶层业务参数:包括 user(账号)、password(密码密文)、call_id(时间戳)、api_key(应用公钥)、sig(签名)等。服务器返回的响应体为 Gzip 压缩格式,解压后实际内容为:{"error_code":10002, "error_msg":"账号或密码输入错误,请检查后重试"}




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

点击搜索结果,调用方法的反编译原型如下:
[Java] 纯文本查看 复制代码
public static void a(final String str, final String str2, int i, String str3, String str4, final Context context, final LoginStatusListener loginStatusListener)




分析a方法,首先调用A函数生成一个jsonObjectA对象
[Java] 纯文本查看 复制代码
JsonObject jsonObjectA = A(false, true);

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

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

生成第一个sig。

[Java] 纯文本查看 复制代码
String[] keys = jsonObjectA.getKeys();  // 获取所有参数名
for (String str5 : keys) {
    // 把 参数名=参数值 用 & 连成一长串
    sb.append(str5).append('=').append(URLEncoder.encode(string)).append('&');
    // 如果参数值太长(>50个字符),只截取前50个
}
// 调用 a(strArr, jhS) 生成最终的sig
jsonObjectA.put("sig", a(strArr, jhS));

跟进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的密钥值了。

[Java] 纯文本查看 复制代码
<string name="secretkey">ad974a0756d84cec80fcea72fcbfba9f</string>

再次回到最开始的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方法的代码最后几行:
[Java] 纯文本查看 复制代码
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进行赋值

回到对jhT的赋值处,也可以看到jhT也是通过string进行赋值为secretkey的值,也就是说第二次sig的密钥secretKey为secretkey的值,和第一次sig的密钥jhS的值相同

这里对数据包以及发送数据包的代码分析已经完毕,但是在这段代码中没有看到真正的登录逻辑,在模拟器中我们是点击登录按钮才触发的上面的一系列的事件,众所周知,按钮需要关注的代码为onClick,对a函数进行交叉引用,可以看到搜索结果有两处是在onClick中调用了a方法。两个onClick不同的是一个在loginB中调用,一个实在loginfree中调用。

这里为什么有两个onClick调用,猜测应该是在登录页面时有两种登录方式对应了两个onClick的调用。我们这里使用的是“人人账号登录”,为了定位到想分析的onClick,这里使用ddms的方法刨析进行验证。        win+r输入ddms即可打开ddms。        点击 DDMS 工具栏上开始方法分析的按钮。是三个小箭头指向一个红点。在弹出的对话框中,直接点击 OK 使用默认设置即可。在模拟器上快速点击“登录”按钮。再次快速点击 DDMS 中那个开始分析的按钮来停止记录。这时会打开Traceview工具窗口,展示完整的分析结果。在打开的方法刨析窗口中搜索onClick,成功找到点击登录时调用的onClick的具体位置:com.renren.mobile.android.loginB.register.ui.LoginFromQuickRegisterFragment.onClick很显然我们的登录方式是在loginB中被使用,追踪分析反编译代码。主要使用了 switch-case 语句,根据被点击视图的 ID(view.getId())来决定执行哪一段代码逐个按钮进行分析1.R.id.password_inputtype_change —— 密码可见/不可见切换fKI 是一个布尔值,记录当前密码是否可见,fMk 是一个 ImageView,用来显示小眼睛图标,fPk 是密码输入框(EditText)。点击后切换密码的显示方式,同时更换图标,最后把光标移到输入框末尾(方便继续输入)。
[Java] 纯文本查看 复制代码
if (this.fKI) {
    // 当前是可见状态,改为不可见
    this.fMk.setImageResource(R.drawable.intput_passwod_visiable);
    this.fPk.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
    this.fKI = false;
} else {
    // 当前是不可见状态,改为可见
    this.fKI = true;
    this.fMk.setImageResource(R.drawable.intput_passwod_unvisiable);
    this.fPk.setTransformationMethod(PasswordTransformationMethod.getInstance());
}
this.fPk.setSelection(this.fPk.getText().length());
2. R.id.planb_login_from_publish_forgetpsd_btn —— 忘记密码创建一个数据包 Bundle,放入“返回按钮文字”、“中间标题”和一个 URL(this.fKr),然后调用 TerminalIAcitvity.a 方法,启动一个 WebView 页面,用于显示忘记密码的网页。
[Java] 纯文本查看 复制代码
Bundle bundle = new Bundle();
bundle.putString("titleLeft", ...);
bundle.putString("titleMiddle", ...);
bundle.putString("url", this.fKr);
TerminalIAcitvity.a(SY(), (Class<?>) BaseWebViewFragment.class, bundle);
3. R.id.planb_login_from_publish_login_btn —— 登录按钮清空一些缓存设置
[Java] 纯文本查看 复制代码
SettingManager.bwT().nD("");[/size][/font][font=宋体][size=4]获取用户输入的用户名和密码,fPl 是用户名输入框,fPk 是密码输入框,它们被存到静态变量 Variables 中
[/size][/font][font=宋体][size=4][mw_shl_code=java,true]Variables.hJU = this.fPl.getText().toString().trim();
Variables.password = this.fPk.getText().toString().trim();

接下来检查输入是否为空,如果用户名为空,弹出提示“账号不能为空”。如果密码为空,弹出提示“密码不能为空”。然后检查用户名是否包含中文,Methods.qq 判断是否包含中文字符的方法。如果包含中文,提示“账号不能包含中文”。
[Java] 纯文本查看 复制代码
if (Methods.qq(Variables.hJU)) { ... }
再检查密码是否包含中文,如果密码包含中文,提示“密码不能包含中文”。密码加密——如果服务器成功下发了RSA公钥,就用 RSA 非对称加密 保护密码。如果获取公钥失败,就降级为 MD5 加密。
[Java] 纯文本查看 复制代码
dPl = RSA.ccz();
this.n = RSA.ccB();
this.e = RSA.ccA();
if (dPl != null) {
    // 使用 RSA 公钥加密密码
    Variables.password = RSA.D(Variables.password, this.n, this.e);
    RSA.lAT = 1;
} else {
    // 否则使用 MD5 加密
    Variables.password = Md5.toMD5(Variables.password);
    RSA.lAT = 2;
}
发起登录请求,显示加载进度,然后调用 ServiceProvider.a 发送登录请求,参数里包含了加密后的密码。
[Asm] 纯文本查看 复制代码
if (用户名和密码非空) {
    if (this.fPp != null) {
        this.fPp.show(); // 显示一个进度条
    }
    // 调用网络请求,把用户名、密码等发给服务器
    ServiceProvider.a(Variables.hJU, Variables.password, 1, "", dPl, SY(), this.dPt);
}
4.R.id.service_configuration —— 调试配置启动一个调试管理页面
[Asm] 纯文本查看 复制代码
startActivityForResult(new Intent(SY(), (Class<?>) DebugManagerActivity.class), -1);
5.第三方登录按钮(微信、QQ、微博)调用同一个方法 oy(),传入不同数字区分平台,进行第三方授权登录
[Asm] 纯文本查看 复制代码
case R.id.third_login_layout_weixin_button:
    oy(2);  // 微信
    break;
case R.id.third_login_layout_qq_button:
    oy(1);  // QQ
    break;
case R.id.third_login_layout_weibo_button:
    oy(3);  // 微博
    break;
至此,登录的核心逻辑也已经分析完毕,这里补充一下查看按钮ID值的方法:打开下载SDK目录下的tools文件夹中的uiautomatorviewer.bat工具栏主要包含四个按钮(从右向左数):Open Files:打开已保存的 .uix 布局文件和截图。Device Screenshot:连接设备并获取当前屏幕的界面布局(最常用)。Compressed Hierarchy:获取简化版的界面布局,会过滤掉一些纯装饰性的布局节点。Save:保存当前捕获的界面信息,会生成一个截图文件和一个 .uix (XML布局结构) 文件。接下来分析用来加密的算法RSA。算法主要做了两件事,一是从服务器获取 RSA 公钥(auN() 方法),二是用公钥加密明文密码(D() 方法)。一、静态变量(存储加密参数)dPl 就是之前登录代码里判断是否为 null 的那个变量。lAT 用来标记最终使用的是哪种加密方式,便于后续日志或统计。
[Java] 纯文本查看 复制代码
public static String dPl = null;   // 公钥字符串(从服务器获取)
public static String e = null;     // RSA 加密指数(Exponent)
public static String n = null;     // RSA 模数(Modulus)
public static int lAT = 0;         // 加密模式标记:1=RSA,2=MD5(降级)
二、auN():从服务器获取 RSA 公钥向 http://login.renren.com/ajax/getEncryptKey 发起 HTTP GET 请求。读取服务器返回的第一行内容(应为公钥字符串)。如果读取不到,返回 null。
[Java] 纯文本查看 复制代码
static /* synthetic */ String auN() throws IOException {
    BufferedReader bufferedReader = new BufferedReader(
        new InputStreamReader(
            new URL(" http://login.renren.com/ajax/getEncryptKey").openStream(), 
            "GB2312"
        )
    );
    String line = bufferedReader.readLine();
    bufferedReader.close();
    if (line != null) {
        return line;
    }
    return null;
}
三、D():RSA 加密核心方法创建 RSA 加密器(Cipher.getInstance)。用传入的模数 str2(n)和指数 str3(e)构造公钥,初始化加密模式。对明文密码(str.getBytes())进行 RSA 加密,得到加密后的字节数组。将字节数组逐字节转为 16 进制字符串,拼接成最终的密文。
[Java] 纯文本查看 复制代码
public static String D(String str, String str2, String str3) 
    throws BadPaddingException, NoSuchPaddingException, 
           IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeyException {
    
    Cipher cipher = Cipher.getInstance(RSAUtil.ALGORITHM_RSA);
    cipher.init(1, bu(str2, str3));  // 1 = ENCRYPT_MODE
    byte[] bArrDoFinal = cipher.doFinal(str.getBytes());
    
    StringBuffer stringBuffer = new StringBuffer();
    for (byte b : bArrDoFinal) {
        stringBuffer.append(Integer.toString((b & 255) + 256, 16).substring(1));
    }
    return stringBuffer.toString();
}
bu(str2, str3) 是 new RSAPublicKeySpec(n, e) 的封装,用于从 n 和 e 生成 Java 的 PublicKey 对象。至此,登录的加密算法也已经分析完毕,采用的是标准 RSA 非对称加密来保护密码传输。Frida HOOK RSA中的D算法
[Java] 纯文本查看 复制代码
jscode ="""
Java.perform(function () {
    var RSA = Java.use('com.renren.mobile.utils.RSA');

    // 精确指定重载:D(String, String, String)
    var D = RSA.D.overload('java.lang.String', 'java.lang.String', 'java.lang.String');

    D.implementation = function (plaintext, n, e) {
        console.log("\n[+] Hook D() 被触发!");
        console.log("[+] 明文密码: " + plaintext);
        console.log("[+] 模数 n: " + n);
        console.log("[+] 指数 e: " + e);

        // 调用原方法计算密文
        var result = this.D(plaintext, n, e);

        console.log("[+] RSA 密文: " + result);
        console.log("[+] Hook 结束\n");

        return result;
    };

    console.log("[*] RSA.D() Hook 已安装");
});
"""
Xposed HOOK RSA中的D算法
[Java] 纯文本查看 复制代码
package com.qianyu.xposedhook;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Module implements IXposedHookLoadPackage {

    @Override
    public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.renren.mobile.android")) {
            return;
        }

        XposedBridge.log("目标包名: " + lpparam.packageName);

        // Hook RSA.D(String, String, String) —— RSA 加密方法
        XposedHelpers.findAndHookMethod(
            "com.renren.mobile.utils.RSA",          // 类名
            lpparam.classLoader,                    // 类加载器
            "D",                                    // 方法名
            String.class,                           // 参数1:明文密码
            String.class,                           // 参数2:模数 n
            String.class,                           // 参数3:指数 e
            new XC_MethodHook() {

                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    XposedBridge.log("========== RSA.D() 被调用 ==========");
                    XposedBridge.log("密码明文: " + param.args[0]);
                    XposedBridge.log("模数 n  : " + param.args[1]);
                    XposedBridge.log("指数 e  : " + param.args[2]);

                    // 打印调用堆栈(帮助定位调用来源)
                    XposedBridge.log("调用堆栈:");
                    StackTraceElement[] stack = new Throwable().getStackTrace();
                    for (StackTraceElement elem : stack) {
                        XposedBridge.log("  " + elem.toString());
                    }
                }

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    // 获取加密后的结果(十六进制字符串)
                    Object result = param.getResult();
                    XposedBridge.log("RSA 密文: " + (result != null ? result.toString() : "null"));
                    XposedBridge.log("========== RSA.D() 结束 ==========");
                    // 注意:不要修改 result,直接返回原值
                }
            }
        );
编译打包后,安装模块执行后HOOK的结果链接: https://pan.baidu.com/s/1bigpkMPgi9UqBmnYaKqnRw 提取码: 6md3

免费评分

参与人数 2吾爱币 +2 热心值 +1 收起 理由
springlu + 1 我很赞同!
shiyue2925 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| xvguu 发表于 2026-6-25 14:41
图片不知道为什么挂掉了很多,大家将就看吧
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-22 04:39

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表