吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 203|回复: 2
上一主题 下一主题
收起左侧

[会员申请] 申请会员ID:daxi0ng【申请通过,未报到】

[复制链接]
跳转到指定楼层
楼主
吾爱游客  发表于 2026-1-16 15:25 回帖奖励 |自己
1、申 请 I D :daxi0ng
2、个人邮箱:2031553790@qq.com
3、原创技术文章:   
https://blog.csdn.net/qq_40989258/article/details/156990279

[安卓逆向+lposed]破解某中介招工app加锁信息功能
​
一、逆向学习,发现手头正好有个app就尝试一下。

首先进入招工信息列表页,reqable抓包发现服务器返回的全部都是密文,但是点进具体的招工详情就没有数据包发送了,并且在联系方式处可以通过下载广告app解锁明文,所以app客户端肯定是有解密功能,这个时候直截了当2个思路

1、欺骗app已经下载广告app

2、解密明文显示(也是这次的路线)

二、FrIDA启动,这里写了一个进行点击操作后app调用了具体哪个active的脚本,能帮助我们快速定位hook的位置
[JavaScript] 纯文本查看 复制代码
'use strict';

Java.perform(function () {

    console.log('=== click trace start ===');

    // 1️⃣ Hook 所有 View 点击(最重要)
    var View = Java.use('android.view.View');

    View.performClick.implementation = function () {
        try {
            var cls = this.getClass().getName();
            console.log('[CLICK] View.performClick -> ' + cls);

            // 打印父类,帮助判断 RecyclerView
            var parent = this.getParent();
            if (parent) {
                try {
                    console.log('  parent -> ' + parent.getClass().getName());
                } catch (e) {}
            }
        } catch (e) {}

        return this.performClick();
    };


    // 2️⃣ Hook RecyclerView onBind(确认是不是 RecyclerView)
    try {
        var RVAdapter = Java.use('androidx.recyclerview.widget.RecyclerView$Adapter');
        RVAdapter.onBindViewHolder.implementation = function (holder, position) {
            console.log('[RV] onBindViewHolder position = ' + position +
                ' holder = ' + holder.getClass().getName());
            return this.onBindViewHolder(holder, position);
        };
    } catch (e) {
        console.log('[RV] Adapter hook failed (maybe no RecyclerView)');
    }


    // 3️⃣ Hook View.setOnClickListener(看点击逻辑在哪)
    View.setOnClickListener.implementation = function (listener) {
        try {
            var viewCls = this.getClass().getName();
            var lCls = listener ? listener.getClass().getName() : 'null';
            console.log('[SET_CLICK] ' + viewCls + ' -> ' + lCls);
        } catch (e) {}

        return this.setOnClickListener(listener);
    };


    // 4️⃣ Hook Activity 跳转(看是不是点 item 后进详情页)
    var Activity = Java.use('android.app.Activity');

    Activity.startActivity.overload('android.content.Intent').implementation =
        function (intent) {

            try {
                var target = intent.getComponent();
                if (target) {
                    console.log('[START_ACTIVITY] ' + target.getClassName());
                } else {
                    console.log('[START_ACTIVITY] implicit intent');
                }
            } catch (e) {}

            return this.startActivity(intent);
        };


    console.log('=== click trace hook installed ===');
});


三、bingo!找到了进入招工详情页时实际调用的组件com.android.xxx.InfoActivity

四、打开我们的jadx看看反编译的java代码

五、这里就可以看看代码具体做了什么,数据是怎么传进 InfoActivity 的?

翻了一下发现这串代码

[JavaScript] 纯文本查看 复制代码
@Override
public final void d(Intent intent) {
    intent.getStringExtra(b.a.o("XgY="));
    this.f3650b =
        (MMList.DataEntity) intent.getParcelableExtra(b.a.o("UwMfEg=="));
}


重点来了:

    MMList.DataEntity 是一个实现了Parcelable接口的类

    列表页点击子项时,等于直接把当前 item 对象 putExtra 进 Intent

六、但是问题接着来了,当我尝试引用f3650b的时候,发现没有找到名为 f3650b 的字段。

[JavaScript] 纯文本查看 复制代码
'use strict';
 
Java.perform(function () {
 
    const InfoActivity = Java.use('com.android.xxx.InfoActivity');
 
    function findFieldRecursive(obj, fieldName) {
        let clazz = obj.getClass();
        while (clazz !== null) {
            try {
                const field = clazz.getDeclaredField(fieldName);
                field.setAccessible(true);
                return field;
            } catch (e) {
                clazz = clazz.getSuperclass();
            }
        }
        return null;
    }
 
    InfoActivity.d.overload('android.content.Intent').implementation = function (intent) {
 
        this.d(intent);
 
        const field = findFieldRecursive(this, 'f3650b');
 
        if (field === null) {
            console.log('[!] f3650b not found');
            return;
        }
 
        const value = field.get(this);
        console.log('[+] f3650b found:', value);
    };
 
});


七、那么只有一种解释了,

f3650b 这个字段名在真实 dex 里根本不存在,在 jadx 里看到的 f3650b,只是 反编译器“重命名后的别名”。

看到的代码是:

public MMList.DataEntity f3650b;

但实际上在 dex 里,很可能是:

public Object a;

或者:

public MMList$DataEntity b;

很显然JVM并不知道,它只认识真实的字段名,所以现在也不要再去猜测,直接从 Intent 里把 Parcelable 拿出来,真正的数据源是 Intent.getParcelableExtra()

于是有了这个代码,成功获取到解密后的数据:

[JavaScript] 纯文本查看 复制代码
'use strict';
 
Java.perform(function () {
 
    const InfoActivity = Java.use('com.android.xxx.InfoActivity');
 
    InfoActivity.d.overload('android.content.Intent').implementation = function (intent) {
 
        // 调用原方法
        this.d(intent);
 
        const extras = intent.getExtras();
        if (!extras) return;
 
        extras.keySet().toArray().forEach(function (key) {
            const val = extras.get(key);
 
            console.log('\n[key] ' + key);
            console.log('[class] ' + (val ? val.getClass().getName() : 'null'));
            console.log('[value] ' + val);
 
            // 直接 dump 字段
            if (val) dumpFields(val);
        });
    };
 
    function dumpFields(obj) {
        try {
            obj.getClass().getDeclaredFields().forEach(function (f) {
                f.setAccessible(true);
                console.log(f.getName() + ' = ' + f.get(obj));
            });
        } catch (e) {
            console.log('[dump error]', e);
        }
    }
 
});


八、目前位置我们做到了

点击加锁信息单元->frida脚本显示加锁内容

继续我想既然明文可以获取到,直接让明文代替到需要解锁才能显示的UI位置不就好了,说干就干。

我们在InfoActivity看到这么一串代码,经过调用解密发现“xxx”这一串就是“点击解锁查看”,也就是UI需要替换的位置。
[JavaScript] 纯文本查看 复制代码
    public final void m(TextView textView, String str) {
        StringBuilder sbQ = androidx.appcompat.app.a.q(str);
        sbQ.append(b.a.o("xxx"));
        String string = sbQ.toString();
        SpannableString spannableString = new SpannableString(string);
        spannableString.setSpan(new ForegroundColorSpan(Color.parseColor(b.a.o("FAQNR1ZLBA=="))), 3, string.length(), 17);
        textView.setText(spannableString);
        textView.setOnClickListener(new a());
    }


九、经过一番捣鼓,搞定下列代码,成功在UI页面原需解锁位置直接显示明文
[JavaScript] 纯文本查看 复制代码
Java.perform(function () {
 
    // Hook 的目标 Activity
    var Info = Java.use("com.android.xxx.InfoActivity");
 
    // UI 相关类(富文本 + 颜色)
    var SpannableString = Java.use("android.text.SpannableString");
    var ForegroundColorSpan = Java.use("android.text.style.ForegroundColorSpan");
    var Color = Java.use("android.graphics.Color");
    var JString = Java.use("java.lang.String");
 
    // 反射读取对象字段,失败返回空字符串
    function getField(obj, name) {
        try {
            var f = obj.getClass().getDeclaredField(name);
            f.setAccessible(true);
            var v = f.get(obj);
            return v ? v.toString() : "";
        } catch (e) {
            return "";
        }
    }
 
    // Hook InfoActivity.m(TextView, String)
    Info.m.overload(
        'android.widget.TextView',
        'java.lang.String'
    ).implementation = function (tv, label) {
 
        // 先执行原方法,保证原 UI 正常
        this.m(tv, label);
 
        try {
            // 获取启动 Intent
            var intent = this.getIntent();
            if (!intent) return;
 
            // 取页面数据实体(Parcelable)
            var data = intent.getParcelableExtra("data");
            if (!data) return;
 
            // 从实体中读取真实字段值
            var phone   = getField(data, "phone");
            var qq      = getField(data, "qq");
            var wechat  = getField(data, "wechat");
            var address = getField(data, "address");
 
            // 根据 label 决定展示哪个字段
            var value = "";
            if (label.indexOf("QQ") !== -1) {
                value = qq;
            } else if (label.indexOf("微信") !== -1) {
                value = wechat;
            } else if (label.indexOf("电话") !== -1) {
                value = phone;
            } else if (label.indexOf("地址") !== -1) {
                value = address;
            } else {
                return;
            }
 
            // 拼接完整文本(即使 value 为空也显示)
            var full = label + value;
 
            // 构造富文本并设置 value 为灰色
            var sp = SpannableString.$new(JString.$new(full));
            var color = Color.parseColor("#ff808080");
            sp.setSpan(
                ForegroundColorSpan.$new(color),
                label.length,
                full.length,
                17 // Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
            );
 
            // 替换 TextView 内容
            tv.setText(sp);
 
            // 日志输出
            console.log("[unlock]", full);
 
        } catch (e) {
            // 防止 UI 崩溃
            console.log("[!] m ui error:", e);
        }
    };
 
    console.log("[+] InfoActivity.m dynamic unlock");
});


十、完美解决

但是总挂一个firda多麻烦呀,不如改成lposed模块,java代码如下,用as编译成apk安装启用就ok
[Java] 纯文本查看 复制代码
package com.example.decrypthook;
 
// Android 基础类
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.widget.TextView;
 
// Xposed 相关接口和工具
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;
 
import java.lang.reflect.Field;
 
// Xposed 入口类
public class HookEntry implements IXposedHookLoadPackage {
 
    // 每加载一个 App 都会调用
    @Override
    public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
 
        try {
            // 找到目标 Activity 类
            Class<?> infoCls = XposedHelpers.findClass(
                    "com.android.xxx.InfoActivity",
                    lpparam.classLoader
            );
 
            // Hook InfoActivity.m(TextView, String)
            XposedHelpers.findAndHookMethod(
                    infoCls,
                    "m",                 // 方法名
                    TextView.class,      // 参数1
                    String.class,        // 参数2
                    new XC_MethodHook() {
 
                        // 原方法执行完成后再执行这里
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
 
                            // 第一个参数:TextView
                            TextView tv = (TextView) param.args[0];
 
                            // 第二个参数:label 文本
                            String label = (String) param.args[1];
 
                            // 当前 Activity 实例
                            Activity act = (Activity) param.thisObject;
 
                            try {
                                // 取启动 Activity 的 Intent
                                Intent intent = act.getIntent();
                                if (intent == null) return;
 
                                // 从 Intent 中取 Parcelable 数据对象
                                Object data = intent.getParcelableExtra("data");
                                if (data == null) return;
 
                                // 通过反射读取 data 中的字段
                                String phone   = getField(data, "phone");
                                String qq      = getField(data, "qq");
                                String wechat  = getField(data, "wechat");
                                String address = getField(data, "address");
 
                                // 根据 label 内容判断显示哪个字段
                                String value = null;
                                if (label.contains("QQ")) {
                                    value = qq;
                                } else if (label.contains("微信")) {
                                    value = wechat;
                                } else if (label.contains("电话")) {
                                    value = phone;
                                } else if (label.contains("地址")) {
                                    value = address;
                                }
 
                                // 即使为 null 也强制显示
                                if (value == null) value = "";
 
                                // 拼接 label + 真实数据
                                String full = label + value;
 
                                // 构造富文本
                                SpannableString sp = new SpannableString(full);
 
                                // 设置灰色字体
                                int color = Color.parseColor("#ff808080");
 
                                // 只给 value 部分上色
                                sp.setSpan(
                                        new ForegroundColorSpan(color),
                                        label.length(),         // 起始位置
                                        full.length(),          // 结束位置
                                        SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
                                );
 
                                // &#11088; 替换 TextView 显示内容
                                tv.setText(sp);
 
                            } catch (Throwable t) {
                                // 防止 Hook 失败导致闪退
                                XposedBridge.log("[DecryptHook] m ui error: " + t);
                            }
                        }
                    }
            );
 
        } catch (Throwable t) {
            // Hook 类或方法失败
            XposedBridge.log("[DecryptHook] hook failed: " + t);
        }
    }
 
    // 通过反射读取对象字段值
    private static String getField(Object obj, String name) {
        try {
            // 获取字段
            Field f = obj.getClass().getDeclaredField(name);
            f.setAccessible(true);
 
            // 读取字段值
            Object v = f.get(obj);
 
            // 转成字符串返回
            return v != null ? v.toString() : "";
        } catch (Throwable t) {
            // 字段不存在或读取失败
            return "";
        }
    }
}


十一、整体的流程大概就是这个样子

[列表页 RecyclerView item]
            │
            │ 点击
            ▼
[startActivity(Intent)]
            │
            │ intent.putExtra("data", MMList.DataEntity)
            ▼
[InfoActivity.onCreate / onResume]
            │
            │ 系统 inflate 布局
            │
            ▼
[InfoActivity.f()]
  ├─ tv.setText(...)
  ├─ &#9888;&#65039; m(tvQq,   "QQ:")
  ├─ &#9888;&#65039; m(tvWechat,"微信:")
  ├─ &#9888;&#65039; m(tvPhone,"电话:")
  ├─ &#9888;&#65039; m(tvAddress,"地址:")
            │
            │ m() 内部逻辑
            ▼
[m(TextView, label)]
  ├─ label + b.a.o("xxx")
  ├─ 显示“点击解锁信息”
  └─ setOnClickListener(弹广告)
            │
            │
            │【我们 Hook 在这里】
            ▼
[LSPosed hook InfoActivity.m]
  ├─ afterHookedMethod()
  ├─ 从 Activity.getIntent() 取 data
  ├─ 反射 data.phone / qq / wechat / address
  ├─ 判断 label 是哪一行
  ├─ label + 明文值(空也覆盖)
  ├─ SpannableString 重建
  └─ tv.setText(新内容)
            │
            ▼
[UI 直接显示明文,无需点击解锁]

十二、走过的一些弯路

1、开始执着于解密算法,但后面发现对于info页面是多此一举,因为列表页 / 详情页 ≠ 同一条数据链,InfoActivity 里已经是 明文 DataEntity。

2、取字段过早,试图在 onCreate / onResume 直接改 TextView 内容,UI 尚未绑定 TextView,m() 还没被调用,导致空值 / 错位。
&#8203;

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

沙发
Hmily 发表于 2026-1-16 18:32
I D :daxi0ng
邮箱:2031553790@qq.com

申请通过,欢迎光临吾爱破解论坛,期待吾爱破解有你更加精彩,ID和密码自己通过邮件密码找回功能修改,请即时登陆并修改密码!
登陆后请在一周内在此帖报道,否则将删除ID信息。
3#
daxi0ng 发表于 2026-1-19 14:05

本版积分规则

返回列表

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

GMT+8, 2026-1-24 06:06

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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