Эмуляция и перехват SIM-команд через SIM Toolkit на Android 5.1 и ниже (CVE-2015-3843)



    Я обнаружил эту уязвимость, исследуя возможность перехвата одноразовых паролей, которые отправлялись банком поставщику телекоммуникационных услуг, а затем поступали на специальное приложение 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 (англоязычная версия материала)
    • +23
    • 34.6k
    • 6
    Positive Technologies
    102.57
    Company
    Share post

    Comments 6

      +7
      Несколько вопросов, если не сложно:
      1. Многие ли банки используют приложения SIM-карты для одноразовых паролей? В России сколько примерно?
      2. По какой причине решают использовать приложения SIM-карты? Считают что это более безопасно чем простые SMS-сообщения или Push Notifications?
      3. Немного отстал от темы. Перехват текста SMS-сообщения с одноразовым паролем является более безопасным решением на данный момент?

      В ходе моего исследования несколько SIM-карт вышло из строя, так и не загрузив меню.

      Как, если не секрет? Значит ли это, что приложение без особых привелегий тоже может вывести из строя SIM-карту?
        0
        Перехват SMS сообщение делается очень просто, при некотором везении пользователь даже не заметит входящего сообщения.
        У меня есть приложение, которое перехватывает SMS от GSM модуля моего автомобиля. Также есть приложение для обработки поступлений на р.с. в банке, потому что сбербанк не особо желает автоматизировать это все дело.
          0
          1. У нескольких российских банков было желание внедрить подобный механизм. Они отказались после нашего исследования. По зарубежным данных нет.
          2. Основная причина — предотвратить возможность перехвата одноразовых паролей с помощью вредоносных приложений для андроида.
          3. Не понял вопроса.
          4. Сложно сказать почему. Во время тестирования я генерировал окна SIM Toolkit menu, при нажатии на кнопки Ok и Cancel в сим карту отправлялись соответсвующие команды. Возможно это как то повлияло на неё. К сожалению, без доступа к исходникам ПО сим карты, назвать точную причину я не могу. Под вышли из строя — подразумевается прекращение работы SIM Toolkit, в остальном же сим карта продолжала работать исправно, принимать звонки и т.п
          0
          Очень интересно, каков процент оперативно обновляемых (до 1 месяца) устройств на Android.
          Больше 5?
          «устранила эту ошибку в обновлении» звучит слишком хорошо.
            0
            Но оригинальное сообщение все равно появится потом а экране? Очередь очищается не по событию отправки ответа, а по событию нажатия на кнопку, я правильно понимаю?
              0
              Оригинальное окно можно скрыть, отправив команду android.intent.action.stk.session_end

            Only users with full accounts can post comments. Log in, please.