Я обнаружил эту уязвимость, исследуя возможность перехвата одноразовых паролей, которые отправлялись банком поставщику телекоммуникационных услуг, а затем поступали на специальное приложение SIM-карты и выводились на пользовательский интерфейс Android.
Перехват
Представьте, что на SIM-карте есть небольшое приложение, которое получает сообщение от оператора связи и показывает его на экране вашего Android-устройства. Если покопаться в исходниках Android, можно наткнуться на класс com.android.internal.telephony.cat.CatService, который отвечает за передачу команд между слоем радиоинтерфейса (Radio Interface Layer, RIL) и ОС.
public void handleMessage(Message msg) {
CatLog.d(this, "handleMessage[" + msg.what + "]");
switch (msg.what) {
case MSG_ID_SESSION_END:
case MSG_ID_PROACTIVE_COMMAND:
case MSG_ID_EVENT_NOTIFY:
case MSG_ID_REFRESH:
CatLog.d(this, "ril message arrived,slotid:" + mSlotId);
String data = null;
if (msg.obj != null) {
AsyncResult ar = (AsyncResult) msg.obj;
if (ar != null && ar.result != null) {
try {
data = (String) ar.result;
} catch (ClassCastException e) {
break;
}
}
}
mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, data));
break;
case MSG_ID_CALL_SETUP:
mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, null));
break;
case MSG_ID_ICC_RECORDS_LOADED:
break;
case MSG_ID_RIL_MSG_DECODED:
handleRilMsg((RilMessage) msg.obj);
break;
case MSG_ID_RESPONSE:
handleCmdResponse((CatResponseMessage) msg.obj);
break;
Из всех типов сообщений нас интересует MSG_ID_RIL_MSG_DECODED
.
private void handleRilMsg(RilMessage rilMsg) {
if (rilMsg == null) {
return;
}
// dispatch messages
CommandParams cmdParams = null;
switch (rilMsg.mId) {
case MSG_ID_EVENT_NOTIFY:
if (rilMsg.mResCode == ResultCode.OK) {
cmdParams = (CommandParams) rilMsg.mData;
if (cmdParams != null) {
handleCommand(cmdParams, false);
}
}
break;
case MSG_ID_PROACTIVE_COMMAND:
try {
cmdParams = (CommandParams) rilMsg.mData;
} catch (ClassCastException e) {
// for error handling : cast exception
CatLog.d(this, "Fail to parse proactive command");
// Don't send Terminal Resp if command detail is not available
if (mCurrntCmd != null) {
sendTerminalResponse(mCurrntCmd.mCmdDet, ResultCode.CMD_DATA_NOT_UNDERSTOOD,
false, 0x00, null);
}
break;
}
if (cmdParams != null) {
if (rilMsg.mResCode == ResultCode.OK) {
handleCommand(cmdParams, true);
} else {
// for proactive commands that couldn't be decoded
// successfully respond with the code generated by the
// message decoder.
sendTerminalResponse(cmdParams.mCmdDet, rilMsg.mResCode,
false, 0, null);
}
}
break;
Оба оператора switch приводят к вызову метода handleCommand()
, однако второй параметр в каждом случае разный:
MSG_ID_EVENT_NOTIFY
— обычное уведомление, которое не требует ответа от пользователя;MSG_ID_PROACTIVE_COMMAND
— а это, как раз наоборот, требует.
Переходим к handleCommand
:
/**
* Handles RIL_UNSOL_STK_EVENT_NOTIFY or RIL_UNSOL_STK_PROACTIVE_COMMAND command
* from RIL.
* Sends valid proactive command data to the application using intents.
* RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE will be send back if the command is
* from RIL_UNSOL_STK_PROACTIVE_COMMAND.
*/
private void handleCommand(CommandParams cmdParams, boolean isProactiveCmd) {
CatLog.d(this, cmdParams.getCommandType().name());
CharSequence message;
CatCmdMessage cmdMsg = new CatCmdMessage(cmdParams);
switch (cmdParams.getCommandType()) {
case SET_UP_MENU:
if (removeMenu(cmdMsg.getMenu())) {
mMenuCmd = null;
} else {
mMenuCmd = cmdMsg;
}
sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, null);
break;
case DISPLAY_TEXT:
break;
case REFRESH:
// ME side only handles refresh commands which meant to remove IDLE
// MODE TEXT.
cmdParams.mCmdDet.typeOfCommand = CommandType.SET_UP_IDLE_MODE_TEXT.value();
break;
case SET_UP_IDLE_MODE_TEXT:
sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, null);
break;
case SET_UP_EVENT_LIST:
if (isSupportedSetupEventCommand(cmdMsg)) {
sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, null);
} else {
sendTerminalResponse(cmdParams.mCmdDet, ResultCode.BEYOND_TERMINAL_CAPABILITY,
false, 0, null);
}
break;
case PROVIDE_LOCAL_INFORMATION:
ResponseData resp;
switch (cmdParams.mCmdDet.commandQualifier) {
case CommandParamsFactory.DTTZ_SETTING:
resp = new DTTZResponseData(null);
sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, resp);
break;
case CommandParamsFactory.LANGUAGE_SETTING:
resp = new LanguageResponseData(Locale.getDefault().getLanguage());
sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, resp);
break;
default:
sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, null);
}
// No need to start STK app here.
return;
case LAUNCH_BROWSER:
if ((((LaunchBrowserParams) cmdParams).mConfirmMsg.text != null)
&& (((LaunchBrowserParams) cmdParams).mConfirmMsg.text.equals(STK_DEFAULT))) {
message = mContext.getText(com.android.internal.R.string.launchBrowserDefault);
((LaunchBrowserParams) cmdParams).mConfirmMsg.text = message.toString();
}
break;
case SELECT_ITEM:
case GET_INPUT:
case GET_INKEY:
break;
case SEND_DTMF:
case SEND_SMS:
case SEND_SS:
case SEND_USSD:
if ((((DisplayTextParams)cmdParams).mTextMsg.text != null)
&& (((DisplayTextParams)cmdParams).mTextMsg.text.equals(STK_DEFAULT))) {
message = mContext.getText(com.android.internal.R.string.sending);
((DisplayTextParams)cmdParams).mTextMsg.text = message.toString();
}
break;
case PLAY_TONE:
break;
case SET_UP_CALL:
if ((((CallSetupParams) cmdParams).mConfirmMsg.text != null)
&& (((CallSetupParams) cmdParams).mConfirmMsg.text.equals(STK_DEFAULT))) {
message = mContext.getText(com.android.internal.R.string.SetupCallDefault);
((CallSetupParams) cmdParams).mConfirmMsg.text = message.toString();
}
break;
case OPEN_CHANNEL:
case CLOSE_CHANNEL:
case RECEIVE_DATA:
case SEND_DATA:
BIPClientParams cmd = (BIPClientParams) cmdParams;
/* Per 3GPP specification 102.223,
* if the alpha identifier is not provided by the UICC,
* the terminal MAY give information to the user
* noAlphaUsrCnf defines if you need to show user confirmation or not
*/
boolean noAlphaUsrCnf = false;
try {
noAlphaUsrCnf = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_stkNoAlphaUsrCnf);
} catch (NotFoundException e) {
noAlphaUsrCnf = false;
}
if ((cmd.mTextMsg.text == null) && (cmd.mHasAlphaId || noAlphaUsrCnf)) {
CatLog.d(this, "cmd " + cmdParams.getCommandType() + " with null alpha id");
// If alpha length is zero, we just respond with OK.
if (isProactiveCmd) {
sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, null);
} else if (cmdParams.getCommandType() == CommandType.OPEN_CHANNEL) {
mCmdIf.handleCallSetupRequestFromSim(true, null);
}
return;
}
// Respond with permanent failure to avoid retry if STK app is not present.
if (!mStkAppInstalled) {
CatLog.d(this, "No STK application found.");
if (isProactiveCmd) {
sendTerminalResponse(cmdParams.mCmdDet,
ResultCode.BEYOND_TERMINAL_CAPABILITY,
false, 0, null);
return;
}
}
/*
* CLOSE_CHANNEL, RECEIVE_DATA and SEND_DATA can be delivered by
* either PROACTIVE_COMMAND or EVENT_NOTIFY.
* If PROACTIVE_COMMAND is used for those commands, send terminal
* response here.
*/
if (isProactiveCmd &&
((cmdParams.getCommandType() == CommandType.CLOSE_CHANNEL) ||
(cmdParams.getCommandType() == CommandType.RECEIVE_DATA) ||
(cmdParams.getCommandType() == CommandType.SEND_DATA))) {
sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, null);
}
break;
default:
CatLog.d(this, "Unsupported command");
return;
}
mCurrntCmd = cmdMsg;
broadcastCatCmdIntent(cmdMsg);
}
И, наконец, broadcastCatCmdIntent()
:
private void broadcastCatCmdIntent(CatCmdMessage cmdMsg) {
Intent intent = new Intent(AppInterface.CAT_CMD_ACTION);
intent.putExtra("STK CMD", cmdMsg);
intent.putExtra("SLOT_ID", mSlotId);
CatLog.d(this, "Sending CmdMsg: " + cmdMsg+ " on slotid:" + mSlotId);
mContext.sendBroadcast(intent);
}
А вот эта часть довольно занятная:
AppInterface.CAT_CMD_ACTION
равняетсяandroid.intent.action.stk.command
;SLOT_ID
используется для устройств с несколькими SIM-картами;STK CMD
— команда в качестве объектаParcelable
.
Проблема заключается в том, что для отправки команды другому приложению CatService использует неявный интент без ограничения привилегий.
Как злоумышленник может этим воспользоваться?
Например, использовать вредоносное приложение, не требующее дополнительных привилегий, для перехвата команд, отправляемых SIM-картой на телефон. Для этого необходимо лишь зарегистрировать receiver с действием android.intent.action.stk.command и получить STK CMD из интента.
Пример перехваченной команды:
Это объект Parcelable
в байтах. Преобразовав Hex в ASCII, вы получите сообщение SIM-карты.
Эмуляция
Однако это лишь половина уязвимости. Рассмотрим приложение, которое получает вот такое широковещательное сообщение:
Вид сообщения
Это приложение называется SIM Toolkit (STK) и является частью стандартного Android-фреймворка. Исходники можно найти тут.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
package="com.android.stk"
android:sharedUserId="android.uid.phone">
<original-package android:name="com.android.stk" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.GET_TASKS"/>
<application android:icon="@drawable/ic_launcher_sim_toolkit"
android:label="@string/app_name"
android:clearTaskOnLaunch="true"
android:process="com.android.phone"
android:taskAffinity="android.task.stk">
...
<receiver android:name="com.android.stk.StkCmdReceiver">
<intent-filter>
<action android:name= "android.intent.action.stk.command" />
<action android:name= "android.intent.action.stk.session_end" />
<action android:name= "android.intent.action.stk.icc_status_change" />
<action android:name= "android.intent.action.stk.alpha_notify" />
<action android:name= "android.intent.action.LOCALE_CHANGED" />
</intent-filter>
</receiver>
Выше приведен фрагмент файла AndroidManifest.xml
, относящийся к компоненту receiver
. Как видно, компонент полностью экспортирован. Это позволяет не только перехватывать команды SIM-карты, но и создавать при помощи вредоносных программ объект Parcelable
, а затем отправлять его на com.android.stk.StkCmdReceiver
. Receiver
не проверяет отправителя, а действие android.intent.action.stk.command не объявлено в системном файле AndroidManifest.xml в качестве защищенного сообщения, что позволяет мошенникам эмулировать отправку команд SIM-карты.
Например:
1. SIM-карта запрашивает подтверждение некоторой операции, скажем, транзакции в интернет-банке, выводя на экран телефона сообщение типа «Подтвердить транзакцию № 1234 на сумму 100 500 рублей» с двумя опциями — «ОК» и «Отмена». Код на StkDialogActivity.java:
public void onClick(View v) {
String input = null;
switch (v.getId()) {
case OK_BUTTON:
CatLog.d(LOG_TAG, "OK Clicked!, mSlotId: " + mSlotId);
cancelTimeOut();
sendResponse(StkAppService.RES_ID_CONFIRM, true);
break;
case CANCEL_BUTTON:
CatLog.d(LOG_TAG, "Cancel Clicked!, mSlotId: " + mSlotId);
cancelTimeOut();
sendResponse(StkAppService.RES_ID_CONFIRM, false);
break;
}
finish();
}
2. Если пользователь нажмет «ОК», будет вызвана команда sendResponse(StkAppService.RES_ID_CONFIRM, true)
; в противном случае — sendResponse(StkAppService.RES_ID_CONFIRM, false)
;.
3. Что, если при помощи действия android.intent.action.stk.command
создать такое же диалоговое окно с другим текстом (поддельное) и вывести его на экран за несколько секунд до генерации SIM-картой оригинального сообщения («Подтвердить транзакцию № 1234 на сумму 100 500 рублей»)? В тексте сообщения напишем «Нажмите ОК для закрытия», а кнопки оставим те же — «ОК» и «Отмена».
4. Пользователь не увидит оригинальный диалог с подтверждением транзакции, пока не выберет одну из этих опций в поддельном окне, так как все команды, требующие взаимодействия с пользователем, помещаются в очередь.
5. Итак, мы остановились на следующем:
- SIM-карта ожидает ответа от пользователя;
- Android показывает пользователю первый (поддельный) диалог.
Если нажать «ОК», будет вызван метод sendResponse()
с флагом «true» и SIM-карта получит команду «ОК», как если бы она была отправлена из оригинального диалога. Даже если пользователь выберет во втором окне опцию «Отмена», это никак не повлияет на предыдущую команду. SIM-карта воспримет это как новый отклик, которого она не ожидает. В исходниках мне удалось найти описание подобной ситуации:
private void handleCmdResponse(CatResponseMessage resMsg) {
// Make sure the response details match the last valid command. An invalid
// response is a one that doesn't have a corresponding proactive command
// and sending it can "confuse" the baseband/ril.
// One reason for out of order responses can be UI glitches. For example,
// if the application launch an activity, and that activity is stored
// by the framework inside the history stack. That activity will be
// available for relaunch using the latest application dialog
// (long press on the home button). Relaunching that activity can send
// the same command's result again to the CatService and can cause it to
// get out of sync with the SIM. This can happen in case of
// non-interactive type Setup Event List and SETUP_MENU proactive commands.
// Stk framework would have already sent Terminal Response to Setup Event
// List and SETUP_MENU proactive commands. After sometime Stk app will send
// Envelope Command/Event Download. In which case, the response details doesn't
// match with last valid command (which are not related).
// However, we should allow Stk framework to send the message to ICC.
Здесь сообщается, что «Недопустимым является отклик, который не имеет соответствующей проактивной команды и отправка которого может “сбить с толку” baseband/ril». На деле, если RIL или SIM-карта будут получать от вас неожиданные отклики, последствия могут быть непредсказуемыми. В ходе моего исследования несколько SIM-карт вышло из строя, так и не загрузив меню.
Заключение
Команда AOSP устранила эту ошибку в обновлении Android 5.1.1 для Nexus-устройств (сборка LMY48I).
Вот некоторые из моих патчей:
For /platform/frameworks/opt/telephony/+/master/:
--- a/src/java/com/android/internal/telephony/cat/CatService.java
+++ b/src/java/com/android/internal/telephony/cat/CatService.java
@@ -501,7 +501,7 @@
intent.putExtra("STK CMD", cmdMsg);
intent.putExtra("SLOT_ID", mSlotId);
CatLog.d(this, "Sending CmdMsg: " + cmdMsg+ " on slotid:" + mSlotId);
- mContext.sendBroadcast(intent);
+ mContext.sendBroadcast(intent,"android.permission.RECEIVE_STK_COMMANDS");
}
/**
@@ -514,7 +514,7 @@
mCurrntCmd = mMenuCmd;
Intent intent = new Intent(AppInterface.CAT_SESSION_END_ACTION);
intent.putExtra("SLOT_ID", mSlotId);
- mContext.sendBroadcast(intent);
+ mContext.sendBroadcast(intent,"android.permission.RECEIVE_STK_COMMANDS");
}
@@ -868,7 +868,7 @@
intent.putExtra(AppInterface.CARD_STATUS, cardPresent);
CatLog.d(this, "Sending Card Status: "
+ cardState + " " + "cardPresent: " + cardPresent);
- mContext.sendBroadcast(intent);
+ mContext.sendBroadcast(intent,"android.permission.RECEIVE_STK_COMMANDS");
}
private void broadcastAlphaMessage(String alphaString) {
@@ -877,7 +877,7 @@
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(AppInterface.ALPHA_STRING, alphaString);
intent.putExtra("SLOT_ID", mSlotId);
- mContext.sendBroadcast(intent);
+ mContext.sendBroadcast(intent,"android.permission.RECEIVE_STK_COMMANDS");
}
@Override
For /platform/frameworks/base/ :
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -303,6 +303,11 @@
<protected-broadcast android:name="android.intent.action.ACTION_SET_RADIO_CAPABILITY_DONE" />
<protected-broadcast android:name="android.intent.action.ACTION_SET_RADIO_CAPABILITY_FAILED" />
+ <protected-broadcast android:name="android.intent.action.stk.command" />
+ <protected-broadcast android:name="android.intent.action.stk.session_end" />
+ <protected-broadcast android:name="android.intent.action.stk.icc_status_change" />
+ <protected-broadcast android:name="android.intent.action.stk.alpha_notify" />
+
<!-- ====================================== -->
<!-- Permissions for things that cost money -->
<!-- ====================================== -->
@@ -2923,6 +2928,9 @@
android:description="@string/permdesc_bindCarrierMessagingService"
android:protectionLevel="signature|system" />
+ <permission android:name="android.permission.RECEIVE_STK_COMMANDS"
+ android:protectionLevel="signature|system" />
+
<!-- The system process is explicitly the only one allowed to launch the
confirmation UI for full backup/restore -->
<uses-permission android:name="android.permission.CONFIRM_FULL_BACKUP"/>
For /platform/packages/apps/Stk/ :
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -24,6 +24,7 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.GET_TASKS"/>
+ <uses-permission android:name="android.permission.RECEIVE_STK_COMMANDS"/>
<application android:icon="@drawable/ic_launcher_sim_toolkit"
android:label="@string/app_name"
Автор: Руководитель отдела безопасности мобильных приложений Positive Technologies (англоязычная версия материала)
Автор: Positive Technologies