跳过内容

移动应用代码质量

移动应用开发者使用各种编程语言和框架。因此,当忽略安全编程实践时,SQL 注入、缓冲区溢出和跨站脚本 (XSS) 等常见漏洞可能会在应用中显现。

相同的编程缺陷可能会在某种程度上影响 Android 和 iOS 应用,因此我们将在本指南的通用部分概述最常见的漏洞类别。在后面的章节中,我们将介绍特定于操作系统的实例和漏洞缓解功能。

注入缺陷

注入缺陷描述了一类安全漏洞,当用户输入被插入到后端查询或命令中时会发生此类漏洞。通过注入元字符,攻击者可以执行被错误地解释为命令或查询一部分的恶意代码。例如,通过操纵 SQL 查询,攻击者可以检索任意数据库记录或操纵后端数据库的内容。

此类漏洞在服务器端 Web 服务中最为普遍。可利用的实例也存在于移动应用中,但发生率较低,并且攻击面较小。

例如,虽然应用可能会查询本地 SQLite 数据库,但此类数据库通常不存储敏感数据(假设开发者遵循了基本的安全实践)。这使得 SQL 注入成为不可行的攻击向量。然而,有时会发生可利用的注入漏洞,这意味着适当的输入验证是程序员必要的最佳实践。

SQL 注入

SQL 注入攻击涉及将 SQL 命令集成到输入数据中,模仿预定义的 SQL 命令的语法。成功的 SQL 注入攻击允许攻击者读取或写入数据库,并可能执行管理命令,具体取决于服务器授予的权限。

Android 和 iOS 上的应用都使用 SQLite 数据库作为控制和组织本地数据存储的一种方式。假设一个 Android 应用通过将用户凭据存储在本地数据库中来处理本地用户身份验证(为了本示例起见,我们将忽略这种不良的编程实践)。登录时,该应用会查询数据库以搜索包含用户输入用户名和密码的记录

SQLiteDatabase db;

String sql = "SELECT * FROM users WHERE username = '" +  username + "' AND password = '" + password +"'";

Cursor c = db.rawQuery( sql, null );

return c.getCount() != 0;

让我们进一步假设攻击者在“用户名”和“密码”字段中输入以下值

username = 1' or '1' = '1
password = 1' or '1' = '1

这将导致以下查询

SELECT * FROM users WHERE username='1' OR '1' = '1' AND Password='1' OR '1' = '1'

由于条件 '1' = '1' 始终评估为真,因此此查询返回数据库中的所有记录,导致登录功能返回 true,即使没有输入有效的用户帐户。

Ostorlab 使用此 SQL 注入有效负载,通过 adb 利用了 Yahoo 的天气移动应用的排序参数。

Mark Woods 在 QNAP NAS 存储设备上运行的“Qnotes”和“Qget”Android 应用中发现了另一个客户端 SQL 注入的真实示例。这些应用导出了容易受到 SQL 注入攻击的内容提供程序,从而允许攻击者检索 NAS 设备的凭据。有关此问题的详细描述,请参见 Nettitude 博客

XML 注入

XML 注入 攻击中,攻击者注入 XML 元字符以在结构上更改 XML 内容。这既可以用于破坏基于 XML 的应用程序或服务的逻辑,也可能允许攻击者利用处理内容的 XML 解析器的操作。

这种攻击的一个流行变体是 XML 外部实体 (XXE)。在这里,攻击者将包含 URI 的外部实体定义注入到输入 XML 中。在解析期间,XML 解析器通过访问 URI 指定的资源来扩展攻击者定义的实体。解析应用程序的完整性最终决定了攻击者拥有的能力,恶意用户可以执行以下任何(或所有)操作:访问本地文件、触发对任意主机和端口的 HTTP 请求、发起 跨站点请求伪造 (CSRF) 攻击,并导致拒绝服务情况。OWASP Web 测试指南包含 以下 XXE 示例

<?xml version="1.0" encoding="ISO-8859-1"?>
 <!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///dev/random" >]><foo>&xxe;</foo>

在此示例中,本地文件 /dev/random 被打开,其中返回无限的字节流,可能导致拒绝服务。

当前的应用开发趋势主要集中在基于 REST/JSON 的服务,因为 XML 变得越来越不常见。但是,在极少数情况下,当用户提供的或以其他方式不受信任的内容用于构造 XML 查询时,它可能会被本地 XML 解析器(例如 iOS 上的 NSXMLParser)解释。因此,应始终验证上述输入并转义元字符。

注入攻击向量

移动应用的攻击面与典型的 Web 和网络应用程序截然不同。移动应用通常不会在网络上公开服务,并且应用用户界面上的可行攻击向量很少。针对应用的注入攻击最有可能通过进程间通信 (IPC) 接口发生,恶意应用攻击设备上运行的另一个应用。

查找潜在漏洞首先要

  • 识别不受信任输入的可能入口点,然后从这些位置进行跟踪,以查看目标是否包含潜在的脆弱函数。
  • 识别已知、危险的库/API 调用(例如 SQL 查询),然后检查未经检查的输入是否成功与相应的查询接口。

在手动安全审查期间,您应该同时采用这两种技术。通常,不受信任的输入通过以下渠道进入移动应用

  • IPC 调用
  • 自定义 URL 方案
  • QR 码
  • 通过蓝牙、NFC 或其他方式接收的输入文件
  • 剪贴板
  • 用户界面

验证是否遵循了以下最佳实践

  • 不受信任的输入已进行类型检查和/或使用可接受值的列表进行验证。
  • 执行数据库查询时,使用带有变量绑定的预处理语句(即参数化查询)。如果定义了预处理语句,则会自动分离用户提供的数据和 SQL 代码。
  • 解析 XML 数据时,请确保解析器应用程序配置为拒绝解析外部实体,以防止 XXE 攻击。
  • 当处理 x509 格式的证书数据时,请确保使用安全的解析器。例如,Bouncy Castle 低于 1.6 的版本允许通过不安全的反射执行远程代码。

我们将在特定于操作系统的测试指南中介绍与输入源和潜在脆弱的 API 相关的详细信息。

跨站脚本缺陷

跨站脚本 (XSS) 问题允许攻击者将客户端脚本注入到用户查看的网页中。这种类型的漏洞在 Web 应用程序中很常见。当用户在浏览器中查看注入的脚本时,攻击者能够绕过同源策略,从而实现各种攻击(例如窃取会话 Cookie、记录按键、执行任意操作等)。

本机应用 的上下文中,XSS 风险远不如前者普遍,原因很简单,此类应用程序不依赖于 Web 浏览器。但是,使用 WebView 组件(例如 iOS 上的 WKWebView 或已弃用的 UIWebView 以及 Android 上的 WebView)的应用可能容易受到此类攻击。

一个较早但众所周知的示例是 iOS 版 Skype 应用中的本地 XSS 问题,最初由 Phil Purviance 发现。Skype 应用未能正确编码消息发送者的姓名,从而允许攻击者注入恶意 JavaScript,以便在用户查看消息时执行。在这个概念验证中,Phil 展示了如何利用该问题并窃取用户的地址簿。

静态分析 - 安全测试注意事项

仔细查看任何存在的 WebView,并调查该应用呈现的不受信任的输入。

如果 WebView 打开的 URL 部分由用户输入确定,则可能存在 XSS 问题。以下示例来自 Linus Särud 报告的 Zoho Web Service 中的 XSS 问题。

Java

webView.loadUrl("javascript:initialize(" + myNumber + ");");

Kotlin

webView.loadUrl("javascript:initialize($myNumber);")

用户输入确定的 XSS 问题的另一个示例是公共重写方法。

Java

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
  if (url.substring(0,6).equalsIgnoreCase("yourscheme:")) {
    // parse the URL object and execute functions
  }
}

Kotlin

    fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
        if (url.substring(0, 6).equals("yourscheme:", ignoreCase = true)) {
            // parse the URL object and execute functions
        }
    }

Sergey Bobrov 在以下 HackerOne 报告中利用了这一点。 Quora 的 ActionBarContentActivity 中将信任 HTML 参数的任何输入。使用 adb、通过 ModalContentActivity 的剪贴板数据以及来自第三方应用程序的 Intent 成功利用了有效负载。

  • ADB
$ adb shell
$ am start -n com.quora.android/com.quora.android.ActionBarContentActivity \
-e url 'http://test/test' -e html 'XSS<script>alert(123)</script>'
  • 剪贴板数据
$ am start -n com.quora.android/com.quora.android.ModalContentActivity  \
-e url 'http://test/test' -e html \
'<script>alert(QuoraAndroid.getClipboardData());</script>'
  • Java 或 Kotlin 中的第三方 Intent
Intent i = new Intent();
i.setComponent(new ComponentName("com.quora.android",
"com.quora.android.ActionBarContentActivity"));
i.putExtra("url","http://test/test");
i.putExtra("html","XSS PoC <script>alert(123)</script>");
view.getContext().startActivity(i);
val i = Intent()
i.component = ComponentName("com.quora.android",
"com.quora.android.ActionBarContentActivity")
i.putExtra("url", "http://test/test")
i.putExtra("html", "XSS PoC <script>alert(123)</script>")
view.context.startActivity(i)

如果 WebView 用于显示远程网站,则转义 HTML 的重任转移到服务器端。如果 Web 服务器上存在 XSS 缺陷,则可以使用它在 WebView 的上下文中执行脚本。因此,对 Web 应用程序源代码执行静态分析非常重要。

验证是否遵循了以下最佳实践

  • 除非绝对必要,否则不会在 HTML、JavaScript 或其他解释上下文中呈现不受信任的数据。
  • 应用适当的编码来转义字符,例如 HTML 实体编码。注意:当 HTML 嵌套在其他代码中时,转义规则会变得复杂,例如,呈现位于 JavaScript 块中的 URL。

考虑数据在响应中的呈现方式。例如,如果在 HTML 上下文中呈现数据,则必须转义六个控制字符

字符 已转义
& &amp;
< &lt;
> &gt;
" &quot;
' &#x27;
/ &#x2F;

有关转义规则和其他预防措施的完整列表,请参阅 OWASP XSS 预防备忘单

动态分析 - 安全测试注意事项

使用手动和/或自动输入模糊测试可以最好地检测 XSS 问题,即,将 HTML 标记和特殊字符注入到所有可用的输入字段中,以验证 Web 应用程序是否拒绝无效输入或转义其输出中的 HTML 元字符。

反射型 XSS 攻击是指通过恶意链接注入恶意代码的漏洞利用。为了测试这些攻击,自动化输入模糊测试被认为是有效的方法。例如,BURP Scanner 在识别反射型 XSS 漏洞方面非常有效。与自动化分析一样,始终确保通过手动查看测试参数来覆盖所有输入向量。

内存损坏错误

内存损坏错误是黑客常用的伎俩。此类错误是由编程错误导致的,该错误导致程序访问意外的内存位置。在适当的条件下,攻击者可以利用此行为来劫持易受攻击程序的执行流程并执行任意代码。这种漏洞以多种方式发生

  • 缓冲区溢出:这描述了一种编程错误,其中应用程序写入超出特定操作的已分配内存范围。攻击者可以使用此缺陷覆盖位于相邻内存中的重要控制数据,例如函数指针。缓冲区溢出曾经是最常见的内存损坏缺陷类型,但由于多种因素,这些年来变得不太普遍。值得注意的是,开发人员对使用不安全的 C 库函数存在的风险的认识现在是一种常见的最佳实践,并且捕获缓冲区溢出错误相对简单。但是,仍然值得测试此类缺陷。

  • 越界访问:有缺陷的指针算法可能导致指针或索引引用超出预期内存结构(例如缓冲区或列表)边界的位置。当应用程序尝试写入越界地址时,会发生崩溃或意外行为。如果攻击者可以控制目标偏移量并在一定程度上操纵写入的内容,则 可能存在代码执行漏洞利用

  • 悬空指针:当引用内存位置的传入引用的对象被删除或解除分配时,但对象指针未重置时,会发生这种情况。如果程序稍后使用悬空指针调用已解除分配对象的虚函数,则可以通过覆盖原始 vtable 指针来劫持执行。或者,可以读取或写入对象变量或悬空指针引用的其他内存结构。

  • 释放后使用:这是引用已释放(已解除分配)内存的悬空指针的特例。在清除内存地址后,引用该位置的所有指针都将失效,从而导致内存管理器将该地址返回到可用内存池。当最终重新分配此内存位置时,访问原始指针将读取或写入新分配的内存中包含的数据。这通常会导致数据损坏和未定义的行为,但狡猾的攻击者可以设置适当的内存位置来控制指令指针。

  • 整数溢出:当算术运算的结果超过程序员定义的整数类型的最大值时,这会导致值“环绕”最大整数值,不可避免地导致存储较小的值。相反,当算术运算的结果小于整数类型的最小值时,会发生整数下溢,其中结果大于预期。特定整数溢出/下溢错误是否可利用取决于整数的使用方式。例如,如果要使整数类型表示缓冲区的长度,则这可能会导致缓冲区溢出漏洞。

  • 格式字符串漏洞:当未经检查的用户输入传递给 printf C 函数族的格式字符串参数时,攻击者可能会注入格式标记(例如“%c”和“%n”)以访问内存。格式字符串错误由于其灵活性而易于利用。如果程序输出字符串格式化操作的结果,攻击者可以任意读取和写入内存,从而绕过诸如 ASLR 之类的保护功能。

利用内存损坏的主要目标通常是将程序流重定向到攻击者放置了被称为shellcode的汇编机器指令的位置。在 iOS 上,数据执行保护功能(顾名思义)可防止从定义为数据段的内存执行。为了绕过此保护,攻击者利用返回导向编程 (ROP)。此过程涉及链接文本段中小的预先存在的代码块(“gadget”),其中这些 gadget 可以执行对攻击者有用的功能,或者调用 mprotect 以更改攻击者存储 shellcode 的位置的内存保护设置。

Android 应用程序大部分以 Java 实现,从设计上来说本质上是免受内存损坏问题的。但是,使用 JNI 库的本机应用程序容易受到这种错误的影响。在极少数情况下,使用 XML/JSON 解析器来解包 Java 对象的 Android 应用程序也容易受到内存损坏错误的影响。一个示例 是在 PayPal 应用程序中发现的此类漏洞。

同样,iOS 应用程序可以将 C/C++ 调用包装在 Obj-C 或 Swift 中,从而使它们容易受到此类攻击。

示例

以下代码段显示了导致缓冲区溢出漏洞的简单示例。

 void copyData(char *userId) {
    char  smallBuffer[10]; // size of 10
    strcpy(smallBuffer, userId);
 }

要识别潜在的缓冲区溢出,请查找不安全的字符串函数(strcpystrcat、以“str”前缀开头的其他函数等)和潜在的脆弱编程构造的使用,例如将用户输入复制到大小受限的缓冲区中。以下内容应被视为不安全字符串函数的危险信号

  • strcat
  • strcpy
  • strncat
  • strlcat
  • strncpy
  • strlcpy
  • sprintf
  • snprintf
  • gets

此外,查找实现为“for”或“while”循环的复制操作的实例,并验证是否正确执行了长度检查。

验证是否遵循了以下最佳实践

  • 当使用整数变量进行数组索引、缓冲区长度计算或任何其他安全关键操作时,请验证是否使用了无符号整数类型,并执行前提条件测试以防止整数环绕的可能性。
  • 该应用程序不使用不安全的字符串函数,例如 strcpy,大多数以“str”前缀开头的其他函数、sprintvsprintfgets 等;
  • 如果应用程序包含 C++ 代码,则使用 ANSI C++ 字符串类;
  • 如果是 memcpy,请确保您检查目标缓冲区是否至少与源缓冲区的大小相等,并且两个缓冲区都不重叠。
  • 用 Objective-C 编写的 iOS 应用程序使用 NSString 类。 iOS 上的 C 应用程序应使用 CFString,即字符串的核心 Foundation 表示形式。
  • 没有不受信任的数据连接到格式字符串中。

静态分析安全测试注意事项

底层代码的静态代码分析是一个复杂的主题,很容易写成一本书。诸如 RATS 之类的自动化工具,结合有限的手动检查工作,通常足以识别唾手可得的东西。但是,内存损坏状况通常源于复杂的原因。例如,释放后使用错误实际上可能是复杂、违反直觉的竞争条件的结果,而这种竞争条件并非显而易见。从被忽视的代码缺陷的深层实例中显现出来的错误通常是通过动态分析或通过花费时间深入了解程序的测试人员来发现的。

动态分析安全测试注意事项

通过输入模糊测试可以最好地发现内存损坏错误:一种自动化的黑盒软件测试技术,其中将格式错误的数据不断发送到应用程序以调查潜在的漏洞情况。在此过程中,监视应用程序是否存在故障和崩溃。如果发生崩溃,希望(至少对于安全测试人员而言)是造成崩溃的条件揭示了可利用的安全漏洞。

模糊测试技术或脚本(通常称为“fuzzer”)通常会以半正确的方式生成多个结构化输入实例。从本质上讲,生成的值或参数至少会被目标应用程序部分接受,但同时还包含无效元素,可能会触发输入处理缺陷和意外的程序行为。一个好的 fuzzer 会公开大量的程序执行路径(即高覆盖率输出)。输入可以从头开始生成(“基于生成”),也可以从变异已知、有效输入数据派生(“基于变异”)。

有关模糊测试的更多信息,请参阅 OWASP 模糊测试指南

二进制保护机制

位置无关代码

PIC(位置无关代码)是一种代码,放在主内存中的某个位置后,无论其绝对地址如何,都可以正确执行。 PIC 通常用于共享库,以便可以将相同的库代码加载到每个程序地址空间中的某个位置,该位置不会与正在使用的其他内存(例如,其他共享库)重叠。

PIE(位置无关可执行文件)是由 PIC 完全生成的可执行二进制文件。 PIE 二进制文件用于启用 ASLR(地址空间布局随机化),它随机排列进程的关键数据区域的地址空间位置,包括可执行文件的基址以及堆栈、堆和库的位置。

内存管理

自动引用计数

ARC(自动引用计数)是 Clang 编译器的一种内存管理功能,仅限于 Objective-CSwift。当不再需要类实例时,ARC 会自动释放类实例使用的内存。 ARC 与跟踪垃圾回收的不同之处在于,没有后台进程可以在运行时异步释放对象。

与跟踪垃圾回收不同,ARC 不会自动处理引用循环。这意味着只要有对对象的“强”引用,就不会释放该对象。强交叉引用可能会导致死锁和内存泄漏。开发人员需要通过使用弱引用来打破循环。您可以在 此处 了解它与垃圾回收的不同之处。

垃圾回收

垃圾回收 (GC) 是一些语言(例如 Java/Kotlin/Dart)的自动内存管理功能。垃圾回收器尝试回收程序分配但不再引用的内存,也称为垃圾。 Android 运行时 (ART) 使用 改进版本的 GC。您可以在 此处 了解它与 ARC 的不同之处。

手动内存管理

手动内存管理 通常是在 C/C++ 中编写的本机库中所需的,其中 ARC 和 GC 不适用。开发人员负责进行适当的内存管理。已知手动内存管理在错误使用时会在程序中启用几个主要类别的错误,特别是违反 内存安全内存泄漏

更多信息可以在 “内存损坏错误” 中找到。

栈溢出保护

栈金丝雀通过在返回指针之前的堆栈上存储隐藏的整数值来帮助防止栈缓冲区溢出攻击。然后在函数执行返回语句之前验证该值。缓冲区溢出攻击通常会覆盖内存区域以覆盖返回指针并控制程序流。如果启用了栈金丝雀,它们也会被覆盖,CPU 将知道内存已被篡改。

栈缓冲区溢出是一种更通用的编程漏洞类型,称为 缓冲区溢出(或缓冲区过冲)。堆栈上缓冲区的过度填充比堆上缓冲区的过度填充更可能 破坏程序执行,因为堆栈包含所有活动函数调用的返回地址。