Spoofing and intercepting SIM commands through STK framework (Android 5.1 and below) (CVE-2015-3843)
found this vulnerability while researching the possibility to intercept one-time password, which sent from bank to carrier to custom application on SIM card and next to Android UI.
Intercepting
So imagine you have a tiny app on your SIM card, which main goal is to receive a custom message from carrier and show it to you through the Android UI. If you dig deep into Android sources, you will find com.android.internal.telephony.cat.CatService class which is responsible for receiving commands from Radio Interface Layer (RIL) to OS and vice versa.
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;
From all of 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 switches lead to call of handleCommand() method with the difference of the second parameter
- MSG_ID_EVENT_NOTIFY - just a notification message and doesn't expect any response from the user
- MSG_ID_PROACTIVE_COMMAND - on the other side, requires respond
/** * 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); }
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 equals to "android.intent.action.stk.command"
- "SLOT_ID" is used for multi-sim devices
- "STK CMD" - command as parcelable object
The problem is CatService uses implicit intent to send the command to another application and it's not protected by any permissionWhat an attacker can do with it?Intercept commands which has been sent from SIM card to Telephony using zero-permission malicious application on the system. You just need to register your own receiver with action "android.intent.action.stk.command" and get "STK CMD" extra from the intent.Example of intercepted command:22:08:37: Receive action: android.intent.action.stk.command 22:08:37: STK CMD 3100000063006F006D002E0061006E00640072006F00690064002E0069006E007400650072006E0061006C002E00740065006C006500700068006F006E0079002E006300610074002E0043006F006D006D0061006E006400440065007400610069006C0073000000010000000100000021000000810000002E00000063006F006D002E0061006E00640072006F00690064002E0069006E007400650072006E0061006C002E00740065006C006500700068006F006E0079002E006300610074002E0054006500780074004D006500730073006100670065000000000000000000000000000F0000003300350035003700360032003000350032003200370038003900310030000000FFFFFFFF00000000010000000100000001000000FFFFFFFFFFFFFFFFFFFFFFFF00000000 (com.android.internal.telephony.cat.CatCmdMessage)
It is Parcelable object in bytes. Just hex2ascii it and you will see a text message from SIM.
Spoofing
But it's just a half of vulnerability. Lets take a look on the application which originally receives this broadcasts.
How message is displayed |
It's called SIM Toolkit, or STK, which is a part of default Android framework. You can find sources 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>
Part of the AndroidManifest.xml file related to broadcast receiver. As you can see it's all exported. Not only you can intercept SIM commands, but also generate Parcelable object using malicious app and send it to com.android.stk.StkCmdReceiver. Receiver don't validate the sender of broadcast and "android.intent.action.stk.command" action isn't declared as protected broadcast in system AndroidManifest.xml, so we can emulate SIM card command sending. For example:
- SIM card asks you to approve some operation, like transaction in the internet bank, with text "Approve transaction #1234 with ammount $100500" and two options - "Ok" and "Cancel". Here is code from 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(); }- If user clicks "Ok" - sendResponse(StkAppService.RES_ID_CONFIRM, true); will be called, otherwise - sendResponse(StkAppService.RES_ID_CONFIRM, false);
- What if we will generate the same dialog (call it "fake") using "android.intent.action.stk.command" action with different text, in a few seconds before SIM card generates original dialog with "Approve transaction #1234 with ammount $100500"? Something like "Press Ok to close" - with two options - Ok and Cancel.
- User will not see original dialog with "confirmation of transaction" until he presses Ok or Cancel in the first "fake" dialog, because all of the commands which required user intercation are placed in a queue.
- So the state is:
Now, if user clicks Ok in "fake" dialog the sendResponse() method with flag "true" will be called and SIM card will receive "Ok" command, like it was clicked on original dialog. Even if user will click Cancel on the second "original" dialog, it will not affect the previous command. For SIM card it would be like a new response, which it isn't waiting. Interesting moment I found in sources:
- SIM card is waiting for user response
- Android is showing the first "fake" dialog to the user
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.
Epilogue
AOSP team fix this bug in Nexus Build: 5.1.1 (LMY48I). Here is some patches I provided: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"I AM NOT RESPONBILE FOR ANY INLEGAL THINGS.
Labels: Hacking and other things
0 Comments:
Post a Comment
Subscribe to Post Comments [Atom]
<< Home