📜 ⬆️ ⬇️

Emulate and intercept SIM commands via the SIM Toolkit on Android 5.1 and below (CVE-2015-3843)



I discovered this vulnerability by exploring the possibility of intercepting one-time passwords that were sent by the bank to the telecommunications service provider, and then entered a special SIM card application and were output to the Android user interface.

Intercept


Imagine that there is a small application on the SIM card that receives a message from the service provider and displays it on the screen of your Android device. If you delve into the source code of Android, you can stumble upon the com.android.internal.telephony.cat.CatService class, which is responsible for sending commands between the Radio Interface Layer (RIL) layer and the OS.
')
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; 

Of all the message types, we are interested in 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; 

Both switch statements call the handleCommand() method, but the second parameter is different in each case:


Go to 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); } 

And finally, 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); } 

But this part is quite entertaining:


The problem is that to send a command to another application, the CatService uses an implicit intent without restriction of privileges.

How can an attacker take advantage of this?

For example, use a malicious application that does not require additional privileges to intercept commands sent by a SIM card to a phone. To do this, you only need to register the receiver with the action android.intent.action.stk.command and get the STK CMD from the intent.

An example of an intercepted command:



This is a Parcelable object in bytes. When converting Hex to ASCII, you will receive a SIM card message.

Emulation


However, this is only half the vulnerability. Consider an application that receives a broadcast message like this:



Type of message

This application is called SIM Toolkit (STK) and is part of the standard Android framework. Sources can be found here .

 <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> 

The above is a fragment of the AndroidManifest.xml file related to the receiver component. As you can see, the component is fully exported. This allows not only intercepting SIM-card commands, but also creating a Parcelable object with the help of malicious programs, and then sending it to com.android.stk.StkCmdReceiver . Receiver does not check the sender, and the android.intent.action.stk.command action is not announced in the AndroidManifest.xml system file as a secure message, which allows fraudsters to emulate sending SIM commands.

For example:

1. The SIM card requests confirmation of a certain operation, say, a transaction in the Internet bank, displaying on the phone screen a message like “Confirm transaction No. 1234 worth 100,500 rubles” with two options - “OK” and “Cancel”. The code on 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. If the user clicks OK, the sendResponse(StkAppService.RES_ID_CONFIRM, true) command will be called sendResponse(StkAppService.RES_ID_CONFIRM, true) ; otherwise, sendResponse(StkAppService.RES_ID_CONFIRM, false)

3. What if using the android.intent.action.stk.command action create the same dialog box with other text (fake) and display it on the screen a few seconds before the SIM card generates the original message (“Confirm transaction No. 1234 on the amount of 100 500 rubles ")? In the text of the message we will write “Click OK to close”, and the buttons will remain the same - “OK” and “Cancel”.

4. The user will not see the original transaction confirmation dialog until he selects one of these options in the fake window, since all commands requiring user interaction are queued.

5. So, we stopped at the following:


If you click "OK", the sendResponse() method will be called with the "true" flag and the SIM card will receive the "OK" command as if it were sent from the original dialog. Even if the user selects the “Cancel” option in the second window, this will not affect the previous command in any way. The SIM card will take this as a new response, which it does not expect. In the source I was able to find a description of a similar situation:

  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. 

It says that "Invalid is a response that does not have an appropriate proactive command and sending which can“ confuse ”baseband / ril”. In fact, if a RIL or SIM card gets unexpected feedback from you, the consequences can be unpredictable. In the course of my research, several SIM cards failed, without loading the menu.

Conclusion


The AOSP team fixed this error in updating Android 5.1.1 for Nexus devices (build LMY48I).

Here are some of my patches:

 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" 

Author : Head of Mobile Applications Security, Positive Technologies ( English version of the material )

Source: https://habr.com/ru/post/280095/


All Articles