飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 202|回复: 2

[Android] [实战] 我破我自己:3层防御,防了个寂寞

[复制链接]
  • TA的每日心情
    开心
    2019-3-15 11:00
  • 签到天数: 262 天

    [LV.8]以坛为家I

    发表于 昨天 20:56 | 显示全部楼层 |阅读模式
    本帖最后由 梦幻的彼岸 于 2026-8-4 21:26 编辑

    内容概要
    欢迎光临 Android 赛博修理铺! 今天接了个狠活:我破我自己。我手写了一个 Demo,自以为搞了 3 层防御,结果一拆……嗯,防了个寂寞。

    本期“维修”记录:

    青铜防御:明文硬编码。全局搜索,顺藤摸瓜,一秒拿下。
    白银防御:Base64 伪装。给代码上点“赛博卸妆水”,看看它的素颜。
    黄金防御:控制流“绕口令”。故意写的字符拼接逻辑,在控制流图 (CFG) 面前,就像看地铁线路图一样清晰。
    隐藏内鬼:!代码里藏着的真实安全漏洞。
    样本信息:

    文件名:CTF_Demo.apk
    MD5: c8b1fcce18bd73a257f37665fb97ddbd
    注:本修理铺所有拆解仅限个人技术探索。自己写的代码自己拆,遵纪守法,文明修机。

    一、 样本透视:载入 APK,防御在 Dashboard 前“裸奔”
    打开逆向工具,载入 CTF_Demo.apk。等待解析完成后,工具会生成一张“体检报告”(Dashboard 界面)。这张图往往是逆向者的第一手情报。
    1.jpg
    1. 夸张的“Dex Elements”:28万字符串的迷宫 最扎眼的是紫色的 39.17% string: 281560。28万多个字符串、23万多个方法。
    技术推测:一个轻量级 Demo 不可能有这么多代码。这通常是因为引入了庞大的第三方库(如 Kotlin 标准库、协程库 kotlinx.coroutines、AndroidX 等)。
    逆向视角:字符串越多,Flag 藏在里面的概率越大,但“噪音”也越多。这要求我们掌握过滤系统库的技巧,而不是盲目大海捞针。
    2. 左侧导航栏:技术栈底裤被看穿 看 DexClass 树状图:
    kotlin 和 kotlinx.coroutines 暴露了技术栈。协程常被用来做异步混淆,打乱逆向者的时间线分析。
    com 包是真正的“主战场”,业务逻辑和 Flag 必然藏在自定义包名下。
    修理铺小结: 光是载入这一步,APK 的技术栈、代码规模就已经透明了。接下来,我们正式动刀。
    二、 实战拆解:三层“纸糊”防御的覆灭
    在 CTF 逆向中,有一套经典链路:搜字符串 → 找交叉引用 → 分析代码逻辑。我们用这个思路来拆解这三层防御。
    2.1 青铜防御:明文硬编码与交叉引用(XREF)
    【第一步:过滤与搜索】 不要直接搜 AllStrings(包含系统库噪音),而是点击 AppStrings(仅应用自身字符串)。按 Ctrl+F 搜索关键词 flag。
    3.jpg
    在搜索结果底部,赫然出现:flag{hello_gda_simple}。 这就是第一层 Flag!所谓的“明文硬编码”,耗时不超过 3 秒。
    4.jpg
    CTF技巧:
    CTF题目中,Flag通常以字符串形式存在
    常用关键词:flag、ctf、key、secret、success
    善用 AppStrings 过滤系统库,专注应用自身字符串
    【第二步:交叉引用定位逻辑】 光找到字符串不够,需确认其作用。双击该字符串(或按 X 键),触发交叉引用(Cross Reference)。
    9.jpg
    操作步骤:
    在搜索结果中双击 flag{hello_gda_simple}跳转到字符串位置
    之后选中后按快捷键 X
    GDA弹出"GDA Searching"窗口,显示所有引用该字符串的代码位置
    8.jpg
    结果分析: 看右边的搜索结果:
    callor(1 results):显示只有 1 个地方引用了这个字符串
    checkFlag(String):void:引用它的函数名居然直接叫 checkFlag(检查Flag)!
    修理铺吐槽:我作为出题人,连函数名都懒得混淆,直接写 checkFlag……这哪里是防御,这简直是给逆向者指路:"嘿,Flag验证逻辑在这个函数里,快来拆我!"
    CTF技巧:
    交叉引用(XREF)是逆向工程的核心技能
    从"数据"(字符串)定位到"代码"(函数)
    函数名往往暴露其功能(如checkFlag、verify、validate)
    2.2 白银防御:Base64 伪装与“赛博卸妆”
    双击 checkFlag 进入代码全览,三层防御的骨架彻底暴露:
    10.jpg
    [Java] 纯文本查看 复制代码
    private void checkFlag(String input){
        String simpleFlag = "flag{hello_gda_simple}"; // 青铜
        String encodedFlag = "ZmxhZ3tiYXNlNjRfaXNfZWFzeX0="; // 白银
     
        // ... 省略部分逻辑 ...
        if (input.equals(simpleFlag)) {
            this.showSuccess(R$string.level1_passed); return;
        } else {
            // 技术细节:Base64.NO_WRAP 对应数值 2,表示编码不换行
            String userBase64 = Base64.encodeToString(input.getBytes(StandardCharsets.UTF_8), 2);
            if (userBase64.equals(encodedFlag)) {
                this.showSuccess(R$string.level2_passed); return;
            }
            // ... 黄金防御 ...
        }
    }

    破解白银防御】 看到 ZmxhZ3tiYXNlNjRfaXNfZWFzeX0=,传统思路是切浏览器找在线解码。但高效的逆向者会直接使用工具内置的算法模块。
    11.jpg
    选择 Base64 解密,一键执行,明文 flag{base64_is_easy} 直接浮现。
    原理延伸:Base64 只是编码(Encoding)而非加密(Encryption),它没有密钥,目的仅是数据格式化,因此防不住逆向。
    5.jpg
    2.3 黄金防御:控制流混淆与反编译“照妖镜”
    代码第 30 行:input.equals(this.OBFUSCATED_FLAG)。这个变量是在构造函数中通过 generateObfuscatedFlag() 动态生成的。
    2026-07-28-14-21-07-image.png
    【源码 vs 反编译代码对比】 我故意在源码中加入了控制流混淆,但反编译引擎直接将其“降维”:

    对比项原始源代码 (Java/Kotlin)反编译结果 (伪代码)原理推测
    循环结构for (int i = 0; i < 5; i++)while (i < 5)编译器底层控制流还原
    字符串构建sb.append("fla")sb = sb+"fla"语法糖剥离,回归底层拼接
    Foreach循环for (char c : chars)for (int ix = 0; ...)迭代器语法被还原为索引遍历
    字符数组{'c','o','n','t'...}{'c','o','n','t'...}核心数据一字不改


    【技术深挖:无效计算(Opaque Predicate)】 源码中有一段奇怪的循环:
    [Java] 纯文本查看 复制代码
    int x = 10;
    for (int i = 0; i < 5; i++) {
        x = x + i;
        if (x > 15) { x = x - 2; }
    }

    手动模拟可知,这段代码无论怎么跑,都不会影响最终拼接的字符串。这在逆向工程中称为“不透明谓词”或“无效计算”,旨在干扰人工阅读。但在现代反编译引擎面前,只要它不改变最终字符数组 {'c','o','n'...} 的值,就形同虚设。 一眼读出第三层 Flag:flag{control_flow_test}。
    6.jpg
    三、 效率进阶:从“迷路”到“传送门”
    在破解过程中,我们其实可以走“捷径”。传统逆向往往从 AndroidManifest.xml 找入口 Activity,再一层层翻找 onCreate,在大型 APK 中极易“迷路”。
    3.1 入口点函数:一键直达核心
    12.jpg
    利用工具顶部的入口点函数按钮(紫色 Android 图标),可直接跳转至当前类的 onCreate 方法。 结合双击跳转和交叉引用(XREF),我们可以构建一条极速分析链路:
    入口点直达 onCreate -> 发现按钮监听器 MainActivity$$ExternalSyntheticLambda0。
    双击跳转 至 Lambda 类 -> 发现核心调用 this.checkFlag(userInput)。
    双击跳转 至 checkFlag -> 三层 Flag 全部拿下。
    逆向方法论总结:
    宏观定位:用入口点功能快速锁定程序起点。
    微观追踪:用 XREF(交叉引用)追踪数据(字符串/变量)的流向。
    全局搜索:用 Ctrl+F 查漏补缺,寻找遗漏的敏感词。
    四、 安全审计:代码里的“隐藏内鬼”
    除了 CTF 解题,这段代码在真实商业环境中,其实埋下了严重的安全隐患。通过全代码审计,我们发现了以下“内鬼”:
    2.jpg
    漏洞 1:敏感信息硬编码(高危)
    代码位置: checkFlag() 方法第 16-17 行
    [Java] 纯文本查看 复制代码
    String simpleFlag = "flag{hello_gda_simple}";String encodedFlag = "ZmxhZ3tiYXNlNjRfaXNfZWFzeX0=";

    风险原理:
    明文存储:Flag 直接以字符串形式嵌入代码,反编译后立即可见
    弱编码伪装:ZmxhZ3tiYXNlNjRfaXNfZWFzeX0= 仅是 Base64 编码,不是加密,解码即得 flag{base64_is_easy}
    字符串常量池暴露:Java 编译器会将字符串存储在 Dex 文件的字符串常量池中,即使使用 ProGuard/R8 混淆,也不会加密字符串内容,只会改变变量名
    攻击场景: 攻击者使用 GDA 的 AppStrings 功能搜索关键词 flag,3 秒内即可定位所有硬编码的敏感数据。
    修复方案:

    [Java] 纯文本查看 复制代码
    // 方案1:服务器端验证(推荐)
    private void verifyFlagOnServer(String input) {
        // 将用户输入发送到服务器验证
        // 客户端不存储任何 Flag
        okhttp3.Request request = new okhttp3.Request.Builder()
            .url("https://api.example.com/verify?input=" + hash(input))
            .build();
    }
     
    // 方案2:Native层存储(增加逆向难度)
    public native String getSecretKey();
    // 在 C/C++ 代码中存储,使用 JNI 调用
     
    // 方案3:运行时动态解密
    private String decryptFlag() throws Exception {
        byte[] encrypted = Base64.decode("ZW5jcnlwdGVkX2RhdGE=", Base64.DEFAULT);
        SecretKeySpec keySpec = new SecretKeySpec(getKey(), "AES");
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, keySpec);
        return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8);
    }

    漏洞 2:敏感日志泄露(中危)
    代码位置: checkFlag() 方法第 18 行
    [Java] 纯文本查看 复制代码
    Log.d("CTF_DEBUG", "User submitted sensitive data: " + input);

    风险原理:
    实时数据泄露:用户输入的 Flag(可能是密码、密钥等敏感信息)被写入 Android 系统日志
    无需权限读取:在 Root 设备上,任何应用只需执行 adb logcat 或申请 READ_LOGS 权限,即可实时截获日志
    调试代码未移除:生产环境保留了开发阶段的调试日志,违反了安全开发最佳实践
    攻击演示:
    [Java] 纯文本查看 复制代码
    # 攻击者连接设备后执行:
    adb logcat | grep "CTF_DEBUG"# 
    输出示例:# D/CTF_DEBUG: User submitted sensitive data: flag{hello_gda_simple}

    7.jpg
    修复方案:
    [Java] 纯文本查看 复制代码
    // 方案1:使用 BuildConfig 区分环境
    private static final boolean DEBUG = BuildConfig.DEBUG;
     
    private void logDebug(String message) {
        if (DEBUG) {
            Log.d("CTF", message);
        }
    }
     
    // 方案2:ProGuard 自动移除日志
    // 在 proguard-rules.pro 中添加:
    // -assumenosideeffects class android.util.Log {
    //     public static *** d(...);
    //     public static *** e(...);
    //     public static *** i(...);
    //     public static *** v(...);
    // }
     
    // 方案3:使用 Timber 日志框架
    if (BuildConfig.DEBUG) {
        Timber.d("User input length: %d", input != null ? input.length() : 0);
    }
    // Release 版本自动剥离日志
     
    // 方案4:数据脱敏
    private void logInput(String input) {
        // 不打印完整数据,只打印长度或哈希
        String masked = input != null ? 
            input.substring(0, Math.min(3, input.length())) + "***" : "null";
        Log.d("CTF", "Input received: " + masked);
    }

    漏洞 3:弱验证逻辑(中危)
    代码位置: checkFlag() 方法第 19-34 行

    [Java] 纯文本查看 复制代码
    if (input.equals(simpleFlag)) {
        this.showSuccess(R$string.level1_passed);
        return;
    }

    风险原理:

    内存明文对比:String.equals() 方法在内存中直接进行字符串逐字符比较,攻击者可使用 Frida、Xposed 等动态插桩工具 Hook 该方法
    无完整性保护:验证逻辑完全在客户端执行,没有数字签名、HMAC 等防篡改机制
    可被绕过:攻击者无需知道 Flag,只需修改验证结果即可通过
    Frida 攻击演示:

    [JavaScript] 纯文本查看 复制代码
    // 攻击者编写 Frida 脚本:
    Java.perform(function() {
        var MainActivity = Java.use("com.gda.test.ctfdemo.MainActivity");
     
        // 方法1:直接 Hook checkFlag
        MainActivity.checkFlag.implementation = function(input) {
            // 无视输入,直接调用成功逻辑
            this.showSuccess(2131099648); // R$string.level1_passed 的资源 ID
        };
     
        // 方法2:Hook String.equals
        var String = Java.use("java.lang.String");
        String.equals.overload("java.lang.Object").implementation = function(other) {
            // 强制返回 true
            return true;
        };
    });
     
    // 执行:
    // frida -U -f com.gda.test.ctfdemo -l bypass.js

    修复方案:


    [Java] 纯文本查看 复制代码
    // 方案1:服务器端验证(彻底解决)
    private void verifyFlag(String input) {
        String timestamp = String.valueOf(System.currentTimeMillis());
        String signature = generateHMAC(input + timestamp, SECRET_KEY);
     
        // 发送到服务器验证
        RequestBody body = new FormBody.Builder()
            .add("input", hash(input))
            .add("timestamp", timestamp)
            .add("signature", signature)
            .build();
     
        // 服务器返回验证结果
    }
     
    // 方案2:多层验证(增加难度)
    private boolean verifyFlag(String input) {
        // 第一层:哈希对比(仍不推荐,但比明文好)
        String inputHash = sha256(input);
        if (!inputHash.equals(EXPECTED_HASH)) {
            return false;
        }
     
        // 第二层:时间戳验证(防重放)
        long currentTime = System.currentTimeMillis();
        if (currentTime - lastVerifyTime < 1000) {
            return false; // 1秒内重复验证,疑似自动化攻击
        }
        lastVerifyTime = currentTime;
     
        // 第三层:环境检测
        if (isRooted() || isEmulator()) {
            return false; // Root 或模拟器环境,拒绝验证
        }
     
        return true;
    }
     
    // 方案3:代码混淆 + 反调试
    // 使用 ProGuard/R8 混淆类名和方法名
    // 添加反调试检测,发现 Frida/Xposed 时退出应用

    漏洞 4:控制流可预测(低危)
    代码位置: generateObfuscatedFlag() 方法

    [Java] 纯文本查看 复制代码
    char[] chars = {
        'c', 'o', 'n', 't', 'r', 'o', 'l', '_', 
        'f', 'l', 'o', 'w', '_', 't', 'e', 's', 't'
    };

    风险原理:

    字符数组明文:虽然使用了字符数组拼接,但数组内容在 Dex 文件中完全可见
    无效混淆:while 循环中的 x = x + i; if (x > 15) x = x - 2; 是不透明谓词(Opaque Predicate),但计算结果不影响最终 Flag,逆向者可直接忽略
    内存驻留:Flag 在运行时以明文形式存在于内存中,可被内存扫描工具提取
    修复方案:

    [Java] 纯文本查看 复制代码
    // 方案1:Native层动态生成
    public native String generateFlag();
    // 在 C/C++ 代码中实现,增加逆向难度
     
    // 方案2:运行时解密 + 内存清理
    private String getDecryptedFlag() {
        byte[] encrypted = {0x12, 0x34, 0x56, ...}; // 加密数据
        byte[] key = getKeyFromNative();            // 从 Native 层获取密钥
     
        try {
            byte[] decrypted = aesDecrypt(encrypted, key);
            String flag = new String(decrypted, StandardCharsets.UTF_8);
     
            // 立即清理敏感数据
            Arrays.fill(encrypted, (byte) 0);
            Arrays.fill(decrypted, (byte) 0);
            Arrays.fill(key, (byte) 0);
     
            return flag;
        } catch (Exception e) {
            return null;
        }
    }
     
    // 方案3:分片存储 + 动态拼接
    private String buildFlag() {
        // 从不同位置获取碎片
        String part1 = getResources().getString(R.string.flag_part1);
        String part2 = getFromSharedPreferences("flag_part2");
        String part3 = getFromNative();
     
        // 动态拼接
        return part1 + part2 + part3;
    }



    评分

    参与人数 2威望 +2 飘云币 +3 收起 理由
    wx69wx2025 + 1 PYG有你更精彩!
    smallhorse + 2 + 2 原创精品 感谢分享!

    查看全部评分

    PYG19周年生日快乐!
  • TA的每日心情
    开心
    2025-1-14 13:49
  • 签到天数: 393 天

    [LV.9]以坛为家II

    发表于 昨天 22:29 | 显示全部楼层
    PYG有你更精彩!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2025-1-12 18:24
  • 签到天数: 1515 天

    [LV.Master]伴坛终老

    发表于 昨天 22:32 | 显示全部楼层
    顶一波原创
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 加入我们

    本版积分规则

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