- PVSM.RU - https://www.pvsm.ru -
Я обнаружил эту уязвимость, исследуя возможность перехвата одноразовых паролей, которые отправлялись банком поставщику телекоммуникационных услуг, а затем поступали на специальное приложение SIM-карты и выводились на пользовательский интерфейс Android.
Представьте, что на SIM-карте есть небольшое приложение, которое получает сообщение от оператора связи и показывает его на экране вашего Android-устройства. Если покопаться в исходниках Android, можно наткнуться на класс com.android.internal.telephony.cat.CatService [1], который отвечает за передачу команд между слоем радиоинтерфейса (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-фреймворка. Исходники можно найти тут [2].
<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 [3]:
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. Итак, мы остановились на следующем:
Если нажать «ОК», будет вызван метод 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 (англоязычная версия материала [4])
Автор: Positive Technologies
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/116011
Ссылки в тексте:
[1] com.android.internal.telephony.cat.CatService: https://android.googlesource.com/platform/frameworks/opt/telephony/+/android-5.1.0_r5/src/java/com/android/internal/telephony/cat/CatService.java
[2] тут: https://android.googlesource.com/platform/packages/apps/Stk/+/master
[3] StkDialogActivity.java: https://android.googlesource.com/platform/packages/apps/Stk/+/master/src/com/android/stk/StkDialogActivity.java
[4] англоязычная версия материала: http://blog.0xb.in/2015/08/spoofing-and-intercepting-sim-commands.html
[5] Источник: https://habrahabr.ru/post/280095/
Нажмите здесь для печати.