drozer模組的編寫及模組動態載入問題研究

wyzsk發表於2020-08-19
作者: thor · 2016/03/08 16:51

drozer是MWR Labs開發的一款Android安全測試框架。是目前最好的Android安全測試工具之一。drozer提供了命令列互動式介面,使用drozer進行安全測試,使用者在自己的console端輸入命令,drozer會將命令傳送到Android裝置上的drozer agent代理程式執行。drozer採用了模組化的設計,使用者可以定製開發需要的測試模組。編寫drozer模組主要涉及python模組及dex模組。python模組在drozer console端執行,類似於metasploit中的外掛,可以擴充套件drozer console的測試功能。dex模組是java編寫的android程式碼,類似於android的dex外掛,在android手機上執行,用於擴充套件drozer agent的功能。

0x00 簡單的drozer模組demo程式碼


首先看看drozer wiki給出的demo,該模組的功能就是在android裝置上反射呼叫java.util.Random類生成一個隨機數並返回:

#!python
from drozer.modules import Module

class GetInteger(Module):

    name = ""
    description = ""
    examples = ""
    author = "Joe Bloggs (@jbloggs)"
    date = "2012-12-21"
    license = "BSD (3-clause)"
    path = ["exp", "test"]

    def execute(self, arguments):
        random = self.new("java.util.Random")
        integer = random.nextInt()

        self.stdout.write("int: %d\n" % integer)

GetInteger類就是一個簡單的drozer模組,它繼承自drozer提供的模組基類Module。每個繼承了Module的類都對應著一個drozer模組,模組具體實現的功能則是在類中重寫excute函式,實現新的功能。在drozer console中執行以下命令就可以執行該模組了:

#!bash
dz> run exp.test.getinteger

執行效果如下:

1

drozer 透過Module類的metadata來配置和管理每個模組,因此模組編寫時需要包含以下 metadata資訊:

name          模組的名稱
description   模組的功能描述 
examples      模組的使用示例
author        作者
date          日期
license       許可
path          描述模組命令空間

這些資訊中比較重要的就是path變數,它描述了模組在drozer namespace中的路徑,結合對應的classname可以唯一確定drozer中的模組。例如demo中的path = ["exp", "test"],類名為GetInteger,那麼在drozer console中該模組就以exp.test.getinteger唯一確定。需要注意的是,儘管類的名字有大小寫之分,但執行該模組的時候,drozer console中的名字都為小寫。

0x01 drozer模組倉庫的建立及模組安裝


drozer模組安裝有兩種方法,一種是直接在repository中按照python包管理的方法新建目錄結構,將python檔案放入相應目錄中,另一種是在drozer console中透過module install命令直接安裝模組。

這兩種方法都必須先在本地建立一個drozer 的repository目錄,可以直接在drozer console中透過命令建立:

#!bash
 dz> module repository create [/path/to/repository]

也可以在~/.drozer_config檔案中指定本地repository目錄

#!bash
[repositories]  
/path/to/repository  =  /path/to/repository

建立好本地repository後就可以安裝自己的模組了。兩種安裝方法:

1) 按照python包管理的方式,在本地repository目錄下建立目錄exp,新建__int__.py空白檔案,然後將Python模組原始碼放入exp目錄即可。例如將test.py放入exp目錄下,test.py的內容如下:

#!python
from drozer.modules import Module

class GetInteger(Module):

    name = ""
    description = ""
    examples = ""
    author = "Joe Bloggs (@jbloggs)"
    date = "2012-12-21"
    license = "BSD (3-clause)"
    path = ["exp", "test"]

    def execute(self, arguments):
        random = self.new("java.util.Random")
        integer = random.nextInt()

        self.stdout.write("int: %d\n" % integer)

安裝好模組之後即可在drozer console端透過命令run exp.test.getinteger執行該模組了。

2) 透過drozer console中的命令module install 安裝。首先將編輯好的python模組原始檔命名為 exp.test2,檔案的內容同上。在drozer console中執行

#!bash
dz> module install  [/path/to/exp.test2]      

執行成功後則可以在本地repository目錄下exp目錄中看到生成了test2.py檔案,內容和原來的exp.test2檔案一致。安裝成功後及可執行該模組了。module install除了可以安裝本地倉庫的模組外,還可以遠端安裝gitbub上的模組,地址為

https://raw.github.com/mwrlabs/drozer-modules/repository/

例如執行

#!bash
dz>module install jubax.javascript     

將遠端下載並安裝scanner.misc.checkjavascriptbridge模組,安裝完成後執行

#!bash
dz> run scanner.misc.checkjavascriptbridge

就可以執行該模組,該模組的功能是檢查webview中addJavascriptInterface的使用是否存在安全隱患。

0x02 利用drozer提供的API擴充套件功能


drozer封裝了android中大部分API功能,使得能夠在python中方便的使用這些API擴充套件功能,發揮drozer及python的強大威力。

1)利用反射直接與Dalvik虛擬機器互動,其實就是Python直接在寫android程式碼,非常簡單方便。drozer主要是利用了drozer agent代理實現相關功能,例項化某個類的程式碼如下:

#!python
my_object = self.new("some.package.MyClass")

例如drozer.android模組中封裝了Intent類,使用者可以透過如下方式構造需要的Intent:

#!python
someintent = android.Intent(action=act, category=cat, data_uri=data, component=comp, extras=extr, flags=flgs)

然後透過intent開啟某個activity:

#!python 
self.getContext().startActivity(someintent)

2) drozer針對比較常用的功能還二次封裝了很多python的mixins工具類,提供了更簡單易用的API,這些mixins都在drozer.modules.common包中:

  • Assets
  • BusyBox
  • ClassLoader
  • FileSystem
  • Filters
  • PackageManager
  • Provider
  • ServiceBinding
  • Shell
  • Strings
  • SuperUser
  • TableFormatter
  • ZipFile

例如FileSystem類提供了訪問android手機檔案系統的介面,可以方便地讀寫、建立及刪除andoid手機上的目錄和檔案。ZipFile類提供瞭解壓zip檔案的功能。 為了使用這些mixin類提供的功能,在模組中可以直接繼承這些類就可以了:

#!python
from drozer.modules import common, Module

class MyModule(Module, common.FileSystem, common.ZipFile):
           ……
           ……
       self.deleteFile(“somepath”)
           ……
           ……
       dex_file = self.extractFromZip("classes.dex", path, self.cacheDir())

其中,self.deleteFile來自FileSystem類,self.extractFromZip來自ZipFile類。

0x03 實現find port及find IP模組


1) app開放埠查詢模組

Android app通常會監聽某些埠進行本地IPC或者遠端網路通訊,但是這些暴露的埠卻代表了潛在的本地或遠端攻擊面,具體可以參考大牛的文章:

淺談Android開放網路埠的安全風險

文章中提供了查詢開放埠及對應app的python指令碼,我們將其重寫為drozer模組,方便測試時使用:

#!python
from drozer.modules import Module,common
import re

class findport(Module,common.Shell):

    name = ""
    description = "find open port in android"
    examples = "run exp.work.findport"
    author = "[email protected]"
    date = "2015-12-02"
    license = "BSD (3-clause)"
    path = ["exp","work"]

    def toHexPort(self,port):
        hexport = str(hex(int(port)))
        return hexport.strip('0x').upper()     

    def finduid(self,protocol, entry):
        if (protocol=='tcp' or protocol=='tcp6'):
            uid = entry.split()[-10]

        else: # udp or udp6
            uid = entry.split()[-6]

        try: 
            uid = int(uid)
        except:
            return -1   
        if (uid > 10000): # just for non-system app
            return 'u0_a'+str(uid-10000) 
        else:
            return -1

    def execute(self, arguments):        

        proc_net = "/proc/net/"      
        ret = self.shellExec("netstat -anp | grep -Ei 'listen|udp*'")
        list_line = ret.split('\n')
        apps = []
        strip_listline = []
        #pattern = re.compile("^Proto") # omit the first line

        for line in list_line:              
                if (line != ''):               
                   socket_entry = line.split()
                   protocol = socket_entry[0]  
                   port = socket_entry[3].split(':')[-1]
                   grep_appid = 'grep  '+ self.toHexPort(port) + ' ' + proc_net + protocol                    

                   net_entry = self.shellExec(grep_appid)                         
                   uid = self.finduid(protocol, net_entry)

                   if (uid == -1): 
                       continue

                   applist = self.shellExec('ps | grep ' + uid).split()    
                   app = applist[8]
                   apps.append(app)
                   strip_listline.append(line)

        itapp= iter(apps)
        itline=iter(strip_listline)

        self.stdout.write("Proto  Recv-Q Send-Q  Local Address        Foreign Address        State            APP\r\n")
        try:
            while True:
                self.stdout.write( itline.next() + ' '*10 + itapp.next() + '\n')

        except StopIteration:
            pass

        self.stdout.write('\n')

該模組的主要功能都是在findport類中的execute函式中實現,查詢開放埠及app的方法和原來文章中的一樣,這裡主要用到了drozer提供的common.Shell類,用於在android裝置上執行shell命令:

#!python 
ret = self.shellExec("netstat -anp | grep -Ei 'listen|udp*'") 

在drozer console中直接執行如下命令即可:

#!bash 
dz> run exp.work.findport

執行效果如下:

image

2)app中IP地址掃描模組

drozer的scanner.misc.weburls提供了掃描app中http及https URL地址的功能,仿照該模組的功能,我們實現了app中IP地址的掃描模組,這些收集到的IP地址可以在web滲透測試中使用:

#!python
import re
from pydiesel.reflection import ReflectionException
from drozer.modules import common, Module 

class findips(Module, common.FileSystem, common.PackageManager, common.Provider, common.Strings, common.ZipFile):

    name = "Find IPs specified in packages."
    description = """
    Find IPs in apk files
    """
    examples = ""
    author = "[email protected]"
    date = "2015-12-9"
    license = ""
    path = ["exp", "server"]
    permissions = ["com.mwr.dz.permissions.GET_CONTEXT"]

    def add_arguments(self, parser):
        parser.add_argument("-a", "--package", help="specify a package to search")

    def execute(self, arguments):
        self.ip_matcher = re.compile(r"((?:(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d))))")
        if arguments.package != None:
            self.check_package(arguments.package, arguments)
        else:
            for package in self.packageManager().getPackages(common.PackageManager.GET_PERMISSIONS):
                try:
                    self.check_package(package.packageName, arguments)
                except Exception, e:
                    print str(e)

    def check_package(self, package, arguments):
        self.deleteFile("/".join([self.cacheDir(), "classes.dex"]))
        ips = []

        for path in self.packageManager().getSourcePaths(package):
            strings = []
            if ".apk" in path:
                dex_file = self.extractFromZip("classes.dex", path, self.cacheDir())
                if dex_file != None:
                    strings = self.getStrings(dex_file.getAbsolutePath())

                    dex_file.delete()
                    strings += self.getStrings(path.replace(".apk", ".odex")) 
            elif (".odex" in path):
                strings = self.getStrings(path)
            else:
                continue

            for s in strings:
                m = self.ip_matcher.search(s)
                if m is not None:
                    ips.append(s)

            if len(ips) > 0:
                self.stdout.write("%s\n" % str(package))

            for ip in ips:
                    self.stdout.write("  %s\n" % ip)

            if len(ips) > 0 :
                self.stdout.write("\n")

add_arguments函式是drozer提供的介面,用於新增命令列引數,這裡我們新增了--package引數,用於指定app名稱,如果沒有指定--package引數,那麼預設會查詢所有app中的IP地址,比較耗時。check_package函式主要實現指定app掃描IP地址的功能,該函式首先從app相關目錄中查詢apk檔案、odex檔案,如果是apk檔案則從apk檔案中解壓出classes.dex檔案:

#!python
for path in self.packageManager().getSourcePaths(package):
    strings = []
    if ".apk" in path:
        dex_file = self.extractFromZip("classes.dex", path, self.cacheDir())

然後從得到的dex、odex檔案中獲取到所有的strings:

#!python
strings = self.getStrings(path)

這裡的getStrings是drozer提供的API,實現了類似linux下strings命令的功能。

找到app中的所有strings後再用re匹配得到相應的IP地址:

#!python
for s in strings:
    m = self.ip_matcher.search(s)
    if m is not None:
        ips.append(s)

ip_matcher的正規表示式為:

#!python
self.ip_matcher = re.compile(r"((?:(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d))))")

最後,在drozer console中透過如下命令執行該模組:

#!bash
dz> run exp.server.findips -a com.dianping.v1

執行效果如下所示:

image

0x04 編寫dex外掛


除了利用drozer以python程式碼形式提供的API,使用者還可以用java程式碼編寫dex外掛。 例如下面的java程式碼就可以編譯為drozer的dex外掛:

#!python
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import android.util.Base64;
import android.util.Log;
import android.widget.Toast;
import android.net.Uri;
import android.content.ContentResolver;
import android.database.Cursor;
import android.provider.ContactsContract;
import android.content.Context;


public class dextest {

  private static final int BUFFER_SIZE = 4096;


    public static String test(Context c, String number) {

            String name = null;
            Uri uri = Uri.parse("content://com.android.contacts/data/phones/filter/" + number);
            ContentResolver resolver = c.getContentResolver();
            Cursor cursor = resolver.query(uri, new String[]{android.provider.ContactsContract.Data.DISPLAY_NAME}, null, null, null);
            if (cursor.moveToFirst()) {
                name = cursor.getString(0);
                Log.d("drozer", name);
            }
            cursor.close();


         Log.d("drozer","this is a drozer dex module!");
         return "hello world! this is a test! " + number + ": " + name;
  }

}

首先我們將該java檔案編譯為class檔案:

#!bash
javac -cp lib/android.jar dextest.java

然後用android sdk提供的dx工具將class檔案轉換為dex檔案:

#!bash     
dx  --dex --output=dextest.apk dextest*.class

最後將生成的dextest.apk檔案放到drozer的modules/common目錄下,在編寫drozer模組時可以透過以下方式呼叫該dex外掛:

#!python
dextest = self.loadClass("common/dextest.apk", "dextest")
self.stdout.write("[color red]get string from dex plugin: %s  [/color]\n" % dextest.test(self.getContext(),"181" ) )   

該測試外掛根據提供的部分電話號碼去匹配手機通訊錄中的聯絡人,並返回匹配到的聯絡人姓名,執行效果如下:

2

dex外掛是由drozer上傳到android手機上載入執行,它的作用還是為drozer模組提供更方便易用的介面,擴充套件更多的功能。由於dex外掛是Java編寫的原生android程式碼,在執行效率上比透過反射呼叫更高。drozer的modules/common目錄下包含了多個dex外掛的原始碼,有興趣的同學可以自己檢視。

0x05 drozer模組的reload及動態載入問題


編寫drozer module難免會涉及到除錯的問題,drozer console提供了debug選項,會在console中列印異常資訊,但是比較麻煩的是,修改module原始碼後必須要重啟drozer console才能生效。

檢視drozer原始碼,發現drozer在debug模式下提供了reload命令,但是測試了下,在mac下並沒有用,還是要重啟console才能生效。仔細研究drozer loader.py的相關原始碼:

#!python
def all(self, base):
     """
     Loads all modules from the specified module repositories, and returns a  collection of module identifiers.
     """

    if(len(self.__modules) == 0):
        self.__load(base)

    return sorted(self.__modules.keys())

def get(self, base, key):
    """
    Gets a module implementation, given its identifier.
    """

    if(len(self.__modules) == 0):
        self.__load(base)

    return self.__modules[key]

def reload(self):
    self.__modules = {}

reload命令將self.__modules置為空,在get中按理說就會重新載入所有的drozer模組。但是在mac下始終無法實現該功能,其他平臺未做測試。這裡就涉及到python模組的import及reload機制問題,在網上查詢到python的reload機制一些解釋:

reload會重新載入已載入的模組,但原來已經使用的例項還是會使用舊的模組, 而新生產的例項會使用新的模組, reload後還是用原來的記憶體地址;不能支援from。。import。。格式的模組進行重新載入。
http://blog.csdn.net/five3/article/details/7762870

猜測可能就是這個問題,雖然用python的reload機制可以重新載入模組,但是以前使用的模組可能還是在使用中,導致修改的原始碼沒有生效。

為什麼不在執行時動態載入模組呢?這樣可以保證載入的模組原始碼是最新的。

分析了drozer相關的所有原始碼,終於在session.py中找到例項化模組類的程式碼:

#!python
def __module(self, key):
    """
    Gets a module instance, by identifier, and initialises it with the
    required session parameters.
    """

    module = None

    try:
        module = self.modules.get(self.__module_name(key))
    except KeyError:
        pass

    if module == None:
        try:
            module = self.modules.get(key)
        except KeyError:
            pass

    if module == None:
        raise KeyError(key)
    else:
        return module(self)

該函式的功能就是根據模組類的key例項化該模組,從而執行該模組。因此,我們可以在這裡實現動態載入要執行的模組類,放棄已經載入的模組:

#!python
 def __module(self, key):

    """
    Gets a module instance, by identifier, and initialises it with the
    required session parameters.
    """

    module = None

    try:
        module = self.modules.get(self.__module_name(key))
    except KeyError:
        pass

    if module == None:
        try:
            module = self.modules.get(key)
        except KeyError:
            pass

    if module == None:
        raise KeyError(key)
    else:

        #reload module
        mod = reload(sys.modules[module.__module__])

        module_class_name = module.__name__
        module_class = getattr(mod,module_class_name)  #get module class object
        return module_class(self)

關鍵的程式碼如下:

#!python
#reload module   
mod = reload(sys.modules[module.__module__])

module_class_name = module.__name__
module_class = getattr(mod,module_class_name)  #get module class object
return module_class(self)

首先使用python的reload函式重新載入指定的模組,然後再在重新載入的模組中查詢到drozer模組關聯的類,最後例項化並返回。只需新增幾行程式碼便可實現動態載入模組類,這樣除錯的時候就不用每次重啟drozer console了。這裡只是提供了一種簡單的實現動態載入模組的方法,主要是方便模組的編寫及測試。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章