安卓应用安全指南 4.8 输出到 LogCat
4.8 输出到 LogCat
原书:Android Application Secure Design/Secure Coding Guidebook
译者:飞龙
在 Android 中有一种名为 LogCat 的日志机制,不仅系统日志信息,还有应用日志信息也会输出到 LogCat。 LogCat 中的日志信息可以从同一设备中的其他应用中读出 [17],因此向L ogcat 输出敏感信息的应用,被认为具有信息泄露的漏洞。 敏感信息不应输出到 LogCat。
[17] 输出到 LogCat 的日志信息,可以由声明
READ_LOGS
权限的应用读取。 但是,在 Android 4.1 及更高版本中,无法读取其他应用输出的日志信息。 但智能手机用户可以通过 ADB ,阅读输出到 logcat 的每个日志信息。
从安全角度来看,在发行版应用中,最好不要输出任何日志。 但是,即使在发行版应用的情况下,在某些情况下也会出于某种原因输出日志。 在本章中,我们将介绍一些方法,以安全的方式将消息输出到 LogCat,即使在发行版应用中也是如此。 除此解释外,请参考“4.8.3.1 发行版应用中日志输出的两种思路”。
4.8.1 示例代码
接下来是在发行版应用中,通过 ProGuard 控制输出到 LogCat 的日志的方法。 ProGuard 是自动删除不需要的代码(如未使用的方法等)的优化工具之一。
android.util.Log
类有五种类型的日志输出方法:Log.e()
, Log.w()
, Log.i()
, Log.d()
, Log.v()
。对于日志信息,有意输出的日志信息(以下称为“操作日志信息”)应该区分于不适合发行版应用的信息(以下称为开发日志信息),例如调试日志。建议使用Log.e()/w()/i()
输出操作日志信息,并使用Log.d()/v()
输出开发日志。正确使用五种日志输出方法的详细信息,请参阅“4.8.3.2 日志级别和日志输出方法的选择标准”,另外请参考“4.8.3.3 调试日志和VERBOSE
日志并不总是自动删除”。
这是一个以安全方式使用 LogCat 的例子。此示例包括用于输出调试日志的Log.d()
和Log.v()
。如果应用用于发布,这两种方法将被自动删除。在此示例代码中,ProGuard 用于自动删除调用Log.d()/v()
的代码块。
要点:
1) 敏感信息不能由Log.e()/w()/i()
,System.out / err
输出。
2) 敏感信息应由Log.d()/v()
在需要时输出。
3) 不应使用Log.d()/v()
的返回值(以替换或比较为目的)。
4) 当你构建应用来发布时,你应该在代码中引入机制,自动删除不合适的日志记录方法(如Log.d()
或Log.v()
)。
5) 必须使用发行版构建配置来创建用于(发布)发行的 APK 文件。
ProGuardActivity.java
package org.jssec.android.log.proguard;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class ProGuardActivity extends Activity {
final static String LOG_TAG = "ProGuardActivity";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_proguard);
// *** POINT 1 *** Sensitive information must not be output by Log.e()/w()/i(), System.out/err.
Log.e(LOG_TAG, "Not sensitive information (ERROR)");
Log.w(LOG_TAG, "Not sensitive information (WARN)");
Log.i(LOG_TAG, "Not sensitive information (INFO)");
// *** POINT 2 *** Sensitive information should be output by Log.d()/v() in case of need.
// *** POINT 3 *** The return value of Log.d()/v()should not be used (with the purpose of substitution or comparison).
Log.d(LOG_TAG, "sensitive information (DEBUG)");
Log.v(LOG_TAG, "sensitive information (VERBOSE)");
}
}
proguard-project.txt
# prevent from changing class name and method name etc.
-dontobfuscate
# *** POINT 4 *** In release build, the build configurations in which Log.d()/v() are deleted automatica
lly should be constructed.
-assumenosideeffects class android.util.Log {
public static int d(...);
public static int v(...);
}
要点 5:必须使用发行版构建配置来创建用于(发布)发行的 APK 文件。
开发版应用(调试版本)和发行版应用(发布版本)之间的LogCat 输出差异如下图 4.8-2 所示。
4.8.2 规则书
输出消息记录时,遵循以下规则:
4.8.2.1 操作日志信息中不能包含敏感信息(必需)
输出到 LogCat 的日志可以从其他应用中读取,因此敏感信息(如用户的登录信息)不应该由发行版应用输出。 在开发过程中,不必编写输出敏感信息的代码,或者在发布之前需要删除所有这些代码。
为了遵循这个规则,首先,不要在操作日志信息中包含敏感信息。 此外,建议构建系统,在构建发行版时,删除输出敏感信息的代码。 请参阅“4.8.2.2 构建生成系统,在构建发行版时,自动删除输出开发日志信息的代码(推荐)”。
4.8.2.2 构建生成系统,在构建发行版时,自动删除输出开发日志信息的代码(推荐)
开发应用时,有时最好将敏感信息输出到日志中,来检查过程内容和调试,例如复杂逻辑过程中的临时操作结果,程序内部状态信息,通信协议的数据结构。在开发过程中,将敏感信息作为调试日志输出并不重要,在这种情况下,相应的日志输出代码应该在发布之前删除,如“4.8.2.1 操作日志信息中不能包含敏感信息(必需)”所述。
为了在构建发行版时,确实删除了输出开发日志信息的代码,应该构建系统,使用某些工具自动执行代码删除。 “4.8.1 示例代码”中介绍的 ProGuard 可以用于此方法。如下所述,用 ProGuard 删除代码有一些值得注意的地方。这里应该将系统用于一些应用,它通过Log.d()/ v()
输出开发日志信息,根据“4.8.3.2 日志级别和日志输出方法的选择标准”。
ProGuard 会自动删除不需要的代码,如未使用的方法。通过指定Log.d()/ v()
作为-assumenosideeffects
选项的参数,Log.d(),
Log.v()`的调用被视为不必要的代码,并且这些代码将被删除。
-assumenosideeffects class android.util.Log {
public static int d(...);
public static int v(...);
}
如果使用这个自动删除系统,请注意Log.d()
,Log.v()
代码在使用其返回值时不会被删除,因此不应该使用Log.d()
,Log.v()
的返回值。 例如,下一个代码中的Log.v()
不会被删除。
int i = android.util.Log.v("tag", "message");
System.out.println(String.format("Log.v() returned %d. ", i)); //Use the returned value of Log.v() for examination
如果你想重复使用源代码,则应保持项目环境的一致性,包括 ProGuard 设置。 例如,预设Log.d()
和Log.v()
的源代码将被上面的 ProGuard 设置自动删除。 如果在未设置 ProGuard 的其他项目中使用此源代码,则不会删除Log.d()
和Log.v()
,因此可能会泄露敏感信息。 重用源代码时,应确保包括 ProGuard 设置在内的项目环境的一致性。
4.8.2.3 输出Throwable
对象时,使用Log.d()/v()
(推荐)
如“4.8.1 示例代码”和“4.8.3.2 日志级别和日志输出方法的选择标准”中所述,输出敏感信息不应通过Log.e()/w()/i()
输出来记录。 另一方面,为了使开发者输出程序异常的细节来记录,当异常发生时,在某些情况下,堆栈踪迹通过Log.e(..., Throwable tr)/w(..., Throwable tr)/i(..., Throwable
输出到 LogCat。 但是,敏感信息有时可能包含在堆栈踪迹中,因为它显示程序的详细内部结构。 例如,当
tr)SQLiteException
按原样输出时,会输出 SQL 语句的类型,因此可能会提供 SQL 注入攻击的线索。 因此,建议在输出Throwable
对象时,仅使用Log.d()/v()
方法。
4.8.2.4 仅仅将android.util.Log
类的方法用于日志输出(推荐)
在开发过程中,你可以通过System.out / err
输出日志,来验证应用的行为是否按预期工作。 当然,日志可以通过System.out / err
的print()/ println()
方法输出到 LogCat,但强烈建议仅使用android.util.Log
类的方法,原因如下。
在输出日志时,一般根据信息的紧急程度,正确使用最合适的输出方法,并控制输出。 例如,使用严重错误,注意,简单应用的信息通知等类别。 然而,在这种情况下,在发布时需要输出的信息(操作日志信息),和可以包括敏感信息(开发日志信息)的信息,通过相同的方法输出。 所以,当删除输出敏感信息的代码时,可能会存在一些删除操作被忽略掉的危险。
除此之外,当使用android.util.Log
和System.out / err
进行日志输出时,与仅使用android.util.Log
相比,需要考虑的因素会增加,因此可能会出现一些错误,比如 一些删除被忽略掉了。
为了减少上述错误发生的风险,建议仅使用android.util.Log
类的方法。
4.8.3 高级话题
4.8.3.1 发布版应用中日志输出的两种思路
发布版应用中有两种思考日志输出的方式。一个是任何日志都不应该输出,另一个是用于以后分析的必要信息应该作为日志输出。从安全角度来看,最好是,任何日志都不应该在发行版应用中输出,但有时候,即使在发行版本应用中,出于各种原因也会输出日志。每种思考方式按照以下描述。
前者是“任何日志都不应该输出”,这是因为,在发行版应用中输出日志没有那么重要,并且存在泄露敏感信息的风险。这是因为开发人员没有办法在 Android 应用运行环境中收集发行版应用的日志信息,这与许多 Web 应用的运行环境不同。基于这种思想,日志代码仅用于开发阶段,并且在构建发行版应用时删除所有日志代码。
后者是“必要的信息应作为日志输出,以供日后分析”,作为客户支持中,分析应用错误的最终选项,以防你的客户支持有任何疑问。 基于这个想法,如上所述,有必要准备系统来防止人为错误并将其引入到项目中,因为如果你没有系统,则必须记住避免在发行版应用中记录敏感信息。
更多日志方法的信息,请参考下面的链接:
适用于贡献者/日志的代码风格指南
http://source.android.com/source/code-style.html#log-sparingly
4.8.3.2 日志级别和日志输出方法的选择标准
在 Android 中的android.util.Log
类中定义了五个日志级别(ERROR
,WARN
,INFO
,DEBUG
,VERBOSE
)。 使用android.util.Log
类输出日志消息时,应该选择最合适的方法,如表 4.8-1 所示,它展示了日志级别和方法的选择标准。
表 4.8-1 日志级别和方法的选择标准
日志级别 | 方法 | 要输出的日志信息 |
---|---|---|
ERROR | Log.e() | 应用处于错误状态时,输出的日志信息 |
WARN | Log.w() | 应用面临非预期严重情况时,输出的日志信息 |
INFO | Log.i() | 与上面不同,用于提示应用状态中任何值得注意的更改或者结果 |
DEBUG | Log.d() | 应用的内部状态信息,开发应用时,需要临时输出,用于分析特定 bug 的成因 |
VERBOSE | Log.v() | 不属于上面任何一个的日志信息。应用开发者以多种目的输出。例如,输出服务器通信信息来转储。 |
发行版应用的注意事项:
e/w/i
:
日志信息可能由用户参考,因此可以在开发版应用和发行版应用中输出。 因此,敏感信息不应该在这些级别输出。
d/v
:
日志信息仅适用于应用开发人员。 因此,这种类型的信息不应该在发行版的情况下输出。
更多日志方法的信息,请参考下面的链接:
适用于贡献者/日志的代码风格指南
http://source.android.com/source/code-style.html#log-sparingly
4.8.3.3 DEBUG
和VERBOSE
日志并不总是自动删除
以下引用自android.util.Log
类 [18] 的开发人员参考。
[18] ttp://developer.android.com/reference/android/util/Log.html
按照啰嗦程度的顺序排列,从最少到最多是ERROR
,WARN
,INFO
,DEBUG
,VERBOSE
。 除了在开发期间,绝不应该将VERBOSE
编译进应用。 DEBUG
日志被编译但在运行时剥离。 始终保留ERROR
,WARN
,INFO
日志。
在阅读了上述文章之后,一些开发人员可能会误解Log
类的行为,如下所示。
- 构建发行版时不编译
Log.v()
调用,VERBOSE
日志从不输出。 - 编译
Log.v()
调用,但执行时绝不输出DEBUG
日志。
但是,日志记录方法从来不会表现成这样,并且无论使用调试模式还是发布模式编译,都会输出所有消息。 如果仔细阅读文档,你将能够认识到,文档的要点与日志方法的行为无关,而是日志的基本策略。
在本章中,我们通过使用 ProGuard 引入了示例代码以获得上述的预期结果。
4.8.3.4 从汇编中移除敏感信息
如果为了删除Log.d()
方法而使用 ProGuard 构建以下代码,有必要记住,ProGuard
会保留为日志信息构造字符串的语句(代码的第一行),即使它删除了 Log.d()
方法的调用(代码的第二行)。
String debug_info = String.format("%s:%s", "Sensitive information1", "Sensitive information2");
if (BuildConfig.DEBUG) android.util.Log.d(TAG, debug_info);
以下反汇编显示了使用 ProGuard 发布上述代码的结果。 实际上,没有Log.d()
调用过程,但你可以看到字符串一致性定义,例如Sensitive information1
,和String#format()
方法的调用过程,不会被删除并仍然存在。
const-string v1, "%s:%s"
const/4 v2, 0x2
new-array v2, v2, [Ljava/lang/Object;
const/4 v3, 0x0
const-string v4, "Sensitive information 1"
aput-object v4, v2, v3
const/4 v3, 0x1
const-string v4, "Sensitive information 2"
aput-object v4, v2, v3
invoke-static {v1, v2}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang
/String;
move-result-object v0
实际上,找到反汇编 APK 文件的组装日志输出信息特定部分并不容易。 但是,在某些处理机密信息的应用中,这种类型的过程在某些情况下不应保留在 APK 文件中。 你应该像下面那样实现你的应用,来避免在字节码中保留敏感信息的后果。 在发行版中,编译器优化将完全删除以下代码。
if (BuildConfig.DEBUG) {
String debug_info = String.format("%s:%s", " Snsitive information 1", "Sensitive information 2");
if (BuildConfig.DEBUG) android.util.Log.d(TAG, debug_info);
}
此外,ProGuard 无法删除以下日志消息代码("result:" + value)
。
Log.d(TAG, "result:" + value);
在这种情况下,你可以通过以下方式解决问题。
if (BuildConfig.DEBUG) Log.d(TAG, "result:" + value);
4.8.3.5 意图的内容输出到了 LogCat
使用活动时,需要注意,因为ActivityManager
将意图的内容输出到 LogCat。 请参阅“4.1.3.5 使用活动时的日志输出”。
4.8.3.6 限制输出到System.out / err
的日志
System.out / err
方法将所有消息输出到 LogCat。 即使开发者没有在他们的代码中使用这些方法,Android 也可以向System.out / err
发送一些消息,例如,在以下情况下,Android 会将堆栈踪迹发送到System.err
方法。
- 使用
Exception#printStackTrace()
时 - 隐式输出到
System.err
时(当异常没有被应用捕获时,它会由系统提供给Exception#printStackTrace()
。)
你应该适当地处理错误和异常,因为堆栈踪迹包含应用的独特信息。
我们介绍一种改变System.out / err
默认输出目标的方法。 当你构建发行版应用时,以下代码将System.out / err
方法的输出重定向到任何地方。 但是,你应该考虑此重定向是否会导致应用或系统故障,因为代码会暂时覆盖System.out / err
方法的默认行为。 此外,这种重定向仅对你的应用有效,对系统进程毫无价值。
OutputRedirectApplication.java
package org.jssec.android.log.outputredirection;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import android.app.Application;
public class OutputRedirectApplication extends Application {
// PrintStream which is not output anywhere
private final PrintStream emptyStream = new PrintStream(new OutputStream() {
public void write(int oneByte) throws IOException {
// do nothing
}
});
@Override
public void onCreate() {
// Redirect System.out/err to PrintStream which doesn't output anywhere, when release build.
// Save original stream of System.out/err
PrintStream savedOut = System.out;
PrintStream savedErr = System.err;
// Once, redirect System.out/err to PrintStream which doesn't output anywhere
System.setOut(emptyStream);
System.setErr(emptyStream);
// Restore the original stream only when debugging. (In release build, the following 1 line is deleted byProGuard.)
resetStreams(savedOut, savedErr);
}
// All of the following methods are deleted byProGuard when release.
private void resetStreams(PrintStream savedOut, PrintStream savedErr) {
System.setOut(savedOut);
System.setErr(savedErr);
}
}
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jssec.android.log.outputredirection" >
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:name=".OutputRedirectApplication"
android:allowBackup="false" >
<activity
android:name=".LogActivity"
android:label="@string/app_name"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
proguard-project.txt
# Prevent from changing class name and method name, etc
-dontobfuscate
# In release build, delete call from Log.d()/v() automatically.
-assumenosideeffects class android.util.Log {
public static int d(...);
public static int v(...);
}
# In release build, delete resetStreams() automatically.
-assumenosideeffects class org.jssec.android.log.outputredirection.OutputRedirectApplication {
private void resetStreams(...);
}
开发版应用(调试版)和发布版应用(发行版)之间的 LogCat 输出差异如下图 4.8-3 所示。